diff --git a/.docs-drift.yaml b/.docs-drift.yaml new file mode 100644 index 0000000000..1c12540ee1 --- /dev/null +++ b/.docs-drift.yaml @@ -0,0 +1,100 @@ +# docs-drift configuration for opentdf/platform. +# +# This file is consumed by the docs-drift skill +# (https://github.com/virtru-corp/agent-skills/tree/main/skills/developers/docs-drift) +# when a contributor runs `/docs-drift` after editing SDK code. The skill scans +# for new/changed exported Go symbols and proto RPCs, drafts MDX stubs for +# opentdf/docs, and prepares a PR. +# +# Most of the values below mirror the skill's built-in OpenTDF defaults — they're +# committed as a documentation artifact so the contract is discoverable in this +# repo. The `mappings:` section below them adds repo-specific routing the skill +# wouldn't otherwise know about. + +docs: + repo: ../docs # Sibling clone of opentdf/docs + root: docs/sdks # Docs root within opentdf/docs + +version: + tag_prefix: "sdk/" # SDK is independently versioned (sdk/v0.X.Y) + +scan: + go_paths: # Where the scanner looks for Go source + - sdk/ # the public SDK package + - protocol/go/internal/ # source-file codegen helpers (PR #3232 pattern) + proto_paths: # Where the scanner looks for .proto files + - service/ + exclude_paths: # Substring match — these are generated or test-only + - sdk/sdkconnect/ # auto-generated Connect-RPC wrappers + - sdk/gen/ # generated proto code (legacy path) + - sdk/internal/ # Go internal package + +# Mappings route new symbols to the right MDX file when the name-only sniff +# can't find an existing reference. Keys are glob patterns matched against +# the fully-qualified symbol name; values are doc paths (relative to the +# docs repo root) with an optional #section anchor for the heading to +# append under. +# +# First-match-wins, ordered by insertion. Put the most specific patterns +# first; the catch-all patterns last. +mappings: + + # ── EntityIdentifier constructor helpers (PR #3232 pattern) ────────────── + # Live in protocol/go/internal/authorization/v2/ and get codegen-copied + # into protocol/go/authorization/v2/*.gen.go. They're documented in + # authorization.mdx under the ## EntityIdentifier section. + "ForToken": docs/sdks/authorization.mdx#entityidentifier + "ForClientID": docs/sdks/authorization.mdx#entityidentifier + "ForEmail": docs/sdks/authorization.mdx#entityidentifier + "ForUserName": docs/sdks/authorization.mdx#entityidentifier + "ForRegisteredResource": docs/sdks/authorization.mdx#entityidentifier + "WithRequestToken": docs/sdks/authorization.mdx#entityidentifier + + # ── SDK discovery / attribute checks ───────────────────────────────────── + # Methods on the SDK struct that read platform state without writing. + # All land in discovery.mdx alongside ListAttributes / AttributeExists / etc. + "SDK.List*": docs/sdks/discovery.mdx + "SDK.Attribute*": docs/sdks/discovery.mdx + "SDK.Validate*": docs/sdks/discovery.mdx + "SDK.GetEntityAttributes": docs/sdks/discovery.mdx + + # ── TDF mechanics and re-wrap helpers ──────────────────────────────────── + # Package-level functions for TDF inspection and option building. + # CreateTDF, LoadTDF, and IsValidTdf already live in tdf.mdx; new + # adjacent helpers (e.g., WithPolicyFrom from DSPX-2603) belong there too. + "IsTDF": docs/sdks/tdf.mdx + "IsValidTdf": docs/sdks/tdf.mdx + "WithPolicyFrom": docs/sdks/tdf.mdx + "BulkDecrypt": docs/sdks/tdf.mdx + + # ── Platform-client setup options ──────────────────────────────────────── + # The "Initializing the SDK client" section of platform-client.mdx is + # where setup-time options like WithPlatformEndpoint live. Newly-added + # With*-style construction options default here unless overridden above. + "With*": docs/sdks/platform-client.mdx#initializing-the-sdk-client + + # ── Policy enum aliases (PR #3408 pattern) ─────────────────────────────── + # Constants like policy.OperatorIn / policy.BooleanAnd added in + # protocol/go/internal/policy/enums.go. Documented under the enum tables + # in policy.mdx. + "Operator*": docs/sdks/policy.mdx + "Boolean*": docs/sdks/policy.mdx + "Rule*": docs/sdks/policy.mdx + "State*": docs/sdks/policy.mdx + + # ── Proto service RPCs ─────────────────────────────────────────────────── + # Per-service mapping. Each service's RPCs map to its dedicated MDX. + "AuthorizationService.*": docs/sdks/authorization.mdx + "AttributesService.*": docs/sdks/policy.mdx + "ActionsService.*": docs/sdks/policy.mdx + "NamespaceService.*": docs/sdks/policy.mdx + "SubjectMappingService.*": docs/sdks/policy.mdx + "ResourceMappingService.*": docs/sdks/policy.mdx + "RegisteredResourcesService.*": docs/sdks/policy.mdx + "ObligationsService.*": docs/sdks/obligations.mdx + "KeyAccessServerRegistryService.*": docs/sdks/policy.mdx + "KeyManagementService.*": docs/sdks/policy.mdx + "UnsafeService.*": docs/sdks/policy.mdx + "EntityResolutionService.*": docs/sdks/authorization.mdx + "AccessService.*": docs/sdks/tdf.mdx + "WellKnownService.*": docs/sdks/platform-client.mdx diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6cc5de1268..fc46f661cc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,7 +7,9 @@ version: 2 updates: # Dependabot for CI - package-ecosystem: github-actions - directory: / + directories: + - "/" + - "/otdfctl/e2e" schedule: interval: monthly commit-message: @@ -15,8 +17,12 @@ updates: # Dependabot for internal deps # Add explicit entry as any go.mods need internal dep checks + + # examples + BDD tests grouped together - package-ecosystem: gomod - directory: "/examples" + directories: + - "/examples" + - "/tests-bdd" commit-message: prefix: "fix(deps)" groups: @@ -25,6 +31,23 @@ updates: - "github.com/opentdf/*" schedule: interval: daily + + # lib/* modules grouped together + - package-ecosystem: gomod + directories: + - "/lib/fixtures" + - "/lib/flattening" + - "/lib/identifier" + - "/lib/ocrypto" + commit-message: + prefix: "fix(deps)" + groups: + external: + exclude-patterns: + - "github.com/opentdf/*" + schedule: + interval: daily + - package-ecosystem: gomod directory: "/sdk" commit-message: @@ -35,6 +58,7 @@ updates: - "github.com/opentdf/*" schedule: interval: daily + - package-ecosystem: gomod directory: "/service" commit-message: @@ -45,3 +69,25 @@ updates: - "github.com/opentdf/*" schedule: interval: daily + + - package-ecosystem: gomod + directory: "/otdfctl" + commit-message: + prefix: "fix(deps)" + groups: + external: + exclude-patterns: + - "github.com/opentdf/*" + schedule: + interval: daily + + - package-ecosystem: gomod + directory: "/protocol/go" + commit-message: + prefix: "fix(deps)" + groups: + external: + exclude-patterns: + - "github.com/opentdf/*" + schedule: + interval: daily diff --git a/.github/release-please/release-please-config.main.json b/.github/release-please/release-please-config.main.json index 1a532d69fc..438dd5b50b 100644 --- a/.github/release-please/release-please-config.main.json +++ b/.github/release-please/release-please-config.main.json @@ -19,6 +19,15 @@ "lib/identifier": { "component": "lib/identifier" }, + "otdfctl": { + "component": "otdfctl", + "extra-files": [ + { + "type": "generic", + "path": "pkg/config/config.go" + } + ] + }, "protocol/go": { "component": "protocol/go" }, @@ -41,4 +50,4 @@ ] } } -} +} \ No newline at end of file diff --git a/.github/release-please/release-please-config.otdfctl.json b/.github/release-please/release-please-config.otdfctl.json new file mode 100644 index 0000000000..66d64b2622 --- /dev/null +++ b/.github/release-please/release-please-config.otdfctl.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "go", + "versioning": "always-bump-patch", + "separate-pull-requests": true, + "include-component-in-tag": true, + "pull-request-title-pattern": "chore(release): release ${component} ${version}", + "tag-separator": "/", + "packages": { + "otdfctl": { + "component": "otdfctl", + "extra-files": [ + { + "type": "generic", + "path": "pkg/config/config.go" + } + ] + } + } +} diff --git a/.github/release-please/release-please-manifest.json b/.github/release-please/release-please-manifest.json index ce553d6bc1..53ff597025 100644 --- a/.github/release-please/release-please-manifest.json +++ b/.github/release-please/release-please-manifest.json @@ -1,9 +1,10 @@ { - "lib/fixtures": "0.4.0", - "lib/ocrypto": "0.8.0", + "lib/fixtures": "0.5.0", + "lib/ocrypto": "0.12.0", "lib/flattening": "0.1.3", - "lib/identifier": "0.2.0", - "protocol/go": "0.14.0", - "sdk": "0.11.0", - "service": "0.11.0" + "lib/identifier": "0.4.0", + "otdfctl": "0.32.0", + "protocol/go": "0.32.0", + "sdk": "0.21.0", + "service": "0.16.0" } \ No newline at end of file diff --git a/.github/scripts/connectivity-test.sh b/.github/scripts/connectivity-test.sh index 66657ec679..95140b48a4 100755 --- a/.github/scripts/connectivity-test.sh +++ b/.github/scripts/connectivity-test.sh @@ -22,8 +22,8 @@ while true; do # Introduce random delay before each execution (between 1 and 4 seconds) sleep $((RANDOM % 4 + 1)) - echo "Running randomly selected command './otdfctl policy $random_subcommand list...'" - result=$(./otdfctl policy $random_subcommand list --with-client-creds '{"clientId":"opentdf","clientSecret":"secret"}' --host http://localhost:8080 | grep -i "success") + echo "Running randomly selected command './bin/otdfctl policy $random_subcommand list...'" + result=$(./bin/otdfctl policy $random_subcommand list --with-client-creds '{"clientId":"opentdf","clientSecret":"secret"}' --host http://localhost:8080 | grep -i "success") echo $result if [ -z "$result" ]; then echo "Failure: 'success' not found in output; CLI failed." diff --git a/.github/scripts/init-temp-keys.sh b/.github/scripts/init-temp-keys.sh index 4bb94ea76b..e9227c74d8 100755 --- a/.github/scripts/init-temp-keys.sh +++ b/.github/scripts/init-temp-keys.sh @@ -50,6 +50,9 @@ openssl rsa -in "$opt_output/kas-private.pem" -pubout -out "$opt_output/kas-cert openssl ecparam -name prime256v1 >ecparams.tmp openssl req -x509 -nodes -newkey ec:ecparams.tmp -subj "/CN=kas" -keyout "$opt_output/kas-ec-private.pem" -out "$opt_output/kas-ec-cert.pem" -days 365 +# Generate hybrid post-quantum key pairs (X-Wing, P256+ML-KEM-768, P384+ML-KEM-1024) +go run ./service/cmd/keygen -output "$opt_output" + mkdir -p keys openssl req -x509 -nodes -newkey RSA:2048 -subj "/CN=ca" -keyout keys/keycloak-ca-private.pem -out keys/keycloak-ca.pem -days 365 printf "subjectAltName=DNS:localhost,IP:127.0.0.1" >keys/sanX509.conf diff --git a/.github/scripts/watch.sh b/.github/scripts/watch.sh index defedcd798..acde7b6b75 100755 --- a/.github/scripts/watch.sh +++ b/.github/scripts/watch.sh @@ -53,18 +53,54 @@ done file_to_watch="$1" shift +file_signature() { + if [[ ! -e "$1" ]]; then + echo "missing" + return + fi + + if stat -c '%i:%s:%Y' "$1" >/dev/null 2>&1; then + stat -c '%i:%s:%Y' "$1" + return + fi + + stat -f '%i:%z:%m' "$1" +} + wait_for_change_to() { - if which inotifywait; then - echo "[INFO] inotifywaiting to [${file_to_watch}]" - inotifywait -e modify -e move -e create -e delete -e attrib -r "${file_to_watch}" + if command -v inotifywait >/dev/null 2>&1; then + local watch_dir + local watch_name + local changed_file + + watch_dir=$(dirname "${file_to_watch}") + watch_name=$(basename "${file_to_watch}") + + echo "[INFO] inotifywaiting to [${file_to_watch}] via [${watch_dir}]" + while true; do + changed_file=$(inotifywait -q \ + -e close_write \ + -e moved_to \ + -e delete \ + -e attrib \ + --format '%f' \ + "${watch_dir}") + + if [[ "${changed_file}" == "${watch_name}" ]]; then + return + fi + done else - m=$(date -r "${file_to_watch}" +%s) + local m + local n + + m=$(file_signature "${file_to_watch}") echo "[INFO] stat checking [${file_to_watch}] from [${m}]" while true; do sleep 1 - n=$(date -r "${file_to_watch}" +%s) - echo "[INFO] stat checking [${file_to_watch}] from [${m} < ${n}]" - if [[ $m < $n ]]; then + n=$(file_signature "${file_to_watch}") + echo "[INFO] stat checking [${file_to_watch}] from [${m} != ${n}]" + if [[ "${m}" != "${n}" ]]; then return fi done diff --git a/.github/scripts/work-init.sh b/.github/scripts/work-init.sh index 878f7fd6ef..78e86b5336 100755 --- a/.github/scripts/work-init.sh +++ b/.github/scripts/work-init.sh @@ -36,10 +36,16 @@ if ! cd "$ROOT_DIR"; then exit 1 fi +# Preserve the toolchain directive from the original go.work so that CI steps +# reading go-version-file: go.work (e.g. govulncheck) continue to use the +# correct Go version after the workspace is regenerated. +ORIG_TOOLCHAIN=$(awk '/^toolchain / {print $2; exit}' go.work 2>/dev/null) + echo "[INFO] Rebuilding partial go.work for [${component}]" case $component in lib/ocrypto | lib/fixtures | lib/flattening | lib/identifier | protocol/go) echo "[INFO] skipping for leaf package" + exit 0 ;; sdk) rm -f go.work go.work.sum && @@ -59,8 +65,25 @@ examples) go work init && go work use ./examples ;; +otdfctl) + rm -f go.work go.work.sum && + go work init && + go work use ./otdfctl && + # service and examples are needed for release branch checks + go work use ./service && + go work use ./examples + ;; *) echo "[ERROR] unknown component [${component}]" exit 1 ;; esac + +# Restore the toolchain directive if it was present in the original go.work. +if [[ -n "${ORIG_TOOLCHAIN:-}" ]]; then + if ! go work edit -toolchain="$ORIG_TOOLCHAIN"; then + echo "[ERROR] unable to restore original toolchain [${ORIG_TOOLCHAIN}] in go.work" + exit 1 + fi + echo "[INFO] Restored toolchain ${ORIG_TOOLCHAIN} in go.work" +fi diff --git a/.github/workflows/action-lint.yaml b/.github/workflows/action-lint.yaml index 5152554446..6550e98b46 100644 --- a/.github/workflows/action-lint.yaml +++ b/.github/workflows/action-lint.yaml @@ -19,7 +19,7 @@ jobs: pull-requests: write checks: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run reviewdog actionlint" @@ -36,7 +36,7 @@ jobs: actions: read # only needed for private repos steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 62df5c151e..d59a4eb0c8 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -18,7 +18,15 @@ on: - main types: - checks_requested + schedule: + - cron: '25 5 * * *' # 5:25am UTC = 12:25am EST / 8:25am EEST workflow_call: + workflow_dispatch: + inputs: + testrail-run-name-for-cli-test: + description: 'Name for the TestRail test run' + required: false + default: '' permissions: {} @@ -33,6 +41,7 @@ jobs: matrix: directory: - examples + - otdfctl - sdk - service - lib/ocrypto @@ -41,16 +50,17 @@ jobs: - lib/identifier - tests-bdd steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version-file: ${{ matrix.directory }}/go.mod + go-version-file: go.work check-latest: false cache-dependency-path: | examples/go.sum + otdfctl/go.sum protocol/go/go.sum sdk/go.sum service/go.sum @@ -69,15 +79,29 @@ jobs: - run: go work use . if: env.IS_RELEASE_BRANCH == 'true' working-directory: ${{ matrix.directory }} - - name: govluncheck + - name: govulncheck + id: govulncheck + continue-on-error: true uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 with: - go-version-input: "1.24.6" + go-version-input: "" + go-version-file: go.work work-dir: ${{ matrix.directory }} + - if: steps.govulncheck.outcome == 'failure' + run: echo "$MODULE_DIR" > "/tmp/govulncheck-failure-${JOB_INDEX}.txt" + env: + MODULE_DIR: ${{ matrix.directory }} + JOB_INDEX: ${{ strategy.job-index }} + - if: steps.govulncheck.outcome == 'failure' + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + with: + name: govulncheck-failure-${{ strategy.job-index }} + path: /tmp/govulncheck-failure-${{ strategy.job-index }}.txt + retention-days: 1 - name: golangci-lint - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: - version: v2.1 + version: v2.8.0 working-directory: ${{ matrix.directory }} skip-cache: true only-new-issues: true @@ -87,6 +111,14 @@ jobs: working-directory: ${{ matrix.directory }} - if: matrix.directory == 'service' run: go test ./service/integration -race -failfast + - name: setup sqlc + if: matrix.directory == 'service' + uses: sqlc-dev/setup-sqlc@bac53b7fb28c039a6c7f5736fd1e89744021bdd6 # v5.0.0 + with: + sqlc-version: '1.31.0' + - name: verify sqlc generate + if: matrix.directory == 'service' + run: make policy-sql-gen - name: check go fmt and go mod tidy run: |- go mod tidy @@ -99,6 +131,58 @@ jobs: run: git diff-files --quiet --ignore-submodules if: env.IS_RELEASE_BRANCH == 'false' + comment-govulncheck: + if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && !cancelled() + permissions: + contents: read + pull-requests: write + needs: go + runs-on: ubuntu-22.04 + steps: + - name: download govulncheck failures + id: download + continue-on-error: true + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + pattern: govulncheck-failure-* + path: govulncheck-failures + merge-multiple: true + - name: build comment body + id: comment-body + if: steps.download.outcome == 'success' + env: + RUN_ID: ${{ github.run_id }} + SERVER_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + run: | + modules=$(find govulncheck-failures -name '*.txt' | sort | while IFS= read -r f; do echo "- \`$(cat "$f")\`"; done) + run_url="${SERVER_URL}/${REPO}/actions/runs/${RUN_ID}" + body="## :warning: Govulncheck found vulnerabilities :warning: + + The following modules have known vulnerabilities: + + ${modules} + + See the [workflow run](${run_url}) for details." + { + echo "body<> "$GITHUB_OUTPUT" + - name: post govulncheck comment + if: steps.download.outcome == 'success' + uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2 + with: + header: govulncheck-results + recreate: true + message: ${{ steps.comment-body.outputs.body }} + - name: delete govulncheck comment + if: steps.download.outcome != 'success' + uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2 + with: + header: govulncheck-results + delete: true + integration: permissions: contents: read @@ -107,12 +191,12 @@ jobs: env: TLS_ENABLED: "true" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version-file: "service/go.mod" + go-version-file: go.work check-latest: false cache-dependency-path: | service/go.sum @@ -186,12 +270,12 @@ jobs: env: TLS_ENABLED: "true" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version-file: "service/go.mod" + go-version-file: go.work check-latest: false cache-dependency-path: | service/go.sum @@ -238,7 +322,7 @@ jobs: run: cd examples && go build -o examples . - name: run bulk rewrap benchmark tests run: | - OUTPUT=$(./examples/examples benchmark-bulk --tdf tdf3 --count 100) + OUTPUT=$(./examples/examples benchmark-bulk --count 100) echo "$OUTPUT" echo "$OUTPUT" >> "$GITHUB_STEP_SUMMARY" { @@ -283,16 +367,6 @@ jobs: echo "$OUTPUT"; echo "EOO" } >> "$GITHUB_ENV" - - name: run nanotdf benchmark tests - run: | - OUTPUT=$(./examples/examples benchmark --storeCollectionHeaders=false --tdf=nanotdf --count=5000 --concurrent=50) - echo "$OUTPUT" - echo "$OUTPUT" >> "$GITHUB_STEP_SUMMARY" - { - echo "BENCHMARK_NANO_OUTPUT<> "$GITHUB_ENV" - name: collect the metrics from the benchmark tests run: | OUTPUT=$(./examples/examples metrics) @@ -325,7 +399,6 @@ jobs: h2 "${BENCHMARK_METRICS_OUTPUT}" "Standard Benchmark Metrics" h2 "${BENCHMARK_BULK_OUTPUT}" "Bulk Benchmark" h2 "${BENCHMARK_TDF3_OUTPUT}" "TDF3 Benchmark" - h2 "${BENCHMARK_NANO_OUTPUT}" "Nano Benchmark" echo EOO } >>"$GITHUB_OUTPUT" @@ -350,7 +423,7 @@ jobs: needs: benchmark runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -377,7 +450,7 @@ jobs: outputs: proto: ${{ steps.check.outputs.proto }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false @@ -386,7 +459,7 @@ jobs: run: | if [ "${{ github.event_name }}" = "pull_request" ]; then BASE_SHA="${{ github.event.pull_request.base.sha }}" - if git diff --name-only "$BASE_SHA" HEAD | grep -q '\.proto$'; then + if git diff --name-only "$BASE_SHA" HEAD | grep -qE '\.proto$|^Makefile$|^buf\.|^protocol/codegen/|^protocol/go/internal/|^sdk/codegen/'; then echo "proto=true" >> "$GITHUB_OUTPUT" else echo "proto=false" >> "$GITHUB_OUTPUT" @@ -402,7 +475,7 @@ jobs: name: image build runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 @@ -446,12 +519,12 @@ jobs: sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert - name: "Checkout" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version-file: ./tests-bdd/go.mod + go-version-file: go.work cache: false - name: Build local platform-cukes image for testing @@ -475,6 +548,8 @@ jobs: cat cukes_platform_compose.log echo "********** Docker logs **********" docker ps -aq | xargs -L 1 docker logs + echo "********** Cukes Platform Service log **********" + cat cukes_platform_service.log - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 if: ${{ !cancelled() }} with: @@ -484,23 +559,28 @@ jobs: cukes_platform_report.log retention-days: 1 - # test latest otdfctl CLI 'main' against platform PR branch + # test otdfctl CLI e2e against platform PR branch otdfctl-test: permissions: contents: read name: otdfctl e2e tests runs-on: ubuntu-latest steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false - name: Install GNU parallel run: | sudo apt update sudo apt install -y parallel - - uses: opentdf/platform/test/start-up-with-containers@main + - uses: ./test/start-up-with-containers with: platform-ref: ${{ github.event.pull_request.head.sha || github.sha }} - - uses: opentdf/otdfctl/e2e@main + provision-policy-fixtures: "false" + - uses: ./otdfctl/e2e with: - otdfctl-ref: "main" + testrail-run-name-for-cli-test: ${{ inputs.testrail-run-name-for-cli-test }} env: TESTRAIL_USER: ${{ secrets.TESTRAIL_USER }} TESTRAIL_PASS: ${{ secrets.TESTRAIL_PASS }} @@ -513,13 +593,13 @@ jobs: name: Protocol Buffer Lint and Gencode Up-to-date check runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0 with: github_token: ${{ github.token }} - version: "1.56.0" + version: "1.70.0" - uses: bufbuild/buf-lint-action@06f9dd823d873146471cfaaf108a993fe00e5325 # v1.1.1 with: input: service @@ -529,7 +609,7 @@ jobs: against: "https://github.com/opentdf/platform.git#branch=${{ github.event.pull_request.base.ref || github.base_ref || 'main' }},subdir=service" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version-file: "service/go.mod" + go-version-file: go.work check-latest: false cache-dependency-path: | service/go.sum @@ -538,8 +618,8 @@ jobs: examples/go.sum - run: cd service && go get github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc - run: cd service && go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc - - run: cd service && go install github.com/sudorandom/protoc-gen-connect-openapi@v0.18.0 - - run: cd service && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1 + - run: cd service && go install github.com/sudorandom/protoc-gen-connect-openapi@v0.25.6 + - run: cd service && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0 - run: make proto-generate - name: generate connect wrappers run: make connect-wrapper-generate @@ -582,12 +662,12 @@ jobs: name: license check runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version-file: "service/go.mod" + go-version-file: go.work check-latest: false cache: false - name: check service licenses diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 226498418c..9eafa54c3b 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index c3e874251d..12de522ef1 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -37,13 +37,13 @@ jobs: - name: Checkout if: ${{ github.event_name != 'merge_group' }} - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: 'Dependency Review' if: ${{ github.event_name != 'merge_group' }} - uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 with: fail-on-severity: ${{ inputs.fail-on-severity }} deny-licenses: > diff --git a/.github/workflows/friendly-reminders.yaml b/.github/workflows/friendly-reminders.yaml index 7d2ed38b0f..6f261434e7 100644 --- a/.github/workflows/friendly-reminders.yaml +++ b/.github/workflows/friendly-reminders.yaml @@ -13,12 +13,12 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version-file: "service/go.mod" + go-version-file: go.work - name: Check Go Mod Tidy id: go-mod-tidy diff --git a/.github/workflows/label.yaml b/.github/workflows/label.yaml index 1765a90ce0..790d8d1673 100644 --- a/.github/workflows/label.yaml +++ b/.github/workflows/label.yaml @@ -16,7 +16,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 size-pr: permissions: diff --git a/.github/workflows/nightly-build.yaml b/.github/workflows/nightly-build.yaml index fff23d5597..d74a07e7dd 100644 --- a/.github/workflows/nightly-build.yaml +++ b/.github/workflows/nightly-build.yaml @@ -14,7 +14,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Authenticate to Google Cloud (Push to Public registry)" @@ -27,10 +27,10 @@ jobs: create_credentials_file: false - name: Install Cosign - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # 3.9.2 + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # 4.0.0 - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 diff --git a/.github/workflows/nightly-checks.yaml b/.github/workflows/nightly-checks.yaml index 03e9345712..4ff14db92c 100644 --- a/.github/workflows/nightly-checks.yaml +++ b/.github/workflows/nightly-checks.yaml @@ -16,17 +16,18 @@ jobs: contents: read steps: ######## CHECKOUT/SETUP PLATFORM ############# - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 path: platform persist-credentials: false - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version-file: "platform/service/go.mod" + go-version-file: "platform/go.work" check-latest: false cache-dependency-path: | platform/examples/go.sum + platform/otdfctl/go.sum platform/protocol/go/go.sum platform/sdk/go.sum platform/service/go.sum @@ -60,18 +61,9 @@ jobs: wait-for: 90s working-directory: platform - ######## CHECKOUT/BUILD 'otdfctl' ############# - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - repository: opentdf/otdfctl - ref: main - fetch-depth: 0 - path: otdfctl - persist-credentials: false - - run: go build -o otdfctl - working-directory: otdfctl - - run: cp otdfctl ../platform - working-directory: otdfctl + ######## BUILD 'otdfctl' (now part of platform monorepo) ############# + - run: go build -o ../bin/otdfctl . + working-directory: platform/otdfctl ######## RUN TESTS ############# - run: ./.github/scripts/connectivity-test.sh diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 7f55c603ec..00ae548827 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -46,6 +46,7 @@ jobs: # - main: used for automated releases # - core: related to any core need such as the core service or monorepo # - ci: anything related to ci + # - cli: related to otdfctl # - deps: dependency update # - docs: anything related solely to documentation # - sdk: related to sdk changes in the /sdk directory @@ -56,6 +57,7 @@ jobs: main core ci + cli deps docs sdk diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml index fd26e62307..21740137b3 100644 --- a/.github/workflows/release-build.yaml +++ b/.github/workflows/release-build.yaml @@ -1,19 +1,23 @@ -name: Build Platform Container Image +name: "Build Platform Container Image" on: release: types: [published] +defaults: + run: + shell: bash + permissions: {} jobs: build: - if: startsWith(github.event.release.tag_name, 'service/') + if: ${{ startsWith(github.event.release.tag_name, 'service/') }} runs-on: ubuntu-22.04 permissions: id-token: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -21,35 +25,34 @@ jobs: id: "gcp-auth" uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 with: - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY }} - service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - token_format: "access_token" - create_credentials_file: false + workload_identity_provider: ${{ vars.CENTRAL_WIF_PROVIDER }} + project_id: ${{ vars.CENTRAL_WIF_PROJECT_ID }} + + - name: "Set up Cloud SDK" + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 + + - name: "Configure gcloud as Docker credential helper" + run: | + gcloud auth configure-docker us-docker.pkg.dev --quiet - - name: Install Cosign - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # 3.9.2 + - name: "Install Cosign" + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # 4.0.0 - - name: Install Trivy - uses: aquasecurity/setup-trivy@9ea583eb67910444b1f64abf338bd2e105a0a93d # 0.2.3 + - name: "Install Trivy" + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 # 0.2.6 with: - version: v0.57.1 + version: v0.69.3 - - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + - name: "Set up QEMU" + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - name: Set up Docker Buildx + - name: "Set up Docker Buildx" uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 with: cache-binary: false - - name: "Docker login to Artifact Registry" - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - with: - registry: us-docker.pkg.dev - username: oauth2accesstoken - password: ${{ steps.gcp-auth.outputs.access_token }} - - - id: docker_meta + - name: "Generate Docker metadata" + id: docker_meta uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: ${{ secrets.DOCKER_REPO }} @@ -60,7 +63,7 @@ jobs: labels: | org.opencontainers.image.documentation=https://docs.opentdf.io - - name: Build and Push container images + - name: "Build and Push container images" uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 id: build-and-push with: @@ -68,31 +71,31 @@ jobs: push: true tags: ${{ steps.docker_meta.outputs.tags }} - - name: Sign the images with GitHub OIDC Token + - name: "Sign the images with GitHub OIDC Token" env: DIGEST: ${{ steps.build-and-push.outputs.digest }} TAGS: ${{ steps.docker_meta.outputs.tags }} run: | - images=() - for tag in ${TAGS}; do - images+=("${tag}@${DIGEST}") - done - cosign sign --yes "${images[@]}" + images=() + for tag in ${TAGS}; do + images+=("${tag}@${DIGEST}") + done + cosign sign --yes "${images[@]}" - - name: Generate Reports + - name: "Generate Reports" + env: + REPO: ${{ secrets.DOCKER_REPO }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} run: | trivy image --scanners vuln --format cyclonedx --output bom-cyclonedx.json "${REPO}@${DIGEST}" trivy image --format spdx-json --output bom-spdx.json "${REPO}@${DIGEST}" trivy image --format cosign-vuln --output cosign-vuln.json "${REPO}@${DIGEST}" + + - name: "Cosign Attest SBOM" env: REPO: ${{ secrets.DOCKER_REPO }} DIGEST: ${{ steps.build-and-push.outputs.digest }} - - - name: Cosign Attest SBOM run: | cosign attest --type cyclonedx --predicate bom-cyclonedx.json "${REPO}@${DIGEST}" cosign attest --type spdxjson -predicate bom-spdx.json "${REPO}@${DIGEST}" cosign attest --type vuln --predicate cosign-vuln.json "${REPO}@${DIGEST}" - env: - REPO: ${{ secrets.DOCKER_REPO }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} diff --git a/.github/workflows/release-otdfctl.yaml b/.github/workflows/release-otdfctl.yaml new file mode 100644 index 0000000000..d9afccd5c5 --- /dev/null +++ b/.github/workflows/release-otdfctl.yaml @@ -0,0 +1,43 @@ +name: "Build otdfctl CLI Binaries" + +on: + release: + types: [published] + +permissions: {} + +jobs: + build: + if: ${{ startsWith(github.event.release.tag_name, 'otdfctl/') }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + with: + go-version-file: go.work + + - name: Extract version from tag + id: version + env: + TAG: ${{ github.event.release.tag_name }} + run: | + VERSION="${TAG#otdfctl/v}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Build cross-platform binaries + working-directory: otdfctl + env: + SEM_VER: ${{ steps.version.outputs.version }} + COMMIT_SHA: ${{ github.sha }} + run: make build + + - name: Upload release artifacts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$RELEASE_TAG" ./otdfctl/output/* diff --git a/.github/workflows/reusable_backport.yaml b/.github/workflows/reusable_backport.yaml index 544c7ef277..8ed35a62ec 100644 --- a/.github/workflows/reusable_backport.yaml +++ b/.github/workflows/reusable_backport.yaml @@ -35,7 +35,7 @@ jobs: private-key: ${{ secrets.AUTOMATION_KEY }} - name: "Checkout" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: true token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/reusable_create-release-branch.yaml b/.github/workflows/reusable_create-release-branch.yaml index 0e5afccbf6..6b46fd5da6 100644 --- a/.github/workflows/reusable_create-release-branch.yaml +++ b/.github/workflows/reusable_create-release-branch.yaml @@ -33,7 +33,7 @@ jobs: private-key: ${{ secrets.AUTOMATION_KEY }} - name: "Checkout" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: true fetch-depth: 0 diff --git a/.github/workflows/reusable_release-please.yaml b/.github/workflows/reusable_release-please.yaml index 1f9ea3eeb5..a871093461 100644 --- a/.github/workflows/reusable_release-please.yaml +++ b/.github/workflows/reusable_release-please.yaml @@ -70,7 +70,7 @@ jobs: exit 1 - name: "Checkout" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -137,7 +137,7 @@ jobs: private-key: ${{ secrets.AUTOMATION_KEY }} - name: "Run release-please" - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4.2.0 + uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 id: release-please with: token: ${{ steps.generate_token.outputs.token }} @@ -161,7 +161,7 @@ jobs: private-key: ${{ secrets.AUTOMATION_KEY }} - name: "Checkout repo" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: true ref: ${{ fromJSON(needs.release-please.outputs.prs)[0].headBranchName }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 24242af4f1..348747e555 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -19,14 +19,14 @@ jobs: steps: - name: "Checkout repo" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Setup Go" uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: "1.23" + go-version-file: go.work check-latest: false cache-dependency-path: | service/go.sum @@ -55,7 +55,7 @@ jobs: contents: read steps: - name: "Checkout repo" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false @@ -65,7 +65,7 @@ jobs: name: code-coverage-report - name: "SonarCloud Scan" - uses: SonarSource/sonarqube-scan-action@1a6d90ebcb0e6a6b1d87e37ba693fe453195ae25 #v5.3.1 + uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 #v6.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 35117313ec..6fa4b4a748 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -12,7 +12,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: days-before-stale: 120 # negative number means they will never be closed automatically [https://github.com/actions/stale#days-before-close] diff --git a/.github/workflows/traffic.yaml b/.github/workflows/traffic.yaml index 9b18d30af7..a51e9d931e 100644 --- a/.github/workflows/traffic.yaml +++ b/.github/workflows/traffic.yaml @@ -31,7 +31,7 @@ jobs: private-key: "${{ secrets.AUTOMATION_KEY }}" owner: opentdf - name: checkout repo - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/vulnerability-check.yaml b/.github/workflows/vulnerability-check.yaml deleted file mode 100644 index 05d85258af..0000000000 --- a/.github/workflows/vulnerability-check.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: "Vulnerability Checks" - -on: - pull_request: - branches: - - main - schedule: - - cron: "0 0 * * *" - -permissions: {} - -jobs: - vulncheck: - permissions: - contents: read - name: vulncheck - runs-on: ubuntu-22.04 - strategy: - matrix: - directory: - - examples - - sdk - - service - steps: - - name: govluncheck - uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 - with: - go-version-input: "1.24.6" - work-dir: ${{ matrix.directory }} diff --git a/.gitignore b/.gitignore index 96b5c570c6..7d89ba5ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ opentdf.yaml tmp-gen/ /examples/examples /**/kas-*.pem +!/service/pkg/server/testdata/kas-*.pem /opentdf /sdkjava/target /serviceapp @@ -50,3 +51,44 @@ traces/ # Cucumber / BDD log files *.log + +.cache/* + +# Claude AI files +.claude/ + +# otdfctl specific ignores +# ========================= +otdfctl/bin/.DS_Store +otdfctl/.DS_Store +otdfctl/target/ +otdfctl/.vscode/launch.json +otdfctl/otdfctl.yaml + +# Ignore the binaries +otdfctl/otdfctl +otdfctl/otdfctl.* +otdfctl/otdfctl_testbuild +otdfctl/otdfctl_testbuild.* + +# Test artifacts +otdfctl/creds.json +otdfctl/**/creds.json + +# TestRail-related files +otdfctl/testrail.config.json +otdfctl/testname-to-testrail-id.json +otdfctl/mapping-report.txt +otdfctl/bats-results.tap + +# Hugo +otdfctl/public/ +otdfctl/.hugo_build.lock +otdfctl/output/ + +# Ignore any TDF files created by the CLI +otdfctl/*.tdf + +# Ignore go.cache +otdfctl/.gocache +# ========================= diff --git a/.golangci.yaml b/.golangci.yaml index 19cdbd1217..9513d465d3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -186,9 +186,31 @@ linters: - linters: - goimport text: http://www.apache.org/licenses/LICENSE-2.0 + # otdfctl: defer refactoring-level lint fixes to follow-up + - path: otdfctl/ + linters: + - contextcheck + text: should pass the context parameter + - path: otdfctl/ + linters: + - revive + text: unused-parameter + - path: otdfctl/ + linters: + - revive + text: unexported-return + - path: otdfctl/ + linters: + - revive + text: var-naming + - path: otdfctl/ + linters: + - nolintlint + text: "exhaustive" paths: - .*\.pb\.go - .*\.pb\.gw.go + - otdfctl/tui/ # excluded during migration, matching original otdfctl lint config - third_party$ - builtin$ - examples$ @@ -204,6 +226,7 @@ formatters: paths: - .*\.pb\.go - .*\.pb\.gw.go + - otdfctl/tui/ - third_party$ - builtin$ - examples$ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..7fc59a8f40 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,67 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +This repo is a Go workspace (`go.work`) containing multiple Go modules: + +- `service/`: main OpenTDF server and platform services (binary entrypoint: `service/main.go`). +- `sdk/`: Go SDK and generated clients. +- `lib/*/`: shared libraries (e.g., `lib/ocrypto`, `lib/identifier`). +- `protocol/` and `service/`: protobuf sources; generated Go lives under `protocol/go/` and docs under `docs/grpc/` + `docs/openapi/`. +- `tests-bdd/`: BDD/integration-style tests (Godog) and feature files (`tests-bdd/features/`). +- `docs/`, `examples/`, `adr/`: documentation, example code, and architecture decisions. + +## Build, Test, and Development Commands + +Prefer `make` targets at repo root: + +- `make toolcheck`: verifies required tooling (Buf, golangci-lint, generators). +- `make build`: regenerates protos/codegen and builds `opentdf` + `sdk` + `examples`. +- `make lint`: runs `buf lint`, `golangci-lint`, and `govulncheck` across modules. +- `make test`: runs `go test ./... -race` across core modules (does **not** include `tests-bdd/`). +- `docker compose up`: brings up local infra (Postgres + Keycloak). See `docs/Contributing.md`. + +## Coding Style & Naming Conventions + +- Go formatting is enforced: run `make fmt` (uses `golangci-lint fmt`; Go uses tabs for indentation). +- Imports should be goimports-compatible; keep package names lowercase; exported identifiers use `PascalCase`. +- Protobuf changes must pass `buf lint` and should be regenerated via `make proto-generate`. +- Always run `gofumpt` on Go files after making changes +- The project uses `gofumpt` (stricter than `gofmt`) for formatting +- Before completing Go-related tasks, run: `~/go/bin/gofumpt -w ` + +## Testing Guidelines + +### Required Tests Before Committing + +**CRITICAL**: All Go code changes must pass these checks before being marked as complete: + +1. **Linting**: `golangci-lint run ./path/to/changed/files.go` + - Must pass with 0 issues + - Fixes common issues: formatting, shadowing, unused code, suspicious constructs + - Never let the user discover linting issues from CI + +2. **Unit Tests**: `go test ./...` (or `make test` from repo root) + - All existing tests must continue to pass + - Add tests for new functionality + +3. **README Code Block Tests**: + - SDK README examples: `cd sdk && go test -run TestREADMECodeBlocks` + - Ensures documentation examples remain compilable + +### Test Types + +- **Unit tests**: `*_test.go` next to code; run `make test`. +- **BDD tests**: run `cd tests-bdd && go test ./...` (requires Docker; feature files are `tests-bdd/features/*.feature`). +- **Integration tests** may require the compose stack; follow module README(s) under `service/`. +- **README tests**: verify code examples in documentation compile and work correctly. + +## Commit & Pull Request Guidelines + +- Commit messages follow Conventional Commits (e.g., `feat(sdk): ...`, `fix(core): ...`). +- DCO sign-off is required: use `git commit -s -m "feat(scope): summary"`. See `CONTRIBUTING.md`. +- PRs should describe changes, include testing notes, and update docs/tests when applicable (see `.github/pull_request_template.md`). + +## Security & Configuration Tips + +- Don’t commit secrets/keys. Use local configs like `opentdf-dev.yaml` and follow `SECURITY.md`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..661cbf883f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# Claude Code Instructions + +## Overview + +This file provides instructions for AI agents working on the OpenTDF Platform codebase. + +**For complete repository guidelines, coding standards, and development workflow, see [AGENTS.md](AGENTS.md).** + +## Critical Requirements + +### Before Completing ANY Go Code Changes + +**MANDATORY CHECKS** - Run these before marking work as complete: + +1. **Linting**: `golangci-lint run ./path/to/changed/files.go` + - Must pass with 0 issues + - User should NEVER discover linting issues from CI + +2. **Unit Tests**: `go test ./...` or `make test` + - All existing tests must pass + - Add tests for new functionality + +3. **README Tests** (if SDK changes): `cd sdk && go test -run TestREADMECodeBlocks` + - Ensures documentation examples remain compilable + +### Formatting + +- Run `gofumpt -w ` on all changed Go files before completing +- Use tabs for indentation (Go standard) + +### Key Guidelines from AGENTS.md + +- **Project Structure**: Go workspace with multiple modules (service, sdk, lib/*) +- **Build Commands**: Use `make` targets (build, test, lint, fmt) +- **Commit Style**: Conventional Commits with DCO sign-off (`git commit -s`) +- **Testing**: Unit tests, BDD tests, integration tests - see AGENTS.md for details + +## Remember + +✅ Run linting checks proactively - don't let CI catch issues +✅ Test changes locally before marking complete +✅ Follow patterns and conventions in AGENTS.md +✅ Update documentation when changing APIs \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 0820929726..7571a0b6b7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,6 +41,10 @@ /sdk/ @opentdf/go-sdk @opentdf/architecture /sdk/go.* @opentdf/go-sdk @opentdf/architecture @opentdf/security +## CLI + +/otdfctl/ @opentdf/cli + ## High Security Area CODEOWNERS @opentdf/architecture @opentdf/security diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22f727c8bf..99c990fd24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,20 @@ +# Contributing to OpenTDF + +This project requires two things from every commit: + +1. **DCO sign-off** — a `Signed-off-by` trailer asserting your right to contribute the code +2. **Commit signature verification** — a cryptographic (GPG or SSH) signature proving the commit came from you + +Both are enforced by CI and org-level rulesets. The combined command is: + +```bash +git commit -s -S -m "Your descriptive commit message here" +``` + +Read on for setup details. + +--- + ## Developer Certificate of Origin (DCO) To ensure that contributions are properly licensed and that the project has the right to distribute them, this project requires that all contributions adhere to the Developer Certificate of Origin (DCO). @@ -24,44 +41,44 @@ This automatically appends the Signed-off-by line to your commit message using t By adding the Signed-off-by line, you are certifying to the following (from [developercertificate.org](https://developercertificate.org/)): -> Developer Certificate of Origin -> Version 1.1 -> -> Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -> -> Everyone is permitted to copy and distribute verbatim copies of this -> license document, but changing it is not allowed. +> Developer Certificate of Origin +> Version 1.1 +> +> Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +> +> Everyone is permitted to copy and distribute verbatim copies of this +> license document, but changing it is not allowed. +> +> +> Developer's Certificate of Origin 1.1 +> +> By making a contribution to this project, I certify that: > -> -> Developer's Certificate of Origin 1.1 -> -> By making a contribution to this project, I certify that: -> > (a) The contribution was created in whole or in part by me and I > have the right to submit it under the open source license -> indicated in the file; or -> +> indicated in the file; or +> > (b) The contribution is based upon previous work that, to the best > of my knowledge, is covered under an appropriate open source > license and I have the right under that license to submit that > work with modifications, whether created in whole or in part > by me, under the same open source license (unless I am > permitted to submit under a different license), as indicated -> in the file; or -> +> in the file; or +> > (c) The contribution was provided directly to me by some other > person who certified (a), (b) or (c) and I have not modified -> it. -> +> it. +> > (d) I understand and agree that this project and the contribution > are public and that a record of the contribution (including all > personal information I submit with it, including my sign-off) is > maintained indefinitely and may be redistributed consistent with > this project or the open source license(s) involved. -### Using Your Real Name +### Using Your Real Name -Please use your real name (not a pseudonym or anonymous contributions) in the Signed-off-by line. +Please use your real name (not a pseudonym or anonymous contributions) in the Signed-off-by line. ### What if I forgot to sign off my commits? @@ -79,4 +96,79 @@ git rebase -i --signoff HEAD~N # Replace N with the number of commits to rebase ``` Follow the instructions during the interactive rebase. You might need to force-push (git push --force-with-lease) your changes if you've already pushed the branch. Be careful when force-pushing, especially on shared branches. -We appreciate your contributions and your adherence to this process ensures the legal integrity of the project for everyone involved. If you have any questions about the DCO, please don't hesitate to ask. \ No newline at end of file +--- + +## Commit Signature Verification + +In addition to the DCO sign-off, this organization requires that every commit is **cryptographically signed** so that GitHub can verify the commit actually came from you. This is a separate requirement from the DCO — you need both. + +| | DCO sign-off (`-s`) | Commit signature (`-S`) | +|---|---|---| +| **What it is** | `Signed-off-by:` trailer in the commit message | Cryptographic GPG or SSH signature on the commit | +| **What it proves** | You have the right to submit the code | The commit actually came from you | +| **Checked by** | DCO probot | GitHub signature verification + org ruleset | + +### Setting Up Commit Signing + +You can sign commits with either a **GPG key** or an **SSH key**. Choose whichever you prefer. + +#### Option A: GPG signing + +Follow GitHub's guide to [generate a new GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) and [add it to your GitHub account](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account), then configure Git: + +```bash +git config --global user.signingkey YOUR_GPG_KEY_ID +git config --global gpg.format openpgp +``` + +#### Option B: SSH signing + +If you already have an SSH key added to GitHub, you can reuse it for commit signing. See GitHub's guide on [SSH commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#ssh-commit-signature-verification), then configure Git: + +```bash +git config --global user.signingkey ~/.ssh/id_ed25519.pub # Replace with /path/to/your/public_key.pub if different +git config --global gpg.format ssh +``` + +### Making It the Default + +To avoid passing `-S` on every commit, enable automatic signing: + +```bash +git config --global commit.gpgsign true +``` + +With this set, `git commit -s -m "message"` will automatically sign and sign-off every commit. + +### Verifying Your Setup + +After configuring, make a test commit and check that it shows as **Verified** on GitHub: + +```bash +git log --show-signature -1 +``` + +### What if my commits are not verified? + +If your PR is blocked because commits are not verified: + +For the most recent commit: +```bash +git commit --amend -S --no-edit +``` + +For older commits: +```bash +git rebase --exec 'git commit --amend --no-edit -S' HEAD~N # Replace N with the number of commits +``` + +You will need to force-push afterward: +```bash +git push --force-with-lease +``` + +For full details, see [GitHub's commit signature verification docs](https://docs.github.com/en/authentication/managing-commit-signature-verification). + +--- + +We appreciate your contributions and your adherence to this process ensures the legal integrity of the project for everyone involved. If you have any questions about the DCO or commit signing, please don't hesitate to ask. diff --git a/Dockerfile b/Dockerfile index 94754d7db1..d33632eb23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ COPY protocol/ protocol/ COPY sdk/ sdk/ COPY lib/ lib/ COPY service/ service/ +COPY otdfctl/ otdfctl/ COPY examples/ examples/ COPY tests-bdd/ tests-bdd/ COPY go.work ./ diff --git a/Makefile b/Makefile index e740289c7c..017a9c33ca 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,12 @@ # make # To run all lint checks: `LINT_OPTIONS= make lint` -.PHONY: all build clean connect-wrapper-generate docker-build fix fmt go-lint license lint proto-generate proto-lint sdk/sdk test tidy toolcheck +.PHONY: all buf-check build clean connect-wrapper-generate docker-build fix fmt go-lint license lint otdfctl/otdfctl policy-sql-gen proto-generate proto-helper-generate proto-lint sdk/sdk sqlc-check test tidy toolcheck -MODS=protocol/go lib/ocrypto lib/fixtures lib/flattening lib/identifier sdk service examples -HAND_MODS=lib/ocrypto lib/fixtures lib/flattening lib/identifier sdk service examples -REQUIRED_BUF_VERSION=1.56.0 +MODS=protocol/go lib/ocrypto lib/fixtures lib/flattening lib/identifier sdk service examples otdfctl tests-bdd +HAND_MODS=lib/ocrypto lib/fixtures lib/flattening lib/identifier sdk service examples otdfctl tests-bdd +REQUIRED_BUF_VERSION=1.70.0 +REQUIRED_SQLC_VERSION=1.31.0 ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) @@ -17,23 +18,35 @@ all: toolcheck clean build lint license test toolcheck: @echo "Checking for required tools..." - @which buf > /dev/null || (echo "buf not found, please install it from https://docs.buf.build/installation" && exit 1) - @BUF_VERSION=$$(buf --version | head -n 1 | awk '{print $$1}'); \ - if [ "$$(printf '%s\n' '$(REQUIRED_BUF_VERSION)' "$$BUF_VERSION" | sort -V | head -n 1)" != "$(REQUIRED_BUF_VERSION)" ]; then \ - echo "Error: buf version $(REQUIRED_BUF_VERSION) or later is required, but found $$BUF_VERSION."; \ - echo "Please upgrade buf. See https://docs.buf.build/installation for instructions."; \ - exit 1; \ - fi - @which golangci-lint > /dev/null || (echo "golangci-lint not found, run 'go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.1.6'" && exit 1) + @$(MAKE) --no-print-directory buf-check + @which golangci-lint > /dev/null || (echo "golangci-lint not found, run 'go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0'" && exit 1) @which protoc-gen-doc > /dev/null || (echo "protoc-gen-doc not found, run 'go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@v1.5.1'" && exit 1) @which protoc-gen-connect-openapi > /dev/null || (echo "protoc-gen-connect-openapi not found, run 'go install github.com/sudorandom/protoc-gen-connect-openapi@v0.18.0'" && exit 1) - @required_golangci_lint_version="2.1.0"; \ + @required_golangci_lint_version="2.8.0"; \ current_version=$$(golangci-lint --version | grep -Eo 'v?[0-9]+\.[0-9]+\.[0-9]+' | sed 's/^v//' | head -n 1); \ [ "$$(printf '%s\n' "$$required_golangci_lint_version" "$$current_version" | sort -V | head -n1)" = "$$required_golangci_lint_version" ] || \ (echo "golangci-lint version must be v$$required_golangci_lint_version or later [found: $$current_version]" && exit 1) @which goimports >/dev/null || (echo "goimports not found, run 'go install golang.org/x/tools/cmd/goimports@latest'") @govulncheck -version >/dev/null || (echo "govulncheck not found, run 'go install golang.org/x/vuln/cmd/govulncheck@latest'") +buf-check: + @which buf > /dev/null || (echo "buf not found, please install it from https://docs.buf.build/installation" && exit 1) + @BUF_VERSION=$$(buf --version | head -n 1 | awk '{print $$1}' | sed 's/^v//'); \ + if [ "$$(printf '%s\n' '$(REQUIRED_BUF_VERSION)' "$$BUF_VERSION" | sort -V | head -n 1)" != "$(REQUIRED_BUF_VERSION)" ]; then \ + echo "Error: buf version $(REQUIRED_BUF_VERSION) or later is required, but found $$BUF_VERSION."; \ + echo "Please upgrade buf. See https://docs.buf.build/installation for instructions."; \ + exit 1; \ + fi + +sqlc-check: + @which sqlc > /dev/null || { echo "sqlc not found, please install it: https://docs.sqlc.dev/en/stable/overview/install.html"; exit 1; } + @SQLC_VERSION=$$(sqlc version | head -n 1 | grep -Eo 'v?[0-9]+\.[0-9]+\.[0-9]+' | sed 's/^v//'); \ + if [ "$$(printf '%s\n' '$(REQUIRED_SQLC_VERSION)' "$$SQLC_VERSION" | sort -V | head -n 1)" != "$(REQUIRED_SQLC_VERSION)" ]; then \ + echo "Error: sqlc version $(REQUIRED_SQLC_VERSION) or later is required, but found $$SQLC_VERSION."; \ + echo "Please upgrade sqlc. See https://docs.sqlc.dev/en/stable/overview/install.html for instructions."; \ + exit 1; \ + fi + fix: tidy fmt fmt: @@ -74,23 +87,22 @@ govulncheck: proto-generate: toolcheck # remove all generated directories under protocol/go - find protocol/go -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + + find protocol/go -mindepth 1 -maxdepth 1 -type d ! -name internal -exec rm -rf {} + rm -rf docs/grpc docs/openapi buf generate service buf generate service --template buf.gen.grpc.docs.yaml buf generate service --template buf.gen.openapi.docs.yaml - buf generate buf.build/grpc-ecosystem/grpc-gateway -o tmp-gen - buf generate buf.build/grpc-ecosystem/grpc-gateway -o tmp-gen --template buf.gen.grpc.docs.yaml - buf generate buf.build/grpc-ecosystem/grpc-gateway -o tmp-gen --template buf.gen.openapi.docs.yaml - + cd protocol/codegen && go run . go run ./sdk/codegen connect-wrapper-generate: go run ./sdk/codegen -policy-sql-gen: - @which sqlc > /dev/null || { echo "sqlc not found, please install it: https://docs.sqlc.dev/en/stable/overview/install.html"; exit 1; } +proto-helper-generate: + cd protocol/codegen && go run . + +policy-sql-gen: sqlc-check sqlc generate -f service/policy/db/sqlc.yaml policy-erd-gen: @@ -111,9 +123,9 @@ bench: clean: for m in $(MODS); do (cd $$m && go clean) || exit 1; done - rm -f opentdf examples/examples + rm -f opentdf examples/examples otdfctl/otdfctl -build: proto-generate connect-wrapper-generate opentdf sdk/sdk examples/examples +build: proto-generate connect-wrapper-generate opentdf sdk/sdk examples/examples otdfctl/otdfctl opentdf: $(shell find service) go build -o opentdf -v service/main.go @@ -124,5 +136,8 @@ sdk/sdk: $(shell find sdk) examples/examples: $(shell find examples) (cd examples && go build -o examples .) +otdfctl/otdfctl: $(shell find otdfctl) + (cd otdfctl && go build -o otdfctl .) + docker-build: build docker build -t opentdf . diff --git a/adr/decisions/2026-01-02-authz-fine-grain-resource-support.md b/adr/decisions/2026-01-02-authz-fine-grain-resource-support.md new file mode 100644 index 0000000000..05f58085fd --- /dev/null +++ b/adr/decisions/2026-01-02-authz-fine-grain-resource-support.md @@ -0,0 +1,671 @@ +# Resource-Level Authorization Specification + +**Status:** Proposed (active implementation) +**Authors:** Platform Team +**Created:** 2024-12-30 +**Last Updated:** 2026-03-05 +**Record Date:** 2026-01-02 (ADR filename/index date) + +## Problem Statement + +The current authorization system uses **path-based RBAC** via Casbin, where policies match on gRPC method paths and HTTP routes. This provides coarse-grained access control (e.g., "admins can access all policy endpoints") but lacks the ability to enforce **resource-level permissions** (e.g., "user A can only modify attributes in namespace X"). + +### Current State (v1) + +``` +Model: (subject, resource, action) + where resource = gRPC path pattern (e.g., "policy.attributes.AttributesService/*") + and subject = roles extracted from JWT claims +``` + +### Desired State (v2) + +``` +Model: (subject, rpc, dimensions) + where: + - rpc = full gRPC method path (e.g., "/policy.attributes.AttributesService/UpdateAttribute") + - dimensions = service-specific key-value pairs (e.g., {"namespace": "hr", "attribute": "classification"}) + The RPC method itself implies the action (Get* = read, Update* = write, etc.) +``` + +## Goals + +1. **Namespace-scoped authorization** - Restrict users to resources within specific namespaces +2. **Governance & auditability** - Authorization decisions are logged with full context for compliance +3. **Developer experience** - Service maintainers have clear patterns for implementing authorization +4. **Extensibility** - Architecture supports future instance-level authorization +5. **Backwards compatibility via mode selection** - Existing path-based policies continue to work when running in v1 mode (`policy.version=v1`) + +## Non-Goals (v1) + +1. Instance-level authorization (user A can edit attribute X but not Y) - future consideration +2. Real-time policy updates without restart +3. External PDP integration (OPA, Cedar, etc.) - future consideration + +--- + +## Architecture + +### Component Overview + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Request Flow │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Client Request │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ConnectRPC Interceptor │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 1. Extract JWT claims → subject (roles, username) │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 2. Call service resolver → AuthzContext{dimensions: {...}} │ │ │ +│ │ │ (IoC / "Hollywood Principle" - framework calls service) │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 3. Enforce → Casbin(sub, rpc, serialized_dims) │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ (if allowed) │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Service Handler │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Key Components + +| Component | Owner | Responsibility | +|-----------|-------|----------------| +| AuthzContext | Platform | Contract struct between resolvers and enforcer | +| Interceptor | Platform | Orchestrates the authorization flow | +| Resolver Interface | Platform | Defines the hook contract | +| Resolver Implementation | Service | Enriches context with resource relationships | +| Casbin Model | Platform | Defines policy matching dimensions | +| Casbin Policies | Deployer | Configures actual access rules | + +--- + +## Implementation Concepts + +### 1. ResolverContext (Platform-Owned Contract) + +```go +// service/internal/auth/authz/resolver.go + +// ResolverResource represents a single resource's authorization dimensions. +// Each key-value pair is a dimension (e.g., "namespace" -> "hr"). +type ResolverResource map[string]string + +// EvaluationMode controls how multiple resources are interpreted. +type EvaluationMode string + +const ( + // EvaluationModeAllOf requires every resource to be authorized. + EvaluationModeAllOf EvaluationMode = "all_of" + // EvaluationModeFilter keeps only authorized resources (used by List operations). + EvaluationModeFilter EvaluationMode = "filter" +) + +// ResolverContext holds the resolved authorization context for a request. +// Multiple resources are supported for operations like "move from A to B" +// where authorization is required for both source and destination. +type ResolverContext struct { + Resources []*ResolverResource + Mode EvaluationMode // defaults to EvaluationModeAllOf when empty +} + +// Key methods: +// - NewResolverContext() - creates empty context +// - NewResource() *ResolverResource - adds and returns a new resource +// - AddDimension(key, value string) - adds dimension to a resource +// - SetEvaluationMode(mode) - sets all_of or filter semantics +``` + +**Resource semantics:** +- Each `ResolverResource` is an independent authorization target. +- Resource dimensions are evaluated per-resource; they are not flattened across resources. +- The same dimension key may appear in multiple resources with different values (e.g., source namespace and destination namespace). +- `all_of` mode is the default for non-list RPCs and requires every resource to pass authorization. +- `filter` mode is for list-style RPCs where unauthorized resources are dropped before handler execution. + +### 2. Resolver Interface (Platform-Owned) + +```go +// service/internal/auth/authz/resolver.go + +// ResolverFunc is the function signature for service-provided resolvers. +// Services implement this to extract authorization dimensions from requests. +// +// Parameters: +// - ctx: Request context (includes auth info, can be used for DB calls) +// - req: The connect request (use type assertion to get typed proto) +// +// Returns: +// - ResolverContext with populated dimensions +// - Error if resolution fails (results in 403) +type ResolverFunc func(ctx context.Context, req connect.AnyRequest) (ResolverContext, error) + +// ResolverRegistry holds resolver functions keyed by service method. +// Thread-safe for concurrent read/write access. +type ResolverRegistry struct { + resolvers map[string]ResolverFunc // full method path -> resolver +} + +// ScopedResolverRegistry provides a namespace-scoped view of the registry. +// Validates method ownership against ServiceDesc to prevent cross-service registration. +type ScopedResolverRegistry struct { + parent *ResolverRegistry + serviceDesc grpc.ServiceDesc +} +``` + +### 3. Service Resolver Implementation (Service-Owned) + +Service maintainers implement resolvers as methods on their service struct. Each method +handles one RPC and extracts dimensions from the request (with DB lookups as needed). + +**Implementation pattern (from service/policy/attributes/attributes.go):** + +```go +// updateAttributeAuthzResolver resolves namespace from attribute lookup. +func (s *AttributesService) updateAttributeAuthzResolver(ctx context.Context, req connect.AnyRequest) (authz.ResolverContext, error) { + resolverCtx := authz.NewResolverContext() + msg, ok := req.Any().(*attributes.UpdateAttributeRequest) + if !ok { + return resolverCtx, fmt.Errorf("unexpected request type: %T", req.Any()) + } + + // DB lookup to resolve attribute -> namespace relationship + attr, err := s.dbClient.GetAttribute(ctx, msg.GetId()) + if err != nil { + return resolverCtx, fmt.Errorf("failed to resolve attribute for authz: %w", err) + } + + // Populate dimensions + res := resolverCtx.NewResource() + res.AddDimension("namespace", attr.GetNamespace().GetName()) + res.AddDimension("attribute", attr.GetName()) + + return resolverCtx, nil +} +``` + +**Different services use different dimensions:** + +| Service | Typical Dimensions | +|---------|-------------------| +| Policy (attributes, namespaces) | `namespace`, `attribute` | +| KAS | `kas_id` | +| Authorization | (service-specific) | + +### 4. Service Registration (Service-Owned) + +Services register their resolvers during service startup via `RegistrationParams.AuthzResolverRegistry`: + +```go +// In service registration (service/policy/attributes/attributes.go) +RegisterFunc: func(srp serviceregistry.RegistrationParams) { + // ... service setup ... + + // Register authz resolvers per-method + if srp.AuthzResolverRegistry != nil { + srp.AuthzResolverRegistry.MustRegister("CreateAttribute", as.createAttributeAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("GetAttribute", as.getAttributeAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("GetAttributeValuesByFqns", as.getAttributeValuesByFqnsAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("ListAttributes", as.listAttributesAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("UpdateAttribute", as.updateAttributeAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("DeactivateAttribute", as.deactivateAttributeAuthzResolver) + } +} +``` + +The `ScopedResolverRegistry` validates that method names match the service's `ServiceDesc`. + +### 5. Casbin Model (Platform-Owned) + +The v2 Casbin model uses 3 fields: `(subject, rpc, dimensions)`. The RPC method path +itself implies the action, simplifying the model compared to v1's 4-field format. + +```conf +# service/internal/auth/authz/casbin/model.conf + +[request_definition] +# sub: subject (roles from JWT, or username) +# rpc: full gRPC method path (e.g., /policy.attributes.AttributesService/UpdateAttribute) +# dims: serialized key=value pairs, sorted by key (e.g., "namespace=hr&attribute=x") +r = sub, rpc, dims + +[policy_definition] +# Same structure as request, with eft (effect) for allow/deny +p = sub, rpc, dims, eft + +[role_definition] +g = _, _ + +[policy_effect] +# Allow if any policy explicitly allows AND no policy explicitly denies +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + +[matchers] +# g(r.sub, p.sub): role/group membership check +# keyMatch(r.rpc, p.rpc): RPC path matching with wildcards +# dimensionMatch(r.dims, p.dims): custom function for dimension matching +m = g(r.sub, p.sub) && keyMatch(r.rpc, p.rpc) && dimensionMatch(r.dims, p.dims) +``` + +#### Dimension Matching + +Dimension boundary contract (normative): +- Resolver contract: `ResolverResource` is a service-owned `map[string]string`. +- Authorizer boundary: each resource map is canonically serialized to a string. +- Casbin request contract: `r.dims` is that serialized string. +- Matcher contract: `dimensionMatch` compares request and policy dimension strings. + +Canonical serialization rules: +- Sort keys lexicographically. +- Serialize as `key=value&key2=value2`. +- Empty dimensions serialize to `*`. + +The platform provides a custom Casbin matcher function `dimensionMatch` that compares request dimensions (serialized string) against policy dimensions (string format). + +**Policy dimension format:** +- `*` - matches any dimensions (global wildcard) +- `key=value` - matches single dimension +- `key=value&key2=value2` - matches multiple dimensions (AND logic) +- `key=*` - matches any value for that key + +**Matching rules:** +- All policy dimensions must be satisfied (AND logic) +- Policy can omit dimensions (partial match OK) +- OR logic is achieved via multiple policy lines + +The matcher is registered with Casbin via `enforcer.AddFunction("dimensionMatch", ...)` during initialization. + +### 6. Interceptor Flow (Platform-Owned) + +The authorization interceptor orchestrates the authorization flow as a ConnectRPC interceptor: + +``` +1. Extract subject from JWT claims (e.g., "role:hr-admin") + +2. Call service resolver to get ResolverContext with dimensions + └─ No resolver registered? Use empty dimensions (matches wildcard policies) + └─ Resolution failure? Return 403 PermissionDenied + +3. Enforce policy via Casbin (v2 model), per-resource: + For each resource in `ResolverContext.Resources` (or one empty resource if none): + - Build dimensions for that single resource + - Evaluate subjects against Casbin: + enforcer.Enforce(subject, rpc, resource_dimensions) + - Resource is allowed if any subject matches an allow policy and no deny policy matches + + Where: + - subject = roles extracted from JWT (e.g., "role:hr-admin") + - rpc = full method path (e.g., "/policy.attributes.AttributesService/UpdateAttribute") + - resource_dimensions = serialized key=value pairs for one resource + +4. Aggregate per-resource decisions by `ResolverContext.Mode`: + - `all_of` (default): + - Allow only if all resources are allowed + - Deny if any resource is denied (403) + - `filter` (list operations): + - Keep only resources that are allowed + - Continue with filtered resources (may be empty) + - Any resolver/enforcement evaluation error returns system error (500) + +5. Log authorization decision (mode, subjects, rpc, per-resource dimensions, allow/deny) + +6. Proceed to handler: + - `all_of`: only if aggregate decision is allow + - `filter`: with authorized resources attached to context for handler filtering +``` + +This flow describes **v2 mode**. In **v1 mode**, authorization uses the legacy path+action model and does not invoke resource resolvers. + +**Key behaviors:** +- No resolver registered = empty dimensions serialized as `*` (matches wildcard policies) +- Resolver error = authorization failure (403) +- Non-list multi-resource operations use ALL-OF request semantics (every resource must be authorized) +- Resource evaluations are fail-closed (deny/error on any resource denies request progress) +- Resource dimensions are not merged into a `map[string][]string`; each resource is evaluated independently +- Resource evaluations may run in parallel for performance, but must use bounded concurrency and preserve the same fail-closed ALL-OF result +- List operations use `filter` mode when resolver provides candidate resources; handlers must only return data within authorized resources +- No per-request fallback between versions. `v1` and `v2` are mutually exclusive runtime modes selected by config. +- Backwards compatibility is provided by selecting `policy.version=v1` during migration. +- v1 authorization is Casbin-only. + +### 7. Example Policies (Deployer-Owned) + +Policies use the v2 4-field format: `p, subject, rpc, dimensions, effect`. +The RPC field is the full gRPC method path (e.g., `/policy.attributes.AttributesService/UpdateAttribute`). +Dimensions use `&` as the AND delimiter (e.g., `namespace=hr&attribute=classification`). + +```csv +# ============================================================================ +# Format: p, subject, rpc, dimensions, effect +# - subject: role:rolename or username +# - rpc: gRPC method path or HTTP path (supports * wildcard) +# - dimensions: namespace=value&attribute=value (supports * wildcard) +# - effect: allow or deny +# ============================================================================ + +# ============================================================================ +# Global Admin - Full Access +# ============================================================================ +p, role:admin, *, *, allow + +# ============================================================================ +# Policy Service - Namespace-scoped roles +# ============================================================================ + +# Finance admin: full access to finance namespace (all policy service methods) +p, role:finance-admin, /policy.*, namespace=finance.com, allow + +# HR admin: full access to hr namespace +p, role:hr-admin, /policy.*, namespace=hr.io, allow + +# Cross-namespace read-only auditor (Get* and List* methods only) +p, role:auditor, /policy.*/Get*, *, allow +p, role:auditor, /policy.*/List*, *, allow + +# Standard role: read all namespaces via policy services +p, role:standard, /policy.attributes.AttributesService/Get*, *, allow +p, role:standard, /policy.attributes.AttributesService/List*, *, allow +p, role:standard, /policy.namespaces.NamespaceService/Get*, *, allow +p, role:standard, /policy.namespaces.NamespaceService/List*, *, allow + +# Contractors cannot deactivate anything +p, role:contractor, /policy.*/Deactivate*, *, deny + +# ============================================================================ +# KAS Service - KAS instance scoped roles +# ============================================================================ + +# KAS-1 admin: can manage KAS instance 1 +p, role:kas1-admin, /kas.AccessService/*, kas_id=kas-1, allow + +# KAS operator: read access to all KAS instances +p, role:kas-operator, /kas.AccessService/Get*, *, allow +p, role:kas-operator, /kas.AccessService/List*, *, allow + +# ============================================================================ +# Fine-grained policies (AND logic with &) +# ============================================================================ + +# Specific user: must match BOTH namespace AND attribute dimensions +p, user:alice@example.com, /policy.attributes.AttributesService/Update*, namespace=hr&attribute=classification, allow + +# Wildcard dimension value: allow any namespace +p, role:ns-reader, /policy.namespaces.NamespaceService/Get*, namespace=*, allow + +# ============================================================================ +# OR logic via multiple policies +# ============================================================================ + +# User can access hr namespace OR finance namespace (two separate policies) +p, role:hr-or-finance, /policy.attributes.AttributesService/*, namespace=hr, allow +p, role:hr-or-finance, /policy.attributes.AttributesService/*, namespace=finance, allow +``` + +#### Policy Format Reference + +| Format | Meaning | +|--------|---------| +| `*` | Match any value (global wildcard) | +| `/policy.*` | Match any RPC path starting with `/policy.` | +| `/policy.*/Get*` | Match any Get method in any policy service | +| `namespace=hr` | Match only when namespace dimension is "hr" | +| `namespace=*` | Match any namespace value (but dimension must be present) | +| `namespace=hr&attribute=x` | Match both dimensions (AND logic) | +| Multiple policies with same subject | OR logic across policies | + +--- + +## Maintainer Responsibilities + +### Platform Maintainer + +| Responsibility | Artifacts | +|----------------|-----------| +| Define ResolverContext contract | `service/internal/auth/authz/resolver.go` | +| Implement interceptor | `service/internal/auth/authn.go` (authorization flow) | +| Define authorizer interface | `service/internal/auth/authz/authorizer.go` | +| Maintain Casbin model | `service/internal/auth/authz/casbin/model.conf` | +| Documentation | Architecture docs, migration guides | + +### Service Maintainer + +| Responsibility | Artifacts | +|----------------|-----------| +| Implement resolver functions | Methods on service struct (e.g., `*AttributesService`) | +| Register resolvers at startup | In service `RegisterFunc` via `AuthzResolverRegistry.MustRegister()` | +| Unit test resolver logic | Service test file | + +### Deployer / Operator + +| Responsibility | Artifacts | +|----------------|-----------| +| Define Casbin policies | Config file or policy adapter | +| Map IdP roles to platform roles | Casbin `g` groupings | +| Monitor authorization denials | Logs, metrics | + +--- + +## Governance & Auditability + +### Runtime Audit Logging + +Authorization logging is normative for v2 and SHOULD be emitted once per request decision. + +Required fields: + +| Field | Type | Derivation | +|-------|------|------------| +| `timestamp` | string (RFC3339) | Emission time | +| `decision` | string | `allow`, `deny`, or `error` | +| `mode` | string | Authorization mode (`v1` or `v2`) | +| `evaluation_mode` | string | `all_of` or `filter` (v2; default `all_of`) | +| `method` | string | Full RPC/HTTP method path | +| `subjects` | array[string] | Evaluated subjects (`role:*` and/or username) | +| `resource_count` | integer | Number of resources evaluated | +| `resource_decisions` | array[object] | Per-resource decision objects: `{index, dimensions, decision}` | +| `trace_id` | string | Request trace/correlation id | + +Optional fields: + +| Field | Type | Derivation | +|-------|------|------------| +| `matched_subject` | string | Subject that produced an allow decision (if applicable) | +| `matched_policy` | string | Policy identifier/rule hint when available | +| `action` | string | Derived from RPC method prefix (`Get/List` => read, etc.) | +| `resource_type` | string | Service-defined resource label | +| `reason` | string | Human-readable decision reason | +| `error` | string | Error detail when `decision=error` | + +Dimensions in logs MUST use canonical `key=value&...` format (same as Casbin boundary serialization). + +```json +{ + "level": "info", + "msg": "authorization decision", + "mode": "v2", + "evaluation_mode": "all_of", + "method": "/policy.attributes.AttributesService/UpdateAttribute", + "subjects": ["role:hr-admin", "alice@example.com"], + "resource_count": 1, + "resource_decisions": [ + { + "index": 0, + "dimensions": "attribute=classification&namespace=hr", + "decision": "allow" + } + ], + "decision": "allow", + "matched_subject": "role:hr-admin", + "trace_id": "abc123", + "timestamp": "2026-03-05T12:00:00Z" +} +``` + +--- + +## Concerns & Mitigations + +| Concern | Mitigation | +|---------|------------| +| **Performance**: DB lookups in resolver add latency | Caching layer in resolver; batch lookups where possible | +| **Complexity**: Service maintainers must implement resolvers | Provide base resolver implementations; clear patterns | +| **Consistency**: Resolvers could diverge in behavior | Platform-owned contract; integration tests | +| **Migration**: Existing policies use path-based model | Phased rollout; maintain backwards compatibility | +| **Testing**: Hard to test authorization in isolation | Mock resolver interface; provide test utilities | + +--- + +## Decision Log + +### Decided + +| # | Decision | Rationale | Date | +|---|----------|-----------|------| +| D1 | Resolver follows IoC pattern (platform calls service) | Centralizes enforcement while allowing service-specific enrichment logic | 2024-12-30 | +| D2 | Dynamic dimensions via `map[string]string` | Different services have different resource hierarchies (policy uses namespace, KAS uses kas_id). Fixed fields would impose platform concepts on all services. | 2024-12-30 | +| D3 | Start with namespace-level granularity | Covers primary use case; instance-level can be added later | 2024-12-30 | +| D4 | Use canonical string serialization at the Casbin boundary | Resolvers keep dynamic `map[string]string` dimensions, but authorizer serializes each resource to canonical `key=value&...` before `Enforce`. This keeps the service contract flexible while matching Casbin CSV/model expectations. | 2025-01-02 | +| D5 | Use `&` as dimension AND delimiter in policies | Semantically correct (& means AND), visually distinct, enables future extensibility for `\|` OR logic within single policy line | 2025-01-02 | +| D6 | Resolver registration per-service namespace | Service maintainers register resolvers for each RPC in their service; `ScopedAuthzResolverRegistry` ensures services can only register for their own methods (validated against `ServiceDesc`) | 2025-01-02 | +| D7 | Empty resolver response treated as no dimensions | If no resolver is registered or resolver returns empty dimensions, Casbin evaluates with wildcard dimensions (`*`). Policies expecting specific dimensions (non-wildcard) will deny; wildcard policies will allow. | 2025-01-02 | +| D8 | Multiple resources supported in single AuthzContext | `AuthzResolverContext.Resources` is a slice of `*AuthzResolverResource`, supporting operations like "move from A to B" that require authorization on multiple resources | 2025-01-02 | +| D9 | Version selection is deployment-time, not per-request fallback | `v1` and `v2` are mutually exclusive runtime modes selected by `policy.version`. Backwards compatibility comes from running v1 explicitly during migration. v1 remains Casbin-only. | 2025-01-02 | +| D10 | Multi-resource authorization uses per-resource evaluation with mode-based aggregation | Each resource is enforced independently (preserving resource boundaries). `all_of` mode requires all resources allowed; `filter` mode produces an allowed subset for list responses. Any evaluation error returns 500 (fail-closed). | 2025-01-02 | +| D11 | List authorization uses explicit filter evaluation mode | `ResolverContext.Mode=filter` allows interceptor enforcement to produce an authorized resource subset for handlers. This prevents list-result data leakage while preserving centralized policy enforcement. | 2025-01-02 | + +### Open Questions + +| # | Question | Options | Leaning | Notes | +|---|----------|---------|---------|-------| +| Q1 | How to test resolver implementations? | A) Provide mock DB client
B) Provide test harness
C) Integration tests only | TBD | DX concern | +| Q2 | Caching strategy for resolved namespaces? | A) Resolver owns caching
B) Platform provides cache to resolver
C) No caching initially | TBD | Platform has `CacheManager` available. Also consider DB client caching since successful authz will repeat the same query in the handler. | + +### Future Considerations + +| Topic | Notes | +|-------|-------| +| Proto annotations for schema definition | Could define annotations in proto files to enable governance tooling and documentation generation. Deferred. | +| Policy UI integration | Future work to provide UI for policy management | +| Governance tooling | Future work for permission matrix generation | + +### Rejected Alternatives + +| Alternative | Reason for Rejection | +|-------------|---------------------| +| Service-side explicit authz calls (no interceptor) | No centralized governance; easy to forget; inconsistent | +| Pure ABAC with CEL expressions | Too complex for v1; can revisit if needed | +| External PDP (OPA, Cedar) | Adds operational complexity; Casbin sufficient for v1 | +| Fixed-field AuthzContext (namespace, resource_id) | Different services have different resource hierarchies. "Namespace" is a policy concept but not KAS or authz service concept. Fixed fields impose platform-centric thinking on all services. | +| Positional Casbin model with fixed dimensions | Doesn't accommodate service-specific dimensions; would require model changes for each new dimension type | +| Semicolon (`;`) as dimension delimiter | Neutral semantically but less visually distinct; `&` implies AND logic correctly | +| Pipe (`\|`) as dimension delimiter | Semantically implies OR, which would be confusing since dimensions within a policy are AND conditions | +| Full request-side serialization | Unnecessary complexity; passing map directly to custom matcher is simpler | + +--- + +## Migration Path + +### Phase 1: Infrastructure (Platform) + +1. Implement AuthzContext and resolver interface +2. Extend interceptor to support resource authorization +3. Update Casbin model to support new dimensions +4. Document and validate mode selection semantics (`policy.version=v1` or `policy.version=v2`) with no mixed-mode fallback + +### Phase 2: Pilot Service (Platform + Service) + +1. Implement AttributeResolver for `policy.attributes` service +2. Write integration tests +3. Deploy to staging with permissive policies +4. Validate audit logging + +### Phase 3: Rollout (Service Teams) + +1. Document patterns and provide examples +2. Services implement resolvers as needed + +### Phase 4: Enforcement (Deployers) + +1. Define namespace-scoped policies +2. Migrate from path-based to resource-based policies +3. Monitor for authorization failures +4. Iterate on policy granularity + +--- + +## Implementation Progress + +### Phase 1: Infrastructure (Platform) - IN PROGRESS + +**Completed:** + +- [x] Authorizer interface with pluggable backends (`service/internal/auth/authz/authorizer.go`) +- [x] ResolverRegistry and ResolverContext types (`service/internal/auth/authz/resolver.go`) +- [x] Casbin model v2 with `dimensionMatch` custom function (`authz/casbin/model.conf`) +- [x] CasbinAuthorizer supporting v1 (path-based) and v2 (RPC+dimensions) modes +- [x] Default v2 policy with role-based access (`authz/casbin/policy.csv`) +- [x] Config version flag (defaults to "v1" for backwards compatibility) +- [x] Authentication integration with Authorizer and ResolverRegistry +- [x] Unit tests for dimension matching and policy evaluation +- [x] Attributes service resolvers (CreateAttribute, GetAttribute, GetAttributeValuesByFqns, ListAttributes, UpdateAttribute, DeactivateAttribute) + +**Remaining:** + +- [ ] Additional service resolvers (KAS, namespaces, etc.) +- [ ] Integration tests for resolver + authorizer flow + +### Key Files + +| File | Purpose | +|------|---------| +| `internal/auth/authz/authorizer.go` | Authorizer interface and factory | +| `internal/auth/authz/resolver.go` | ResolverRegistry and ResolverContext types | +| `internal/auth/authz/policy.go` | PolicyConfig type | +| `internal/auth/authz/casbin/casbin.go` | CasbinAuthorizer with v1/v2 support | +| `internal/auth/authz/casbin/model.conf` | Casbin model for v2 authorization | +| `internal/auth/authz/casbin/policy.csv` | Default v2 policy (embedded) | +| `policy/attributes/attributes.go` | Example resolver implementations (lines 544-688) | + +## Open Work + +- [ ] Finalize answers to open questions (Q1-Q2) +- [ ] Design caching strategy for resolver lookups +- [ ] Define integration test patterns +- [ ] Performance benchmarks with resolver overhead + +--- + +## References + +- [Casbin Documentation](https://casbin.org/docs/overview) +- [XACML Architecture](https://en.wikipedia.org/wiki/XACML) (PDP/PEP/PIP pattern) +- [Google Zanzibar](https://research.google/pubs/pub48190/) (relationship-based access control) +- Current implementation: `service/internal/auth/` + +--- + +## TODO List + +- [ ] Define bounded concurrency defaults for multi-resource evaluation (and whether configurable) +- [ ] Define cancellation behavior for parallel evaluation (early-stop on deny/error) +- [ ] Define audit log schema for per-resource decisions plus aggregate request decision +- [ ] Add conformance tests for canonical dimension serialization (`key` ordering, empty => `*`, invalid key rejection) +- [ ] Implement `ResolverContext.Mode` (`all_of`/`filter`) in authz runtime path +- [ ] Implement handler-context propagation of authorized resource subset for `filter` mode +- [ ] Add shared helper for services to apply authorized-resource filtering in List handlers +- [ ] Add conformance tests for list filter mode (scoped allow, scoped deny, unscoped filtered results, empty result set) +- [ ] Finalize resolver testing strategy and test harness guidance (Q2) +- [ ] Finalize resolver caching ownership/behavior (Q2) +- [ ] Add conformance tests for multi-resource all-of semantics (allow, deny, system error) diff --git a/adr/decisions/2026-03-24-otdfctl-migration.md b/adr/decisions/2026-03-24-otdfctl-migration.md new file mode 100644 index 0000000000..a8ca2015c7 --- /dev/null +++ b/adr/decisions/2026-03-24-otdfctl-migration.md @@ -0,0 +1,26 @@ +## Summary + +We are planning to migrate the `otdfctl` CLI from this standalone repository into the [`opentdf/platform`](https://github.com/opentdf/platform) monorepo. After migration, this repository will be archived and marked read-only. + +## Why + +- otdfctl already depends heavily on platform (SDK, protocol, libs) and uses platform's reusable CI workflows +- Both repos run each other's e2e tests in CI — consolidating eliminates cross-repo coordination overhead +- The platform monorepo already supports per-component releases (service, sdk, libs), so otdfctl can maintain independent release cadence + +## What changes for users + +- **Go module path** will change from `github.com/opentdf/otdfctl` to `github.com/opentdf/platform/otdfctl` +- **Release tags** will change from `v0.X.Y` to `otdfctl/v0.X.Y` +- **This repository** will be archived (read-only) — all existing releases and tags will remain accessible +- A notice will be added to this README pointing to the new location + +## What stays the same + +- The `otdfctl` binary name and CLI interface +- Separate release cadence (not coupled to platform service releases) +- All existing CI tests continue to run + +## Feedback + +If you have concerns or questions about this migration, please comment on this issue. diff --git a/adr/decisions/2026-03-30-namespaced-subject-mappings-decisioning.md b/adr/decisions/2026-03-30-namespaced-subject-mappings-decisioning.md new file mode 100644 index 0000000000..3391a2faf1 --- /dev/null +++ b/adr/decisions/2026-03-30-namespaced-subject-mappings-decisioning.md @@ -0,0 +1,94 @@ +--- +status: 'proposed' +date: '2026-03-30' +tags: + - policy + - authorization + - namespaced-policy +driver: '@elizabethhealy' +--- + +# Namespaced Subject Mapping Decisioning in PDP + +## Context and Problem Statement + +Policy objects are moving toward strict namespace ownership, but access decisioning still treats actions as unscoped names in several evaluation paths. Subject mappings are also transitioning from legacy unnamespaced records to namespace-owned records. This creates ambiguity when a request action name exists in multiple namespaces and when a single resource includes attributes from multiple namespaces. + +We need a decisioning model that is namespace-correct, fail-closed, and compatible with staged rollout using the `EnforceNamespacedEntitlements` feature flag. + +## Decision Drivers + +- Preserve existing multi-namespace resource semantics (`AND` behavior) while adding namespace correctness. +- Prevent cross-namespace action matches when namespaced policy mode is enabled. +- Keep rollout safe via feature-flagged behavior split. +- Avoid startup coupling by keeping standard-action checks lazy at evaluation time. + +## Decision Outcome + +Chosen option: **Resolve request action identity within each evaluation namespace context**. + +For `GetDecisionRequest`/`GetDecisionMultiResourceRequest`, request validation still requires `action.name` (current proto contract). During evaluation, matching is always applied per namespace context (derived from the rule/value being evaluated), not globally. + +Request-action matching precedence is explicit (given the request action object): + +1. `action.id` (exact identity, when present) +2. `action.name + action.namespace` (scoped identity, when namespace is present) +3. `action.name` only (contextual identity) + +When identity is explicit (`id` or `name+namespace`), decisioning does not fall back to looser name-only matching. It fails closed only if that explicit identity is unresolved or mismatched for the evaluated namespace context. + +Feature-flag mode split: + +- `EnforceNamespacedEntitlements=false`: preserve existing legacy behavior (no new namespace filtering semantics introduced by this change). +- `EnforceNamespacedEntitlements=true`: enforce namespaced subject mapping evaluation (unnamespaced SMs are ignored) and require action namespace equality for each evaluated namespace. + +Direct entitlements in strict mode: + +- Direct entitlements are still modeled as action names per attribute-value FQN. +- During PDP evaluation, each direct-entitlement action is hydrated with the namespace of its attributed value context. +- This makes direct entitlements participate in the same namespace-aware action matching rules as subject-mapping-derived entitlements. +- Direct-entitlement actions are merged with subject-mapping actions per value FQN (not replacing them). + +Subject mapping namespace enforcement (strict mode): + +- Subject mapping namespace must match the namespace of the referenced attribute value. +- Subject mapping namespace must match the namespace of the referenced subject condition set. +- Name-based action matching is evaluated in the same namespace context as the SM/value under evaluation. + +For multi-namespace resources, existing `AND` semantics remain unchanged: all required namespace-scoped checks must pass, and missing action support in any required namespace denies access. + +## Consequences + +- 🟩 **Good**, because action evaluation becomes deterministic and namespace-safe. +- 🟩 **Good**, because feature-flagged split allows staged migration without mixed-mode ambiguity. +- 🟩 **Good**, because fail-closed behavior prevents accidental entitlement via cross-namespace action reuse. +- 🟥 **Bad**, because policy admins must ensure required actions exist in each relevant namespace. +- 🟥 **Bad**, because debugging becomes harder without explicit namespace-aware logs. + +## Validation + +Validation is done through PDP and decisioning tests covering: + +- mode split (`EnforceNamespacedEntitlements=false` vs `true`) for subject mapping inclusion, +- strict-mode subject mapping namespace scoping (unnamespaced SMs skipped), +- namespace-aware action matching in rule evaluation paths, +- multi-namespace resource behavior where one missing namespace action causes deny, +- regression checks to confirm existing `AND` behavior is preserved. + +## Implementation Notes + +- Thread `EnforceNamespacedEntitlements` into PDP runtime configuration. +- Filter subject mappings at PDP construction by mode (namespaced vs unnamespaced). +- Enforce subject mapping namespace consistency during create/update operations. +- Centralize action matching in a namespace-aware helper used by all rule/action checks. +- Derive required namespace per evaluated value/rule context. +- Keep standard/custom action existence checks lazy at evaluation time. +- Add debug logs including requested action, required namespace, candidate namespace, and rule/value context. + +## Rollout + +1. Land logic behind `EnforceNamespacedEntitlements`. +2. Keep default mode as legacy (`false`) until policy data migration is complete. +3. Validate namespaced policy data readiness. +4. Flip `EnforceNamespacedEntitlements=true` and monitor mismatch/deny behavior. +5. Remove legacy branch once namespaced mode is stable. diff --git a/adr/decisions/2026-04-01-database-migration-backup-first-rollback.md b/adr/decisions/2026-04-01-database-migration-backup-first-rollback.md new file mode 100644 index 0000000000..29d89f97e5 --- /dev/null +++ b/adr/decisions/2026-04-01-database-migration-backup-first-rollback.md @@ -0,0 +1,84 @@ +--- +status: 'proposed' +date: '2026-04-01' +tags: + - database + - migrations + - operations + - rollback +driver: '@elizabethhealy' +consulted: + - '@jakedoublev' + - '@c-r33d' +--- +# Database Migration Rollback Posture: Backup-First Recovery + +## Context and Problem Statement + +OpenTDF ships schema and data migrations that evolve policy and platform behavior over time. Some migrations are structurally reversible; others are intentionally lossy or operationally risky to reverse in-place, especially when data has been rewritten, deduplicated, or normalized. + +We need a clear, operator-safe stance: what should customers rely on as the primary rollback mechanism when a migration fails, has unexpected impact, or a change must be reverted. + +## Decision Drivers + +* Customer safety in production environments +* Predictable rollback outcomes under incident pressure +* Avoidance of data corruption or accidental data loss from complex down-migrations +* Clear contract between product behavior and operator responsibilities +* Alignment with common enterprise database change-management practices + +## Considered Options + +* Treat migration `Down` scripts as the primary rollback mechanism in all cases +* Define backup/restore as the primary rollback mechanism; keep `Down` as best-effort +* Disallow `Down` scripts entirely and require only forward fixes + +## Decision Outcome + +Chosen option: "Define backup/restore as the primary rollback mechanism; keep `Down` as best-effort". + +### Consequences + +* 🟩 **Good**, because operators have a deterministic and well-understood recovery path (restore known-good backup/snapshot). +* 🟩 **Good**, because this reduces dependence on complex, high-risk data-rewrite rollback logic. +* 🟩 **Good**, because this aligns with how most production DB operations are run in practice. +* 🟨 **Neutral**, because migration `Down` scripts remain useful for development/test and selective operational cases. +* 🟥 **Bad**, because backup/restore may increase recovery time vs. a simple schema-only down migration. +* 🟥 **Bad**, because operators must maintain and test backup/restore procedures. + +## Policy + +Before running OpenTDF migrations in any environment that matters, operators should: + +1. Create a verified database backup/snapshot. +2. Ensure restore procedures are tested and available. +3. Prefer restore from backup if migration outcomes are unacceptable. + +`Down` migrations are provided as operational aids and may be best-effort. They are not a guarantee of lossless restoration to prior logical state in all scenarios. + +## Application to Namespaced Policy Migrations + +For namespaced policy/action migrations, OpenTDF may provide downgrade paths, but these can involve data remapping or identity collapse semantics. If an operator wants to revert namespaced policy adoption with high confidence, the recommended path is restoring the pre-migration backup. + +## Validation + +* Release notes and migration docs must state backup-first guidance. +* Operational runbooks should include pre-migration backup and restore drills. +* PR reviews for migrations should evaluate whether `Down` is safe, lossy, or intentionally no-op. + +## Industry Alignment (Examples) + +This posture is consistent with common real-world practice: + +* **PostgreSQL operations guidance** commonly emphasizes taking backups before major schema/data changes. +* **Managed databases (e.g., AWS RDS/Aurora)** center rollback around snapshots and point-in-time restore. +* **Flyway/Liquibase usage in production** often treats rollback scripts as helpful but not a substitute for tested backup/restore, especially for complex data migrations. +* **Large SaaS/platform upgrade guidance (e.g., GitLab self-managed upgrades)** strongly recommends verified backups before upgrades/migrations. + +## More Information + +This ADR defines the operational contract: + +* We will continue to implement safe `Down` behavior where feasible. +* We will explicitly document lossy/no-op downgrades when needed. +* The primary production safety mechanism remains backup + restore. diff --git a/buf.gen.grpc.docs.yaml b/buf.gen.grpc.docs.yaml index 89d32cc7e4..5bc5d4e02b 100644 --- a/buf.gen.grpc.docs.yaml +++ b/buf.gen.grpc.docs.yaml @@ -4,10 +4,6 @@ managed: disable: - file_option: go_package module: buf.build/bufbuild/protovalidate - - file_option: go_package - module: buf.build/googleapis/googleapis - - file_option: go_package - module: buf.build/grpc-ecosystem/grpc-gateway override: - file_option: go_package_prefix value: github.com/opentdf/platform/protocol/go diff --git a/buf.gen.openapi.docs.yaml b/buf.gen.openapi.docs.yaml index 0298584b91..5a22d90924 100644 --- a/buf.gen.openapi.docs.yaml +++ b/buf.gen.openapi.docs.yaml @@ -4,10 +4,6 @@ managed: disable: - file_option: go_package module: buf.build/bufbuild/protovalidate - - file_option: go_package - module: buf.build/googleapis/googleapis - - file_option: go_package - module: buf.build/grpc-ecosystem/grpc-gateway override: - file_option: go_package_prefix value: github.com/opentdf/platform/protocol/go diff --git a/buf.gen.yaml b/buf.gen.yaml index 13e766c4e8..b5a437dc58 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -4,24 +4,17 @@ managed: disable: - file_option: go_package module: buf.build/bufbuild/protovalidate - - file_option: go_package - module: buf.build/googleapis/googleapis - - file_option: go_package - module: buf.build/grpc-ecosystem/grpc-gateway override: - file_option: go_package_prefix value: github.com/opentdf/platform/protocol/go plugins: - - remote: buf.build/grpc-ecosystem/gateway:v2.19.1 - out: protocol/go - opt: paths=source_relative - remote: buf.build/protocolbuffers/go:v1.33.0 out: protocol/go opt: paths=source_relative - remote: buf.build/grpc/go:v1.3.0 out: protocol/go opt: paths=source_relative - - remote: buf.build/connectrpc/go:v1.17.0 + - remote: buf.build/connectrpc/go:v1.19.1 out: protocol/go opt: - paths=source_relative diff --git a/buf.lock b/buf.lock index 22c5f3b2a3..709ae02396 100644 --- a/buf.lock +++ b/buf.lock @@ -2,11 +2,5 @@ version: v2 deps: - name: buf.build/bufbuild/protovalidate - commit: 6c6e0d3c608e4549802254a2eee81bc8 - digest: b5:a7ca081f38656fc0f5aaa685cc111d3342876723851b47ca6b80cbb810cbb2380f8c444115c495ada58fa1f85eff44e68dc54a445761c195acdb5e8d9af675b6 - - name: buf.build/googleapis/googleapis - commit: 61b203b9a9164be9a834f58c37be6f62 - digest: b5:7811a98b35bd2e4ae5c3ac73c8b3d9ae429f3a790da15de188dc98fc2b77d6bb10e45711f14903af9553fa9821dff256054f2e4b7795789265bc476bec2f088c - - name: buf.build/grpc-ecosystem/grpc-gateway - commit: 4c5ba75caaf84e928b7137ae5c18c26a - digest: b5:c113e62fb3b29289af785866cae062b55ec8ae19ab3f08f3004098928fbca657730a06810b2012951294326b95669547194fa84476b9e9b688d4f8bf77a0691d + commit: 50325440f8f24053b047484a6bf60b76 + digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31 diff --git a/buf.yaml b/buf.yaml index 5031f81191..1ceb5b2577 100644 --- a/buf.yaml +++ b/buf.yaml @@ -5,8 +5,6 @@ modules: - service/logger/audit deps: - buf.build/bufbuild/protovalidate - - buf.build/googleapis/googleapis - - buf.build/grpc-ecosystem/grpc-gateway lint: use: - STANDARD @@ -16,8 +14,6 @@ lint: - PACKAGE_VERSION_SUFFIX ignore_only: PACKAGE_VERSION_SUFFIX: - - service/google/api/annotations.proto - - service/google/api/http.proto - service/google/protobuf/wrappers.proto breaking: use: diff --git a/docker-compose.yaml b/docker-compose.yaml index 54ebd8f793..a8044b7d1e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,6 +4,7 @@ networks: services: keycloak: volumes: + - keycloak_data:/opt/keycloak/data - ${KEYS_DIR:-./keys}/localhost.crt:/etc/x509/tls/localhost.crt - ${KEYS_DIR:-./keys}/localhost.key:/etc/x509/tls/localhost.key - ${KEYS_DIR:-./keys}/ca.jks:/truststore/truststore.jks @@ -19,12 +20,6 @@ services: environment: KC_PROXY: edge KC_HTTP_RELATIVE_PATH: /auth - KC_DB_VENDOR: postgres - KC_DB_URL_HOST: keycloakdb - KC_DB_URL_PORT: 5432 - KC_DB_URL_DATABASE: keycloak - KC_DB_USERNAME: keycloak - KC_DB_PASSWORD: changeme KC_HOSTNAME_STRICT: "false" KC_HOSTNAME_STRICT_BACKCHANNEL: "false" KC_HOSTNAME_STRICT_HTTPS: "false" @@ -205,9 +200,10 @@ services: condition: service_healthy volumes: + keycloak_data: ers_postgres_data: name: ers_test_postgres_data ers_ldap_data: name: ers_test_ldap_data ers_ldap_config: - name: ers_test_ldap_config \ No newline at end of file + name: ers_test_ldap_config diff --git a/docs/Configuring.md b/docs/Configuring.md index c8a9611416..c99fb0e24c 100644 --- a/docs/Configuring.md +++ b/docs/Configuring.md @@ -13,6 +13,7 @@ The platform leverages [viper](https://github.com/spf13/viper) to help load conf - [CORS Configuration](#cors-configuration) - [Additive Configuration](#additive-configuration) - [Programmatic Configuration](#programmatic-configuration) + - [Custom Interceptors](#custom-interceptors) - [Crypto Provider](#crypto-provider) - [Tracing Configuration](#tracing-configuration) - [Database Configuration](#database-configuration) @@ -231,6 +232,34 @@ err := server.Start( All layers are additive. Deduplication is handled automatically (case-insensitive for headers per RFC 7230, case-sensitive for methods per RFC 7231). +### Custom Interceptors + +Applications that embed the OpenTDF platform can inject custom [Connect interceptors](https://connectrpc.com/docs/go/interceptors/) into the server at startup. These interceptors run on every RPC after the built-in auth, validation, and audit interceptors. + +Two option functions are available: + +| Option | Description | +| --- | --- | +| `WithConnectInterceptors(interceptors ...connect.Interceptor)` | Appends server-side interceptors to all external Connect RPCs. | +| `WithIPCInterceptors(interceptors ...connect.Interceptor)` | Appends server-side interceptors to the in-process IPC server used by the SDK in `all`/`core` mode. | + +Both options are variadic and additive: calling them multiple times accumulates interceptors in order. + +```go +import ( + "connectrpc.com/connect" + "github.com/opentdf/platform/service/pkg/server" +) + +err := server.Start( + server.WithConfigFile("opentdf.yaml"), + // Add a logging interceptor to all external RPCs + server.WithConnectInterceptors(loggingInterceptor), + // Add a metrics interceptor to in-process IPC calls + server.WithIPCInterceptors(metricsInterceptor), +) +``` + ### Crypto Provider To configure the Key Access Server, @@ -511,6 +540,7 @@ Root level key `policy` | ---------------------------- | ------------------------------------------------------ | ------- | -------------------------------------------------- | | `list_request_limit_default` | Policy List request limit default when not provided | 1000 | OPENTDF_SERVICES_POLICY_LIST_REQUEST_LIMIT_DEFAULT | | `list_request_limit_max` | Policy List request limit maximum enforced by services | 2500 | OPENTDF_SERVICES_POLICY_LIST_REQUEST_LIMIT_MAX | +| `namespaced_policy` | When enabled, new actions, subject mappings, subject condition sets, and registered resources require a namespace. When disabled (default), namespace fields are accepted but not enforced — objects may be created without a namespace (legacy behavior). Non-namespaced versions are deprecated and this flag will become the default in a future version. | `false` | OPENTDF_SERVICES_POLICY_NAMESPACED_POLICY | Example: @@ -519,6 +549,7 @@ services: policy: list_request_limit_default: 1000 list_request_limit_max: 2500 + namespaced_policy: false ``` ### Casbin Endpoint Authorization diff --git a/docs/Consuming.md b/docs/Consuming.md index 77b554c297..5787afbe36 100644 --- a/docs/Consuming.md +++ b/docs/Consuming.md @@ -65,7 +65,7 @@ https://github.com/opentdf/platform/blob/main/service/go.mod#L3 You can now access platform services at http://localhost:8080/ , and Keycloak at http://localhost:8888/auth/ . ## Next steps -* Try out our CLI (`otdfctl`): https://github.com/opentdf/otdfctl +* Try out our CLI (`otdfctl`): https://github.com/opentdf/platform/otdfctl ```sh otdfctl auth client-credentials --host http://localhost:8080 --client-id opentdf --client-secret secret ``` diff --git a/docs/Contributing.md b/docs/Contributing.md index 45dbc59519..a3e0f1447e 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -86,6 +86,23 @@ go install github.com/sudorandom/protoc-gen-connect-openapi@latest Make sure your Go bin directory (usually `$HOME/go/bin`) is in your `PATH`. +## Cross-Platform Integration Testing (xtests) + +The [opentdf/tests](https://github.com/opentdf/tests) repo contains cross-SDK compatibility tests that validate platform changes against Go, Java, and JavaScript SDKs. Xtests run automatically on every platform PR via the `platform-xtest` CI job. + +### Running xtests against a platform feature branch + +To manually trigger xtests against your platform changes before merging: + +1. Go to the **Actions** tab in [opentdf/tests](https://github.com/opentdf/tests/actions) +2. Find the **xtest** workflow +3. Click the **"Run workflow"** dropdown on the right +4. Set **"Use workflow from"** to the xtest branch to run (`main`, or a companion xtest branch if you have test changes) +5. Set **"platform ref branch"** to the HEAD commit SHA of your platform feature branch +6. Click **Run workflow** + +This is especially useful when your platform changes affect SDK error handling, KAS behavior, or the rewrap flow — areas where cross-SDK compatibility matters. + ## Advice for Code Contributors * Make sure to run our linters with `make lint` diff --git a/docs/architecture/authz_resolver_reference.md b/docs/architecture/authz_resolver_reference.md new file mode 100644 index 0000000000..048b697d37 --- /dev/null +++ b/docs/architecture/authz_resolver_reference.md @@ -0,0 +1,172 @@ +# Authorization Resolver Registry - Component Reference + +**Purpose**: Track authorization resolver components and service registrations for drift detection. + +**Last Updated**: 2026-01-09 + +--- + +## Component Inventory + +### Core Types + +| Type | Location | Purpose | +|------|----------|---------| +| `ResolverResource` | `service/internal/auth/authz/resolver.go:15` | `map[string]string` - Single resource's authorization dimensions (key=dimension name, value=dimension value) | +| `ResolverContext` | `service/internal/auth/authz/resolver.go:20-22` | Container for multiple resources; supports multi-resource operations (e.g., move from A to B) | +| `ResolverFunc` | `service/internal/auth/authz/resolver.go:40` | Function signature: `func(ctx context.Context, req connect.AnyRequest) (ResolverContext, error)` | +| `ResolverRegistry` | `service/internal/auth/authz/resolver.go:45-48` | Global thread-safe registry; keyed by full method path | +| `ScopedResolverRegistry` | `service/internal/auth/authz/resolver.go:89-92` | Namespace-scoped view; validates method ownership against ServiceDesc | + +### Factory Functions + +| Function | Location | Returns | +|----------|----------|---------| +| `NewResolverRegistry()` | `service/internal/auth/authz/resolver.go:51-55` | `*ResolverRegistry` | +| `NewResolverContext()` | `service/internal/auth/authz/resolver.go:127-129` | `ResolverContext` (empty) | + +### Registry Methods + +| Method | Receiver | Purpose | +|--------|----------|---------| +| `register(fullMethodPath, resolver)` | `*ResolverRegistry` | Internal - adds resolver for full method path | +| `Get(method)` | `*ResolverRegistry` | Returns resolver and existence flag for method | +| `ScopedForService(serviceDesc)` | `*ResolverRegistry` | Creates scoped registry for service; panics if serviceDesc is nil | +| `Register(methodName, resolver)` | `*ScopedResolverRegistry` | Validates method exists in ServiceDesc, builds full path, delegates to parent | +| `MustRegister(methodName, resolver)` | `*ScopedResolverRegistry` | Like Register but panics on error | +| `ServiceName()` | `*ScopedResolverRegistry` | Returns scoped service name | + +### Context Methods + +| Method | Receiver | Purpose | +|--------|----------|---------| +| `NewResource()` | `*ResolverContext` | Appends new resource to Resources slice, returns pointer | +| `AddDimension(dimension, value)` | `*ResolverResource` | Sets dimension key-value pair | + +--- + +## Platform Integration Points + +### Registry Creation + +| Location | Line | Action | +|----------|------|--------| +| `service/pkg/server/start.go` | 276 | Creates global `ResolverRegistry` via `authz.NewResolverRegistry()` | +| `service/pkg/server/start.go` | 287 | Passes registry to `startServicesParams` | + +### Scoped Registry Creation + +| Location | Line | Action | +|----------|------|--------| +| `service/pkg/server/services.go` | 213-215 | Creates `ScopedResolverRegistry` per service via `ScopedForService()` | +| `service/pkg/server/services.go` | 230 | Injects scoped registry into `RegistrationParams.AuthzResolverRegistry` | + +### RegistrationParams Field + +| Location | Line | Field | +|----------|------|-------| +| `service/pkg/serviceregistry/serviceregistry.go` | 69-83 | `AuthzResolverRegistry *authz.ScopedResolverRegistry` | + +--- + +## Service Registrations + +### Attributes Service + +**File**: `service/policy/attributes/attributes.go` + +**Registration Location**: Lines 74-81 in `RegisterFunc` + +| Method | Resolver Function | Dimensions Resolved | +|--------|-------------------|---------------------| +| `CreateAttribute` | `createAttributeAuthzResolver` | `namespace` (via DB lookup from namespace_id) | +| `GetAttribute` | `getAttributeAuthzResolver` | `namespace`, `attribute` (via DB lookup) | +| `GetAttributeValuesByFqns` | `getAttributeValuesByFqnsAuthzResolver` | `namespace` (parsed from FQN URLs, multiple resources) | +| `ListAttributes` | `listAttributesAuthzResolver` | `namespace` (optional, from request filter) | +| `UpdateAttribute` | `updateAttributeAuthzResolver` | `namespace`, `attribute` (via DB lookup) | +| `DeactivateAttribute` | `deactivateAttributeAuthzResolver` | `namespace`, `attribute` (via DB lookup) | + +**Resolver Functions Location**: Lines 554-688 + +### Services Without Registrations + +The following services do not currently register authz resolvers: + +| Service | Namespace | File | +|---------|-----------|------| +| Namespaces | policy | `service/policy/namespaces/namespaces.go` | +| Subject Mappings | policy | `service/policy/subjectmapping/subject_mapping.go` | +| Resource Mappings | policy | `service/policy/resourcemapping/resource_mapping.go` | +| KAS Registry | policy | `service/policy/kasregistry/kas_registry.go` | +| Public Key | policy | `service/policy/publickey/public_key.go` | +| Unsafe | policy | `service/policy/unsafe/unsafe.go` | +| KAS | kas | `service/kas/kas.go` | +| Authorization | authorization | `service/authorization/authorization.go` | +| Authorization V2 | authorization | `service/authorization/v2/authorization.go` | +| Entity Resolution | entityresolution | `service/entityresolution/*.go` | +| Health | health | `service/health/health.go` | +| WellKnown | wellknown | `service/wellknownconfiguration/wellknown.go` | + +--- + +## Dimension Schema + +### Known Dimensions + +| Dimension | Used By | Description | +|-----------|---------|-------------| +| `namespace` | Attributes | Policy namespace name (resolved from namespace_id, attribute lookup, or FQN parsing) | +| `attribute` | Attributes | Attribute definition name | + +### Expected Future Dimensions + +| Dimension | Service | Description | +|-----------|---------|-------------| +| `kas_id` | KAS | KAS instance identifier | +| `value` | Attributes | Attribute value name | + +--- + +## Validation Rules + +### Method Validation + +`ScopedResolverRegistry.Register()` validates: +1. Method name exists in `ServiceDesc.Methods` +2. Builds full path as `//` + +### Registration Patterns + +Services MUST: +1. Check `srp.AuthzResolverRegistry != nil` before registering +2. Use `MustRegister()` during initialization (panics are acceptable at startup) +3. Implement resolver functions as service methods (access to DB client) + +Services SHOULD: +1. Resolve dimensions to human-readable names (not UUIDs) +2. Return errors for failed DB lookups (results in 403) +3. Support optional dimensions by omitting from context + +--- + +## Drift Detection Checklist + +### Review + +- [ ] All proto `ResourceAuthz` annotations have matching resolver registrations +- [ ] All registered resolvers match methods in ServiceDesc +- [ ] Dimension keys match proto annotation schemas +- [ ] No orphaned resolver functions (registered but method removed) + +### When Adding New Methods + +1. Add proto annotation with `ResourceAuthz` +2. Implement resolver function +3. Register in `RegisterFunc` +4. Update this document's Service Registrations section + +### When Removing Methods + +1. Remove resolver registration +2. Remove resolver function +3. Update this document's Service Registrations section diff --git a/docs/architecture/platform_feature_development.md b/docs/architecture/platform_feature_development.md new file mode 100644 index 0000000000..c3550f4b4b --- /dev/null +++ b/docs/architecture/platform_feature_development.md @@ -0,0 +1,395 @@ +# Platform Feature Development Guide + +This document describes the architectural patterns for developing new platform-level features in the OpenTDF platform. It explains the Inversion of Control (IoC) pattern used for platform/service separation and provides guidance for implementing new capabilities. + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [The RegistrationParams Pattern](#the-registrationparams-pattern) +3. [Scoped Registries Pattern](#scoped-registries-pattern) +4. [Adding New Platform Capabilities](#adding-new-platform-capabilities) +5. [Implemented Platform Capabilities](#implemented-platform-capabilities) +6. [Checklist for New Features](#checklist-for-new-features) +7. [Anti-Patterns to Avoid](#anti-patterns-to-avoid) +8. [Known Areas Needing Alignment](#known-areas-needing-alignment) + +--- + +## Architecture Overview + +The OpenTDF platform follows an **Inversion of Control (IoC)** architecture where: + +- **Platform** owns infrastructure, lifecycle management, and cross-cutting concerns +- **Services** own domain-specific business logic and implementations +- **RegistrationParams** is the injection point where platform provides capabilities to services +- **Scoped registries** prevent cross-service interference and enforce boundaries + +### Key Principles + +1. **Platform calls service code** - Services register handlers that the platform invokes +2. **Services receive dependencies** - Services don't reach out for platform internals +3. **Scoped access** - Each service receives only the capabilities it needs, scoped to its namespace +4. **Single source of truth** - Platform maintains global registries; services register into them + +### Component Relationships + +``` +Platform Layer (service/pkg/server/, service/internal/) + | + |-- Creates global registries and managers + |-- Initializes database clients per-namespace + |-- Creates scoped registries for each service + |-- Starts services via RegistrationParams + | + v +RegistrationParams (Injection Point) + | + |-- Config (scoped to service namespace) + |-- DBClient (scoped to service namespace) + |-- SDK (for IPC between services) + |-- Logger (scoped to service namespace) + |-- AuthzResolverRegistry (scoped to service's methods) + |-- NewCacheClient function + |-- RegisterReadinessCheck function + |-- WellKnownConfig function + | + v +Service Layer (service/policy/, service/kas/, service/authorization/, etc.) + | + |-- Implements domain logic + |-- Registers handlers via RegisterFunc + |-- Uses injected dependencies +``` + +--- + +## The RegistrationParams Pattern + +`RegistrationParams` is defined in `service/pkg/serviceregistry/serviceregistry.go` and serves as the **sole injection point** for platform capabilities into services. + +### Injection Patterns + +The platform uses three distinct patterns for providing capabilities to services: + +| Pattern | Service Action | When to Use | +|---------|----------------|-------------| +| **Declarative Flag** | Declare need → receive and *consume* resource | Single resource, fixed config (DB) | +| **Factory Function** | Receive function → *create* resource(s) | Multiple instances, varied config (cache) | +| **Scoped Registry** | Receive registry → *contribute* registrations | Register handlers into platform systems (authz) | + +The key distinction: +- **Declarative/Factory**: Service is a *consumer* of resources +- **Scoped Registry**: Service is a *contributor* to a platform-owned system (platform uses the registrations at runtime) + +#### Declarative Flag Pattern + +**Location**: `service/pkg/serviceregistry/serviceregistry.go:92-100` + +Services declare needs in `ServiceOptions.DB`: +``` +DB: serviceregistry.DBRegister{Required: true, Migrations: Migrations} +``` + +Platform sees the flag, creates the resource, and provides it via `RegistrationParams.DBClient`. + +| Flag Field | Type | Purpose | +|------------|------|---------| +| `DB.Required` | `bool` | Platform creates DB client if true | +| `DB.Migrations` | `*embed.FS` | Goose migrations to run | + +**Advantages**: Simpler service code, platform-managed lifecycle, explicit dependencies at registration time. + +**Use when**: Service needs exactly one instance with configuration known at startup. + +#### Factory Function Pattern + +Services receive a function and call it to create resources. Service controls when and how resources are created. + +| Field | Type | Purpose | +|-------|------|---------| +| `NewCacheClient` | `func(cache.Options) (*cache.Cache, error)` | Create cache instance on-demand | + +**Advantages**: Lazy initialization, multiple instances with different options, service controls timing. + +**Use when**: Service may need multiple instances, different configurations, or conditional creation. + +#### Scoped Registry Pattern + +Services receive a scoped registry and call registration methods. Platform creates the registry and handles scoping. + +| Field | Type | Purpose | +|-------|------|---------| +| `AuthzResolverRegistry` | `*auth.ScopedAuthzResolverRegistry` | Register authz resolvers per-method | + +**Advantages**: Platform controls scope validation, prevents cross-service registration, centralized lookup. + +**Use when**: Services need to register handlers/resolvers for their own methods. + +### RegistrationParams Fields + +**Location**: `service/pkg/serviceregistry/serviceregistry.go:32-84` + +| Field | Type | Scope | Pattern | +|-------|------|-------|---------| +| `Config` | `config.ServiceConfig` | Service namespace | Direct injection | +| `Security` | `*config.SecurityConfig` | Platform-wide | Direct injection | +| `OTDF` | `*server.OpenTDFServer` | Platform-wide | Direct injection (deprecated) | +| `DBClient` | `*db.Client` | Service namespace | Declarative flag | +| `SDK` | `*sdk.SDK` | Platform-wide | Direct injection | +| `Logger` | `*logger.Logger` | Service namespace | Direct injection | +| `Tracer` | `trace.Tracer` | Platform-wide | Direct injection | +| `NewCacheClient` | `func(...)` | Service namespace | Factory function | +| `KeyManagerCtxFactories` | `[]trust.NamedKeyManagerCtxFactory` | Platform-wide | Direct injection | +| `WellKnownConfig` | `func(...)` | Platform-wide | Factory function | +| `RegisterReadinessCheck` | `func(...)` | Platform-wide | Factory function | +| `AuthzResolverRegistry` | `*auth.ScopedAuthzResolverRegistry` | Service methods | Scoped registry | + +### Service Reception + +Services receive `RegistrationParams` via their `RegisterFunc` implementation in `ServiceOptions`. + +**Key locations**: +- Service registration: Each service's `NewRegistration()` function +- Platform injection: `service/pkg/server/services.go:218-231` + +### Platform-Side Creation + +**Location**: `service/pkg/server/services.go:218-231` + +The platform iterates through registered namespaces and creates `RegistrationParams` for each service, populating: +1. Scoped fields (Config, DBClient, Logger) from namespace-specific sources +2. Platform-wide fields (SDK, Security, OTDF) from global instances +3. Function fields (WellKnownConfig, RegisterReadinessCheck) from platform services +4. Scoped registries (AuthzResolverRegistry) created per-service + +--- + +## Scoped Registries Pattern + +Scoped registries provide **namespace isolation** - services can only register items for their own methods/namespace, preventing cross-service interference. + +### Pattern Structure + +``` +Global Registry (platform-owned) + | + |-- ScopedForService(serviceDesc) --> ScopedRegistry + | + v +Scoped Registry (service-receives) + | + |-- Validates method belongs to service + |-- Delegates to global registry with full path +``` + +### Implementation Requirements + +| Component | Owner | Responsibility | +|-----------|-------|----------------| +| Global Registry | Platform | Thread-safe storage (`sync.RWMutex`), `Get()` accessor | +| `ScopedForService()` | Platform | Creates scoped view, validates serviceDesc not nil | +| Scoped Registry | Platform | Validates method ownership, builds full path, delegates | +| Registration call | Service | Calls scoped `Register()` or `MustRegister()` in `RegisterFunc` | + +### Validation Flow + +1. Service calls `scopedRegistry.Register(methodName, handler)` +2. Scoped registry checks `methodName` exists in `serviceDesc.Methods` +3. If valid: builds full path `//` and delegates to parent +4. If invalid: returns error (or panics for `MustRegister`) + +### Platform Integration Points + +| Location | Action | +|----------|--------| +| `service/pkg/server/start.go:275` | Creates global registry | +| `service/pkg/server/services.go:211-216` | Creates scoped registry per service | +| `service/pkg/server/services.go:230` | Injects scoped registry into RegistrationParams | + +--- + +## Adding New Platform Capabilities + +Follow these steps to add a new platform-level capability. + +### Choose the Right Pattern + +| Question | If Yes → Pattern | +|----------|------------------| +| Does the service *consume* a single resource with fixed config? | Declarative Flag | +| Does the service *create* multiple instances or need varied config? | Factory Function | +| Does the service *contribute* handlers/registrations to a platform system? | Scoped Registry | + +### For Scoped Registry Pattern + +#### Required Files + +| Step | File | Action | +|------|------|--------| +| 1 | `service/internal//registry.go` | Define global registry with `sync.RWMutex` | +| 2 | `service/internal//scoped_registry.go` | Define scoped registry with validation | +| 3 | `service/pkg/serviceregistry/serviceregistry.go` | Add scoped registry field to `RegistrationParams` | +| 4 | `service/pkg/server/start.go` | Create global registry instance | +| 5 | `service/pkg/server/services.go` | Add to `startServicesParams`, create scoped registry per-service | +| 6 | `service/internal//interceptor.go` | (Optional) Create interceptor using global registry | + +#### Global Registry Requirements + +- Thread-safe storage using `sync.RWMutex` +- `Get(key)` method for retrieval +- Internal `register(key, item)` method (not exported) +- `ScopedForService(serviceDesc)` factory method + +#### Scoped Registry Requirements + +- Reference to parent global registry +- Reference to `*grpc.ServiceDesc` for validation +- `Register(methodName, item)` validates method exists in ServiceDesc +- `MustRegister(methodName, item)` panics on validation failure +- Builds full path as `//` + +### For Declarative Flag Pattern + +#### Required Changes + +| Step | File | Action | +|------|------|--------| +| 1 | `service/pkg/serviceregistry/serviceregistry.go` | Add flag struct (like `DBRegister`) to `ServiceOptions` | +| 2 | `service/pkg/serviceregistry/serviceregistry.go` | Add field to `RegistrationParams` for the resource | +| 3 | `service/pkg/server/services.go` | Check flag, create resource, inject into params | + +#### Flag Struct Requirements + +- Boolean `Required` field to indicate service needs this resource +- Any configuration fields needed for resource creation +- Platform checks flag in service loop before creating resource + +### Platform Integration Requirements (Both Patterns) + +- Resources created in `start.go` or service loop in `services.go` +- Passed to `startServicesParams` struct if created in `start.go` +- Nil check before using or scoping +- Injected into `RegistrationParams` + +--- + +## Implemented Platform Capabilities + +### Authorization Resolver Registry + +**Reference Document**: [AUTHZ_RESOLVER_REFERENCE.md](./AUTHZ_RESOLVER_REFERENCE.md) + +| Component | Location | +|-----------|----------| +| Core types | `service/internal/auth/authz_resolver.go` | +| Global registry creation | `service/pkg/server/start.go:275` | +| Scoped registry creation | `service/pkg/server/services.go:211-216` | +| RegistrationParams field | `service/pkg/serviceregistry/serviceregistry.go:69-83` | + +**Service Integrations**: + +| Service | File | Status | +|---------|------|--------| +| Attributes | `service/policy/attributes/attributes.go:74-80` | 5 methods registered | +| Namespaces | `service/policy/namespaces/namespaces.go` | Not implemented | +| Values | `service/policy/attributes/attributes.go` | Not implemented | +| KAS | `service/kas/kas.go` | Not implemented | + +See [authz_resolver_reference.md](./authz_resolver_reference.md) for complete component inventory and drift detection checklist. + +--- + +## Checklist for New Features + +Use this checklist when adding new platform capabilities: + +### Design Phase + +- [ ] Is this truly a platform-level capability (cross-cutting, infrastructure)? +- [ ] Which injection pattern fits? (See [Choose the Right Pattern](#choose-the-right-pattern)) + - Declarative Flag: Service *consumes* a single resource with fixed config + - Factory Function: Service *creates* multiple instances or needs varied config + - Scoped Registry: Service *contributes* handlers/registrations to platform +- [ ] What validation is needed (method ownership, namespace scoping)? +- [ ] What is the runtime behavior (interceptor, handler, background job)? + +### Implementation Phase (Declarative Flag) + +- [ ] Add flag struct to `ServiceOptions` in `serviceregistry.go` +- [ ] Add resource field to `RegistrationParams` +- [ ] Check flag and create resource in `services.go` +- [ ] Inject resource into `RegistrationParams` + +### Implementation Phase (Factory Function) + +- [ ] Create factory function type +- [ ] Add factory field to `RegistrationParams` +- [ ] Initialize factory in `start.go` or `services.go` +- [ ] Inject factory into `RegistrationParams` + +### Implementation Phase (Scoped Registry) + +- [ ] Create global registry in `service/internal/` with `sync.RWMutex` +- [ ] Create scoped registry with ServiceDesc validation +- [ ] Add scoped registry type to `RegistrationParams` +- [ ] Create global registry in `start.go` +- [ ] Create scoped registries per-service in `services.go` +- [ ] Implement interceptor/handler that uses global registry + +### Testing Phase + +- [ ] Unit test resource creation or registry operations +- [ ] Unit test scoped registry validation (if applicable) +- [ ] Integration test service usage +- [ ] Integration test runtime behavior + +### Documentation Phase + +- [ ] Document in CLAUDE.md if it affects development workflow +- [ ] Update this document with the new capability + +--- + +## Anti-Patterns to Avoid + +| Anti-Pattern | Problem | Correct Approach | +|--------------|---------|------------------| +| **Cross-service data access** | Service A directly accesses Service B's DB client or internal state | Use SDK for IPC between services | +| **Platform internal access** | Service accesses `srp.OTDF.HTTPServer` or other server internals | Use scoped configuration from `srp.Config` | +| **Unscoped registration** | Global registry allows any service to register for any method | Use scoped registries with ServiceDesc validation | +| **Bypassing RegistrationParams** | Global singletons or package-level state for dependencies | Receive all dependencies through RegistrationParams | +| **Direct namespace access** | Accessing other namespace's configuration or state | Only access `srp.Config` for own namespace | + +### Detection Indicators + +- Import of `service/internal/server` in service code (except via RegistrationParams) +- Package-level `var` declarations for DB clients, SDK, or config +- Hardcoded namespace names outside the service's own namespace +- Direct method path strings instead of using ServiceDesc + +--- + +## Known Areas Needing Alignment + +The following areas of the codebase do not fully follow the patterns described above. + +| Area | Location | Issue | Status | +|------|----------|-------|--------| +| **OTDF Server Direct Access** | `service/kas/kas.go:97-98, 142-170` | KAS accesses `srp.OTDF.CryptoProvider`, `PublicHostname`, `HTTPServer.Addr`, `TLSConfig` | TODO at `services.go:226` | +| **DBClient Namespace Sharing** | `service/pkg/server/services.go:188-192` | Services in same namespace share DB client | Mitigated by domain-specific wrappers | +| **SDK Full Access** | Various services | SDK gives access to all service clients, no compile-time enforcement | Intentional for IPC | +| **Function Pointer Registrations** | `serviceregistry.go:63-67` | Health/WellKnown use function pointers not scoped registries | Acceptable for cross-cutting concerns | +| **Logger Namespace Sharing** | `services.go:164-179` | Services in same namespace share logger | Acceptable with `.With()` context | + +### OTDF Server Access Details + +KAS service accesses these platform internals that should be in RegistrationParams: +- `srp.OTDF.CryptoProvider` +- `srp.OTDF.PublicHostname` +- `srp.OTDF.HTTPServer.Addr` +- `srp.OTDF.HTTPServer.TLSConfig` + +**Recommendation**: Add dedicated fields to RegistrationParams: +- `PublicHostname string` +- `TLSEnabled bool` +- Migrate to `KeyManagerCtxFactories` for crypto diff --git a/docs/examples_encrypt.md b/docs/examples_encrypt.md index ac1b85b980..3f85cafda5 100644 --- a/docs/examples_encrypt.md +++ b/docs/examples_encrypt.md @@ -11,7 +11,6 @@ examples encrypt [flags] ``` -a, --data-attributes string space separated list of data attributes (default "https://example.com/attr/attr1/value/value1") -h, --help help for encrypt - --nano Output in nanoTDF format --no-kid-in-kao [deprecated] Disable storing key identifiers in TDF KAOs -o, --output string name or path of output file; - for stdout (default "sensitive.txt.tdf") ``` diff --git a/docs/grpc/index.html b/docs/grpc/index.html index ffc6e85c2a..a59d73dd56 100644 --- a/docs/grpc/index.html +++ b/docs/grpc/index.html @@ -233,10 +233,6 @@

Table of Contents

MAttribute -
  • - MCertificate -
  • -
  • MCondition
  • @@ -803,6 +799,10 @@

    Table of Contents

    +
  • + ESortDirection +
  • + @@ -908,6 +908,14 @@

    Table of Contents

    MAttributeKeyAccessServer +
  • + MAttributeValueObligationTriggerRequest +
  • + +
  • + MAttributesSort +
  • +
  • MCreateAttributeRequest
  • @@ -1045,6 +1053,10 @@

    Table of Contents

    +
  • + ESortAttributesType +
  • +
  • @@ -1151,10 +1163,18 @@

    Table of Contents

    MKasKeyIdentifier
  • +
  • + MKasKeysSort +
  • +
  • MKeyAccessServerGrants
  • +
  • + MKeyAccessServersSort +
  • +
  • MKeyMapping
  • @@ -1272,6 +1292,14 @@

    Table of Contents

    +
  • + ESortKasKeysType +
  • + +
  • + ESortKeyAccessServersType +
  • +
  • @@ -1341,14 +1369,6 @@

    Table of Contents

    policy/namespaces/namespaces.proto
      -
    • - MAssignCertificateToNamespaceRequest -
    • - -
    • - MAssignCertificateToNamespaceResponse -
    • -
    • MAssignKeyAccessServerToNamespaceRequest
    • @@ -1397,10 +1417,6 @@

      Table of Contents

      MListNamespacesResponse -
    • - MNamespaceCertificate -
    • -
    • MNamespaceKey
    • @@ -1410,11 +1426,7 @@

      Table of Contents

    • - MRemoveCertificateFromNamespaceRequest -
    • - -
    • - MRemoveCertificateFromNamespaceResponse + MNamespacesSort
    • @@ -1442,6 +1454,10 @@

      Table of Contents

    • +
    • + ESortNamespacesType +
    • +
    • @@ -1504,6 +1520,14 @@

      Table of Contents

      MGetObligationResponse
    • +
    • + MGetObligationTriggerRequest +
    • + +
    • + MGetObligationTriggerResponse +
    • +
    • MGetObligationValueRequest
    • @@ -1552,6 +1576,10 @@

      Table of Contents

      MListObligationsResponse +
    • + MObligationsSort +
    • +
    • MRemoveObligationTriggerRequest
    • @@ -1581,6 +1609,10 @@

      Table of Contents

      +
    • + ESortObligationsType +
    • +
    • @@ -1675,6 +1707,10 @@

      Table of Contents

      MListRegisteredResourcesResponse
    • +
    • + MRegisteredResourcesSort +
    • +
    • MUpdateRegisteredResourceRequest
    • @@ -1692,6 +1728,10 @@

      Table of Contents

      +
    • + ESortRegisteredResourcesType +
    • +
    • @@ -1901,6 +1941,14 @@

      Table of Contents

      MSubjectConditionSetCreate
    • +
    • + MSubjectConditionSetsSort +
    • + +
    • + MSubjectMappingsSort +
    • +
    • MUpdateSubjectConditionSetRequest
    • @@ -1918,6 +1966,14 @@

      Table of Contents

      +
    • + ESortSubjectConditionSetsType +
    • + +
    • + ESortSubjectMappingsType +
    • +
    • @@ -2367,6 +2423,13 @@

      Action

      + + namespace + Namespace + +

      Namespace context for this action

      + + metadata common.Metadata @@ -2552,48 +2615,18 @@

      Attribute

      - metadata - common.Metadata - -

      Common metadata

      - - - - - - - - - -

      Certificate

      -

      - - - - - - - - - - - - - - - - - - + + - + - + @@ -2742,8 +2775,8 @@

      KasPublicKey

      +To start, these may be `rsa:2048` for RSA-based wrapping and +`ec:secp256r1` for EC-based wrapping, but more formats may be added as needed.

      @@ -3015,13 +3048,6 @@

      Namespace

      - - - - - - -
      FieldTypeLabelDescription
      idstring

      generated uuid in database

      pemstringallow_traversalgoogle.protobuf.BoolValue

      PEM format certificate

      Whether or not we will use the attribute definition during encryption +if the attribute value is missing.

      metadata common.Metadata

      Optional metadata.

      Common metadata

      KasPublicKeyAlgEnum

      A known algorithm type with any additional parameters encoded. -To start, these may be `rsa:2048` for encrypting ZTDF files and -`ec:secp256r1` for nanoTDF, but more formats may be added as needed.

      Keys for the namespace

      root_certsCertificaterepeated

      Root certificates for chain of trust

      @@ -3133,6 +3159,13 @@

      ObligationTrigger

      + + namespace + Namespace + +

      The source namespace for this trigger, derived from the attribute value and action.

      + + metadata common.Metadata @@ -3353,6 +3386,13 @@

      RegisteredResource

      + + namespace + Namespace + +

      + + metadata common.Metadata @@ -3405,6 +3445,13 @@

      RegisteredResourceValue

      + + fqn + string + +

      + + metadata common.Metadata @@ -3572,6 +3619,13 @@

      ResourceMappingGroup

      per namespace

      + + fqn + string + +

      the fully qualified name of the resource mapping group

      + + metadata common.Metadata @@ -3679,6 +3733,15 @@

      SubjectConditionSet

      + + namespace + Namespace + +

      the namespace containing this subject condition set +possible this is empty in the case a subject condition set +has not been migrated to a namespace.

      + + subject_sets SubjectSet @@ -3738,6 +3801,15 @@

      SubjectMapping

      The actions permitted by subjects in this mapping

      + + namespace + Namespace + +

      the namespace containing this subject mapping +possible this is empty. If so that means +the Subject Mapping has not been migrated to a namespace.

      + + metadata common.Metadata @@ -4044,6 +4116,24 @@

      Algorithm

      + + ALGORITHM_HPQT_XWING + 6 +

      + + + + ALGORITHM_HPQT_SECP256R1_MLKEM768 + 7 +

      + + + + ALGORITHM_HPQT_SECP384R1_MLKEM1024 + 8 +

      + + @@ -4155,6 +4245,24 @@

      KasPublicKeyAlgEnum

      + + KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + 10 +

      + + + + KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + 11 +

      + + + + KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + 12 +

      + + @@ -4924,54 +5032,6 @@

      AuthorizationService

      - - -

      Methods with HTTP bindings

      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Method NameMethodPatternBody
      GetDecisionsPOST/v1/authorization*
      GetDecisionsByTokenPOST/v1/token/authorization
      GetEntitlementsPOST/v1/entitlements*
      - -
      @@ -5915,44 +5975,6 @@

      EntityResolutionService

      - - -

      Methods with HTTP bindings

      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Method NameMethodPatternBody
      ResolveEntitiesPOST/entityresolution/resolve*
      CreateEntityChainFromJwtPOST/entityresolution/entitychain*
      - -
      @@ -6293,7 +6315,7 @@

      KeyAccess

      Type of key wrapping used for the data encryption key Required: Always -Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped)

      +Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped), 'hybrid-wrapped' (experimental X-Wing-wrapped)

      @@ -6339,9 +6361,8 @@

      KeyAccess

      header bytes -

      Complete NanoTDF header containing all metadata and policy information -Required: NanoTDF only -ZTDF: Omitted (policy and metadata are separate) +

      Complete header containing all metadata and policy information (for formats that embed it) +Optional: Not used by ZTDF (policy and metadata are separate) Contains magic bytes, version, algorithm, policy, and ephemeral key information

      @@ -6351,7 +6372,7 @@

      KeyAccess

      Ephemeral public key for ECDH key derivation (ec-wrapped type only) Required: When key_type="ec-wrapped" (experimental ECDH-based ZTDF) -Omitted: When key_type="wrapped" (RSA-based ZTDF) +Omitted: When key_type="wrapped" or key_type="hybrid-wrapped" Should be a PEM-encoded PKCS#8 (ASN.1) formatted public key Used to derive the symmetric key for unwrapping the DEK

      @@ -6678,8 +6699,8 @@

      RewrapResponse

      string

      KAS's ephemeral session public key in PEM format -Required: For EC-based operations (NanoTDF and ZTDF with key_type="ec-wrapped") -Optional: Empty for RSA-based ZTDF (key_type="wrapped") +Required: For EC-based operations (key_type="ec-wrapped") +Optional: Empty for RSA-based or X-Wing-based ZTDF (key_type="wrapped" or key_type="hybrid-wrapped") Used by client to perform ECDH key agreement and decrypt the kas_wrapped_key values

      @@ -6948,8 +6969,7 @@

      UnsignedRewrapRequest.WithP repeated

      List of Key Access Objects associated with this policy Required: Always (at least one) -NanoTDF: Exactly one KAO per policy -ZTDF: One or more KAOs per policy

      +Some formats require exactly one KAO per policy

      @@ -6966,7 +6986,7 @@

      UnsignedRewrapRequest.WithP

      Cryptographic algorithm identifier for the TDF type Optional: Defaults to rsa:2048 if omitted -Values: "ec:secp256r1" (NanoTDF), "rsa:2048" (ZTDF), "" (defaults to rsa:2048) +Values: "ec:secp256r1" (EC-based), "rsa:2048" (RSA-based), "" (defaults to rsa:2048) Example: "ec:secp256r1"

      @@ -7043,72 +7063,24 @@

      Methods with deprecated option

      -

      Methods with HTTP bindings

      +

      Methods with idempotency_level option

      - - - + - - - - - + - - - - - - - - - - - - - - - - - - - - -
      Method NameMethodPatternBodyOption
      PublicKeyGET/kas/v2/kas_public_key

      NO_SIDE_EFFECTS

      LegacyPublicKeyGET/kas/kas_public_key
      RewrapPOST/kas/v2/rewrap*
      - - - - -

      Methods with idempotency_level option

      - - - - - - - - - - - - - - - - - + @@ -7457,6 +7429,35 @@

      PageResponse

      +

      SortDirection

      +

      Sorting direction shared across list APIs.

      When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED,

      the endpoint's request message defines the default ordering; see the

      specific List* request docs.

      +
      Method NameOption
      PublicKey

      NO_SIDE_EFFECTS

      LegacyPublicKey

      NO_SIDE_EFFECTS

      NO_SIDE_EFFECTS

      + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_DIRECTION_UNSPECIFIED0

      SORT_DIRECTION_ASC1

      SORT_DIRECTION_DESC2

      + @@ -7486,6 +7487,22 @@

      CreateActionRequest

      Required

      + + namespace_id + string + +

      Optional namespace ID for the custom action. +If omitted, create targets legacy (namespace_id = NULL) behavior unless enforced by server config.

      + + + + namespace_fqn + string + +

      Optional namespace FQN for the custom action. +If omitted, create targets legacy (namespace_id = NULL) behavior unless enforced by server config.

      + + metadata common.MetadataMutable @@ -7596,6 +7613,22 @@

      GetActionRequest

      + + namespace_id + string + +

      Optional namespace ID to scope name-based lookup. +If omitted for name-based lookup, action search is limited to legacy (namespace_id = NULL) actions.

      + + + + namespace_fqn + string + +

      Optional namespace FQN to scope name-based lookup. +If omitted for name-based lookup, action search is limited to legacy (namespace_id = NULL) actions.

      + + @@ -7644,6 +7677,20 @@

      ListActionsRequest

      + + namespace_id + string + +

      ID of the namespace to scope results. If omitted, returns actions across namespaces.

      + + + + namespace_fqn + string + +

      FQN of the namespace to scope results. If omitted, returns actions across namespaces.

      + + pagination policy.PageRequest @@ -8081,6 +8128,82 @@

      AttributeKeyAccessServer

      AttributeValueObligationTriggerRequest +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      obligation_valuecommon.IdFqnIdentifier

      Required. Existing obligation value to associate with the newly created attribute value.

      actioncommon.IdNameIdentifier

      Required. Action that, together with the newly created attribute value, triggers the obligation value.

      contextpolicy.RequestContext

      Optional. Request context for the obligation trigger.

      metadatacommon.MetadataMutable

      Optional. Common metadata for the obligation trigger.

      + + + + + +

      AttributesSort

      +

      + + + + + + + + + + + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      fieldSortAttributesType

      directionpolicy.SortDirection

      + + + + +

      CreateAttributeRequest

      @@ -8121,6 +8244,18 @@

      CreateAttributeRequest

      The stored attribute value will be normalized to lower case.

      + + allow_traversal + google.protobuf.BoolValue + +

      Optional +Setting allow_traversal=true allows TDF creation to be front-loaded, meaning a customer +can create encrypted content with an attribute definitions key mapping before +creating the attribute values needed to decrypt. +Content will be able to be encrypted with missing attribute values, +but will not be able to be decrypted until such attribute values exist.

      + + metadata common.MetadataMutable @@ -8183,6 +8318,14 @@

      CreateAttributeValueReque

      Required

      + + obligation_triggers + AttributeValueObligationTriggerRequest + repeated +

      Optional +Existing obligation values to trigger for the newly created attribute value.

      + + metadata common.MetadataMutable @@ -8698,6 +8841,17 @@

      ListAttributesRequest

      Optional

      + + sort + AttributesSort + repeated +

      Optional - CONSTRAINT: max 1 item +Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC

      + + @@ -9117,6 +9271,41 @@

      ValueKeyAccessServer

      +

      SortAttributesType

      +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_ATTRIBUTES_TYPE_UNSPECIFIED0

      SORT_ATTRIBUTES_TYPE_NAME1

      SORT_ATTRIBUTES_TYPE_CREATED_AT2

      SORT_ATTRIBUTES_TYPE_UPDATED_AT3

      + @@ -9142,7 +9331,8 @@

      AttributesService

      ListAttributeValues ListAttributeValuesRequest ListAttributeValuesResponse -

      +

      Deprecated +Use GetAttribute

      @@ -9282,6 +9472,11 @@

      Methods with deprecated option

      + + ListAttributeValues +

      true

      + + AssignKeyAccessServerToAttribute

      true

      @@ -9308,34 +9503,6 @@

      Methods with deprecated option

      -

      Methods with HTTP bindings

      - - - - - - - - - - - - - - - - - - - - - - -
      Method NameMethodPatternBody
      GetAttributeValuesByFqnsGET/attributes/*/fqn
      - - - -

      Methods with idempotency_level option

      @@ -10118,6 +10285,37 @@

      KasKeyIdentifier

      +

      KasKeysSort

      +

      + + +
      + + + + + + + + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      fieldSortKasKeysType

      directionpolicy.SortDirection

      + + + + +

      KeyAccessServerGrants

      Deprecated

      @@ -10163,7 +10361,38 @@

      KeyAccessServerGrants

      -

      KeyMapping

      +

      KeyAccessServersSort

      +

      + + + + + + + + + + + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      fieldSortKeyAccessServersType

      directionpolicy.SortDirection

      + + + + + +

      KeyMapping

      @@ -10338,6 +10567,17 @@

      ListKeyAccessServersRequ

      Optional

      + + sort + KeyAccessServersSort + repeated +

      Optional - CONSTRAINT: max 1 item +Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC

      + + @@ -10501,6 +10741,17 @@

      ListKeysRequest

      Pagination request for the list of keys

      + + sort + KasKeysSort + repeated +

      Optional - CONSTRAINT: max 1 item +Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC

      + + @@ -11322,6 +11573,82 @@

      UpdatePublicKeyResponse

      +

      SortKasKeysType

      +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_KAS_KEYS_TYPE_UNSPECIFIED0

      SORT_KAS_KEYS_TYPE_KEY_ID1

      SORT_KAS_KEYS_TYPE_CREATED_AT2

      SORT_KAS_KEYS_TYPE_UPDATED_AT3

      + +

      SortKeyAccessServersType

      +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED0

      SORT_KEY_ACCESS_SERVERS_TYPE_NAME1

      SORT_KEY_ACCESS_SERVERS_TYPE_URI2

      SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT3

      SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT4

      + @@ -11460,34 +11787,6 @@

      Methods with deprecated option

      -

      Methods with HTTP bindings

      - - - - - - - - - - - - - - - - - - - - - - -
      Method NameMethodPatternBody
      ListKeyAccessServersGET/key-access-servers
      - - - -

      Methods with idempotency_level option

      @@ -11908,75 +12207,6 @@

      policy/namespaces/namespaces.proto

      -

      AssignCertificateToNamespaceRequest

      -

      - - -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      FieldTypeLabelDescription
      namespacecommon.IdFqnIdentifier

      Required - namespace identifier (id or fqn)

      pemstring

      Required - PEM format certificate

      metadatacommon.MetadataMutable

      Optional

      - - - - - -

      AssignCertificateToNamespaceResponse

      -

      - - - - - - - - - - - - - - - - - - - - - - - -
      FieldTypeLabelDescription
      namespace_certificateNamespaceCertificate

      The mapping of the namespace to the certificate.

      certificatepolicy.Certificate

      Return the full certificate object for convenience

      - - - - -

      AssignKeyAccessServerToNamespaceRequest

      Deprecated: utilize AssignPublicKeyToNamespaceRequest

      @@ -12267,6 +12497,17 @@

      ListNamespacesRequest

      Optional

      + + sort + NamespacesSort + repeated +

      Optional - CONSTRAINT: max 1 item +Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC

      + + @@ -12305,37 +12546,6 @@

      ListNamespacesResponse

      -

      NamespaceCertificate

      -

      Maps a namespace to a certificate (similar to NamespaceKey pattern)

      - - - - - - - - - - - - - - - - - - - - - - - -
      FieldTypeLabelDescription
      namespacecommon.IdFqnIdentifier

      Required - namespace identifier (id or fqn)

      certificate_idstring

      Required (The id from the Certificate object)

      - - - - -

      NamespaceKey

      @@ -12398,7 +12608,7 @@

      NamespaceKeyAccessServer

      RemoveCertificateFromNamespaceRequest +

      NamespacesSort

      @@ -12409,34 +12619,17 @@

      RemoveCertifica - namespace_certificate - NamespaceCertificate + field + SortNamespacesType -

      The namespace and certificate to unassign.

      +

      - - - - - - - -

      RemoveCertificateFromNamespaceResponse

      -

      - - - - - - - - - - + + - + @@ -12606,6 +12799,47 @@

      UpdateNamespaceResponse

      +

      SortNamespacesType

      +

      +
      FieldTypeLabelDescription
      namespace_certificateNamespaceCertificatedirectionpolicy.SortDirection

      The unassigned namespace and certificate.

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_NAMESPACES_TYPE_UNSPECIFIED0

      SORT_NAMESPACES_TYPE_NAME1

      SORT_NAMESPACES_TYPE_FQN2

      SORT_NAMESPACES_TYPE_CREATED_AT3

      SORT_NAMESPACES_TYPE_UPDATED_AT4

      + @@ -12683,20 +12917,6 @@

      NamespaceService

      - - AssignCertificateToNamespace - AssignCertificateToNamespaceRequest - AssignCertificateToNamespaceResponse -

      Namespace <> Certificate RPCs

      - - - - RemoveCertificateFromNamespace - RemoveCertificateFromNamespaceRequest - RemoveCertificateFromNamespaceResponse -

      - - @@ -12762,7 +12982,7 @@

      policy/obligations/obligations.pro

      AddObligationTriggerRequest

      -

      Triggers

      +

      Obligation Triggers are owned by the namespace that owns the action and attribute value, which must

      be the same. In this way, a trigger can intentionally cross namespace boundaries: associating

      obligation values of a different namespace than the one that owns the action being taken or the attribute value.

      @@ -13105,7 +13325,7 @@

      DeleteObligationValueR

      GetObligationRequest

      -

      Definitions

      +

      @@ -13159,6 +13379,54 @@

      GetObligationResponse

      +

      GetObligationTriggerRequest

      +

      Triggers

      + + +
      + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      idstring

      Required

      + + + + + +

      GetObligationTriggerResponse

      +

      + + + + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      triggerpolicy.ObligationTrigger

      + + + + +

      GetObligationValueRequest

      Values

      @@ -13472,6 +13740,17 @@

      ListObligationsRequest

      Optional

      + + sort + ObligationsSort + repeated +

      Optional - CONSTRAINT: max 1 item +Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC

      + + @@ -13510,6 +13789,37 @@

      ListObligationsResponse

      +

      ObligationsSort

      +

      + + + + + + + + + + + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      fieldSortObligationsType

      directionpolicy.SortDirection

      + + + + +

      RemoveObligationTriggerRequest

      @@ -13745,6 +14055,47 @@

      ValueTriggerRequest

      +

      SortObligationsType

      +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_OBLIGATIONS_TYPE_UNSPECIFIED0

      SORT_OBLIGATIONS_TYPE_NAME1

      SORT_OBLIGATIONS_TYPE_FQN2

      SORT_OBLIGATIONS_TYPE_CREATED_AT3

      SORT_OBLIGATIONS_TYPE_UPDATED_AT4

      + @@ -13834,6 +14185,13 @@

      Service

      + + GetObligationTrigger + GetObligationTriggerRequest + GetObligationTriggerResponse +

      + + AddObligationTrigger AddObligationTriggerRequest @@ -13896,6 +14254,11 @@

      Methods with idempotency_level option

      NO_SIDE_EFFECTS

      + + GetObligationTrigger +

      NO_SIDE_EFFECTS

      + + ListObligationTriggers

      NO_SIDE_EFFECTS

      @@ -13979,11 +14342,25 @@

      CreateRegist values string repeated -

      Optional +

      Optional Registered Resource Values (when provided) must be alphanumeric strings, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case.

      + + namespace_id + string + +

      + + + + namespace_fqn + string + +

      + + metadata common.MetadataMutable @@ -14215,6 +14592,20 @@

      GetRegisteredRe

      + + namespace_fqn + string + +

      + + + + namespace_id + string + +

      + + @@ -14430,9 +14821,58 @@

      ListReg pagination - policy.PageResponse + policy.PageResponse + +

      + + + + + + + + + +

      ListRegisteredResourcesRequest

      +

      + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + @@ -14442,7 +14882,7 @@

      ListReg -

      ListRegisteredResourcesRequest

      +

      ListRegisteredResourcesResponse

      @@ -14452,11 +14892,18 @@

      ListRegistere

      + + + + + + + - + - + @@ -14466,7 +14913,7 @@

      ListRegistere -

      ListRegisteredResourcesResponse

      +

      RegisteredResourcesSort

      @@ -14477,15 +14924,15 @@

      ListRegister

      - - - + + + - - + + @@ -14647,6 +15094,41 @@

      Update +

      SortRegisteredResourcesType

      +

      +
      FieldTypeLabelDescription
      namespace_idstring

      namespace_fqnstring

      paginationpolicy.PageRequest

      Optional

      sortRegisteredResourcesSortrepeated

      Optional - CONSTRAINT: max 1 item +Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC

      resourcespolicy.RegisteredResourcerepeated

      paginationpolicy.PageRequestpolicy.PageResponse

      Optional

      resourcespolicy.RegisteredResourcerepeatedfieldSortRegisteredResourcesType

      paginationpolicy.PageResponsedirectionpolicy.SortDirection

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED0

      SORT_REGISTERED_RESOURCES_TYPE_NAME1

      SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT2

      SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT3

      + @@ -15627,6 +16109,20 @@

      CreateSubjectCon

      + + namespace_id + string + +

      + + + + namespace_fqn + string + +

      + + @@ -15699,6 +16195,21 @@

      CreateSubjectMappingR

      Create new SubjectConditionSet (NOTE: ignored if existing_subject_condition_set_id is provided)

      + + namespace_id + string + +

      Optional +Namespace ID or FQN for the subject mapping

      + + + + namespace_fqn + string + +

      + + metadata common.MetadataMutable @@ -15977,6 +16488,20 @@

      ListSubjectCondit + + namespace_id + string + +

      + + + + namespace_fqn + string + +

      + + pagination policy.PageRequest @@ -15984,6 +16509,17 @@

      ListSubjectCondit

      Optional

      + + sort + SubjectConditionSetsSort + repeated +

      Optional - CONSTRAINT: max 1 item +Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC

      + + @@ -16032,6 +16568,20 @@

      ListSubjectMappingsReq + + namespace_id + string + +

      + + + + namespace_fqn + string + +

      + + pagination policy.PageRequest @@ -16039,6 +16589,17 @@

      ListSubjectMappingsReq

      Optional

      + + sort + SubjectMappingsSort + repeated +

      Optional - CONSTRAINT: max 1 item +Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC

      + + @@ -16157,6 +16718,68 @@

      SubjectConditionSetCrea +

      SubjectConditionSetsSort

      +

      + + + + + + + + + + + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      fieldSortSubjectConditionSetsType

      directionpolicy.SortDirection

      + + + + + +

      SubjectMappingsSort

      +

      + + + + + + + + + + + + + + + + + + + + + + + +
      FieldTypeLabelDescription
      fieldSortSubjectMappingsType

      directionpolicy.SortDirection

      + + + + +

      UpdateSubjectConditionSetRequest

      @@ -16307,6 +16930,64 @@

      UpdateSubjectMapping +

      SortSubjectConditionSetsType

      +

      + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED0

      SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT1

      SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT2

      + +

      SortSubjectMappingsType

      +

      + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameNumberDescription
      SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED0

      SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT1

      SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT2

      + @@ -16866,6 +17547,17 @@

      UnsafeUpdateAttributeRequest Updating the rule of an Attribute will retroactively alter access to existing TDFs of the Attribute name.

      + + allow_traversal + google.protobuf.BoolValue + +

      Optional +WARNING!! +Updating allow_traversal allows TDF creation to be front-loaded, meaning a customer +can create encrypted content with an attribute definitions key mapping before +creating the attribute values needed to decrypt.

      + + values_order string @@ -17235,34 +17927,6 @@

      WellKnownService

      -

      Methods with HTTP bindings

      - - - - - - - - - - - - - - - - - - - - - - -
      Method NameMethodPatternBody
      GetWellKnownConfigurationGET/.well-known/opentdf-configuration
      - - - -

      Methods with idempotency_level option

      diff --git a/docs/openapi/authorization/authorization.openapi.yaml b/docs/openapi/authorization/authorization.openapi.yaml index 582d5392ab..1f3648bae8 100644 --- a/docs/openapi/authorization/authorization.openapi.yaml +++ b/docs/openapi/authorization/authorization.openapi.yaml @@ -2,12 +2,22 @@ openapi: 3.1.0 info: title: authorization paths: - /v1/authorization: + /authorization.AuthorizationService/GetDecisions: post: tags: - authorization.AuthorizationService summary: GetDecisions operationId: authorization.AuthorizationService.GetDecisions + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' requestBody: content: application/json: @@ -27,107 +37,28 @@ paths: application/json: schema: $ref: '#/components/schemas/authorization.GetDecisionsResponse' - /v1/token/authorization: + /authorization.AuthorizationService/GetDecisionsByToken: post: tags: - authorization.AuthorizationService summary: GetDecisionsByToken operationId: authorization.AuthorizationService.GetDecisionsByToken parameters: - - name: decisionRequests.actions.id - in: query - description: Generated uuid in database - schema: - type: string - title: id - description: Generated uuid in database - - name: decisionRequests.actions.standard - in: query - description: Deprecated - schema: - title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - - name: decisionRequests.actions.custom - in: query - description: Deprecated - schema: - type: string - title: custom - description: Deprecated - - name: decisionRequests.actions.name - in: query - schema: - type: string - title: name - - name: decisionRequests.actions.metadata.createdAt.seconds - in: query - description: |- - Represents seconds of UTC time since Unix epoch - 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to - 9999-12-31T23:59:59Z inclusive. - schema: - type: - - integer - - string - title: seconds - format: int64 - description: |- - Represents seconds of UTC time since Unix epoch - 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to - 9999-12-31T23:59:59Z inclusive. - - name: decisionRequests.actions.metadata.createdAt.nanos - in: query - description: |- - Non-negative fractions of a second at nanosecond resolution. Negative - second values with fractions must still have non-negative nanos values - that count forward in time. Must be from 0 to 999,999,999 - inclusive. - schema: - type: integer - title: nanos - format: int32 - description: |- - Non-negative fractions of a second at nanosecond resolution. Negative - second values with fractions must still have non-negative nanos values - that count forward in time. Must be from 0 to 999,999,999 - inclusive. - - name: decisionRequests.actions.metadata.labels.key - in: query - schema: - type: string - title: key - - name: decisionRequests.actions.metadata.labels.value - in: query - schema: - type: string - title: value - - name: decisionRequests.tokens.id - in: query - description: ephemeral id for tracking between request and response + - name: Connect-Protocol-Version + in: header + required: true schema: - type: string - title: id - description: ephemeral id for tracking between request and response - - name: decisionRequests.tokens.jwt - in: query - description: the token - schema: - type: string - title: jwt - description: the token - - name: decisionRequests.resourceAttributes.resourceAttributesId - in: query - schema: - type: string - title: resource_attributes_id - - name: decisionRequests.resourceAttributes.attributeValueFqns - in: query + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header schema: - type: array - items: - type: string - title: attribute_value_fqns + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/authorization.GetDecisionsByTokenRequest' + required: true responses: default: description: Error @@ -141,12 +72,22 @@ paths: application/json: schema: $ref: '#/components/schemas/authorization.GetDecisionsByTokenResponse' - /v1/entitlements: + /authorization.AuthorizationService/GetEntitlements: post: tags: - authorization.AuthorizationService summary: GetEntitlements operationId: authorization.AuthorizationService.GetEntitlements + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' requestBody: content: application/json: @@ -168,27 +109,6 @@ paths: $ref: '#/components/schemas/authorization.GetEntitlementsResponse' components: schemas: - authorization.DecisionResponse.Decision: - type: string - title: Decision - enum: - - DECISION_UNSPECIFIED - - DECISION_DENY - - DECISION_PERMIT - authorization.Entity.Category: - type: string - title: Category - enum: - - CATEGORY_UNSPECIFIED - - CATEGORY_SUBJECT - - CATEGORY_ENVIRONMENT - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT authorization.DecisionRequest: type: object properties: @@ -213,7 +133,6 @@ components: Example Request Get Decisions to answer the question - Do Bob (represented by entity chain ec1) and Alice (represented by entity chain ec2) have TRANSMIT authorization for 2 resources; resource1 (attr-set-1) defined by attributes foo:bar resource2 (attr-set-2) defined by attribute foo:bar, color:red ? - { "actions": [ { @@ -285,13 +204,11 @@ components: Example response for a Decision Request - Do Bob (represented by entity chain ec1) and Alice (represented by entity chain ec2) have TRANSMIT authorization for 2 resources; resource1 (attr-set-1) defined by attributes foo:bar resource2 (attr-set-2) defined by attribute foo:bar, color:red ? - Results: - bob has permitted authorization to transmit for a resource defined by attr-set-1 attributes and has a watermark obligation - bob has denied authorization to transmit a for a resource defined by attr-set-2 attributes - alice has permitted authorization to transmit for a resource defined by attr-set-1 attributes - alice has denied authorization to transmit a for a resource defined by attr-set-2 attributes - { "entityChainId": "ec1", "resourceAttributesId": "attr-set-1", @@ -315,70 +232,92 @@ components: "resourceAttributesId": "attr-set-2", "decision": "DECISION_DENY" } + authorization.DecisionResponse.Decision: + type: string + title: Decision + enum: + - DECISION_UNSPECIFIED + - DECISION_DENY + - DECISION_PERMIT authorization.Entity: type: object - oneOf: - - properties: - claims: - title: claims - $ref: '#/components/schemas/google.protobuf.Any' - title: claims - required: - - claims + allOf: - properties: - clientId: + id: type: string + title: id + description: ephemeral id for tracking between request and response + category: + title: category + $ref: '#/components/schemas/authorization.Entity.Category' + - oneOf: + - type: object + properties: + claims: + title: claims + $ref: '#/components/schemas/google.protobuf.Any' + title: claims + required: + - claims + - type: object + properties: + clientId: + type: string + title: client_id title: client_id - title: client_id - required: - - clientId - - properties: - custom: + required: + - clientId + - type: object + properties: + custom: + title: custom + $ref: '#/components/schemas/authorization.EntityCustom' title: custom - $ref: '#/components/schemas/authorization.EntityCustom' - title: custom - required: - - custom - - properties: - emailAddress: - type: string + required: + - custom + - type: object + properties: + emailAddress: + type: string + title: email_address + description: one of the entity options must be set title: email_address - description: one of the entity options must be set - title: email_address - required: - - emailAddress - - properties: - remoteClaimsUrl: - type: string + required: + - emailAddress + - type: object + properties: + remoteClaimsUrl: + type: string + title: remote_claims_url title: remote_claims_url - title: remote_claims_url - required: - - remoteClaimsUrl - - properties: - userName: - type: string + required: + - remoteClaimsUrl + - type: object + properties: + userName: + type: string + title: user_name title: user_name - title: user_name - required: - - userName - - properties: - uuid: - type: string + required: + - userName + - type: object + properties: + uuid: + type: string + title: uuid title: uuid - title: uuid - required: - - uuid - properties: - id: - type: string - title: id - description: ephemeral id for tracking between request and response - category: - title: category - $ref: '#/components/schemas/authorization.Entity.Category' + required: + - uuid title: Entity additionalProperties: false description: PE (Person Entity) or NPE (Non-Person Entity) + authorization.Entity.Category: + type: string + title: Category + enum: + - CATEGORY_UNSPECIFIED + - CATEGORY_SUBJECT + - CATEGORY_ENVIRONMENT authorization.EntityChain: type: object properties: @@ -466,22 +405,22 @@ components: title: entities description: list of requested entities scope: + oneOf: + - $ref: '#/components/schemas/authorization.ResourceAttribute' + - type: "null" title: scope description: optional attribute fqn as a scope - nullable: true - $ref: '#/components/schemas/authorization.ResourceAttribute' withComprehensiveHierarchy: - type: boolean + type: + - boolean + - "null" title: with_comprehensive_hierarchy description: optional parameter to return a full list of entitlements - returns lower hierarchy attributes - nullable: true title: GetEntitlementsRequest additionalProperties: false description: |- Request to get entitlements for one or more entities for an optional attribute scope - Example: Get entitlements for bob and alice (both represented using an email address - { "entities": [ { @@ -512,7 +451,6 @@ components: additionalProperties: false description: |- Example Response for a request of : Get entitlements for bob and alice (both represented using an email address - { "entitlements": [ { @@ -584,7 +522,6 @@ components: Example Request Get Decisions by Token to answer the question - Do Bob and client1 (represented by token tok1) and Alice and client2 (represented by token tok2) have TRANSMIT authorization for 2 resources; resource1 (attr-set-1) defined by attributes foo:bar resource2 (attr-set-2) defined by attribute foo:bar, color:red ? - { "actions": [ { @@ -647,24 +584,99 @@ components: title: value title: LabelsEntry additionalProperties: false - google.protobuf.Any: + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: type: object properties: type: type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' value: type: string format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. debug: - type: object - additionalProperties: true + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. + google.protobuf.Any: + type: object + properties: + type: + type: string + value: + type: string + format: binary additionalProperties: true description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. + google.protobuf.BoolValue: + type: boolean + description: |- + Wrapper message for `bool`. + + The JSON representation for `BoolValue` is JSON `true` and `false`. + + Not recommended for use in new APIs, but still useful for legacy APIs and + has no plan to be removed. google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -758,81 +770,265 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: + type: string + title: id + description: Generated uuid in database + name: type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard + required: + - standard + title: Action + additionalProperties: false + description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. + policy.KasPublicKey: + type: object + properties: + pem: + type: string + title: pem + maxLength: 8192 + minLength: 1 + description: x509 ASN.1 content in PEM envelope, usually + kid: + type: string + title: kid + maxLength: 32 + minLength: 1 + description: A unique string identifier for this key + alg: + not: + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + title: alg + description: |- + A known algorithm type with any additional parameters encoded. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. + $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' + title: KasPublicKey + additionalProperties: false + description: |- + Deprecated + A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + policy.KasPublicKeySet: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/policy.KasPublicKey' + title: keys + title: KasPublicKeySet + additionalProperties: false + description: |- + Deprecated + A list of known KAS public keys + policy.KeyAccessServer: + type: object properties: id: type: string title: id - description: Generated uuid in database + uri: + type: string + title: uri + description: | + Address of a KAS instance + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. + publicKey: + title: public_key + description: 'Deprecated: KAS can have multiple key pairs' + $ref: '#/components/schemas/policy.PublicKey' + sourceType: + title: source_type + description: 'The source of the KAS: (INTERNAL, EXTERNAL)' + $ref: '#/components/schemas/policy.SourceType' + kasKeys: + type: array + items: + $ref: '#/components/schemas/policy.SimpleKasKey' + title: kas_keys + description: Kas keys associated with this KAS name: type: string title: name + description: |- + Optional + Unique name of the KAS instance metadata: title: metadata + description: Common metadata $ref: '#/components/schemas/common.Metadata' - title: Action + title: KeyAccessServer additionalProperties: false - description: An action an entity can take - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: + description: Key Access Server Registry + policy.Namespace: type: object properties: - code: + id: type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: + title: id + description: generated uuid in database + name: type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + title: name + description: |- + used to partition Attribute Definitions, support by namespace AuthN and + enable federation + fqn: + type: string + title: fqn + active: + title: active + description: active by default until explicitly deactivated + $ref: '#/components/schemas/google.protobuf.BoolValue' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + grants: + type: array + items: + $ref: '#/components/schemas/policy.KeyAccessServer' + title: grants + description: Deprecated KAS grants for the namespace. Use kas_keys instead. + kasKeys: + type: array + items: + $ref: '#/components/schemas/policy.SimpleKasKey' + title: kas_keys + description: Keys for the namespace + title: Namespace + additionalProperties: false + policy.PublicKey: + type: object + oneOf: + - type: object + properties: + cached: + title: cached + description: public key with additional information. Current preferred version + $ref: '#/components/schemas/policy.KasPublicKeySet' + title: cached + required: + - cached + - type: object + properties: + remote: + type: string + title: remote + description: | + kas public key url - optional since can also be retrieved via public key + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. + title: remote + required: + - remote + title: PublicKey + additionalProperties: false + description: Deprecated + policy.SimpleKasKey: + type: object + properties: + kasUri: + type: string + title: kas_uri + description: The URL of the Key Access Server + publicKey: + title: public_key + description: The public key of the Key that belongs to the KAS + $ref: '#/components/schemas/policy.SimpleKasPublicKey' + kasId: + type: string + title: kas_id + description: The ID of the Key Access Server + title: SimpleKasKey + additionalProperties: false + policy.SimpleKasPublicKey: + type: object + properties: + algorithm: + title: algorithm + $ref: '#/components/schemas/policy.Algorithm' + kid: + type: string + title: kid + pem: + type: string + title: pem + title: SimpleKasPublicKey + additionalProperties: false + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. security: [] tags: - name: authorization.AuthorizationService diff --git a/docs/openapi/authorization/v2/authorization.openapi.yaml b/docs/openapi/authorization/v2/authorization.openapi.yaml index e284ea2a29..b4dbfa42d6 100644 --- a/docs/openapi/authorization/v2/authorization.openapi.yaml +++ b/docs/openapi/authorization/v2/authorization.openapi.yaml @@ -37,12 +37,12 @@ paths: application/json: schema: $ref: '#/components/schemas/authorization.v2.GetDecisionResponse' - /authorization.v2.AuthorizationService/GetDecisionMultiResource: + /authorization.v2.AuthorizationService/GetDecisionBulk: post: tags: - authorization.v2.AuthorizationService - summary: GetDecisionMultiResource - operationId: authorization.v2.AuthorizationService.GetDecisionMultiResource + summary: GetDecisionBulk + operationId: authorization.v2.AuthorizationService.GetDecisionBulk parameters: - name: Connect-Protocol-Version in: header @@ -57,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/authorization.v2.GetDecisionMultiResourceRequest' + $ref: '#/components/schemas/authorization.v2.GetDecisionBulkRequest' required: true responses: default: @@ -71,13 +71,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/authorization.v2.GetDecisionMultiResourceResponse' - /authorization.v2.AuthorizationService/GetDecisionBulk: + $ref: '#/components/schemas/authorization.v2.GetDecisionBulkResponse' + /authorization.v2.AuthorizationService/GetDecisionMultiResource: post: tags: - authorization.v2.AuthorizationService - summary: GetDecisionBulk - operationId: authorization.v2.AuthorizationService.GetDecisionBulk + summary: GetDecisionMultiResource + operationId: authorization.v2.AuthorizationService.GetDecisionMultiResource parameters: - name: Connect-Protocol-Version in: header @@ -92,7 +92,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/authorization.v2.GetDecisionBulkRequest' + $ref: '#/components/schemas/authorization.v2.GetDecisionMultiResourceRequest' required: true responses: default: @@ -106,7 +106,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/authorization.v2.GetDecisionBulkResponse' + $ref: '#/components/schemas/authorization.v2.GetDecisionMultiResourceResponse' /authorization.v2.AuthorizationService/GetEntitlements: post: tags: @@ -151,20 +151,6 @@ components: - DECISION_UNSPECIFIED - DECISION_DENY - DECISION_PERMIT - entity.Entity.Category: - type: string - title: Category - enum: - - CATEGORY_UNSPECIFIED - - CATEGORY_SUBJECT - - CATEGORY_ENVIRONMENT - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT authorization.v2.EntityEntitlements: type: object properties: @@ -205,21 +191,19 @@ components: authorization.v2.EntityIdentifier: type: object oneOf: - - properties: + - type: object + properties: entityChain: title: entity_chain - description: |+ + description: | chain of one or more entities and at most 10 - entities must be provided and between 1 and 10 in count: - ``` - has(this.entities) && this.entities.size() > 0 && this.entities.size() <= 10 - ``` - + entity_chain_required // entities must be provided and between 1 and 10 in count $ref: '#/components/schemas/entity.EntityChain' title: entity_chain required: - entityChain - - properties: + - type: object + properties: registeredResourceValueFqn: type: string title: registered_resource_value_fqn @@ -231,30 +215,24 @@ components: title: registered_resource_value_fqn required: - registeredResourceValueFqn - - properties: + - type: object + properties: token: title: token - description: |+ + description: | access token (JWT), which is used to create an entity chain (comprising one or more entities) - token must be provided: - ``` - has(this.jwt) && this.jwt.size() > 0 - ``` - + token_required // token must be provided $ref: '#/components/schemas/entity.Token' title: token required: - token - - properties: + - type: object + properties: withRequestToken: title: with_request_token - description: |+ + description: | derive the entity from the request's authorization access token JWT, rather than passing in the body - with_request_token must be true when set: - ``` - this == true - ``` - + with_request_token_must_be_true // with_request_token must be true when set $ref: '#/components/schemas/google.protobuf.BoolValue' title: with_request_token required: @@ -314,27 +292,19 @@ components: type: array items: type: string - description: |+ - if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs: - ``` - this.size() == 0 || (this.size() <= 50 && this.all(item, item.isUri())) - ``` - + description: | + obligation_value_fqns_valid // if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs title: fulfillable_obligation_fqns - description: |+ + description: | obligations (fully qualified values) the requester is capable of fulfilling i.e. https:///obl//value/ - if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs: - ``` - this.size() == 0 || (this.size() <= 50 && this.all(item, item.isUri())) - ``` - + obligation_value_fqns_valid // if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs title: GetDecisionMultiResourceRequest required: - entityIdentifier - action additionalProperties: false - description: |+ + description: | Can the identified entity/entities access? 1. one entity reference (actor) 2. one action @@ -343,11 +313,7 @@ components: If entitled, checks obligation policy: fulfillable obligations must satisfy all triggered. Note: this is a more performant bulk request for multiple resource decisions, up to 1000 per request - action.name must be provided: - ``` - has(this.action.name) - ``` - + get_decision_multi_request.action_name_required // action.name must be provided authorization.v2.GetDecisionMultiResourceResponse: type: object properties: @@ -381,39 +347,27 @@ components: type: array items: type: string - description: |+ - if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs: - ``` - this.size() == 0 || (this.size() <= 50 && this.all(item, item.isUri())) - ``` - + description: | + obligation_value_fqns_valid // if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs title: fulfillable_obligation_fqns - description: |+ + description: | obligations (fully qualified values) the requester is capable of fulfilling i.e. https:///obl//value/ - if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs: - ``` - this.size() == 0 || (this.size() <= 50 && this.all(item, item.isUri())) - ``` - + obligation_value_fqns_valid // if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs title: GetDecisionRequest required: - entityIdentifier - action - resource additionalProperties: false - description: |+ + description: | Can the identified entity/entities access? 1. one entity reference (actor) 2. one action 3. one resource If entitled, checks obligation policy: fulfillable obligations must satisfy all triggered. - action.name must be provided: - ``` - has(this.action.name) - ``` - + get_decision_request.action_name_required // action.name must be provided authorization.v2.GetDecisionResponse: type: object properties: @@ -431,12 +385,13 @@ components: description: an entity must be identified for entitlement decisioning $ref: '#/components/schemas/authorization.v2.EntityIdentifier' withComprehensiveHierarchy: - type: boolean + type: + - boolean + - "null" title: with_comprehensive_hierarchy description: |- optional parameter to return all entitled values for attribute definitions with hierarchy rules, propagating down the hierarchical values instead of returning solely the value that is directly entitled - nullable: true title: GetEntitlementsRequest required: - entityIdentifier @@ -458,36 +413,35 @@ components: additionalProperties: false authorization.v2.Resource: type: object - oneOf: + allOf: - properties: - attributeValues: - title: attribute_values - description: |+ - a set of attribute value FQNs, such as those on a TDF, between 1 and 20 in count - if provided, resource.attribute_values must be between 1 and 20 in count with all valid FQNs: - ``` - this.fqns.size() > 0 && this.fqns.size() <= 20 && this.fqns.all(item, item.isUri()) - ``` - - $ref: '#/components/schemas/authorization.v2.Resource.AttributeValues' - title: attribute_values - required: - - attributeValues - - properties: - registeredResourceValueFqn: + ephemeralId: type: string + title: ephemeral_id + description: ephemeral id for tracking between request and response + - oneOf: + - type: object + properties: + attributeValues: + title: attribute_values + description: | + a set of attribute value FQNs, such as those on a TDF, between 1 and 20 in count + attribute_values_required // if provided, resource.attribute_values must be between 1 and 20 in count with all valid FQNs + $ref: '#/components/schemas/authorization.v2.Resource.AttributeValues' + title: attribute_values + required: + - attributeValues + - type: object + properties: + registeredResourceValueFqn: + type: string + title: registered_resource_value_fqn + minLength: 1 + format: uri + description: fully qualified name of the registered resource value stored in platform policy title: registered_resource_value_fqn - minLength: 1 - format: uri - description: fully qualified name of the registered resource value stored in platform policy - title: registered_resource_value_fqn - required: - - registeredResourceValueFqn - properties: - ephemeralId: - type: string - title: ephemeral_id - description: ephemeral id for tracking between request and response + required: + - registeredResourceValueFqn title: Resource additionalProperties: false description: Either a set of attribute values (such as those on a TDF) or a registered resource value @@ -554,49 +508,130 @@ components: title: value title: LabelsEntry additionalProperties: false + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. entity.Entity: type: object - oneOf: + allOf: - properties: - claims: - title: claims - description: used by ERS claims mode - $ref: '#/components/schemas/google.protobuf.Any' - title: claims - required: - - claims - - properties: - clientId: + ephemeralId: type: string + title: ephemeral_id + description: ephemeral id for tracking between request and response + category: + title: category + $ref: '#/components/schemas/entity.Entity.Category' + - oneOf: + - type: object + properties: + claims: + title: claims + description: used by ERS claims mode + $ref: '#/components/schemas/google.protobuf.Any' + title: claims + required: + - claims + - type: object + properties: + clientId: + type: string + title: client_id title: client_id - title: client_id - required: - - clientId - - properties: - emailAddress: - type: string + required: + - clientId + - type: object + properties: + emailAddress: + type: string + title: email_address title: email_address - title: email_address - required: - - emailAddress - - properties: - userName: - type: string + required: + - emailAddress + - type: object + properties: + userName: + type: string + title: user_name title: user_name - title: user_name - required: - - userName - properties: - ephemeralId: - type: string - title: ephemeral_id - description: ephemeral id for tracking between request and response - category: - title: category - $ref: '#/components/schemas/entity.Entity.Category' + required: + - userName title: Entity additionalProperties: false description: PE (Person Entity) or NPE (Non-Person Entity) + entity.Entity.Category: + type: string + title: Category + enum: + - CATEGORY_UNSPECIFIED + - CATEGORY_SUBJECT + - CATEGORY_ENVIRONMENT entity.EntityChain: type: object properties: @@ -635,9 +670,6 @@ components: value: type: string format: binary - debug: - type: object - additionalProperties: true additionalProperties: true description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. google.protobuf.BoolValue: @@ -652,8 +684,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -747,81 +779,265 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: + type: string + title: id + description: Generated uuid in database + name: type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard + required: + - standard + title: Action + additionalProperties: false + description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. + policy.KasPublicKey: + type: object + properties: + pem: + type: string + title: pem + maxLength: 8192 + minLength: 1 + description: x509 ASN.1 content in PEM envelope, usually + kid: + type: string + title: kid + maxLength: 32 + minLength: 1 + description: A unique string identifier for this key + alg: + not: + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + title: alg + description: |- + A known algorithm type with any additional parameters encoded. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. + $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' + title: KasPublicKey + additionalProperties: false + description: |- + Deprecated + A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + policy.KasPublicKeySet: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/policy.KasPublicKey' + title: keys + title: KasPublicKeySet + additionalProperties: false + description: |- + Deprecated + A list of known KAS public keys + policy.KeyAccessServer: + type: object properties: id: type: string title: id - description: Generated uuid in database + uri: + type: string + title: uri + description: | + Address of a KAS instance + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. + publicKey: + title: public_key + description: 'Deprecated: KAS can have multiple key pairs' + $ref: '#/components/schemas/policy.PublicKey' + sourceType: + title: source_type + description: 'The source of the KAS: (INTERNAL, EXTERNAL)' + $ref: '#/components/schemas/policy.SourceType' + kasKeys: + type: array + items: + $ref: '#/components/schemas/policy.SimpleKasKey' + title: kas_keys + description: Kas keys associated with this KAS name: type: string title: name + description: |- + Optional + Unique name of the KAS instance metadata: title: metadata + description: Common metadata $ref: '#/components/schemas/common.Metadata' - title: Action + title: KeyAccessServer additionalProperties: false - description: An action an entity can take - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: + description: Key Access Server Registry + policy.Namespace: type: object properties: - code: + id: type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: + title: id + description: generated uuid in database + name: type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + title: name + description: |- + used to partition Attribute Definitions, support by namespace AuthN and + enable federation + fqn: + type: string + title: fqn + active: + title: active + description: active by default until explicitly deactivated + $ref: '#/components/schemas/google.protobuf.BoolValue' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + grants: + type: array + items: + $ref: '#/components/schemas/policy.KeyAccessServer' + title: grants + description: Deprecated KAS grants for the namespace. Use kas_keys instead. + kasKeys: + type: array + items: + $ref: '#/components/schemas/policy.SimpleKasKey' + title: kas_keys + description: Keys for the namespace + title: Namespace + additionalProperties: false + policy.PublicKey: + type: object + oneOf: + - type: object + properties: + cached: + title: cached + description: public key with additional information. Current preferred version + $ref: '#/components/schemas/policy.KasPublicKeySet' + title: cached + required: + - cached + - type: object + properties: + remote: + type: string + title: remote + description: | + kas public key url - optional since can also be retrieved via public key + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. + title: remote + required: + - remote + title: PublicKey + additionalProperties: false + description: Deprecated + policy.SimpleKasKey: + type: object + properties: + kasUri: + type: string + title: kas_uri + description: The URL of the Key Access Server + publicKey: + title: public_key + description: The public key of the Key that belongs to the KAS + $ref: '#/components/schemas/policy.SimpleKasPublicKey' + kasId: + type: string + title: kas_id + description: The ID of the Key Access Server + title: SimpleKasKey + additionalProperties: false + policy.SimpleKasPublicKey: + type: object + properties: + algorithm: + title: algorithm + $ref: '#/components/schemas/policy.Algorithm' + kid: + type: string + title: kid + pem: + type: string + title: pem + title: SimpleKasPublicKey + additionalProperties: false + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. security: [] tags: - name: authorization.v2.AuthorizationService diff --git a/docs/openapi/common/common.openapi.yaml b/docs/openapi/common/common.openapi.yaml index 0d1e60b152..093091b6a7 100644 --- a/docs/openapi/common/common.openapi.yaml +++ b/docs/openapi/common/common.openapi.yaml @@ -13,15 +13,14 @@ components: - ACTIVE_STATE_ENUM_INACTIVE - ACTIVE_STATE_ENUM_ANY description: 'buflint ENUM_VALUE_PREFIX: to make sure that C++ scoping rules aren''t violated when users add new enum values to an enum in a given package' - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE common.IdFqnIdentifier: type: object + allOf: + - oneOf: + - required: + - id + - required: + - fqn properties: id: type: string @@ -36,6 +35,12 @@ components: additionalProperties: false common.IdNameIdentifier: type: object + allOf: + - oneOf: + - required: + - id + - required: + - name properties: id: type: string @@ -46,12 +51,8 @@ components: title: name maxLength: 253 minLength: 1 - description: |+ - Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + description: | + name_format // Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case. title: IdNameIdentifier additionalProperties: false common.Metadata: @@ -109,11 +110,18 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local diff --git a/docs/openapi/entity/entity.openapi.yaml b/docs/openapi/entity/entity.openapi.yaml index 6484c863a0..388fb1f7a3 100644 --- a/docs/openapi/entity/entity.openapi.yaml +++ b/docs/openapi/entity/entity.openapi.yaml @@ -4,56 +4,61 @@ info: paths: {} components: schemas: - entity.Entity.Category: - type: string - title: Category - enum: - - CATEGORY_UNSPECIFIED - - CATEGORY_SUBJECT - - CATEGORY_ENVIRONMENT entity.Entity: type: object - oneOf: + allOf: - properties: - claims: - title: claims - description: used by ERS claims mode - $ref: '#/components/schemas/google.protobuf.Any' - title: claims - required: - - claims - - properties: - clientId: + ephemeralId: type: string + title: ephemeral_id + description: ephemeral id for tracking between request and response + category: + title: category + $ref: '#/components/schemas/entity.Entity.Category' + - oneOf: + - type: object + properties: + claims: + title: claims + description: used by ERS claims mode + $ref: '#/components/schemas/google.protobuf.Any' + title: claims + required: + - claims + - type: object + properties: + clientId: + type: string + title: client_id title: client_id - title: client_id - required: - - clientId - - properties: - emailAddress: - type: string + required: + - clientId + - type: object + properties: + emailAddress: + type: string + title: email_address title: email_address - title: email_address - required: - - emailAddress - - properties: - userName: - type: string + required: + - emailAddress + - type: object + properties: + userName: + type: string + title: user_name title: user_name - title: user_name - required: - - userName - properties: - ephemeralId: - type: string - title: ephemeral_id - description: ephemeral id for tracking between request and response - category: - title: category - $ref: '#/components/schemas/entity.Entity.Category' + required: + - userName title: Entity additionalProperties: false description: PE (Person Entity) or NPE (Non-Person Entity) + entity.Entity.Category: + type: string + title: Category + enum: + - CATEGORY_UNSPECIFIED + - CATEGORY_SUBJECT + - CATEGORY_ENVIRONMENT entity.EntityChain: type: object properties: @@ -92,9 +97,6 @@ components: value: type: string format: binary - debug: - type: object - additionalProperties: true additionalProperties: true description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] diff --git a/docs/openapi/entityresolution/entity_resolution.openapi.yaml b/docs/openapi/entityresolution/entity_resolution.openapi.yaml index d89cbdaa0a..6a57e3b749 100644 --- a/docs/openapi/entityresolution/entity_resolution.openapi.yaml +++ b/docs/openapi/entityresolution/entity_resolution.openapi.yaml @@ -2,18 +2,28 @@ openapi: 3.1.0 info: title: entityresolution paths: - /entityresolution/resolve: + /entityresolution.EntityResolutionService/CreateEntityChainFromJwt: post: tags: - entityresolution.EntityResolutionService - summary: ResolveEntities - description: 'Deprecated: use v2 ResolveEntities instead' - operationId: entityresolution.EntityResolutionService.ResolveEntities + summary: CreateEntityChainFromJwt + description: 'Deprecated: use v2 CreateEntityChainsFromTokens instead' + operationId: entityresolution.EntityResolutionService.CreateEntityChainFromJwt + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' requestBody: content: application/json: schema: - $ref: '#/components/schemas/entityresolution.ResolveEntitiesRequest' + $ref: '#/components/schemas/entityresolution.CreateEntityChainFromJwtRequest' required: true responses: default: @@ -27,19 +37,29 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/entityresolution.ResolveEntitiesResponse' - /entityresolution/entitychain: + $ref: '#/components/schemas/entityresolution.CreateEntityChainFromJwtResponse' + /entityresolution.EntityResolutionService/ResolveEntities: post: tags: - entityresolution.EntityResolutionService - summary: CreateEntityChainFromJwt - description: 'Deprecated: use v2 CreateEntityChainsFromTokens instead' - operationId: entityresolution.EntityResolutionService.CreateEntityChainFromJwt + summary: ResolveEntities + description: 'Deprecated: use v2 ResolveEntities instead' + operationId: entityresolution.EntityResolutionService.ResolveEntities + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' requestBody: content: application/json: schema: - $ref: '#/components/schemas/entityresolution.CreateEntityChainFromJwtRequest' + $ref: '#/components/schemas/entityresolution.ResolveEntitiesRequest' required: true responses: default: @@ -53,90 +73,88 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/entityresolution.CreateEntityChainFromJwtResponse' + $ref: '#/components/schemas/entityresolution.ResolveEntitiesResponse' components: schemas: - authorization.Entity.Category: - type: string - title: Category - enum: - - CATEGORY_UNSPECIFIED - - CATEGORY_SUBJECT - - CATEGORY_ENVIRONMENT - google.protobuf.NullValue: - type: string - title: NullValue - enum: - - NULL_VALUE - description: |- - `NullValue` is a singleton enumeration to represent the null value for the - `Value` type union. - - The JSON representation for `NullValue` is JSON `null`. authorization.Entity: type: object - oneOf: + allOf: - properties: - claims: - title: claims - $ref: '#/components/schemas/google.protobuf.Any' - title: claims - required: - - claims - - properties: - clientId: + id: type: string + title: id + description: ephemeral id for tracking between request and response + category: + title: category + $ref: '#/components/schemas/authorization.Entity.Category' + - oneOf: + - type: object + properties: + claims: + title: claims + $ref: '#/components/schemas/google.protobuf.Any' + title: claims + required: + - claims + - type: object + properties: + clientId: + type: string + title: client_id title: client_id - title: client_id - required: - - clientId - - properties: - custom: + required: + - clientId + - type: object + properties: + custom: + title: custom + $ref: '#/components/schemas/authorization.EntityCustom' title: custom - $ref: '#/components/schemas/authorization.EntityCustom' - title: custom - required: - - custom - - properties: - emailAddress: - type: string + required: + - custom + - type: object + properties: + emailAddress: + type: string + title: email_address + description: one of the entity options must be set title: email_address - description: one of the entity options must be set - title: email_address - required: - - emailAddress - - properties: - remoteClaimsUrl: - type: string + required: + - emailAddress + - type: object + properties: + remoteClaimsUrl: + type: string + title: remote_claims_url title: remote_claims_url - title: remote_claims_url - required: - - remoteClaimsUrl - - properties: - userName: - type: string + required: + - remoteClaimsUrl + - type: object + properties: + userName: + type: string + title: user_name title: user_name - title: user_name - required: - - userName - - properties: - uuid: - type: string + required: + - userName + - type: object + properties: + uuid: + type: string + title: uuid title: uuid - title: uuid - required: - - uuid - properties: - id: - type: string - title: id - description: ephemeral id for tracking between request and response - category: - title: category - $ref: '#/components/schemas/authorization.Entity.Category' + required: + - uuid title: Entity additionalProperties: false description: PE (Person Entity) or NPE (Non-Person Entity) + authorization.Entity.Category: + type: string + title: Category + enum: + - CATEGORY_UNSPECIFIED + - CATEGORY_SUBJECT + - CATEGORY_ENVIRONMENT authorization.EntityChain: type: object properties: @@ -174,6 +192,75 @@ components: description: the token title: Token additionalProperties: false + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. entityresolution.CreateEntityChainFromJwtRequest: type: object properties: @@ -315,9 +402,6 @@ components: value: type: string format: binary - debug: - type: object - additionalProperties: true additionalProperties: true description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. google.protobuf.ListValue: @@ -335,6 +419,16 @@ components: `ListValue` is a wrapper around a repeated field of values. The JSON representation for `ListValue` is JSON array. + google.protobuf.NullValue: + type: string + title: NullValue + enum: + - NULL_VALUE + description: |- + `NullValue` is a singleton enumeration to represent the null value for the + `Value` type union. + + The JSON representation for `NullValue` is JSON `null`. google.protobuf.Struct: type: object additionalProperties: @@ -375,50 +469,6 @@ components: variants. Absence of any variant indicates an error. The JSON representation for `Value` is JSON value. - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' security: [] tags: - name: entityresolution.EntityResolutionService diff --git a/docs/openapi/entityresolution/v2/entity_resolution.openapi.yaml b/docs/openapi/entityresolution/v2/entity_resolution.openapi.yaml index b8e661c75a..52baa90875 100644 --- a/docs/openapi/entityresolution/v2/entity_resolution.openapi.yaml +++ b/docs/openapi/entityresolution/v2/entity_resolution.openapi.yaml @@ -2,12 +2,12 @@ openapi: 3.1.0 info: title: entityresolution.v2 paths: - /entityresolution.v2.EntityResolutionService/ResolveEntities: + /entityresolution.v2.EntityResolutionService/CreateEntityChainsFromTokens: post: tags: - entityresolution.v2.EntityResolutionService - summary: ResolveEntities - operationId: entityresolution.v2.EntityResolutionService.ResolveEntities + summary: CreateEntityChainsFromTokens + operationId: entityresolution.v2.EntityResolutionService.CreateEntityChainsFromTokens parameters: - name: Connect-Protocol-Version in: header @@ -22,7 +22,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/entityresolution.v2.ResolveEntitiesRequest' + $ref: '#/components/schemas/entityresolution.v2.CreateEntityChainsFromTokensRequest' required: true responses: default: @@ -36,13 +36,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/entityresolution.v2.ResolveEntitiesResponse' - /entityresolution.v2.EntityResolutionService/CreateEntityChainsFromTokens: + $ref: '#/components/schemas/entityresolution.v2.CreateEntityChainsFromTokensResponse' + /entityresolution.v2.EntityResolutionService/ResolveEntities: post: tags: - entityresolution.v2.EntityResolutionService - summary: CreateEntityChainsFromTokens - operationId: entityresolution.v2.EntityResolutionService.CreateEntityChainsFromTokens + summary: ResolveEntities + operationId: entityresolution.v2.EntityResolutionService.ResolveEntities parameters: - name: Connect-Protocol-Version in: header @@ -57,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/entityresolution.v2.CreateEntityChainsFromTokensRequest' + $ref: '#/components/schemas/entityresolution.v2.ResolveEntitiesRequest' required: true responses: default: @@ -71,58 +71,40 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/entityresolution.v2.CreateEntityChainsFromTokensResponse' + $ref: '#/components/schemas/entityresolution.v2.ResolveEntitiesResponse' components: schemas: - entity.Entity.Category: - type: string - title: Category - enum: - - CATEGORY_UNSPECIFIED - - CATEGORY_SUBJECT - - CATEGORY_ENVIRONMENT - google.protobuf.NullValue: - type: string - title: NullValue - enum: - - NULL_VALUE - description: |- - `NullValue` is a singleton enumeration to represent the null value for the - `Value` type union. - - The JSON representation for `NullValue` is JSON `null`. authorization.v2.Resource: type: object - oneOf: + allOf: - properties: - attributeValues: - title: attribute_values - description: |+ - a set of attribute value FQNs, such as those on a TDF, between 1 and 20 in count - if provided, resource.attribute_values must be between 1 and 20 in count with all valid FQNs: - ``` - this.fqns.size() > 0 && this.fqns.size() <= 20 && this.fqns.all(item, item.isUri()) - ``` - - $ref: '#/components/schemas/authorization.v2.Resource.AttributeValues' - title: attribute_values - required: - - attributeValues - - properties: - registeredResourceValueFqn: + ephemeralId: type: string + title: ephemeral_id + description: ephemeral id for tracking between request and response + - oneOf: + - type: object + properties: + attributeValues: + title: attribute_values + description: | + a set of attribute value FQNs, such as those on a TDF, between 1 and 20 in count + attribute_values_required // if provided, resource.attribute_values must be between 1 and 20 in count with all valid FQNs + $ref: '#/components/schemas/authorization.v2.Resource.AttributeValues' + title: attribute_values + required: + - attributeValues + - type: object + properties: + registeredResourceValueFqn: + type: string + title: registered_resource_value_fqn + minLength: 1 + format: uri + description: fully qualified name of the registered resource value stored in platform policy title: registered_resource_value_fqn - minLength: 1 - format: uri - description: fully qualified name of the registered resource value stored in platform policy - title: registered_resource_value_fqn - required: - - registeredResourceValueFqn - properties: - ephemeralId: - type: string - title: ephemeral_id - description: ephemeral id for tracking between request and response + required: + - registeredResourceValueFqn title: Resource additionalProperties: false description: Either a set of attribute values (such as those on a TDF) or a registered resource value @@ -136,49 +118,130 @@ components: title: fqns title: AttributeValues additionalProperties: false + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. entity.Entity: type: object - oneOf: + allOf: - properties: - claims: - title: claims - description: used by ERS claims mode - $ref: '#/components/schemas/google.protobuf.Any' - title: claims - required: - - claims - - properties: - clientId: + ephemeralId: type: string + title: ephemeral_id + description: ephemeral id for tracking between request and response + category: + title: category + $ref: '#/components/schemas/entity.Entity.Category' + - oneOf: + - type: object + properties: + claims: + title: claims + description: used by ERS claims mode + $ref: '#/components/schemas/google.protobuf.Any' + title: claims + required: + - claims + - type: object + properties: + clientId: + type: string + title: client_id title: client_id - title: client_id - required: - - clientId - - properties: - emailAddress: - type: string + required: + - clientId + - type: object + properties: + emailAddress: + type: string + title: email_address title: email_address - title: email_address - required: - - emailAddress - - properties: - userName: - type: string + required: + - emailAddress + - type: object + properties: + userName: + type: string + title: user_name title: user_name - title: user_name - required: - - userName - properties: - ephemeralId: - type: string - title: ephemeral_id - description: ephemeral id for tracking between request and response - category: - title: category - $ref: '#/components/schemas/entity.Entity.Category' + required: + - userName title: Entity additionalProperties: false description: PE (Person Entity) or NPE (Non-Person Entity) + entity.Entity.Category: + type: string + title: Category + enum: + - CATEGORY_UNSPECIFIED + - CATEGORY_SUBJECT + - CATEGORY_ENVIRONMENT entity.EntityChain: type: object properties: @@ -322,9 +385,6 @@ components: value: type: string format: binary - debug: - type: object - additionalProperties: true additionalProperties: true description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. google.protobuf.ListValue: @@ -342,6 +402,16 @@ components: `ListValue` is a wrapper around a repeated field of values. The JSON representation for `ListValue` is JSON array. + google.protobuf.NullValue: + type: string + title: NullValue + enum: + - NULL_VALUE + description: |- + `NullValue` is a singleton enumeration to represent the null value for the + `Value` type union. + + The JSON representation for `NullValue` is JSON `null`. google.protobuf.Struct: type: object additionalProperties: @@ -382,50 +452,6 @@ components: variants. Absence of any variant indicates an error. The JSON representation for `Value` is JSON value. - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' security: [] tags: - name: entityresolution.v2.EntityResolutionService diff --git a/docs/openapi/kas/kas.openapi.yaml b/docs/openapi/kas/kas.openapi.yaml index 4193519a3e..147aea0ddf 100644 --- a/docs/openapi/kas/kas.openapi.yaml +++ b/docs/openapi/kas/kas.openapi.yaml @@ -2,28 +2,32 @@ openapi: 3.1.0 info: title: kas paths: - /kas/v2/kas_public_key: - get: + /kas.AccessService/LegacyPublicKey: + post: tags: - kas.AccessService - summary: PublicKey - operationId: kas.AccessService.PublicKey + summary: Endpoint intended for gRPC Gateway's REST endpoint to provide v1 compatibility with older TDF clients + description: |- + This endpoint is not recommended for use in new applications, prefer the v2 endpoint ('PublicKey') instead. + + buf:lint:ignore RPC_RESPONSE_STANDARD_NAME + operationId: kas.AccessService.LegacyPublicKey parameters: - - name: algorithm - in: query - schema: - type: string - title: algorithm - - name: fmt - in: query + - name: Connect-Protocol-Version + in: header + required: true schema: - type: string - title: fmt - - name: v - in: query + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header schema: - type: string - title: v + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/kas.LegacyPublicKeyRequest' + required: true responses: default: description: Error @@ -36,25 +40,30 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/kas.PublicKeyResponse' - /kas/kas_public_key: - get: + $ref: '#/components/schemas/google.protobuf.StringValue' + deprecated: true + /kas.AccessService/PublicKey: + post: tags: - kas.AccessService - summary: LegacyPublicKey - description: |- - Endpoint intended for gRPC Gateway's REST endpoint to provide v1 compatibility with older TDF clients - - This endpoint is not recommended for use in new applications, prefer the v2 endpoint ('PublicKey') instead. - - buf:lint:ignore RPC_RESPONSE_STANDARD_NAME - operationId: kas.AccessService.LegacyPublicKey + summary: PublicKey + operationId: kas.AccessService.PublicKey parameters: - - name: algorithm - in: query + - name: Connect-Protocol-Version + in: header + required: true schema: - type: string - title: algorithm + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/kas.PublicKeyRequest' + required: true responses: default: description: Error @@ -67,13 +76,23 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/google.protobuf.StringValue' - /kas/v2/rewrap: + $ref: '#/components/schemas/kas.PublicKeyResponse' + /kas.AccessService/Rewrap: post: tags: - kas.AccessService summary: Rewrap operationId: kas.AccessService.Rewrap + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' requestBody: content: application/json: @@ -95,16 +114,75 @@ paths: $ref: '#/components/schemas/kas.RewrapResponse' components: schemas: - google.protobuf.NullValue: - type: string - title: NullValue + connect-protocol-version: + type: number + title: Connect-Protocol-Version enum: - - NULL_VALUE - description: |- - `NullValue` is a singleton enumeration to represent the null value for the - `Value` type union. - - The JSON representation for `NullValue` is JSON `null`. + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.ListValue: type: object properties: @@ -120,6 +198,16 @@ components: `ListValue` is a wrapper around a repeated field of values. The JSON representation for `ListValue` is JSON array. + google.protobuf.NullValue: + type: string + title: NullValue + enum: + - NULL_VALUE + description: |- + `NullValue` is a singleton enumeration to represent the null value for the + `Value` type union. + + The JSON representation for `NullValue` is JSON `null`. google.protobuf.StringValue: type: string description: |- @@ -214,7 +302,7 @@ components: description: |- Type of key wrapping used for the data encryption key Required: Always - Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped) + Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped), 'hybrid-wrapped' (experimental X-Wing-wrapped) url: type: string title: kas_url @@ -252,9 +340,8 @@ components: title: header format: byte description: |- - Complete NanoTDF header containing all metadata and policy information - Required: NanoTDF only - ZTDF: Omitted (policy and metadata are separate) + Complete header containing all metadata and policy information (for formats that embed it) + Optional: Not used by ZTDF (policy and metadata are separate) Contains magic bytes, version, algorithm, policy, and ephemeral key information ephemeralPublicKey: type: string @@ -262,7 +349,7 @@ components: description: |- Ephemeral public key for ECDH key derivation (ec-wrapped type only) Required: When key_type="ec-wrapped" (experimental ECDH-based ZTDF) - Omitted: When key_type="wrapped" (RSA-based ZTDF) + Omitted: When key_type="wrapped" or key_type="hybrid-wrapped" Should be a PEM-encoded PKCS#8 (ASN.1) formatted public key Used to derive the symmetric key for unwrapping the DEK title: KeyAccess @@ -270,54 +357,57 @@ components: description: Key Access Object containing cryptographic material and metadata for TDF decryption kas.KeyAccessRewrapResult: type: object - oneOf: + allOf: - properties: - error: + metadata: + type: object + title: metadata + additionalProperties: + title: value + $ref: '#/components/schemas/google.protobuf.Value' + description: |- + Metadata associated with this KAO result (e.g., required obligations) + Optional: May contain obligation requirements or other policy metadata + Common keys: "X-Required-Obligations" with array of obligation FQNs + keyAccessObjectId: type: string - title: error + title: key_access_object_id description: |- - Error message when rewrap failed - Present when status="fail" - Human-readable description of the failure reason - title: error - required: - - error - - properties: - kasWrappedKey: + Identifier matching the key_access_object_id from the request + Required: Always matches the ID from UnsignedRewrapRequest_WithKeyAccessObject + status: type: string - title: kas_wrapped_key - format: byte + title: status description: |- - Successfully rewrapped key encrypted with the session key - Present when status="permit" - Contains the DEK encrypted with the ephemeral session key - title: kas_wrapped_key - required: - - kasWrappedKey - properties: - metadata: - type: object - title: metadata - additionalProperties: - title: value - $ref: '#/components/schemas/google.protobuf.Value' - description: |- - Metadata associated with this KAO result (e.g., required obligations) - Optional: May contain obligation requirements or other policy metadata - Common keys: "X-Required-Obligations" with array of obligation FQNs - keyAccessObjectId: - type: string - title: key_access_object_id - description: |- - Identifier matching the key_access_object_id from the request - Required: Always matches the ID from UnsignedRewrapRequest_WithKeyAccessObject - status: - type: string - title: status - description: |- - Status of the rewrap operation for this KAO - Required: Always - Values: "permit" (success), "fail" (failure) + Status of the rewrap operation for this KAO + Required: Always + Values: "permit" (success), "fail" (failure) + - oneOf: + - type: object + properties: + error: + type: string + title: error + description: |- + Error message when rewrap failed + Present when status="fail" + Human-readable description of the failure reason + title: error + required: + - error + - type: object + properties: + kasWrappedKey: + type: string + title: kas_wrapped_key + format: byte + description: |- + Successfully rewrapped key encrypted with the session key + Present when status="permit" + Contains the DEK encrypted with the ephemeral session key + title: kas_wrapped_key + required: + - kasWrappedKey title: KeyAccessRewrapResult additionalProperties: false description: Result of a key access object rewrap operation @@ -451,8 +541,8 @@ components: title: session_public_key description: |- KAS's ephemeral session public key in PEM format - Required: For EC-based operations (NanoTDF and ZTDF with key_type="ec-wrapped") - Optional: Empty for RSA-based ZTDF (key_type="wrapped") + Required: For EC-based operations (key_type="ec-wrapped") + Optional: Empty for RSA-based or X-Wing-based ZTDF (key_type="wrapped" or key_type="hybrid-wrapped") Used by client to perform ECDH key agreement and decrypt the kas_wrapped_key values schemaVersion: type: string @@ -581,8 +671,7 @@ components: description: |- List of Key Access Objects associated with this policy Required: Always (at least one) - NanoTDF: Exactly one KAO per policy - ZTDF: One or more KAOs per policy + Some formats require exactly one KAO per policy policy: title: policy description: |- @@ -595,68 +684,11 @@ components: description: |- Cryptographic algorithm identifier for the TDF type Optional: Defaults to rsa:2048 if omitted - Values: "ec:secp256r1" (NanoTDF), "rsa:2048" (ZTDF), "" (defaults to rsa:2048) + Values: "ec:secp256r1" (EC-based), "rsa:2048" (RSA-based), "" (defaults to rsa:2048) Example: "ec:secp256r1" title: WithPolicyRequest additionalProperties: false description: Request grouping policy with associated key access objects - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: kas.AccessService diff --git a/docs/openapi/policy/actions/actions.openapi.yaml b/docs/openapi/policy/actions/actions.openapi.yaml index 9474783ab8..b5c6f2c12b 100644 --- a/docs/openapi/policy/actions/actions.openapi.yaml +++ b/docs/openapi/policy/actions/actions.openapi.yaml @@ -2,12 +2,12 @@ openapi: 3.1.0 info: title: policy.actions paths: - /policy.actions.ActionService/GetAction: + /policy.actions.ActionService/CreateAction: post: tags: - policy.actions.ActionService - summary: GetAction - operationId: policy.actions.ActionService.GetAction + summary: CreateAction + operationId: policy.actions.ActionService.CreateAction parameters: - name: Connect-Protocol-Version in: header @@ -22,7 +22,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.GetActionRequest' + $ref: '#/components/schemas/policy.actions.CreateActionRequest' required: true responses: default: @@ -36,13 +36,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.GetActionResponse' - /policy.actions.ActionService/ListActions: + $ref: '#/components/schemas/policy.actions.CreateActionResponse' + /policy.actions.ActionService/DeleteAction: post: tags: - policy.actions.ActionService - summary: ListActions - operationId: policy.actions.ActionService.ListActions + summary: DeleteAction + operationId: policy.actions.ActionService.DeleteAction parameters: - name: Connect-Protocol-Version in: header @@ -57,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.ListActionsRequest' + $ref: '#/components/schemas/policy.actions.DeleteActionRequest' required: true responses: default: @@ -71,13 +71,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.ListActionsResponse' - /policy.actions.ActionService/CreateAction: + $ref: '#/components/schemas/policy.actions.DeleteActionResponse' + /policy.actions.ActionService/GetAction: post: tags: - policy.actions.ActionService - summary: CreateAction - operationId: policy.actions.ActionService.CreateAction + summary: GetAction + operationId: policy.actions.ActionService.GetAction parameters: - name: Connect-Protocol-Version in: header @@ -92,7 +92,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.CreateActionRequest' + $ref: '#/components/schemas/policy.actions.GetActionRequest' required: true responses: default: @@ -106,13 +106,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.CreateActionResponse' - /policy.actions.ActionService/UpdateAction: + $ref: '#/components/schemas/policy.actions.GetActionResponse' + /policy.actions.ActionService/ListActions: post: tags: - policy.actions.ActionService - summary: UpdateAction - operationId: policy.actions.ActionService.UpdateAction + summary: ListActions + operationId: policy.actions.ActionService.ListActions parameters: - name: Connect-Protocol-Version in: header @@ -127,7 +127,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.UpdateActionRequest' + $ref: '#/components/schemas/policy.actions.ListActionsRequest' required: true responses: default: @@ -141,13 +141,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.UpdateActionResponse' - /policy.actions.ActionService/DeleteAction: + $ref: '#/components/schemas/policy.actions.ListActionsResponse' + /policy.actions.ActionService/UpdateAction: post: tags: - policy.actions.ActionService - summary: DeleteAction - operationId: policy.actions.ActionService.DeleteAction + summary: UpdateAction + operationId: policy.actions.ActionService.UpdateAction parameters: - name: Connect-Protocol-Version in: header @@ -162,7 +162,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.DeleteActionRequest' + $ref: '#/components/schemas/policy.actions.UpdateActionRequest' required: true responses: default: @@ -176,78 +176,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.actions.DeleteActionResponse' + $ref: '#/components/schemas/policy.actions.UpdateActionResponse' components: schemas: - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.AttributeRuleTypeEnum: - type: string - title: AttributeRuleTypeEnum - enum: - - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED - - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF - - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF - - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY - policy.ConditionBooleanTypeEnum: - type: string - title: ConditionBooleanTypeEnum - enum: - - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED - - CONDITION_BOOLEAN_TYPE_ENUM_AND - - CONDITION_BOOLEAN_TYPE_ENUM_OR - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - policy.SubjectMappingOperatorEnum: - type: string - title: SubjectMappingOperatorEnum - enum: - - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED - - SUBJECT_MAPPING_OPERATOR_ENUM_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS common.Metadata: type: object properties: @@ -303,6 +234,82 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -315,8 +322,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -410,37 +417,65 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: type: string + title: id + description: Generated uuid in database + name: + type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard - properties: - id: - type: string - title: id - description: Generated uuid in database - name: - type: string - title: name - metadata: - title: metadata - $ref: '#/components/schemas/common.Metadata' + required: + - standard title: Action additionalProperties: false description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.Attribute: type: object properties: @@ -483,6 +518,12 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys associated with the attribute + allowTraversal: + title: allow_traversal + description: |- + Whether or not we will use the attribute definition during encryption + if the attribute value is missing. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Common metadata @@ -491,23 +532,14 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.AttributeRuleTypeEnum: + type: string + title: AttributeRuleTypeEnum + enum: + - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED + - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY policy.Condition: type: object properties: @@ -525,7 +557,6 @@ components: type: array items: type: string - minItems: 1 title: subject_external_values minItems: 1 description: |- @@ -541,6 +572,13 @@ components: * A Condition defines a rule of + policy.ConditionBooleanTypeEnum: + type: string + title: ConditionBooleanTypeEnum + enum: + - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED + - CONDITION_BOOLEAN_TYPE_ENUM_AND + - CONDITION_BOOLEAN_TYPE_ENUM_OR policy.ConditionGroup: type: object properties: @@ -577,18 +615,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -611,13 +662,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -680,12 +727,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: @@ -733,6 +774,10 @@ components: items: $ref: '#/components/schemas/policy.RequestContext' title: context + namespace: + title: namespace + description: The source namespace for this trigger, derived from the attribute value and action. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -817,7 +862,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -825,17 +871,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -896,6 +939,10 @@ components: description: |- the common name for the group of resource mappings, which must be unique per namespace + fqn: + type: string + title: fqn + description: the fully qualified name of the resource mapping group metadata: title: metadata description: Common metadata @@ -939,12 +986,30 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.SubjectConditionSet: type: object properties: id: type: string title: id + namespace: + title: namespace + description: |- + the namespace containing this subject condition set + possible this is empty in the case a subject condition set + has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' subjectSets: type: array items: @@ -982,6 +1047,13 @@ components: $ref: '#/components/schemas/policy.Action' title: actions description: The actions permitted by subjects in this mapping + namespace: + title: namespace + description: |- + the namespace containing this subject mapping + possible this is empty. If so that means + the Subject Mapping has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -990,6 +1062,14 @@ components: description: |- Subject Mapping: A Policy assigning Subject Set(s) to a permitted attribute value + action(s) combination + policy.SubjectMappingOperatorEnum: + type: string + title: SubjectMappingOperatorEnum + enum: + - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + - SUBJECT_MAPPING_OPERATOR_ENUM_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS policy.SubjectSet: type: object properties: @@ -1063,13 +1143,24 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Required - Action name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored action name will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + action_name_format // Action name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored action name will be normalized to lower case. + namespaceId: + type: string + title: namespace_id + format: uuid + description: |- + Optional namespace ID for the custom action. + If omitted, create targets legacy (namespace_id = NULL) behavior unless enforced by server config. + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri + description: |- + Optional namespace FQN for the custom action. + If omitted, create targets legacy (namespace_id = NULL) behavior unless enforced by server config. metadata: title: metadata description: Optional @@ -1110,29 +1201,44 @@ components: additionalProperties: false policy.actions.GetActionRequest: type: object - oneOf: + allOf: - properties: - id: + namespaceId: type: string - title: id + title: namespace_id format: uuid - title: id - required: - - id - - properties: - name: + description: |- + Optional namespace ID to scope name-based lookup. + If omitted for name-based lookup, action search is limited to legacy (namespace_id = NULL) actions. + namespaceFqn: type: string + title: namespace_fqn + minLength: 1 + format: uri + description: |- + Optional namespace FQN to scope name-based lookup. + If omitted for name-based lookup, action search is limited to legacy (namespace_id = NULL) actions. + - oneOf: + - type: object + properties: + id: + type: string + title: id + format: uuid + title: id + required: + - id + - type: object + properties: + name: + type: string + title: name + maxLength: 253 + description: | + action_name_format // Action name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored action name will be normalized to lower case. title: name - maxLength: 253 - description: |+ - Action name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored action name will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - - title: name - required: - - name + required: + - name title: GetActionRequest additionalProperties: false policy.actions.GetActionResponse: @@ -1152,6 +1258,17 @@ components: policy.actions.ListActionsRequest: type: object properties: + namespaceId: + type: string + title: namespace_id + format: uuid + description: ID of the namespace to scope results. If omitted, returns actions across namespaces. + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri + description: FQN of the namespace to scope results. If omitted, returns actions across namespaces. pagination: title: pagination description: Optional @@ -1188,14 +1305,10 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Optional Custom actions only: replaces the existing action name - Action name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored action name will be normalized to lower case.: - ``` - size(this) == 0 || this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + action_name_format // Action name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored action name will be normalized to lower case. metadata: title: metadata description: Common metadata @@ -1216,63 +1329,6 @@ components: $ref: '#/components/schemas/policy.Action' title: UpdateActionResponse additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.actions.ActionService diff --git a/docs/openapi/policy/attributes/attributes.openapi.yaml b/docs/openapi/policy/attributes/attributes.openapi.yaml index 7ff9791913..bb6c0978fe 100644 --- a/docs/openapi/policy/attributes/attributes.openapi.yaml +++ b/docs/openapi/policy/attributes/attributes.openapi.yaml @@ -2,16 +2,13 @@ openapi: 3.1.0 info: title: policy.attributes paths: - /policy.attributes.AttributesService/ListAttributes: + /policy.attributes.AttributesService/AssignKeyAccessServerToAttribute: post: tags: - policy.attributes.AttributesService - summary: ListAttributes - description: |- - --------------------------------------* - Attribute RPCs - --------------------------------------- - operationId: policy.attributes.AttributesService.ListAttributes + summary: AssignKeyAccessServerToAttribute + description: 'Deprecated: utilize AssignPublicKeyToAttribute' + operationId: policy.attributes.AttributesService.AssignKeyAccessServerToAttribute parameters: - name: Connect-Protocol-Version in: header @@ -26,7 +23,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.ListAttributesRequest' + $ref: '#/components/schemas/policy.attributes.AssignKeyAccessServerToAttributeRequest' required: true responses: default: @@ -40,13 +37,15 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.ListAttributesResponse' - /policy.attributes.AttributesService/ListAttributeValues: + $ref: '#/components/schemas/policy.attributes.AssignKeyAccessServerToAttributeResponse' + deprecated: true + /policy.attributes.AttributesService/AssignKeyAccessServerToValue: post: tags: - policy.attributes.AttributesService - summary: ListAttributeValues - operationId: policy.attributes.AttributesService.ListAttributeValues + summary: AssignKeyAccessServerToValue + description: 'Deprecated: utilize AssignPublicKeyToValue' + operationId: policy.attributes.AttributesService.AssignKeyAccessServerToValue parameters: - name: Connect-Protocol-Version in: header @@ -61,7 +60,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.ListAttributeValuesRequest' + $ref: '#/components/schemas/policy.attributes.AssignKeyAccessServerToValueRequest' required: true responses: default: @@ -75,13 +74,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.ListAttributeValuesResponse' - /policy.attributes.AttributesService/GetAttribute: + $ref: '#/components/schemas/policy.attributes.AssignKeyAccessServerToValueResponse' + deprecated: true + /policy.attributes.AttributesService/AssignPublicKeyToAttribute: post: tags: - policy.attributes.AttributesService - summary: GetAttribute - operationId: policy.attributes.AttributesService.GetAttribute + summary: AssignPublicKeyToAttribute + operationId: policy.attributes.AttributesService.AssignPublicKeyToAttribute parameters: - name: Connect-Protocol-Version in: header @@ -96,7 +96,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.GetAttributeRequest' + $ref: '#/components/schemas/policy.attributes.AssignPublicKeyToAttributeRequest' required: true responses: default: @@ -110,31 +110,29 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.GetAttributeResponse' - /attributes/*/fqn: - get: + $ref: '#/components/schemas/policy.attributes.AssignPublicKeyToAttributeResponse' + /policy.attributes.AttributesService/AssignPublicKeyToValue: + post: tags: - policy.attributes.AttributesService - summary: GetAttributeValuesByFqns - operationId: policy.attributes.AttributesService.GetAttributeValuesByFqns + summary: AssignPublicKeyToValue + operationId: policy.attributes.AttributesService.AssignPublicKeyToValue parameters: - - name: fqns - in: query - description: |- - Required - Fully Qualified Names of attribute values (i.e. https:///attr//value/), normalized to lower case. + - name: Connect-Protocol-Version + in: header + required: true schema: - type: array - items: - type: string - maxItems: 250 - minItems: 1 - title: fqns - maxItems: 250 - minItems: 1 - description: |- - Required - Fully Qualified Names of attribute values (i.e. https:///attr//value/), normalized to lower case. + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/policy.attributes.AssignPublicKeyToValueRequest' + required: true responses: default: description: Error @@ -147,7 +145,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.GetAttributeValuesByFqnsResponse' + $ref: '#/components/schemas/policy.attributes.AssignPublicKeyToValueResponse' /policy.attributes.AttributesService/CreateAttribute: post: tags: @@ -183,12 +181,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.attributes.CreateAttributeResponse' - /policy.attributes.AttributesService/UpdateAttribute: + /policy.attributes.AttributesService/CreateAttributeValue: post: tags: - policy.attributes.AttributesService - summary: UpdateAttribute - operationId: policy.attributes.AttributesService.UpdateAttribute + summary: CreateAttributeValue + operationId: policy.attributes.AttributesService.CreateAttributeValue parameters: - name: Connect-Protocol-Version in: header @@ -203,7 +201,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.UpdateAttributeRequest' + $ref: '#/components/schemas/policy.attributes.CreateAttributeValueRequest' required: true responses: default: @@ -217,7 +215,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.UpdateAttributeResponse' + $ref: '#/components/schemas/policy.attributes.CreateAttributeValueResponse' /policy.attributes.AttributesService/DeactivateAttribute: post: tags: @@ -253,16 +251,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.attributes.DeactivateAttributeResponse' - /policy.attributes.AttributesService/GetAttributeValue: + /policy.attributes.AttributesService/DeactivateAttributeValue: post: tags: - policy.attributes.AttributesService - summary: GetAttributeValue - description: |- - --------------------------------------* - Value RPCs - --------------------------------------- - operationId: policy.attributes.AttributesService.GetAttributeValue + summary: DeactivateAttributeValue + operationId: policy.attributes.AttributesService.DeactivateAttributeValue parameters: - name: Connect-Protocol-Version in: header @@ -277,7 +271,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.GetAttributeValueRequest' + $ref: '#/components/schemas/policy.attributes.DeactivateAttributeValueRequest' required: true responses: default: @@ -291,13 +285,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.GetAttributeValueResponse' - /policy.attributes.AttributesService/CreateAttributeValue: + $ref: '#/components/schemas/policy.attributes.DeactivateAttributeValueResponse' + /policy.attributes.AttributesService/GetAttribute: post: tags: - policy.attributes.AttributesService - summary: CreateAttributeValue - operationId: policy.attributes.AttributesService.CreateAttributeValue + summary: GetAttribute + operationId: policy.attributes.AttributesService.GetAttribute parameters: - name: Connect-Protocol-Version in: header @@ -312,7 +306,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.CreateAttributeValueRequest' + $ref: '#/components/schemas/policy.attributes.GetAttributeRequest' required: true responses: default: @@ -326,13 +320,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.CreateAttributeValueResponse' - /policy.attributes.AttributesService/UpdateAttributeValue: + $ref: '#/components/schemas/policy.attributes.GetAttributeResponse' + /policy.attributes.AttributesService/GetAttributeValue: post: tags: - policy.attributes.AttributesService - summary: UpdateAttributeValue - operationId: policy.attributes.AttributesService.UpdateAttributeValue + summary: GetAttributeValue + description: |- + --------------------------------------* + Value RPCs + --------------------------------------- + operationId: policy.attributes.AttributesService.GetAttributeValue parameters: - name: Connect-Protocol-Version in: header @@ -347,7 +345,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.UpdateAttributeValueRequest' + $ref: '#/components/schemas/policy.attributes.GetAttributeValueRequest' required: true responses: default: @@ -361,13 +359,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.UpdateAttributeValueResponse' - /policy.attributes.AttributesService/DeactivateAttributeValue: + $ref: '#/components/schemas/policy.attributes.GetAttributeValueResponse' + /policy.attributes.AttributesService/GetAttributeValuesByFqns: post: tags: - policy.attributes.AttributesService - summary: DeactivateAttributeValue - operationId: policy.attributes.AttributesService.DeactivateAttributeValue + summary: GetAttributeValuesByFqns + operationId: policy.attributes.AttributesService.GetAttributeValuesByFqns parameters: - name: Connect-Protocol-Version in: header @@ -382,7 +380,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.DeactivateAttributeValueRequest' + $ref: '#/components/schemas/policy.attributes.GetAttributeValuesByFqnsRequest' required: true responses: default: @@ -396,14 +394,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.DeactivateAttributeValueResponse' - /policy.attributes.AttributesService/AssignKeyAccessServerToAttribute: + $ref: '#/components/schemas/policy.attributes.GetAttributeValuesByFqnsResponse' + /policy.attributes.AttributesService/ListAttributeValues: post: tags: - policy.attributes.AttributesService - summary: AssignKeyAccessServerToAttribute - description: 'Deprecated: utilize AssignPublicKeyToAttribute' - operationId: policy.attributes.AttributesService.AssignKeyAccessServerToAttribute + summary: ListAttributeValues + description: |- + Deprecated + Use GetAttribute + operationId: policy.attributes.AttributesService.ListAttributeValues parameters: - name: Connect-Protocol-Version in: header @@ -418,7 +418,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.AssignKeyAccessServerToAttributeRequest' + $ref: '#/components/schemas/policy.attributes.ListAttributeValuesRequest' required: true responses: default: @@ -432,15 +432,18 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.AssignKeyAccessServerToAttributeResponse' + $ref: '#/components/schemas/policy.attributes.ListAttributeValuesResponse' deprecated: true - /policy.attributes.AttributesService/RemoveKeyAccessServerFromAttribute: + /policy.attributes.AttributesService/ListAttributes: post: tags: - policy.attributes.AttributesService - summary: RemoveKeyAccessServerFromAttribute - description: 'Deprecated: utilize RemovePublicKeyFromAttribute' - operationId: policy.attributes.AttributesService.RemoveKeyAccessServerFromAttribute + summary: ListAttributes + description: |- + --------------------------------------* + Attribute RPCs + --------------------------------------- + operationId: policy.attributes.AttributesService.ListAttributes parameters: - name: Connect-Protocol-Version in: header @@ -455,7 +458,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.RemoveKeyAccessServerFromAttributeRequest' + $ref: '#/components/schemas/policy.attributes.ListAttributesRequest' required: true responses: default: @@ -469,15 +472,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.RemoveKeyAccessServerFromAttributeResponse' - deprecated: true - /policy.attributes.AttributesService/AssignKeyAccessServerToValue: + $ref: '#/components/schemas/policy.attributes.ListAttributesResponse' + /policy.attributes.AttributesService/RemoveKeyAccessServerFromAttribute: post: tags: - policy.attributes.AttributesService - summary: AssignKeyAccessServerToValue - description: 'Deprecated: utilize AssignPublicKeyToValue' - operationId: policy.attributes.AttributesService.AssignKeyAccessServerToValue + summary: RemoveKeyAccessServerFromAttribute + description: 'Deprecated: utilize RemovePublicKeyFromAttribute' + operationId: policy.attributes.AttributesService.RemoveKeyAccessServerFromAttribute parameters: - name: Connect-Protocol-Version in: header @@ -492,7 +494,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.AssignKeyAccessServerToValueRequest' + $ref: '#/components/schemas/policy.attributes.RemoveKeyAccessServerFromAttributeRequest' required: true responses: default: @@ -506,7 +508,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.AssignKeyAccessServerToValueResponse' + $ref: '#/components/schemas/policy.attributes.RemoveKeyAccessServerFromAttributeResponse' deprecated: true /policy.attributes.AttributesService/RemoveKeyAccessServerFromValue: post: @@ -545,12 +547,12 @@ paths: schema: $ref: '#/components/schemas/policy.attributes.RemoveKeyAccessServerFromValueResponse' deprecated: true - /policy.attributes.AttributesService/AssignPublicKeyToAttribute: + /policy.attributes.AttributesService/RemovePublicKeyFromAttribute: post: tags: - policy.attributes.AttributesService - summary: AssignPublicKeyToAttribute - operationId: policy.attributes.AttributesService.AssignPublicKeyToAttribute + summary: RemovePublicKeyFromAttribute + operationId: policy.attributes.AttributesService.RemovePublicKeyFromAttribute parameters: - name: Connect-Protocol-Version in: header @@ -565,7 +567,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.AssignPublicKeyToAttributeRequest' + $ref: '#/components/schemas/policy.attributes.RemovePublicKeyFromAttributeRequest' required: true responses: default: @@ -579,13 +581,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.AssignPublicKeyToAttributeResponse' - /policy.attributes.AttributesService/RemovePublicKeyFromAttribute: + $ref: '#/components/schemas/policy.attributes.RemovePublicKeyFromAttributeResponse' + /policy.attributes.AttributesService/RemovePublicKeyFromValue: post: tags: - policy.attributes.AttributesService - summary: RemovePublicKeyFromAttribute - operationId: policy.attributes.AttributesService.RemovePublicKeyFromAttribute + summary: RemovePublicKeyFromValue + operationId: policy.attributes.AttributesService.RemovePublicKeyFromValue parameters: - name: Connect-Protocol-Version in: header @@ -600,7 +602,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.RemovePublicKeyFromAttributeRequest' + $ref: '#/components/schemas/policy.attributes.RemovePublicKeyFromValueRequest' required: true responses: default: @@ -614,13 +616,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.RemovePublicKeyFromAttributeResponse' - /policy.attributes.AttributesService/AssignPublicKeyToValue: + $ref: '#/components/schemas/policy.attributes.RemovePublicKeyFromValueResponse' + /policy.attributes.AttributesService/UpdateAttribute: post: tags: - policy.attributes.AttributesService - summary: AssignPublicKeyToValue - operationId: policy.attributes.AttributesService.AssignPublicKeyToValue + summary: UpdateAttribute + operationId: policy.attributes.AttributesService.UpdateAttribute parameters: - name: Connect-Protocol-Version in: header @@ -635,7 +637,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.AssignPublicKeyToValueRequest' + $ref: '#/components/schemas/policy.attributes.UpdateAttributeRequest' required: true responses: default: @@ -649,13 +651,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.AssignPublicKeyToValueResponse' - /policy.attributes.AttributesService/RemovePublicKeyFromValue: + $ref: '#/components/schemas/policy.attributes.UpdateAttributeResponse' + /policy.attributes.AttributesService/UpdateAttributeValue: post: tags: - policy.attributes.AttributesService - summary: RemovePublicKeyFromValue - operationId: policy.attributes.AttributesService.RemovePublicKeyFromValue + summary: UpdateAttributeValue + operationId: policy.attributes.AttributesService.UpdateAttributeValue parameters: - name: Connect-Protocol-Version in: header @@ -670,7 +672,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.RemovePublicKeyFromValueRequest' + $ref: '#/components/schemas/policy.attributes.UpdateAttributeValueRequest' required: true responses: default: @@ -684,7 +686,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.attributes.RemovePublicKeyFromValueResponse' + $ref: '#/components/schemas/policy.attributes.UpdateAttributeValueResponse' components: schemas: common.ActiveStateEnum: @@ -696,75 +698,48 @@ components: - ACTIVE_STATE_ENUM_INACTIVE - ACTIVE_STATE_ENUM_ANY description: 'buflint ENUM_VALUE_PREFIX: to make sure that C++ scoping rules aren''t violated when users add new enum values to an enum in a given package' - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.AttributeRuleTypeEnum: - type: string - title: AttributeRuleTypeEnum - enum: - - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED - - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF - - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF - - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY - policy.ConditionBooleanTypeEnum: - type: string - title: ConditionBooleanTypeEnum - enum: - - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED - - CONDITION_BOOLEAN_TYPE_ENUM_AND - - CONDITION_BOOLEAN_TYPE_ENUM_OR - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - policy.SubjectMappingOperatorEnum: - type: string - title: SubjectMappingOperatorEnum - enum: - - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED - - SUBJECT_MAPPING_OPERATOR_ENUM_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS + common.IdFqnIdentifier: + type: object + allOf: + - oneOf: + - required: + - id + - required: + - fqn + properties: + id: + type: string + title: id + format: uuid + fqn: + type: string + title: fqn + minLength: 1 + format: uri + title: IdFqnIdentifier + additionalProperties: false + common.IdNameIdentifier: + type: object + allOf: + - oneOf: + - required: + - id + - required: + - name + properties: + id: + type: string + title: id + format: uuid + name: + type: string + title: name + maxLength: 253 + minLength: 1 + description: | + name_format // Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case. + title: IdNameIdentifier + additionalProperties: false common.Metadata: type: object properties: @@ -812,14 +787,90 @@ components: common.MetadataMutable.LabelsEntry: type: object properties: - key: + key: + type: string + title: key + value: + type: string + title: value + title: LabelsEntry + additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: type: string - title: key + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' value: type: string - title: value - title: LabelsEntry - additionalProperties: false + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -832,8 +883,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -927,37 +978,65 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: type: string + title: id + description: Generated uuid in database + name: + type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard - properties: - id: - type: string - title: id - description: Generated uuid in database - name: - type: string - title: name - metadata: - title: metadata - $ref: '#/components/schemas/common.Metadata' + required: + - standard title: Action additionalProperties: false description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.Attribute: type: object properties: @@ -1000,6 +1079,12 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys associated with the attribute + allowTraversal: + title: allow_traversal + description: |- + Whether or not we will use the attribute definition during encryption + if the attribute value is missing. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Common metadata @@ -1008,23 +1093,14 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.AttributeRuleTypeEnum: + type: string + title: AttributeRuleTypeEnum + enum: + - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED + - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY policy.Condition: type: object properties: @@ -1042,7 +1118,6 @@ components: type: array items: type: string - minItems: 1 title: subject_external_values minItems: 1 description: |- @@ -1058,6 +1133,13 @@ components: * A Condition defines a rule of + policy.ConditionBooleanTypeEnum: + type: string + title: ConditionBooleanTypeEnum + enum: + - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED + - CONDITION_BOOLEAN_TYPE_ENUM_AND + - CONDITION_BOOLEAN_TYPE_ENUM_OR policy.ConditionGroup: type: object properties: @@ -1094,18 +1176,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -1128,13 +1223,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -1197,12 +1288,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: @@ -1250,6 +1335,10 @@ components: items: $ref: '#/components/schemas/policy.RequestContext' title: context + namespace: + title: namespace + description: The source namespace for this trigger, derived from the attribute value and action. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1334,7 +1423,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -1342,17 +1432,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -1413,6 +1500,10 @@ components: description: |- the common name for the group of resource mappings, which must be unique per namespace + fqn: + type: string + title: fqn + description: the fully qualified name of the resource mapping group metadata: title: metadata description: Common metadata @@ -1456,12 +1547,42 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC + description: |- + Sorting direction shared across list APIs. + When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, + the endpoint's request message defines the default ordering; see the + specific List* request docs. + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.SubjectConditionSet: type: object properties: id: type: string title: id + namespace: + title: namespace + description: |- + the namespace containing this subject condition set + possible this is empty in the case a subject condition set + has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' subjectSets: type: array items: @@ -1499,6 +1620,13 @@ components: $ref: '#/components/schemas/policy.Action' title: actions description: The actions permitted by subjects in this mapping + namespace: + title: namespace + description: |- + the namespace containing this subject mapping + possible this is empty. If so that means + the Subject Mapping has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1507,6 +1635,14 @@ components: description: |- Subject Mapping: A Policy assigning Subject Set(s) to a permitted attribute value + action(s) combination + policy.SubjectMappingOperatorEnum: + type: string + title: SubjectMappingOperatorEnum + enum: + - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + - SUBJECT_MAPPING_OPERATOR_ENUM_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS policy.SubjectSet: type: object properties: @@ -1683,6 +1819,41 @@ components: title: AttributeKeyAccessServer additionalProperties: false description: Deprecated + policy.attributes.AttributeValueObligationTriggerRequest: + type: object + properties: + obligationValue: + title: obligation_value + description: Required. Existing obligation value to associate with the newly created attribute value. + $ref: '#/components/schemas/common.IdFqnIdentifier' + action: + title: action + description: Required. Action that, together with the newly created attribute value, triggers the obligation value. + $ref: '#/components/schemas/common.IdNameIdentifier' + context: + title: context + description: Optional. Request context for the obligation trigger. + $ref: '#/components/schemas/policy.RequestContext' + metadata: + title: metadata + description: Optional. Common metadata for the obligation trigger. + $ref: '#/components/schemas/common.MetadataMutable' + title: AttributeValueObligationTriggerRequest + required: + - obligationValue + - action + additionalProperties: false + policy.attributes.AttributesSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.attributes.SortAttributesType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: AttributesSort + additionalProperties: false policy.attributes.CreateAttributeRequest: type: object properties: @@ -1695,13 +1866,9 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Required - Attribute name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored attribute name will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + attribute_name_format // Attribute name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored attribute name will be normalized to lower case. rule: title: rule description: Required @@ -1712,13 +1879,22 @@ components: type: string maxLength: 253 pattern: ^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$ - uniqueItems: true title: values uniqueItems: true description: |- Optional Attribute values (when provided) must be alphanumeric strings, allowing hyphens and underscores but not as the first or last character. The stored attribute value will be normalized to lower case. + allowTraversal: + title: allow_traversal + description: |- + Optional + Setting allow_traversal=true allows TDF creation to be front-loaded, meaning a customer + can create encrypted content with an attribute definitions key mapping before + creating the attribute values needed to decrypt. + Content will be able to be encrypted with missing attribute values, + but will not be able to be decrypted until such attribute values exist. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Optional @@ -1748,13 +1924,17 @@ components: type: string title: value maxLength: 253 - description: |+ + description: | Required - Attribute value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored attribute value will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + attribute_value_format // Attribute value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored attribute value will be normalized to lower case. + obligationTriggers: + type: array + items: + $ref: '#/components/schemas/policy.attributes.AttributeValueObligationTriggerRequest' + title: obligation_triggers + description: |- + Optional + Existing obligation values to trigger for the newly created attribute value. metadata: title: metadata description: |- @@ -1811,45 +1991,40 @@ components: additionalProperties: false policy.attributes.GetAttributeRequest: type: object - oneOf: + allOf: - properties: - attributeId: + id: type: string - title: attribute_id + title: id format: uuid - description: 'option (buf.validate.oneof).required = true; // TODO: enable this when we remove the deprecated field' - title: attribute_id - required: - - attributeId - - properties: - fqn: - type: string + description: 'Deprecated: utilize identifier' + deprecated: true + - oneOf: + - type: object + properties: + attributeId: + type: string + title: attribute_id + format: uuid + description: 'option (buf.validate.oneof).required = true; // TODO: enable this when we remove the deprecated field' + title: attribute_id + required: + - attributeId + - type: object + properties: + fqn: + type: string + title: fqn + minLength: 1 + format: uri title: fqn - minLength: 1 - format: uri - title: fqn - required: - - fqn - properties: - id: - type: string - title: id - format: uuid - description: 'Deprecated: utilize identifier' - deprecated: true + required: + - fqn title: GetAttributeRequest additionalProperties: false - description: |+ - Either use deprecated 'id' field or one of 'attribute_id' or 'fqn', but not both: - ``` - !(has(this.id) && (has(this.attribute_id) || has(this.fqn))) - ``` - - Either id or one of attribute_id or fqn must be set: - ``` - has(this.id) || has(this.attribute_id) || has(this.fqn) - ``` - + description: | + exclusive_fields // Either use deprecated 'id' field or one of 'attribute_id' or 'fqn', but not both + required_fields // Either id or one of attribute_id or fqn must be set policy.attributes.GetAttributeResponse: type: object properties: @@ -1860,48 +2035,43 @@ components: additionalProperties: false policy.attributes.GetAttributeValueRequest: type: object - oneOf: + allOf: - properties: - fqn: + id: type: string + title: id + format: uuid + description: 'Deprecated: utilize identifier' + deprecated: true + - oneOf: + - type: object + properties: + fqn: + type: string + title: fqn + minLength: 1 + format: uri title: fqn - minLength: 1 - format: uri - title: fqn - required: - - fqn - - properties: - valueId: - type: string + required: + - fqn + - type: object + properties: + valueId: + type: string + title: value_id + format: uuid + description: 'option (buf.validate.oneof).required = true; // TODO: enable this when we remove the deprecated field' title: value_id - format: uuid - description: 'option (buf.validate.oneof).required = true; // TODO: enable this when we remove the deprecated field' - title: value_id - required: - - valueId - properties: - id: - type: string - title: id - format: uuid - description: 'Deprecated: utilize identifier' - deprecated: true + required: + - valueId title: GetAttributeValueRequest additionalProperties: false - description: |+ + description: | / / Value RPC messages / - Either use deprecated 'id' field or one of 'value_id' or 'fqn', but not both: - ``` - !(has(this.id) && (has(this.value_id) || has(this.fqn))) - ``` - - Either id or one of value_id or fqn must be set: - ``` - has(this.id) || has(this.value_id) || has(this.fqn) - ``` - + exclusive_fields // Either use deprecated 'id' field or one of 'value_id' or 'fqn', but not both + required_fields // Either id or one of value_id or fqn must be set policy.attributes.GetAttributeValueResponse: type: object properties: @@ -1917,8 +2087,6 @@ components: type: array items: type: string - maxItems: 250 - minItems: 1 title: fqns maxItems: 250 minItems: 1 @@ -2013,6 +2181,18 @@ components: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.attributes.AttributesSort' + title: sort + maxItems: 1 + description: |- + Optional - CONSTRAINT: max 1 item + Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC title: ListAttributesRequest additionalProperties: false policy.attributes.ListAttributesResponse: @@ -2104,6 +2284,14 @@ components: $ref: '#/components/schemas/policy.attributes.ValueKey' title: RemovePublicKeyFromValueResponse additionalProperties: false + policy.attributes.SortAttributesType: + type: string + title: SortAttributesType + enum: + - SORT_ATTRIBUTES_TYPE_UNSPECIFIED + - SORT_ATTRIBUTES_TYPE_NAME + - SORT_ATTRIBUTES_TYPE_CREATED_AT + - SORT_ATTRIBUTES_TYPE_UPDATED_AT policy.attributes.UpdateAttributeRequest: type: object properties: @@ -2189,63 +2377,6 @@ components: description: Required title: ValueKeyAccessServer additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.attributes.AttributesService diff --git a/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml b/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml index 864f189ec9..8d9f785054 100644 --- a/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml +++ b/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml @@ -2,39 +2,31 @@ openapi: 3.1.0 info: title: policy.kasregistry paths: - /key-access-servers: - get: + /policy.kasregistry.KeyAccessServerRegistryService/CreateKey: + post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: ListKeyAccessServers - operationId: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServers + summary: CreateKey + description: |- + KAS Key Management + Request to create a new key in the Key Access Service. + operationId: policy.kasregistry.KeyAccessServerRegistryService.CreateKey parameters: - - name: pagination.limit - in: query - description: |- - Optional - Set to configured default limit if not provided - Maximum limit set in platform config and enforced by services + - name: Connect-Protocol-Version + in: header + required: true schema: - type: integer - title: limit - format: int32 - description: |- - Optional - Set to configured default limit if not provided - Maximum limit set in platform config and enforced by services - - name: pagination.offset - in: query - description: |- - Optional - Defaulted if not provided + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header schema: - type: integer - title: offset - format: int32 - description: |- - Optional - Defaulted if not provided + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/policy.kasregistry.CreateKeyRequest' + required: true responses: default: description: Error @@ -47,13 +39,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.ListKeyAccessServersResponse' - /policy.kasregistry.KeyAccessServerRegistryService/GetKeyAccessServer: + $ref: '#/components/schemas/policy.kasregistry.CreateKeyResponse' + /policy.kasregistry.KeyAccessServerRegistryService/CreateKeyAccessServer: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: GetKeyAccessServer - operationId: policy.kasregistry.KeyAccessServerRegistryService.GetKeyAccessServer + summary: CreateKeyAccessServer + operationId: policy.kasregistry.KeyAccessServerRegistryService.CreateKeyAccessServer parameters: - name: Connect-Protocol-Version in: header @@ -68,7 +60,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.GetKeyAccessServerRequest' + $ref: '#/components/schemas/policy.kasregistry.CreateKeyAccessServerRequest' required: true responses: default: @@ -82,13 +74,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.GetKeyAccessServerResponse' - /policy.kasregistry.KeyAccessServerRegistryService/CreateKeyAccessServer: + $ref: '#/components/schemas/policy.kasregistry.CreateKeyAccessServerResponse' + /policy.kasregistry.KeyAccessServerRegistryService/DeleteKeyAccessServer: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: CreateKeyAccessServer - operationId: policy.kasregistry.KeyAccessServerRegistryService.CreateKeyAccessServer + summary: DeleteKeyAccessServer + operationId: policy.kasregistry.KeyAccessServerRegistryService.DeleteKeyAccessServer parameters: - name: Connect-Protocol-Version in: header @@ -103,7 +95,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.CreateKeyAccessServerRequest' + $ref: '#/components/schemas/policy.kasregistry.DeleteKeyAccessServerRequest' required: true responses: default: @@ -117,13 +109,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.CreateKeyAccessServerResponse' - /policy.kasregistry.KeyAccessServerRegistryService/UpdateKeyAccessServer: + $ref: '#/components/schemas/policy.kasregistry.DeleteKeyAccessServerResponse' + /policy.kasregistry.KeyAccessServerRegistryService/GetBaseKey: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: UpdateKeyAccessServer - operationId: policy.kasregistry.KeyAccessServerRegistryService.UpdateKeyAccessServer + summary: GetBaseKey + description: Get Default kas keys + operationId: policy.kasregistry.KeyAccessServerRegistryService.GetBaseKey parameters: - name: Connect-Protocol-Version in: header @@ -138,7 +131,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.UpdateKeyAccessServerRequest' + $ref: '#/components/schemas/policy.kasregistry.GetBaseKeyRequest' required: true responses: default: @@ -152,13 +145,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.UpdateKeyAccessServerResponse' - /policy.kasregistry.KeyAccessServerRegistryService/DeleteKeyAccessServer: + $ref: '#/components/schemas/policy.kasregistry.GetBaseKeyResponse' + /policy.kasregistry.KeyAccessServerRegistryService/GetKey: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: DeleteKeyAccessServer - operationId: policy.kasregistry.KeyAccessServerRegistryService.DeleteKeyAccessServer + summary: GetKey + description: Request to retrieve a key from the Key Access Service. + operationId: policy.kasregistry.KeyAccessServerRegistryService.GetKey parameters: - name: Connect-Protocol-Version in: header @@ -173,7 +167,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.DeleteKeyAccessServerRequest' + $ref: '#/components/schemas/policy.kasregistry.GetKeyRequest' required: true responses: default: @@ -187,14 +181,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.DeleteKeyAccessServerResponse' - /policy.kasregistry.KeyAccessServerRegistryService/ListKeyAccessServerGrants: + $ref: '#/components/schemas/policy.kasregistry.GetKeyResponse' + /policy.kasregistry.KeyAccessServerRegistryService/GetKeyAccessServer: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: ListKeyAccessServerGrants - description: Deprecated - operationId: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServerGrants + summary: GetKeyAccessServer + operationId: policy.kasregistry.KeyAccessServerRegistryService.GetKeyAccessServer parameters: - name: Connect-Protocol-Version in: header @@ -209,7 +202,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.ListKeyAccessServerGrantsRequest' + $ref: '#/components/schemas/policy.kasregistry.GetKeyAccessServerRequest' required: true responses: default: @@ -223,17 +216,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.ListKeyAccessServerGrantsResponse' - deprecated: true - /policy.kasregistry.KeyAccessServerRegistryService/CreateKey: + $ref: '#/components/schemas/policy.kasregistry.GetKeyAccessServerResponse' + /policy.kasregistry.KeyAccessServerRegistryService/ListKeyAccessServerGrants: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: CreateKey - description: |- - KAS Key Management - Request to create a new key in the Key Access Service. - operationId: policy.kasregistry.KeyAccessServerRegistryService.CreateKey + summary: ListKeyAccessServerGrants + description: Deprecated + operationId: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServerGrants parameters: - name: Connect-Protocol-Version in: header @@ -248,7 +238,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.CreateKeyRequest' + $ref: '#/components/schemas/policy.kasregistry.ListKeyAccessServerGrantsRequest' required: true responses: default: @@ -262,14 +252,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.CreateKeyResponse' - /policy.kasregistry.KeyAccessServerRegistryService/GetKey: + $ref: '#/components/schemas/policy.kasregistry.ListKeyAccessServerGrantsResponse' + deprecated: true + /policy.kasregistry.KeyAccessServerRegistryService/ListKeyAccessServers: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: GetKey - description: Request to retrieve a key from the Key Access Service. - operationId: policy.kasregistry.KeyAccessServerRegistryService.GetKey + summary: ListKeyAccessServers + operationId: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServers parameters: - name: Connect-Protocol-Version in: header @@ -284,7 +274,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.GetKeyRequest' + $ref: '#/components/schemas/policy.kasregistry.ListKeyAccessServersRequest' required: true responses: default: @@ -298,14 +288,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.GetKeyResponse' - /policy.kasregistry.KeyAccessServerRegistryService/ListKeys: + $ref: '#/components/schemas/policy.kasregistry.ListKeyAccessServersResponse' + /policy.kasregistry.KeyAccessServerRegistryService/ListKeyMappings: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: ListKeys - description: Request to list keys in the Key Access Service. - operationId: policy.kasregistry.KeyAccessServerRegistryService.ListKeys + summary: ListKeyMappings + description: Request to list key mappings in the Key Access Service. + operationId: policy.kasregistry.KeyAccessServerRegistryService.ListKeyMappings parameters: - name: Connect-Protocol-Version in: header @@ -320,7 +310,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.ListKeysRequest' + $ref: '#/components/schemas/policy.kasregistry.ListKeyMappingsRequest' required: true responses: default: @@ -334,14 +324,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.ListKeysResponse' - /policy.kasregistry.KeyAccessServerRegistryService/UpdateKey: + $ref: '#/components/schemas/policy.kasregistry.ListKeyMappingsResponse' + /policy.kasregistry.KeyAccessServerRegistryService/ListKeys: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: UpdateKey - description: Request to update a key in the Key Access Service. - operationId: policy.kasregistry.KeyAccessServerRegistryService.UpdateKey + summary: ListKeys + description: Request to list keys in the Key Access Service. + operationId: policy.kasregistry.KeyAccessServerRegistryService.ListKeys parameters: - name: Connect-Protocol-Version in: header @@ -356,7 +346,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.UpdateKeyRequest' + $ref: '#/components/schemas/policy.kasregistry.ListKeysRequest' required: true responses: default: @@ -370,7 +360,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.UpdateKeyResponse' + $ref: '#/components/schemas/policy.kasregistry.ListKeysResponse' /policy.kasregistry.KeyAccessServerRegistryService/RotateKey: post: tags: @@ -443,13 +433,13 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.kasregistry.SetBaseKeyResponse' - /policy.kasregistry.KeyAccessServerRegistryService/GetBaseKey: + /policy.kasregistry.KeyAccessServerRegistryService/UpdateKey: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: GetBaseKey - description: Get Default kas keys - operationId: policy.kasregistry.KeyAccessServerRegistryService.GetBaseKey + summary: UpdateKey + description: Request to update a key in the Key Access Service. + operationId: policy.kasregistry.KeyAccessServerRegistryService.UpdateKey parameters: - name: Connect-Protocol-Version in: header @@ -464,7 +454,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.GetBaseKeyRequest' + $ref: '#/components/schemas/policy.kasregistry.UpdateKeyRequest' required: true responses: default: @@ -478,14 +468,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.GetBaseKeyResponse' - /policy.kasregistry.KeyAccessServerRegistryService/ListKeyMappings: + $ref: '#/components/schemas/policy.kasregistry.UpdateKeyResponse' + /policy.kasregistry.KeyAccessServerRegistryService/UpdateKeyAccessServer: post: tags: - policy.kasregistry.KeyAccessServerRegistryService - summary: ListKeyMappings - description: Request to list key mappings in the Key Access Service. - operationId: policy.kasregistry.KeyAccessServerRegistryService.ListKeyMappings + summary: UpdateKeyAccessServer + operationId: policy.kasregistry.KeyAccessServerRegistryService.UpdateKeyAccessServer parameters: - name: Connect-Protocol-Version in: header @@ -500,7 +489,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.ListKeyMappingsRequest' + $ref: '#/components/schemas/policy.kasregistry.UpdateKeyAccessServerRequest' required: true responses: default: @@ -514,66 +503,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.kasregistry.ListKeyMappingsResponse' + $ref: '#/components/schemas/policy.kasregistry.UpdateKeyAccessServerResponse' components: schemas: - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.KeyMode: - type: string - title: KeyMode - enum: - - KEY_MODE_UNSPECIFIED - - KEY_MODE_CONFIG_ROOT_KEY - - KEY_MODE_PROVIDER_ROOT_KEY - - KEY_MODE_REMOTE - - KEY_MODE_PUBLIC_KEY_ONLY - description: Describes the management and operational mode of a cryptographic key. - policy.KeyStatus: - type: string - title: KeyStatus - enum: - - KEY_STATUS_UNSPECIFIED - - KEY_STATUS_ACTIVE - - KEY_STATUS_ROTATED - description: The status of the key - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. common.Metadata: type: object properties: @@ -629,6 +561,82 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -641,8 +649,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -734,6 +742,20 @@ components: the Joda Time's [`ISODateTimeFormat.dateTime()`]( http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() ) to obtain a formatter capable of generating timestamps in this format. + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.AsymmetricKey: type: object properties: @@ -811,18 +833,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -870,13 +905,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -904,6 +935,16 @@ components: title: KeyAccessServer additionalProperties: false description: Key Access Server Registry + policy.KeyMode: + type: string + title: KeyMode + enum: + - KEY_MODE_UNSPECIFIED + - KEY_MODE_CONFIG_ROOT_KEY + - KEY_MODE_PROVIDER_ROOT_KEY + - KEY_MODE_REMOTE + - KEY_MODE_PUBLIC_KEY_ONLY + description: Describes the management and operational mode of a cryptographic key. policy.KeyProviderConfig: type: object properties: @@ -926,6 +967,14 @@ components: $ref: '#/components/schemas/common.Metadata' title: KeyProviderConfig additionalProperties: false + policy.KeyStatus: + type: string + title: KeyStatus + enum: + - KEY_STATUS_UNSPECIFIED + - KEY_STATUS_ACTIVE + - KEY_STATUS_ROTATED + description: The status of the key policy.PageRequest: type: object properties: @@ -985,7 +1034,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -993,17 +1043,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -1051,6 +1098,29 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC + description: |- + Sorting direction shared across list APIs. + When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, + the endpoint's request message defines the default ordering; see the + specific List* request docs. + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.kasregistry.ActivatePublicKeyRequest: type: object properties: @@ -1088,13 +1158,9 @@ components: uri: type: string title: uri - description: |+ + description: | Required - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.isUri() - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: Deprecated @@ -1107,13 +1173,9 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Optional - Registered KAS name must be an alphanumeric string, allowing hyphens, and underscores but not as the first or last character. The stored KAS name will be normalized to lower case.: - ``` - size(this) > 0 ? this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') : true - ``` - + kas_name_format // Registered KAS name must be an alphanumeric string, allowing hyphens, and underscores but not as the first or last character. The stored KAS name will be normalized to lower case. metadata: title: metadata description: Common metadata @@ -1143,23 +1205,15 @@ components: description: Required A user-defined identifier for the key keyAlgorithm: title: key_algorithm - description: |+ + description: | Required The algorithm to be used for the key - The key_algorithm must be one of the defined values.: - ``` - this in [1, 2, 3, 4, 5] - ``` - + key_algorithm_defined // The key_algorithm must be one of the defined values. $ref: '#/components/schemas/policy.Algorithm' keyMode: title: key_mode - description: |+ + description: | Required The mode of the key (e.g., local or external) - The key_mode must be one of the defined values (1-4).: - ``` - this >= 1 && this <= 4 - ``` - + key_mode_defined // The key_mode must be one of the defined values (1-4). $ref: '#/components/schemas/policy.KeyMode' publicKeyCtx: title: public_key_ctx @@ -1185,23 +1239,11 @@ components: required: - publicKeyCtx additionalProperties: false - description: |+ + description: | Create a new asymmetric key for the specified Key Access Server (KAS) - The wrapped_key is required if key_mode is KEY_MODE_CONFIG_ROOT_KEY or KEY_MODE_PROVIDER_ROOT_KEY. The wrapped_key must be empty if key_mode is KEY_MODE_REMOTE or KEY_MODE_PUBLIC_KEY_ONLY.: - ``` - ((this.key_mode == 1 || this.key_mode == 2) && this.private_key_ctx.wrapped_key != '') || ((this.key_mode == 3 || this.key_mode == 4) && this.private_key_ctx.wrapped_key == '') - ``` - - Provider config id is required if key_mode is KEY_MODE_PROVIDER_ROOT_KEY or KEY_MODE_REMOTE. It must be empty for KEY_MODE_CONFIG_ROOT_KEY and KEY_MODE_PUBLIC_KEY_ONLY.: - ``` - ((this.key_mode == 1 || this.key_mode == 4) && this.provider_config_id == '') || ((this.key_mode == 2 || this.key_mode == 3) && this.provider_config_id != '') - ``` - - private_key_ctx must not be set if key_mode is KEY_MODE_PUBLIC_KEY_ONLY.: - ``` - !(this.key_mode == 4 && has(this.private_key_ctx)) - ``` - + private_key_ctx_for_public_key_only // private_key_ctx must not be set if key_mode is KEY_MODE_PUBLIC_KEY_ONLY. + private_key_ctx_optionally_required // The wrapped_key is required if key_mode is KEY_MODE_CONFIG_ROOT_KEY or KEY_MODE_PROVIDER_ROOT_KEY. The wrapped_key must be empty if key_mode is KEY_MODE_REMOTE or KEY_MODE_PUBLIC_KEY_ONLY. + provider_config_id_optionally_required // Provider config id is required if key_mode is KEY_MODE_PROVIDER_ROOT_KEY or KEY_MODE_REMOTE. It must be empty for KEY_MODE_CONFIG_ROOT_KEY and KEY_MODE_PUBLIC_KEY_ONLY. policy.kasregistry.CreateKeyResponse: type: object properties: @@ -1290,53 +1332,49 @@ components: additionalProperties: false policy.kasregistry.GetKeyAccessServerRequest: type: object - oneOf: + allOf: - properties: - kasId: + id: type: string - title: kas_id + title: id format: uuid - description: 'option (buf.validate.oneof).required = true; // TODO: enable this when we remove the deprecated field' - title: kas_id - required: - - kasId - - properties: - name: - type: string + description: Deprecated + deprecated: true + - oneOf: + - type: object + properties: + kasId: + type: string + title: kas_id + format: uuid + description: 'option (buf.validate.oneof).required = true; // TODO: enable this when we remove the deprecated field' + title: kas_id + required: + - kasId + - type: object + properties: + name: + type: string + title: name + minLength: 1 title: name - minLength: 1 - title: name - required: - - name - - properties: - uri: - type: string + required: + - name + - type: object + properties: + uri: + type: string + title: uri + minLength: 1 + format: uri title: uri - minLength: 1 - format: uri - title: uri - required: - - uri - properties: - id: - type: string - title: id - format: uuid - description: Deprecated - deprecated: true + required: + - uri title: GetKeyAccessServerRequest additionalProperties: false - description: |+ - Either use deprecated 'id' field or one of 'kas_id' or 'uri', but not both: - ``` - !(has(this.id) && (has(this.kas_id) || has(this.uri) || has(this.name))) - ``` - - Either id or one of kas_id or uri must be set: - ``` - has(this.id) || has(this.kas_id) || has(this.uri) || has(this.name) - ``` - + description: | + exclusive_fields // Either use deprecated 'id' field or one of 'kas_id' or 'uri', but not both + required_fields // Either id or one of kas_id or uri must be set policy.kasregistry.GetKeyAccessServerResponse: type: object properties: @@ -1348,7 +1386,8 @@ components: policy.kasregistry.GetKeyRequest: type: object oneOf: - - properties: + - type: object + properties: id: type: string title: id @@ -1357,7 +1396,8 @@ components: title: id required: - id - - properties: + - type: object + properties: key: title: key $ref: '#/components/schemas/policy.kasregistry.KasKeyIdentifier' @@ -1380,7 +1420,8 @@ components: policy.kasregistry.GetPublicKeyRequest: type: object oneOf: - - properties: + - type: object + properties: id: type: string title: id @@ -1412,41 +1453,56 @@ components: description: Can be namespace, attribute definition, or value policy.kasregistry.KasKeyIdentifier: type: object - oneOf: + allOf: - properties: - kasId: + kid: type: string + title: kid + minLength: 1 + description: Required Key ID of the key in question + - oneOf: + - type: object + properties: + kasId: + type: string + title: kas_id + format: uuid title: kas_id - format: uuid - title: kas_id - required: - - kasId - - properties: - name: - type: string + required: + - kasId + - type: object + properties: + name: + type: string + title: name + minLength: 1 title: name - minLength: 1 - title: name - required: - - name - - properties: - uri: - type: string + required: + - name + - type: object + properties: + uri: + type: string + title: uri + minLength: 1 + format: uri title: uri - minLength: 1 - format: uri - title: uri - required: - - uri - properties: - kid: - type: string - title: kid - minLength: 1 - description: Required Key ID of the key in question + required: + - uri title: KasKeyIdentifier additionalProperties: false description: Nested message for specifying the active key using KAS ID and Key ID + policy.kasregistry.KasKeysSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.kasregistry.SortKasKeysType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: KasKeysSort + additionalProperties: false policy.kasregistry.KeyAccessServerGrants: type: object properties: @@ -1471,6 +1527,17 @@ components: title: KeyAccessServerGrants additionalProperties: false description: Deprecated + policy.kasregistry.KeyAccessServersSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.kasregistry.SortKeyAccessServersType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: KeyAccessServersSort + additionalProperties: false policy.kasregistry.KeyMapping: type: object properties: @@ -1506,43 +1573,31 @@ components: kasId: type: string title: kas_id - description: |+ + description: | Optional Filter LIST by ID of a registered Key Access Server. If neither is provided, grants from all registered KASs to policy attribute objects are returned. - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID kasUri: type: string title: kas_uri - description: |+ + description: | Optional Filter LIST by URI of a registered Key Access Server. If none is provided, grants from all registered KASs to policy attribute objects are returned. - Optional URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - size(this) == 0 || this.isUri() - ``` - + optional_uri_format // Optional URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. kasName: type: string title: kas_name maxLength: 253 - description: |+ + description: | Optional Filter LIST by name of a registered Key Access Server. If none are provided, grants from all registered KASs to policy attribute objects are returned. - Registered KAS name must be an alphanumeric string, allowing hyphens, and underscores but not as the first or last character. The stored KAS name will be normalized to lower case.: - ``` - size(this) == 0 || this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + kas_name_format // Registered KAS name must be an alphanumeric string, allowing hyphens, and underscores but not as the first or last character. The stored KAS name will be normalized to lower case. pagination: title: pagination description: Optional @@ -1576,6 +1631,18 @@ components: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.kasregistry.KeyAccessServersSort' + title: sort + maxItems: 1 + description: |- + Optional - CONSTRAINT: max 1 item + Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC title: ListKeyAccessServersRequest additionalProperties: false policy.kasregistry.ListKeyAccessServersResponse: @@ -1593,28 +1660,31 @@ components: additionalProperties: false policy.kasregistry.ListKeyMappingsRequest: type: object - oneOf: + allOf: - properties: - id: - type: string + pagination: + title: pagination + description: Pagination request for the list of keys + $ref: '#/components/schemas/policy.PageRequest' + - oneOf: + - type: object + properties: + id: + type: string + title: id + format: uuid + description: The unique identifier of the key to retrieve title: id - format: uuid - description: The unique identifier of the key to retrieve - title: id - required: - - id - - properties: - key: + required: + - id + - type: object + properties: + key: + title: key + $ref: '#/components/schemas/policy.kasregistry.KasKeyIdentifier' title: key - $ref: '#/components/schemas/policy.kasregistry.KasKeyIdentifier' - title: key - required: - - key - properties: - pagination: - title: pagination - description: Pagination request for the list of keys - $ref: '#/components/schemas/policy.PageRequest' + required: + - key title: ListKeyMappingsRequest additionalProperties: false policy.kasregistry.ListKeyMappingsResponse: @@ -1634,55 +1704,68 @@ components: additionalProperties: false policy.kasregistry.ListKeysRequest: type: object - oneOf: + allOf: - properties: - kasId: - type: string + keyAlgorithm: + title: key_algorithm + description: | + Filter keys by algorithm + key_algorithm_defined // The key_algorithm must be one of the defined values. + $ref: '#/components/schemas/policy.Algorithm' + legacy: + type: + - boolean + - "null" + title: legacy + description: Optional Filter for legacy keys + pagination: + title: pagination + description: Optional Pagination request for the list of keys + $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.kasregistry.KasKeysSort' + title: sort + maxItems: 1 + description: |- + Optional - CONSTRAINT: max 1 item + Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC + - oneOf: + - type: object + properties: + kasId: + type: string + title: kas_id + format: uuid + description: Filter keys by the KAS ID title: kas_id - format: uuid - description: Filter keys by the KAS ID - title: kas_id - required: - - kasId - - properties: - kasName: - type: string + required: + - kasId + - type: object + properties: + kasName: + type: string + title: kas_name + minLength: 1 + description: Filter keys by the KAS name title: kas_name - minLength: 1 - description: Filter keys by the KAS name - title: kas_name - required: - - kasName - - properties: - kasUri: - type: string + required: + - kasName + - type: object + properties: + kasUri: + type: string + title: kas_uri + minLength: 1 + format: uri + description: Filter keys by the KAS URI title: kas_uri - minLength: 1 - format: uri - description: Filter keys by the KAS URI - title: kas_uri - required: - - kasUri - properties: - keyAlgorithm: - title: key_algorithm - description: |+ - Filter keys by algorithm - The key_algorithm must be one of the defined values.: - ``` - this in [0, 1, 2, 3, 4, 5] - ``` - - $ref: '#/components/schemas/policy.Algorithm' - legacy: - type: boolean - title: legacy - description: Optional Filter for legacy keys - nullable: true - pagination: - title: pagination - description: Optional Pagination request for the list of keys - $ref: '#/components/schemas/policy.PageRequest' + required: + - kasUri title: ListKeysRequest additionalProperties: false description: List all asymmetric keys managed by a specific Key Access Server or with a given algorithm @@ -1704,45 +1787,49 @@ components: description: Response to a ListKeysRequest, containing the list of asymmetric keys and pagination information policy.kasregistry.ListPublicKeyMappingRequest: type: object - oneOf: + allOf: - properties: - kasId: + publicKeyId: type: string - title: kas_id + title: public_key_id format: uuid + description: Optional Public Key ID + pagination: + title: pagination description: Optional - title: kas_id - required: - - kasId - - properties: - kasName: - type: string + $ref: '#/components/schemas/policy.PageRequest' + - oneOf: + - type: object + properties: + kasId: + type: string + title: kas_id + format: uuid + description: Optional + title: kas_id + required: + - kasId + - type: object + properties: + kasName: + type: string + title: kas_name + minLength: 1 + description: Optional title: kas_name - minLength: 1 - description: Optional - title: kas_name - required: - - kasName - - properties: - kasUri: - type: string + required: + - kasName + - type: object + properties: + kasUri: + type: string + title: kas_uri + minLength: 1 + format: uri + description: Optional title: kas_uri - minLength: 1 - format: uri - description: Optional - title: kas_uri - required: - - kasUri - properties: - publicKeyId: - type: string - title: public_key_id - format: uuid - description: Optional Public Key ID - pagination: - title: pagination - description: Optional - $ref: '#/components/schemas/policy.PageRequest' + required: + - kasUri title: ListPublicKeyMappingRequest additionalProperties: false policy.kasregistry.ListPublicKeyMappingResponse: @@ -1813,40 +1900,44 @@ components: additionalProperties: false policy.kasregistry.ListPublicKeysRequest: type: object - oneOf: + allOf: - properties: - kasId: - type: string - title: kas_id - format: uuid + pagination: + title: pagination description: Optional - title: kas_id - required: - - kasId - - properties: - kasName: - type: string + $ref: '#/components/schemas/policy.PageRequest' + - oneOf: + - type: object + properties: + kasId: + type: string + title: kas_id + format: uuid + description: Optional + title: kas_id + required: + - kasId + - type: object + properties: + kasName: + type: string + title: kas_name + minLength: 1 + description: Optional title: kas_name - minLength: 1 - description: Optional - title: kas_name - required: - - kasName - - properties: - kasUri: - type: string + required: + - kasName + - type: object + properties: + kasUri: + type: string + title: kas_uri + minLength: 1 + format: uri + description: Optional title: kas_uri - minLength: 1 - format: uri - description: Optional - title: kas_uri - required: - - kasUri - properties: - pagination: - title: pagination - description: Optional - $ref: '#/components/schemas/policy.PageRequest' + required: + - kasUri title: ListPublicKeysRequest additionalProperties: false policy.kasregistry.ListPublicKeysResponse: @@ -1877,47 +1968,38 @@ components: additionalProperties: false policy.kasregistry.RotateKeyRequest: type: object - oneOf: + allOf: - properties: - id: - type: string + newKey: + title: new_key + description: Information about the new key to be rotated in + $ref: '#/components/schemas/policy.kasregistry.RotateKeyRequest.NewKey' + - oneOf: + - type: object + properties: + id: + type: string + title: id + format: uuid + description: Current Active Key UUID title: id - format: uuid - description: Current Active Key UUID - title: id - required: - - id - - properties: - key: + required: + - id + - type: object + properties: + key: + title: key + description: Alternative way to specify the active key using KAS ID and Key ID + $ref: '#/components/schemas/policy.kasregistry.KasKeyIdentifier' title: key - description: Alternative way to specify the active key using KAS ID and Key ID - $ref: '#/components/schemas/policy.kasregistry.KasKeyIdentifier' - title: key - required: - - key - properties: - newKey: - title: new_key - description: Information about the new key to be rotated in - $ref: '#/components/schemas/policy.kasregistry.RotateKeyRequest.NewKey' + required: + - key title: RotateKeyRequest additionalProperties: false - description: |+ - For the new key, the wrapped_key is required if key_mode is KEY_MODE_CONFIG_ROOT_KEY or KEY_MODE_PROVIDER_ROOT_KEY. The wrapped_key must be empty if key_mode is KEY_MODE_REMOTE or KEY_MODE_PUBLIC_KEY_ONLY.: - ``` - ((this.new_key.key_mode == 1 || this.new_key.key_mode == 2) && this.new_key.private_key_ctx.wrapped_key != '') || ((this.new_key.key_mode == 3 || this.new_key.key_mode == 4) && this.new_key.private_key_ctx.wrapped_key == '') - ``` - - For the new key, provider config id is required if key_mode is KEY_MODE_PROVIDER_ROOT_KEY or KEY_MODE_REMOTE. It must be empty for KEY_MODE_CONFIG_ROOT_KEY and KEY_MODE_PUBLIC_KEY_ONLY.: - ``` - ((this.new_key.key_mode == 1 || this.new_key.key_mode == 4) && this.new_key.provider_config_id == '') || ((this.new_key.key_mode == 2 || this.new_key.key_mode == 3) && this.new_key.provider_config_id != '') - ``` - - private_key_ctx must not be set if key_mode is KEY_MODE_PUBLIC_KEY_ONLY.: - ``` - !(this.new_key.key_mode == 4 && has(this.new_key.private_key_ctx)) - ``` - + description: | + private_key_ctx_for_public_key_only // private_key_ctx must not be set if key_mode is KEY_MODE_PUBLIC_KEY_ONLY. + private_key_ctx_optionally_required // For the new key, the wrapped_key is required if key_mode is KEY_MODE_CONFIG_ROOT_KEY or KEY_MODE_PROVIDER_ROOT_KEY. The wrapped_key must be empty if key_mode is KEY_MODE_REMOTE or KEY_MODE_PUBLIC_KEY_ONLY. + provider_config_id_optionally_required // For the new key, provider config id is required if key_mode is KEY_MODE_PROVIDER_ROOT_KEY or KEY_MODE_REMOTE. It must be empty for KEY_MODE_CONFIG_ROOT_KEY and KEY_MODE_PUBLIC_KEY_ONLY. policy.kasregistry.RotateKeyRequest.NewKey: type: object properties: @@ -1928,23 +2010,15 @@ components: description: Required algorithm: title: algorithm - description: |+ + description: | Required - The key_algorithm must be one of the defined values.: - ``` - this in [1, 2, 3, 4, 5] - ``` - + key_algorithm_defined // The key_algorithm must be one of the defined values. $ref: '#/components/schemas/policy.Algorithm' keyMode: title: key_mode - description: |+ + description: | Required - The new key_mode must be one of the defined values (1-4).: - ``` - this in [1, 2, 3, 4] - ``` - + new_key_mode_defined // The new key_mode must be one of the defined values (1-4). $ref: '#/components/schemas/policy.KeyMode' publicKeyCtx: title: public_key_ctx @@ -2009,7 +2083,8 @@ components: policy.kasregistry.SetBaseKeyRequest: type: object oneOf: - - properties: + - type: object + properties: id: type: string title: id @@ -2018,7 +2093,8 @@ components: title: id required: - id - - properties: + - type: object + properties: key: title: key description: Alternative way to specify the key using KAS ID and Key ID @@ -2044,6 +2120,23 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: SetBaseKeyResponse additionalProperties: false + policy.kasregistry.SortKasKeysType: + type: string + title: SortKasKeysType + enum: + - SORT_KAS_KEYS_TYPE_UNSPECIFIED + - SORT_KAS_KEYS_TYPE_KEY_ID + - SORT_KAS_KEYS_TYPE_CREATED_AT + - SORT_KAS_KEYS_TYPE_UPDATED_AT + policy.kasregistry.SortKeyAccessServersType: + type: string + title: SortKeyAccessServersType + enum: + - SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED + - SORT_KEY_ACCESS_SERVERS_TYPE_NAME + - SORT_KEY_ACCESS_SERVERS_TYPE_URI + - SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT + - SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT policy.kasregistry.UpdateKeyAccessServerRequest: type: object properties: @@ -2055,13 +2148,9 @@ components: uri: type: string title: uri - description: |+ + description: | Optional - Optional URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - size(this) == 0 || this.isUri() - ``` - + optional_uri_format // Optional URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: |- @@ -2081,13 +2170,9 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Optional - Registered KAS name must be an alphanumeric string, allowing hyphens, and underscores but not as the first or last character. The stored KAS name will be normalized to lower case.: - ``` - size(this) == 0 || this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + kas_name_format // Registered KAS name must be an alphanumeric string, allowing hyphens, and underscores but not as the first or last character. The stored KAS name will be normalized to lower case. metadata: title: metadata description: |- @@ -2127,13 +2212,9 @@ components: $ref: '#/components/schemas/common.MetadataUpdateEnum' title: UpdateKeyRequest additionalProperties: false - description: |+ + description: | Update an existing asymmetric key in the Key Management System - Metadata update behavior must be either APPEND or REPLACE, when updating metadata.: - ``` - ((!has(this.metadata)) || (has(this.metadata) && this.metadata_update_behavior != 0)) - ``` - + metadata_update_behavior // Metadata update behavior must be either APPEND or REPLACE, when updating metadata. policy.kasregistry.UpdateKeyResponse: type: object properties: @@ -2171,63 +2252,6 @@ components: $ref: '#/components/schemas/policy.Key' title: UpdatePublicKeyResponse additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.kasregistry.KeyAccessServerRegistryService diff --git a/docs/openapi/policy/keymanagement/key_management.openapi.yaml b/docs/openapi/policy/keymanagement/key_management.openapi.yaml index 61a3e433ba..94f70d9b92 100644 --- a/docs/openapi/policy/keymanagement/key_management.openapi.yaml +++ b/docs/openapi/policy/keymanagement/key_management.openapi.yaml @@ -40,12 +40,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.keymanagement.CreateProviderConfigResponse' - /policy.keymanagement.KeyManagementService/GetProviderConfig: + /policy.keymanagement.KeyManagementService/DeleteProviderConfig: post: tags: - policy.keymanagement.KeyManagementService - summary: GetProviderConfig - operationId: policy.keymanagement.KeyManagementService.GetProviderConfig + summary: DeleteProviderConfig + operationId: policy.keymanagement.KeyManagementService.DeleteProviderConfig parameters: - name: Connect-Protocol-Version in: header @@ -60,7 +60,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.keymanagement.GetProviderConfigRequest' + $ref: '#/components/schemas/policy.keymanagement.DeleteProviderConfigRequest' required: true responses: default: @@ -74,13 +74,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.keymanagement.GetProviderConfigResponse' - /policy.keymanagement.KeyManagementService/ListProviderConfigs: + $ref: '#/components/schemas/policy.keymanagement.DeleteProviderConfigResponse' + /policy.keymanagement.KeyManagementService/GetProviderConfig: post: tags: - policy.keymanagement.KeyManagementService - summary: ListProviderConfigs - operationId: policy.keymanagement.KeyManagementService.ListProviderConfigs + summary: GetProviderConfig + operationId: policy.keymanagement.KeyManagementService.GetProviderConfig parameters: - name: Connect-Protocol-Version in: header @@ -95,7 +95,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.keymanagement.ListProviderConfigsRequest' + $ref: '#/components/schemas/policy.keymanagement.GetProviderConfigRequest' required: true responses: default: @@ -109,13 +109,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.keymanagement.ListProviderConfigsResponse' - /policy.keymanagement.KeyManagementService/UpdateProviderConfig: + $ref: '#/components/schemas/policy.keymanagement.GetProviderConfigResponse' + /policy.keymanagement.KeyManagementService/ListProviderConfigs: post: tags: - policy.keymanagement.KeyManagementService - summary: UpdateProviderConfig - operationId: policy.keymanagement.KeyManagementService.UpdateProviderConfig + summary: ListProviderConfigs + operationId: policy.keymanagement.KeyManagementService.ListProviderConfigs parameters: - name: Connect-Protocol-Version in: header @@ -130,7 +130,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.keymanagement.UpdateProviderConfigRequest' + $ref: '#/components/schemas/policy.keymanagement.ListProviderConfigsRequest' required: true responses: default: @@ -144,13 +144,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.keymanagement.UpdateProviderConfigResponse' - /policy.keymanagement.KeyManagementService/DeleteProviderConfig: + $ref: '#/components/schemas/policy.keymanagement.ListProviderConfigsResponse' + /policy.keymanagement.KeyManagementService/UpdateProviderConfig: post: tags: - policy.keymanagement.KeyManagementService - summary: DeleteProviderConfig - operationId: policy.keymanagement.KeyManagementService.DeleteProviderConfig + summary: UpdateProviderConfig + operationId: policy.keymanagement.KeyManagementService.UpdateProviderConfig parameters: - name: Connect-Protocol-Version in: header @@ -165,7 +165,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.keymanagement.DeleteProviderConfigRequest' + $ref: '#/components/schemas/policy.keymanagement.UpdateProviderConfigRequest' required: true responses: default: @@ -179,16 +179,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.keymanagement.DeleteProviderConfigResponse' + $ref: '#/components/schemas/policy.keymanagement.UpdateProviderConfigResponse' components: schemas: - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE common.Metadata: type: object properties: @@ -244,11 +237,87 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -466,28 +535,31 @@ components: additionalProperties: false policy.keymanagement.GetProviderConfigRequest: type: object - oneOf: + allOf: - properties: - id: + manager: type: string + title: manager + description: Optional - filter by manager type when searching by name + - oneOf: + - type: object + properties: + id: + type: string + title: id + format: uuid title: id - format: uuid - title: id - required: - - id - - properties: - name: - type: string + required: + - id + - type: object + properties: + name: + type: string + title: name + minLength: 1 title: name - minLength: 1 - title: name - required: - - name - properties: - manager: - type: string - title: manager - description: Optional - filter by manager type when searching by name + required: + - name title: GetProviderConfigRequest additionalProperties: false policy.keymanagement.GetProviderConfigResponse: @@ -560,63 +632,6 @@ components: $ref: '#/components/schemas/policy.KeyProviderConfig' title: UpdateProviderConfigResponse additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.keymanagement.KeyManagementService diff --git a/docs/openapi/policy/namespaces/namespaces.openapi.yaml b/docs/openapi/policy/namespaces/namespaces.openapi.yaml index f08ac689ed..506217b193 100644 --- a/docs/openapi/policy/namespaces/namespaces.openapi.yaml +++ b/docs/openapi/policy/namespaces/namespaces.openapi.yaml @@ -2,12 +2,13 @@ openapi: 3.1.0 info: title: policy.namespaces paths: - /policy.namespaces.NamespaceService/GetNamespace: + /policy.namespaces.NamespaceService/AssignKeyAccessServerToNamespace: post: tags: - policy.namespaces.NamespaceService - summary: GetNamespace - operationId: policy.namespaces.NamespaceService.GetNamespace + summary: AssignKeyAccessServerToNamespace + description: 'Deprecated: utilize AssignPublicKeyToNamespace' + operationId: policy.namespaces.NamespaceService.AssignKeyAccessServerToNamespace parameters: - name: Connect-Protocol-Version in: header @@ -22,7 +23,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.GetNamespaceRequest' + $ref: '#/components/schemas/policy.namespaces.AssignKeyAccessServerToNamespaceRequest' required: true responses: default: @@ -36,13 +37,18 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.GetNamespaceResponse' - /policy.namespaces.NamespaceService/ListNamespaces: + $ref: '#/components/schemas/policy.namespaces.AssignKeyAccessServerToNamespaceResponse' + deprecated: true + /policy.namespaces.NamespaceService/AssignPublicKeyToNamespace: post: tags: - policy.namespaces.NamespaceService - summary: ListNamespaces - operationId: policy.namespaces.NamespaceService.ListNamespaces + summary: AssignPublicKeyToNamespace + description: |- + --------------------------------------* + Namespace <> Key RPCs + --------------------------------------- + operationId: policy.namespaces.NamespaceService.AssignPublicKeyToNamespace parameters: - name: Connect-Protocol-Version in: header @@ -57,7 +63,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.ListNamespacesRequest' + $ref: '#/components/schemas/policy.namespaces.AssignPublicKeyToNamespaceRequest' required: true responses: default: @@ -71,7 +77,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.ListNamespacesResponse' + $ref: '#/components/schemas/policy.namespaces.AssignPublicKeyToNamespaceResponse' /policy.namespaces.NamespaceService/CreateNamespace: post: tags: @@ -107,12 +113,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.namespaces.CreateNamespaceResponse' - /policy.namespaces.NamespaceService/UpdateNamespace: + /policy.namespaces.NamespaceService/DeactivateNamespace: post: tags: - policy.namespaces.NamespaceService - summary: UpdateNamespace - operationId: policy.namespaces.NamespaceService.UpdateNamespace + summary: DeactivateNamespace + operationId: policy.namespaces.NamespaceService.DeactivateNamespace parameters: - name: Connect-Protocol-Version in: header @@ -127,7 +133,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.UpdateNamespaceRequest' + $ref: '#/components/schemas/policy.namespaces.DeactivateNamespaceRequest' required: true responses: default: @@ -141,13 +147,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.UpdateNamespaceResponse' - /policy.namespaces.NamespaceService/DeactivateNamespace: + $ref: '#/components/schemas/policy.namespaces.DeactivateNamespaceResponse' + /policy.namespaces.NamespaceService/GetNamespace: post: tags: - policy.namespaces.NamespaceService - summary: DeactivateNamespace - operationId: policy.namespaces.NamespaceService.DeactivateNamespace + summary: GetNamespace + operationId: policy.namespaces.NamespaceService.GetNamespace parameters: - name: Connect-Protocol-Version in: header @@ -162,7 +168,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.DeactivateNamespaceRequest' + $ref: '#/components/schemas/policy.namespaces.GetNamespaceRequest' required: true responses: default: @@ -176,14 +182,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.DeactivateNamespaceResponse' - /policy.namespaces.NamespaceService/AssignKeyAccessServerToNamespace: + $ref: '#/components/schemas/policy.namespaces.GetNamespaceResponse' + /policy.namespaces.NamespaceService/ListNamespaces: post: tags: - policy.namespaces.NamespaceService - summary: AssignKeyAccessServerToNamespace - description: 'Deprecated: utilize AssignPublicKeyToNamespace' - operationId: policy.namespaces.NamespaceService.AssignKeyAccessServerToNamespace + summary: ListNamespaces + operationId: policy.namespaces.NamespaceService.ListNamespaces parameters: - name: Connect-Protocol-Version in: header @@ -198,7 +203,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.AssignKeyAccessServerToNamespaceRequest' + $ref: '#/components/schemas/policy.namespaces.ListNamespacesRequest' required: true responses: default: @@ -212,8 +217,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.AssignKeyAccessServerToNamespaceResponse' - deprecated: true + $ref: '#/components/schemas/policy.namespaces.ListNamespacesResponse' /policy.namespaces.NamespaceService/RemoveKeyAccessServerFromNamespace: post: tags: @@ -251,45 +255,6 @@ paths: schema: $ref: '#/components/schemas/policy.namespaces.RemoveKeyAccessServerFromNamespaceResponse' deprecated: true - /policy.namespaces.NamespaceService/AssignPublicKeyToNamespace: - post: - tags: - - policy.namespaces.NamespaceService - summary: AssignPublicKeyToNamespace - description: |- - --------------------------------------* - Namespace <> Key RPCs - --------------------------------------- - operationId: policy.namespaces.NamespaceService.AssignPublicKeyToNamespace - parameters: - - name: Connect-Protocol-Version - in: header - required: true - schema: - $ref: '#/components/schemas/connect-protocol-version' - - name: Connect-Timeout-Ms - in: header - schema: - $ref: '#/components/schemas/connect-timeout-header' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/policy.namespaces.AssignPublicKeyToNamespaceRequest' - required: true - responses: - default: - description: Error - content: - application/json: - schema: - $ref: '#/components/schemas/connect.error' - "200": - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/policy.namespaces.AssignPublicKeyToNamespaceResponse' /policy.namespaces.NamespaceService/RemovePublicKeyFromNamespace: post: tags: @@ -325,48 +290,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.namespaces.RemovePublicKeyFromNamespaceResponse' - /policy.namespaces.NamespaceService/AssignCertificateToNamespace: - post: - tags: - - policy.namespaces.NamespaceService - summary: AssignCertificateToNamespace - description: Namespace <> Certificate RPCs - operationId: policy.namespaces.NamespaceService.AssignCertificateToNamespace - parameters: - - name: Connect-Protocol-Version - in: header - required: true - schema: - $ref: '#/components/schemas/connect-protocol-version' - - name: Connect-Timeout-Ms - in: header - schema: - $ref: '#/components/schemas/connect-timeout-header' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/policy.namespaces.AssignCertificateToNamespaceRequest' - required: true - responses: - default: - description: Error - content: - application/json: - schema: - $ref: '#/components/schemas/connect.error' - "200": - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/policy.namespaces.AssignCertificateToNamespaceResponse' - /policy.namespaces.NamespaceService/RemoveCertificateFromNamespace: + /policy.namespaces.NamespaceService/UpdateNamespace: post: tags: - policy.namespaces.NamespaceService - summary: RemoveCertificateFromNamespace - operationId: policy.namespaces.NamespaceService.RemoveCertificateFromNamespace + summary: UpdateNamespace + operationId: policy.namespaces.NamespaceService.UpdateNamespace parameters: - name: Connect-Protocol-Version in: header @@ -381,7 +310,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.RemoveCertificateFromNamespaceRequest' + $ref: '#/components/schemas/policy.namespaces.UpdateNamespaceRequest' required: true responses: default: @@ -395,7 +324,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.namespaces.RemoveCertificateFromNamespaceResponse' + $ref: '#/components/schemas/policy.namespaces.UpdateNamespaceResponse' components: schemas: common.ActiveStateEnum: @@ -407,59 +336,6 @@ components: - ACTIVE_STATE_ENUM_INACTIVE - ACTIVE_STATE_ENUM_ANY description: 'buflint ENUM_VALUE_PREFIX: to make sure that C++ scoping rules aren''t violated when users add new enum values to an enum in a given package' - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - common.IdFqnIdentifier: - type: object - properties: - id: - type: string - title: id - format: uuid - fqn: - type: string - title: fqn - minLength: 1 - format: uri - title: IdFqnIdentifier - additionalProperties: false common.Metadata: type: object properties: @@ -515,6 +391,82 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -527,8 +479,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -620,23 +572,20 @@ components: the Joda Time's [`ISODateTimeFormat.dateTime()`]( http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() ) to obtain a formatter capable of generating timestamps in this format. - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.KasPublicKey: type: object properties: @@ -655,18 +604,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -689,13 +651,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -758,12 +716,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.PageRequest: @@ -811,7 +763,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -819,17 +772,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -867,39 +817,29 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false - policy.namespaces.AssignCertificateToNamespaceRequest: - type: object - properties: - namespace: - title: namespace - description: Required - namespace identifier (id or fqn) - $ref: '#/components/schemas/common.IdFqnIdentifier' - pem: - type: string - title: pem - description: Required - PEM format certificate - metadata: - title: metadata - description: Optional - $ref: '#/components/schemas/common.MetadataMutable' - title: AssignCertificateToNamespaceRequest - required: - - namespace - - pem - additionalProperties: false - policy.namespaces.AssignCertificateToNamespaceResponse: - type: object - properties: - namespaceCertificate: - title: namespace_certificate - description: The mapping of the namespace to the certificate. - $ref: '#/components/schemas/policy.namespaces.NamespaceCertificate' - certificate: - title: certificate - description: Return the full certificate object for convenience - $ref: '#/components/schemas/policy.Certificate' - title: AssignCertificateToNamespaceResponse - additionalProperties: false + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC + description: |- + Sorting direction shared across list APIs. + When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, + the endpoint's request message defines the default ordering; see the + specific List* request docs. + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.namespaces.AssignKeyAccessServerToNamespaceRequest: type: object properties: @@ -943,13 +883,9 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Required - Namespace must be a valid hostname. It should include at least one dot, with each segment (label) starting and ending with an alphanumeric character. Each label must be 1 to 63 characters long, allowing hyphens but not as the first or last character. The top-level domain (the last segment after the final dot) must consist of at least two alphabetic characters. The stored namespace will be normalized to lower case.: - ``` - this.matches('^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$') - ``` - + namespace_format // Namespace must be a valid hostname. It should include at least one dot, with each segment (label) starting and ending with an alphanumeric character. Each label must be 1 to 63 characters long, allowing hyphens but not as the first or last character. The top-level domain (the last segment after the final dot) must consist of at least two alphabetic characters. The stored namespace will be normalized to lower case. metadata: title: metadata description: Optional @@ -982,45 +918,40 @@ components: additionalProperties: false policy.namespaces.GetNamespaceRequest: type: object - oneOf: + allOf: - properties: - fqn: + id: type: string + title: id + format: uuid + description: Deprecated + deprecated: true + - oneOf: + - type: object + properties: + fqn: + type: string + title: fqn + minLength: 1 + format: uri title: fqn - minLength: 1 - format: uri - title: fqn - required: - - fqn - - properties: - namespaceId: - type: string + required: + - fqn + - type: object + properties: + namespaceId: + type: string + title: namespace_id + format: uuid + description: 'option (buf.validate.oneof).required = true; // TODO: enable this when we remove the deprecated field' title: namespace_id - format: uuid - description: 'option (buf.validate.oneof).required = true; // TODO: enable this when we remove the deprecated field' - title: namespace_id - required: - - namespaceId - properties: - id: - type: string - title: id - format: uuid - description: Deprecated - deprecated: true + required: + - namespaceId title: GetNamespaceRequest additionalProperties: false - description: |+ - Either use deprecated 'id' field or one of 'namespace_id' or 'fqn', but not both: - ``` - !(has(this.id) && (has(this.namespace_id) || has(this.fqn))) - ``` - - Either id or one of namespace_id or fqn must be set: - ``` - has(this.id) || has(this.namespace_id) || has(this.fqn) - ``` - + description: | + exclusive_fields // Either use deprecated 'id' field or one of 'namespace_id' or 'fqn', but not both + required_fields // Either id or one of namespace_id or fqn must be set policy.namespaces.GetNamespaceResponse: type: object properties: @@ -1042,6 +973,18 @@ components: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.namespaces.NamespacesSort' + title: sort + maxItems: 1 + description: |- + Optional - CONSTRAINT: max 1 item + Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC title: ListNamespacesRequest additionalProperties: false policy.namespaces.ListNamespacesResponse: @@ -1057,24 +1000,6 @@ components: $ref: '#/components/schemas/policy.PageResponse' title: ListNamespacesResponse additionalProperties: false - policy.namespaces.NamespaceCertificate: - type: object - properties: - namespace: - title: namespace - description: Required - namespace identifier (id or fqn) - $ref: '#/components/schemas/common.IdFqnIdentifier' - certificateId: - type: string - title: certificate_id - format: uuid - description: Required (The id from the Certificate object) - title: NamespaceCertificate - required: - - namespace - - certificateId - additionalProperties: false - description: Maps a namespace to a certificate (similar to NamespaceKey pattern) policy.namespaces.NamespaceKey: type: object properties: @@ -1109,25 +1034,16 @@ components: title: NamespaceKeyAccessServer additionalProperties: false description: Deprecated - policy.namespaces.RemoveCertificateFromNamespaceRequest: - type: object - properties: - namespaceCertificate: - title: namespace_certificate - description: The namespace and certificate to unassign. - $ref: '#/components/schemas/policy.namespaces.NamespaceCertificate' - title: RemoveCertificateFromNamespaceRequest - required: - - namespaceCertificate - additionalProperties: false - policy.namespaces.RemoveCertificateFromNamespaceResponse: + policy.namespaces.NamespacesSort: type: object properties: - namespaceCertificate: - title: namespace_certificate - description: The unassigned namespace and certificate. - $ref: '#/components/schemas/policy.namespaces.NamespaceCertificate' - title: RemoveCertificateFromNamespaceResponse + field: + title: field + $ref: '#/components/schemas/policy.namespaces.SortNamespacesType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: NamespacesSort additionalProperties: false policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest: type: object @@ -1164,6 +1080,15 @@ components: $ref: '#/components/schemas/policy.namespaces.NamespaceKey' title: RemovePublicKeyFromNamespaceResponse additionalProperties: false + policy.namespaces.SortNamespacesType: + type: string + title: SortNamespacesType + enum: + - SORT_NAMESPACES_TYPE_UNSPECIFIED + - SORT_NAMESPACES_TYPE_NAME + - SORT_NAMESPACES_TYPE_FQN + - SORT_NAMESPACES_TYPE_CREATED_AT + - SORT_NAMESPACES_TYPE_UPDATED_AT policy.namespaces.UpdateNamespaceRequest: type: object properties: @@ -1189,63 +1114,6 @@ components: $ref: '#/components/schemas/policy.Namespace' title: UpdateNamespaceResponse additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.namespaces.NamespaceService diff --git a/docs/openapi/policy/objects.openapi.yaml b/docs/openapi/policy/objects.openapi.yaml index ef73c4cb70..6bef650b76 100644 --- a/docs/openapi/policy/objects.openapi.yaml +++ b/docs/openapi/policy/objects.openapi.yaml @@ -4,86 +4,6 @@ info: paths: {} components: schemas: - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.AttributeRuleTypeEnum: - type: string - title: AttributeRuleTypeEnum - enum: - - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED - - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF - - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF - - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY - policy.ConditionBooleanTypeEnum: - type: string - title: ConditionBooleanTypeEnum - enum: - - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED - - CONDITION_BOOLEAN_TYPE_ENUM_AND - - CONDITION_BOOLEAN_TYPE_ENUM_OR - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.KeyMode: - type: string - title: KeyMode - enum: - - KEY_MODE_UNSPECIFIED - - KEY_MODE_CONFIG_ROOT_KEY - - KEY_MODE_PROVIDER_ROOT_KEY - - KEY_MODE_REMOTE - - KEY_MODE_PUBLIC_KEY_ONLY - description: Describes the management and operational mode of a cryptographic key. - policy.KeyStatus: - type: string - title: KeyStatus - enum: - - KEY_STATUS_UNSPECIFIED - - KEY_STATUS_ACTIVE - - KEY_STATUS_ROTATED - description: The status of the key - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - policy.SubjectMappingOperatorEnum: - type: string - title: SubjectMappingOperatorEnum - enum: - - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED - - SUBJECT_MAPPING_OPERATOR_ENUM_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS common.Metadata: type: object properties: @@ -128,8 +48,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -223,37 +143,65 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: + type: string + title: id + description: Generated uuid in database + name: type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard - properties: - id: - type: string - title: id - description: Generated uuid in database - name: - type: string - title: name - metadata: - title: metadata - $ref: '#/components/schemas/common.Metadata' + required: + - standard title: Action additionalProperties: false description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.AsymmetricKey: type: object properties: @@ -341,6 +289,12 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys associated with the attribute + allowTraversal: + title: allow_traversal + description: |- + Whether or not we will use the attribute definition during encryption + if the attribute value is missing. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Common metadata @@ -349,23 +303,14 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.AttributeRuleTypeEnum: + type: string + title: AttributeRuleTypeEnum + enum: + - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED + - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY policy.Condition: type: object properties: @@ -383,7 +328,6 @@ components: type: array items: type: string - minItems: 1 title: subject_external_values minItems: 1 description: |- @@ -399,6 +343,13 @@ components: * A Condition defines a rule of + policy.ConditionBooleanTypeEnum: + type: string + title: ConditionBooleanTypeEnum + enum: + - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED + - CONDITION_BOOLEAN_TYPE_ENUM_AND + - CONDITION_BOOLEAN_TYPE_ENUM_OR policy.ConditionGroup: type: object properties: @@ -449,18 +400,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -508,13 +472,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -542,6 +502,16 @@ components: title: KeyAccessServer additionalProperties: false description: Key Access Server Registry + policy.KeyMode: + type: string + title: KeyMode + enum: + - KEY_MODE_UNSPECIFIED + - KEY_MODE_CONFIG_ROOT_KEY + - KEY_MODE_PROVIDER_ROOT_KEY + - KEY_MODE_REMOTE + - KEY_MODE_PUBLIC_KEY_ONLY + description: Describes the management and operational mode of a cryptographic key. policy.KeyProviderConfig: type: object properties: @@ -564,6 +534,14 @@ components: $ref: '#/components/schemas/common.Metadata' title: KeyProviderConfig additionalProperties: false + policy.KeyStatus: + type: string + title: KeyStatus + enum: + - KEY_STATUS_UNSPECIFIED + - KEY_STATUS_ACTIVE + - KEY_STATUS_ROTATED + description: The status of the key policy.Namespace: type: object properties: @@ -599,12 +577,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: @@ -652,6 +624,10 @@ components: items: $ref: '#/components/schemas/policy.RequestContext' title: context + namespace: + title: namespace + description: The source namespace for this trigger, derived from the attribute value and action. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -708,7 +684,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -716,17 +693,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -757,6 +731,9 @@ components: items: $ref: '#/components/schemas/policy.RegisteredResourceValue' title: values + namespace: + title: namespace + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata description: Common metadata @@ -780,6 +757,9 @@ components: items: $ref: '#/components/schemas/policy.RegisteredResourceValue.ActionAttributeValue' title: action_attribute_values + fqn: + type: string + title: fqn metadata: title: metadata description: Common metadata @@ -858,6 +838,10 @@ components: description: |- the common name for the group of resource mappings, which must be unique per namespace + fqn: + type: string + title: fqn + description: the fully qualified name of the resource mapping group metadata: title: metadata description: Common metadata @@ -901,12 +885,30 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.SubjectConditionSet: type: object properties: id: type: string title: id + namespace: + title: namespace + description: |- + the namespace containing this subject condition set + possible this is empty in the case a subject condition set + has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' subjectSets: type: array items: @@ -944,6 +946,13 @@ components: $ref: '#/components/schemas/policy.Action' title: actions description: The actions permitted by subjects in this mapping + namespace: + title: namespace + description: |- + the namespace containing this subject mapping + possible this is empty. If so that means + the Subject Mapping has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -952,6 +961,14 @@ components: description: |- Subject Mapping: A Policy assigning Subject Set(s) to a permitted attribute value + action(s) combination + policy.SubjectMappingOperatorEnum: + type: string + title: SubjectMappingOperatorEnum + enum: + - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + - SUBJECT_MAPPING_OPERATOR_ENUM_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS policy.SubjectProperty: type: object properties: @@ -972,7 +989,6 @@ components: authoritative source such as an IDP (Identity Provider) or User Store. Examples include such ADFS/LDAP, OKTA, etc. For now, a valid property must contain both a selector expression & a resulting value. - The external_selector_value is a specifier to select a value from a flattened external representation of an Entity (such as from idP/LDAP), and the external_value is the value selected by the external_selector_value on that diff --git a/docs/openapi/policy/obligations/obligations.openapi.yaml b/docs/openapi/policy/obligations/obligations.openapi.yaml index 2d23f7419b..946d6ce813 100644 --- a/docs/openapi/policy/obligations/obligations.openapi.yaml +++ b/docs/openapi/policy/obligations/obligations.openapi.yaml @@ -2,12 +2,12 @@ openapi: 3.1.0 info: title: policy.obligations paths: - /policy.obligations.Service/ListObligations: + /policy.obligations.Service/AddObligationTrigger: post: tags: - policy.obligations.Service - summary: ListObligations - operationId: policy.obligations.Service.ListObligations + summary: AddObligationTrigger + operationId: policy.obligations.Service.AddObligationTrigger parameters: - name: Connect-Protocol-Version in: header @@ -22,7 +22,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.ListObligationsRequest' + $ref: '#/components/schemas/policy.obligations.AddObligationTriggerRequest' required: true responses: default: @@ -36,13 +36,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.ListObligationsResponse' - /policy.obligations.Service/GetObligation: + $ref: '#/components/schemas/policy.obligations.AddObligationTriggerResponse' + /policy.obligations.Service/CreateObligation: post: tags: - policy.obligations.Service - summary: GetObligation - operationId: policy.obligations.Service.GetObligation + summary: CreateObligation + operationId: policy.obligations.Service.CreateObligation parameters: - name: Connect-Protocol-Version in: header @@ -57,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.GetObligationRequest' + $ref: '#/components/schemas/policy.obligations.CreateObligationRequest' required: true responses: default: @@ -71,13 +71,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.GetObligationResponse' - /policy.obligations.Service/GetObligationsByFQNs: + $ref: '#/components/schemas/policy.obligations.CreateObligationResponse' + /policy.obligations.Service/CreateObligationValue: post: tags: - policy.obligations.Service - summary: GetObligationsByFQNs - operationId: policy.obligations.Service.GetObligationsByFQNs + summary: CreateObligationValue + operationId: policy.obligations.Service.CreateObligationValue parameters: - name: Connect-Protocol-Version in: header @@ -92,7 +92,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.GetObligationsByFQNsRequest' + $ref: '#/components/schemas/policy.obligations.CreateObligationValueRequest' required: true responses: default: @@ -106,13 +106,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.GetObligationsByFQNsResponse' - /policy.obligations.Service/CreateObligation: + $ref: '#/components/schemas/policy.obligations.CreateObligationValueResponse' + /policy.obligations.Service/DeleteObligation: post: tags: - policy.obligations.Service - summary: CreateObligation - operationId: policy.obligations.Service.CreateObligation + summary: DeleteObligation + operationId: policy.obligations.Service.DeleteObligation parameters: - name: Connect-Protocol-Version in: header @@ -127,7 +127,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.CreateObligationRequest' + $ref: '#/components/schemas/policy.obligations.DeleteObligationRequest' required: true responses: default: @@ -141,13 +141,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.CreateObligationResponse' - /policy.obligations.Service/UpdateObligation: + $ref: '#/components/schemas/policy.obligations.DeleteObligationResponse' + /policy.obligations.Service/DeleteObligationValue: post: tags: - policy.obligations.Service - summary: UpdateObligation - operationId: policy.obligations.Service.UpdateObligation + summary: DeleteObligationValue + operationId: policy.obligations.Service.DeleteObligationValue parameters: - name: Connect-Protocol-Version in: header @@ -162,7 +162,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.UpdateObligationRequest' + $ref: '#/components/schemas/policy.obligations.DeleteObligationValueRequest' required: true responses: default: @@ -176,13 +176,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.UpdateObligationResponse' - /policy.obligations.Service/DeleteObligation: + $ref: '#/components/schemas/policy.obligations.DeleteObligationValueResponse' + /policy.obligations.Service/GetObligation: post: tags: - policy.obligations.Service - summary: DeleteObligation - operationId: policy.obligations.Service.DeleteObligation + summary: GetObligation + operationId: policy.obligations.Service.GetObligation parameters: - name: Connect-Protocol-Version in: header @@ -197,7 +197,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.DeleteObligationRequest' + $ref: '#/components/schemas/policy.obligations.GetObligationRequest' required: true responses: default: @@ -211,7 +211,42 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.DeleteObligationResponse' + $ref: '#/components/schemas/policy.obligations.GetObligationResponse' + /policy.obligations.Service/GetObligationTrigger: + post: + tags: + - policy.obligations.Service + summary: GetObligationTrigger + operationId: policy.obligations.Service.GetObligationTrigger + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/policy.obligations.GetObligationTriggerRequest' + required: true + responses: + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/connect.error' + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/policy.obligations.GetObligationTriggerResponse' /policy.obligations.Service/GetObligationValue: post: tags: @@ -282,12 +317,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.obligations.GetObligationValuesByFQNsResponse' - /policy.obligations.Service/CreateObligationValue: + /policy.obligations.Service/GetObligationsByFQNs: post: tags: - policy.obligations.Service - summary: CreateObligationValue - operationId: policy.obligations.Service.CreateObligationValue + summary: GetObligationsByFQNs + operationId: policy.obligations.Service.GetObligationsByFQNs parameters: - name: Connect-Protocol-Version in: header @@ -302,7 +337,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.CreateObligationValueRequest' + $ref: '#/components/schemas/policy.obligations.GetObligationsByFQNsRequest' required: true responses: default: @@ -316,13 +351,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.CreateObligationValueResponse' - /policy.obligations.Service/UpdateObligationValue: + $ref: '#/components/schemas/policy.obligations.GetObligationsByFQNsResponse' + /policy.obligations.Service/ListObligationTriggers: post: tags: - policy.obligations.Service - summary: UpdateObligationValue - operationId: policy.obligations.Service.UpdateObligationValue + summary: ListObligationTriggers + operationId: policy.obligations.Service.ListObligationTriggers parameters: - name: Connect-Protocol-Version in: header @@ -337,7 +372,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.UpdateObligationValueRequest' + $ref: '#/components/schemas/policy.obligations.ListObligationTriggersRequest' required: true responses: default: @@ -351,13 +386,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.UpdateObligationValueResponse' - /policy.obligations.Service/DeleteObligationValue: + $ref: '#/components/schemas/policy.obligations.ListObligationTriggersResponse' + /policy.obligations.Service/ListObligations: post: tags: - policy.obligations.Service - summary: DeleteObligationValue - operationId: policy.obligations.Service.DeleteObligationValue + summary: ListObligations + operationId: policy.obligations.Service.ListObligations parameters: - name: Connect-Protocol-Version in: header @@ -372,7 +407,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.DeleteObligationValueRequest' + $ref: '#/components/schemas/policy.obligations.ListObligationsRequest' required: true responses: default: @@ -386,13 +421,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.DeleteObligationValueResponse' - /policy.obligations.Service/AddObligationTrigger: + $ref: '#/components/schemas/policy.obligations.ListObligationsResponse' + /policy.obligations.Service/RemoveObligationTrigger: post: tags: - policy.obligations.Service - summary: AddObligationTrigger - operationId: policy.obligations.Service.AddObligationTrigger + summary: RemoveObligationTrigger + operationId: policy.obligations.Service.RemoveObligationTrigger parameters: - name: Connect-Protocol-Version in: header @@ -407,7 +442,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.AddObligationTriggerRequest' + $ref: '#/components/schemas/policy.obligations.RemoveObligationTriggerRequest' required: true responses: default: @@ -421,13 +456,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.AddObligationTriggerResponse' - /policy.obligations.Service/RemoveObligationTrigger: + $ref: '#/components/schemas/policy.obligations.RemoveObligationTriggerResponse' + /policy.obligations.Service/UpdateObligation: post: tags: - policy.obligations.Service - summary: RemoveObligationTrigger - operationId: policy.obligations.Service.RemoveObligationTrigger + summary: UpdateObligation + operationId: policy.obligations.Service.UpdateObligation parameters: - name: Connect-Protocol-Version in: header @@ -442,7 +477,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.RemoveObligationTriggerRequest' + $ref: '#/components/schemas/policy.obligations.UpdateObligationRequest' required: true responses: default: @@ -456,13 +491,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.RemoveObligationTriggerResponse' - /policy.obligations.Service/ListObligationTriggers: + $ref: '#/components/schemas/policy.obligations.UpdateObligationResponse' + /policy.obligations.Service/UpdateObligationValue: post: tags: - policy.obligations.Service - summary: ListObligationTriggers - operationId: policy.obligations.Service.ListObligationTriggers + summary: UpdateObligationValue + operationId: policy.obligations.Service.UpdateObligationValue parameters: - name: Connect-Protocol-Version in: header @@ -477,7 +512,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.ListObligationTriggersRequest' + $ref: '#/components/schemas/policy.obligations.UpdateObligationValueRequest' required: true responses: default: @@ -491,80 +526,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.obligations.ListObligationTriggersResponse' + $ref: '#/components/schemas/policy.obligations.UpdateObligationValueResponse' components: schemas: - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.AttributeRuleTypeEnum: - type: string - title: AttributeRuleTypeEnum - enum: - - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED - - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF - - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF - - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY - policy.ConditionBooleanTypeEnum: - type: string - title: ConditionBooleanTypeEnum - enum: - - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED - - CONDITION_BOOLEAN_TYPE_ENUM_AND - - CONDITION_BOOLEAN_TYPE_ENUM_OR - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - policy.SubjectMappingOperatorEnum: - type: string - title: SubjectMappingOperatorEnum - enum: - - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED - - SUBJECT_MAPPING_OPERATOR_ENUM_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS common.IdFqnIdentifier: type: object + allOf: + - oneOf: + - required: + - id + - required: + - fqn properties: id: type: string @@ -579,6 +551,12 @@ components: additionalProperties: false common.IdNameIdentifier: type: object + allOf: + - oneOf: + - required: + - id + - required: + - name properties: id: type: string @@ -589,12 +567,8 @@ components: title: name maxLength: 253 minLength: 1 - description: |+ - Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + description: | + name_format // Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case. title: IdNameIdentifier additionalProperties: false common.Metadata: @@ -652,6 +626,82 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -664,8 +714,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -759,37 +809,65 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: + type: string + title: id + description: Generated uuid in database + name: type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard - properties: - id: - type: string - title: id - description: Generated uuid in database - name: - type: string - title: name - metadata: - title: metadata - $ref: '#/components/schemas/common.Metadata' + required: + - standard title: Action additionalProperties: false description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.Attribute: type: object properties: @@ -832,6 +910,12 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys associated with the attribute + allowTraversal: + title: allow_traversal + description: |- + Whether or not we will use the attribute definition during encryption + if the attribute value is missing. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Common metadata @@ -840,23 +924,14 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.AttributeRuleTypeEnum: + type: string + title: AttributeRuleTypeEnum + enum: + - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED + - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY policy.Condition: type: object properties: @@ -874,7 +949,6 @@ components: type: array items: type: string - minItems: 1 title: subject_external_values minItems: 1 description: |- @@ -890,6 +964,13 @@ components: * A Condition defines a rule of + policy.ConditionBooleanTypeEnum: + type: string + title: ConditionBooleanTypeEnum + enum: + - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED + - CONDITION_BOOLEAN_TYPE_ENUM_AND + - CONDITION_BOOLEAN_TYPE_ENUM_OR policy.ConditionGroup: type: object properties: @@ -926,18 +1007,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -960,13 +1054,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -1029,12 +1119,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: @@ -1082,6 +1166,10 @@ components: items: $ref: '#/components/schemas/policy.RequestContext' title: context + namespace: + title: namespace + description: The source namespace for this trigger, derived from the attribute value and action. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1166,7 +1254,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -1174,17 +1263,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -1245,6 +1331,10 @@ components: description: |- the common name for the group of resource mappings, which must be unique per namespace + fqn: + type: string + title: fqn + description: the fully qualified name of the resource mapping group metadata: title: metadata description: Common metadata @@ -1288,12 +1378,42 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC + description: |- + Sorting direction shared across list APIs. + When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, + the endpoint's request message defines the default ordering; see the + specific List* request docs. + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.SubjectConditionSet: type: object properties: id: type: string title: id + namespace: + title: namespace + description: |- + the namespace containing this subject condition set + possible this is empty in the case a subject condition set + has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' subjectSets: type: array items: @@ -1331,6 +1451,13 @@ components: $ref: '#/components/schemas/policy.Action' title: actions description: The actions permitted by subjects in this mapping + namespace: + title: namespace + description: |- + the namespace containing this subject mapping + possible this is empty. If so that means + the Subject Mapping has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1339,6 +1466,14 @@ components: description: |- Subject Mapping: A Policy assigning Subject Set(s) to a permitted attribute value + action(s) combination + policy.SubjectMappingOperatorEnum: + type: string + title: SubjectMappingOperatorEnum + enum: + - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + - SUBJECT_MAPPING_OPERATOR_ENUM_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS policy.SubjectSet: type: object properties: @@ -1438,7 +1573,10 @@ components: - action - attributeValue additionalProperties: false - description: Triggers + description: |- + Obligation Triggers are owned by the namespace that owns the action and attribute value, which must + be the same. In this way, a trigger can intentionally cross namespace boundaries: associating + obligation values of a different namespace than the one that owns the action being taken or the attribute value. policy.obligations.AddObligationTriggerResponse: type: object properties: @@ -1449,6 +1587,12 @@ components: additionalProperties: false policy.obligations.CreateObligationRequest: type: object + allOf: + - oneOf: + - required: + - namespaceId + - required: + - namespaceFqn properties: namespaceId: type: string @@ -1463,19 +1607,14 @@ components: type: string title: name maxLength: 253 - description: |+ - Obligation name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + description: | + obligation_name_format // Obligation name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case. values: type: array items: type: string maxLength: 253 pattern: ^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$ - uniqueItems: true title: values uniqueItems: true description: Optional @@ -1499,6 +1638,12 @@ components: additionalProperties: false policy.obligations.CreateObligationValueRequest: type: object + allOf: + - oneOf: + - required: + - obligationId + - required: + - obligationFqn properties: obligationId: type: string @@ -1513,12 +1658,8 @@ components: type: string title: value maxLength: 253 - description: |+ - Obligation value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + description: | + obligation_value_format // Obligation value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case. triggers: type: array items: @@ -1547,6 +1688,12 @@ components: additionalProperties: false policy.obligations.DeleteObligationRequest: type: object + allOf: + - oneOf: + - required: + - id + - required: + - fqn properties: id: type: string @@ -1569,6 +1716,12 @@ components: additionalProperties: false policy.obligations.DeleteObligationValueRequest: type: object + allOf: + - oneOf: + - required: + - id + - required: + - fqn properties: id: type: string @@ -1591,6 +1744,12 @@ components: additionalProperties: false policy.obligations.GetObligationRequest: type: object + allOf: + - oneOf: + - required: + - id + - required: + - fqn properties: id: type: string @@ -1603,7 +1762,6 @@ components: format: uri title: GetObligationRequest additionalProperties: false - description: Definitions policy.obligations.GetObligationResponse: type: object properties: @@ -1612,8 +1770,33 @@ components: $ref: '#/components/schemas/policy.Obligation' title: GetObligationResponse additionalProperties: false + policy.obligations.GetObligationTriggerRequest: + type: object + properties: + id: + type: string + title: id + format: uuid + description: Required + title: GetObligationTriggerRequest + additionalProperties: false + description: Triggers + policy.obligations.GetObligationTriggerResponse: + type: object + properties: + trigger: + title: trigger + $ref: '#/components/schemas/policy.ObligationTrigger' + title: GetObligationTriggerResponse + additionalProperties: false policy.obligations.GetObligationValueRequest: type: object + allOf: + - oneOf: + - required: + - id + - required: + - fqn properties: id: type: string @@ -1644,9 +1827,6 @@ components: type: string minLength: 1 format: uri - maxItems: 250 - minItems: 1 - uniqueItems: true title: fqns maxItems: 250 minItems: 1 @@ -1684,9 +1864,6 @@ components: type: string minLength: 1 format: uri - maxItems: 250 - minItems: 1 - uniqueItems: true title: fqns maxItems: 250 minItems: 1 @@ -1762,6 +1939,18 @@ components: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.obligations.ObligationsSort' + title: sort + maxItems: 1 + description: |- + Optional - CONSTRAINT: max 1 item + Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC title: ListObligationsRequest additionalProperties: false policy.obligations.ListObligationsResponse: @@ -1777,6 +1966,17 @@ components: $ref: '#/components/schemas/policy.PageResponse' title: ListObligationsResponse additionalProperties: false + policy.obligations.ObligationsSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.obligations.SortObligationsType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: ObligationsSort + additionalProperties: false policy.obligations.RemoveObligationTriggerRequest: type: object properties: @@ -1795,6 +1995,15 @@ components: $ref: '#/components/schemas/policy.ObligationTrigger' title: RemoveObligationTriggerResponse additionalProperties: false + policy.obligations.SortObligationsType: + type: string + title: SortObligationsType + enum: + - SORT_OBLIGATIONS_TYPE_UNSPECIFIED + - SORT_OBLIGATIONS_TYPE_NAME + - SORT_OBLIGATIONS_TYPE_FQN + - SORT_OBLIGATIONS_TYPE_CREATED_AT + - SORT_OBLIGATIONS_TYPE_UPDATED_AT policy.obligations.UpdateObligationRequest: type: object properties: @@ -1807,13 +2016,9 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Optional - Obligation name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case.: - ``` - size(this) > 0 ? this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') : true - ``` - + obligation_name_format // Obligation name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case. metadata: title: metadata $ref: '#/components/schemas/common.MetadataMutable' @@ -1842,13 +2047,9 @@ components: type: string title: value maxLength: 253 - description: |+ + description: | Optional - Obligation value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case.: - ``` - size(this) > 0 ? this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') : true - ``` - + obligation_value_format // Obligation value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case. triggers: type: array items: @@ -1896,63 +2097,6 @@ components: - action - attributeValue additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.obligations.Service diff --git a/docs/openapi/policy/registeredresources/registered_resources.openapi.yaml b/docs/openapi/policy/registeredresources/registered_resources.openapi.yaml index fe57ee3008..c22280f626 100644 --- a/docs/openapi/policy/registeredresources/registered_resources.openapi.yaml +++ b/docs/openapi/policy/registeredresources/registered_resources.openapi.yaml @@ -37,12 +37,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.registeredresources.CreateRegisteredResourceResponse' - /policy.registeredresources.RegisteredResourcesService/GetRegisteredResource: + /policy.registeredresources.RegisteredResourcesService/CreateRegisteredResourceValue: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: GetRegisteredResource - operationId: policy.registeredresources.RegisteredResourcesService.GetRegisteredResource + summary: CreateRegisteredResourceValue + operationId: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResourceValue parameters: - name: Connect-Protocol-Version in: header @@ -57,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceRequest' + $ref: '#/components/schemas/policy.registeredresources.CreateRegisteredResourceValueRequest' required: true responses: default: @@ -71,13 +71,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceResponse' - /policy.registeredresources.RegisteredResourcesService/ListRegisteredResources: + $ref: '#/components/schemas/policy.registeredresources.CreateRegisteredResourceValueResponse' + /policy.registeredresources.RegisteredResourcesService/DeleteRegisteredResource: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: ListRegisteredResources - operationId: policy.registeredresources.RegisteredResourcesService.ListRegisteredResources + summary: DeleteRegisteredResource + operationId: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResource parameters: - name: Connect-Protocol-Version in: header @@ -92,7 +92,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.ListRegisteredResourcesRequest' + $ref: '#/components/schemas/policy.registeredresources.DeleteRegisteredResourceRequest' required: true responses: default: @@ -106,13 +106,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.ListRegisteredResourcesResponse' - /policy.registeredresources.RegisteredResourcesService/UpdateRegisteredResource: + $ref: '#/components/schemas/policy.registeredresources.DeleteRegisteredResourceResponse' + /policy.registeredresources.RegisteredResourcesService/DeleteRegisteredResourceValue: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: UpdateRegisteredResource - operationId: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResource + summary: DeleteRegisteredResourceValue + operationId: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResourceValue parameters: - name: Connect-Protocol-Version in: header @@ -127,7 +127,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.UpdateRegisteredResourceRequest' + $ref: '#/components/schemas/policy.registeredresources.DeleteRegisteredResourceValueRequest' required: true responses: default: @@ -141,13 +141,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.UpdateRegisteredResourceResponse' - /policy.registeredresources.RegisteredResourcesService/DeleteRegisteredResource: + $ref: '#/components/schemas/policy.registeredresources.DeleteRegisteredResourceValueResponse' + /policy.registeredresources.RegisteredResourcesService/GetRegisteredResource: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: DeleteRegisteredResource - operationId: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResource + summary: GetRegisteredResource + operationId: policy.registeredresources.RegisteredResourcesService.GetRegisteredResource parameters: - name: Connect-Protocol-Version in: header @@ -162,7 +162,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.DeleteRegisteredResourceRequest' + $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceRequest' required: true responses: default: @@ -176,13 +176,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.DeleteRegisteredResourceResponse' - /policy.registeredresources.RegisteredResourcesService/CreateRegisteredResourceValue: + $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceResponse' + /policy.registeredresources.RegisteredResourcesService/GetRegisteredResourceValue: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: CreateRegisteredResourceValue - operationId: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResourceValue + summary: GetRegisteredResourceValue + operationId: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValue parameters: - name: Connect-Protocol-Version in: header @@ -197,7 +197,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.CreateRegisteredResourceValueRequest' + $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceValueRequest' required: true responses: default: @@ -211,13 +211,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.CreateRegisteredResourceValueResponse' - /policy.registeredresources.RegisteredResourcesService/GetRegisteredResourceValue: + $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceValueResponse' + /policy.registeredresources.RegisteredResourcesService/GetRegisteredResourceValuesByFQNs: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: GetRegisteredResourceValue - operationId: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValue + summary: GetRegisteredResourceValuesByFQNs + operationId: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValuesByFQNs parameters: - name: Connect-Protocol-Version in: header @@ -232,7 +232,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceValueRequest' + $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceValuesByFQNsRequest' required: true responses: default: @@ -246,13 +246,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceValueResponse' - /policy.registeredresources.RegisteredResourcesService/GetRegisteredResourceValuesByFQNs: + $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse' + /policy.registeredresources.RegisteredResourcesService/ListRegisteredResourceValues: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: GetRegisteredResourceValuesByFQNs - operationId: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValuesByFQNs + summary: ListRegisteredResourceValues + operationId: policy.registeredresources.RegisteredResourcesService.ListRegisteredResourceValues parameters: - name: Connect-Protocol-Version in: header @@ -267,7 +267,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceValuesByFQNsRequest' + $ref: '#/components/schemas/policy.registeredresources.ListRegisteredResourceValuesRequest' required: true responses: default: @@ -281,13 +281,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse' - /policy.registeredresources.RegisteredResourcesService/ListRegisteredResourceValues: + $ref: '#/components/schemas/policy.registeredresources.ListRegisteredResourceValuesResponse' + /policy.registeredresources.RegisteredResourcesService/ListRegisteredResources: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: ListRegisteredResourceValues - operationId: policy.registeredresources.RegisteredResourcesService.ListRegisteredResourceValues + summary: ListRegisteredResources + operationId: policy.registeredresources.RegisteredResourcesService.ListRegisteredResources parameters: - name: Connect-Protocol-Version in: header @@ -302,7 +302,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.ListRegisteredResourceValuesRequest' + $ref: '#/components/schemas/policy.registeredresources.ListRegisteredResourcesRequest' required: true responses: default: @@ -316,13 +316,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.ListRegisteredResourceValuesResponse' - /policy.registeredresources.RegisteredResourcesService/UpdateRegisteredResourceValue: + $ref: '#/components/schemas/policy.registeredresources.ListRegisteredResourcesResponse' + /policy.registeredresources.RegisteredResourcesService/UpdateRegisteredResource: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: UpdateRegisteredResourceValue - operationId: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResourceValue + summary: UpdateRegisteredResource + operationId: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResource parameters: - name: Connect-Protocol-Version in: header @@ -337,7 +337,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.UpdateRegisteredResourceValueRequest' + $ref: '#/components/schemas/policy.registeredresources.UpdateRegisteredResourceRequest' required: true responses: default: @@ -351,13 +351,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.UpdateRegisteredResourceValueResponse' - /policy.registeredresources.RegisteredResourcesService/DeleteRegisteredResourceValue: + $ref: '#/components/schemas/policy.registeredresources.UpdateRegisteredResourceResponse' + /policy.registeredresources.RegisteredResourcesService/UpdateRegisteredResourceValue: post: tags: - policy.registeredresources.RegisteredResourcesService - summary: DeleteRegisteredResourceValue - operationId: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResourceValue + summary: UpdateRegisteredResourceValue + operationId: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResourceValue parameters: - name: Connect-Protocol-Version in: header @@ -372,7 +372,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.DeleteRegisteredResourceValueRequest' + $ref: '#/components/schemas/policy.registeredresources.UpdateRegisteredResourceValueRequest' required: true responses: default: @@ -386,78 +386,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.registeredresources.DeleteRegisteredResourceValueResponse' + $ref: '#/components/schemas/policy.registeredresources.UpdateRegisteredResourceValueResponse' components: schemas: - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.AttributeRuleTypeEnum: - type: string - title: AttributeRuleTypeEnum - enum: - - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED - - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF - - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF - - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY - policy.ConditionBooleanTypeEnum: - type: string - title: ConditionBooleanTypeEnum - enum: - - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED - - CONDITION_BOOLEAN_TYPE_ENUM_AND - - CONDITION_BOOLEAN_TYPE_ENUM_OR - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - policy.SubjectMappingOperatorEnum: - type: string - title: SubjectMappingOperatorEnum - enum: - - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED - - SUBJECT_MAPPING_OPERATOR_ENUM_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS common.Metadata: type: object properties: @@ -513,6 +444,82 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -525,8 +532,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -620,37 +627,65 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: type: string + title: id + description: Generated uuid in database + name: + type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard - properties: - id: - type: string - title: id - description: Generated uuid in database - name: - type: string - title: name - metadata: - title: metadata - $ref: '#/components/schemas/common.Metadata' + required: + - standard title: Action additionalProperties: false description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.Attribute: type: object properties: @@ -693,6 +728,12 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys associated with the attribute + allowTraversal: + title: allow_traversal + description: |- + Whether or not we will use the attribute definition during encryption + if the attribute value is missing. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Common metadata @@ -701,23 +742,14 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.AttributeRuleTypeEnum: + type: string + title: AttributeRuleTypeEnum + enum: + - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED + - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY policy.Condition: type: object properties: @@ -735,7 +767,6 @@ components: type: array items: type: string - minItems: 1 title: subject_external_values minItems: 1 description: |- @@ -751,6 +782,13 @@ components: * A Condition defines a rule of + policy.ConditionBooleanTypeEnum: + type: string + title: ConditionBooleanTypeEnum + enum: + - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED + - CONDITION_BOOLEAN_TYPE_ENUM_AND + - CONDITION_BOOLEAN_TYPE_ENUM_OR policy.ConditionGroup: type: object properties: @@ -787,18 +825,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -821,13 +872,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -890,12 +937,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: @@ -943,6 +984,10 @@ components: items: $ref: '#/components/schemas/policy.RequestContext' title: context + namespace: + title: namespace + description: The source namespace for this trigger, derived from the attribute value and action. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1027,7 +1072,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -1035,17 +1081,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -1066,6 +1109,9 @@ components: items: $ref: '#/components/schemas/policy.RegisteredResourceValue' title: values + namespace: + title: namespace + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata description: Common metadata @@ -1089,6 +1135,9 @@ components: items: $ref: '#/components/schemas/policy.RegisteredResourceValue.ActionAttributeValue' title: action_attribute_values + fqn: + type: string + title: fqn metadata: title: metadata description: Common metadata @@ -1167,6 +1216,10 @@ components: description: |- the common name for the group of resource mappings, which must be unique per namespace + fqn: + type: string + title: fqn + description: the fully qualified name of the resource mapping group metadata: title: metadata description: Common metadata @@ -1210,12 +1263,42 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC + description: |- + Sorting direction shared across list APIs. + When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, + the endpoint's request message defines the default ordering; see the + specific List* request docs. + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.SubjectConditionSet: type: object properties: id: type: string title: id + namespace: + title: namespace + description: |- + the namespace containing this subject condition set + possible this is empty in the case a subject condition set + has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' subjectSets: type: array items: @@ -1253,6 +1336,13 @@ components: $ref: '#/components/schemas/policy.Action' title: actions description: The actions permitted by subjects in this mapping + namespace: + title: namespace + description: |- + the namespace containing this subject mapping + possible this is empty. If so that means + the Subject Mapping has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1261,6 +1351,14 @@ components: description: |- Subject Mapping: A Policy assigning Subject Set(s) to a permitted attribute value + action(s) combination + policy.SubjectMappingOperatorEnum: + type: string + title: SubjectMappingOperatorEnum + enum: + - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + - SUBJECT_MAPPING_OPERATOR_ENUM_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS policy.SubjectSet: type: object properties: @@ -1331,7 +1429,8 @@ components: type: object allOf: - oneOf: - - properties: + - type: object + properties: actionId: type: string title: action_id @@ -1339,22 +1438,20 @@ components: title: action_id required: - actionId - - properties: + - type: object + properties: actionName: type: string title: action_name maxLength: 253 - description: |+ - Action name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored action name will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + description: | + action_name_format // Action name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored action name will be normalized to lower case. title: action_name required: - actionName - oneOf: - - properties: + - type: object + properties: attributeValueFqn: type: string title: attribute_value_fqn @@ -1363,7 +1460,8 @@ components: title: attribute_value_fqn required: - attributeValueFqn - - properties: + - type: object + properties: attributeValueId: type: string title: attribute_value_id @@ -1380,23 +1478,30 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Required - Registered Resource Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + rr_name_format // Registered Resource Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case. values: type: array items: type: string maxLength: 253 pattern: ^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$ - uniqueItems: true title: values uniqueItems: true - description: "Optional \n Registered Resource Values (when provided) must be alphanumeric strings, allowing hyphens and underscores but not as the first or last character.\n The stored value will be normalized to lower case." + description: |- + Optional + Registered Resource Values (when provided) must be alphanumeric strings, allowing hyphens and underscores but not as the first or last character. + The stored value will be normalized to lower case. + namespaceId: + type: string + title: namespace_id + format: uuid + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri metadata: title: metadata description: |- @@ -1427,13 +1532,9 @@ components: type: string title: value maxLength: 253 - description: |+ + description: | Required - Registered Resource Value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + rr_value_format // Registered Resource Value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case. actionAttributeValues: type: array items: @@ -1499,29 +1600,38 @@ components: additionalProperties: false policy.registeredresources.GetRegisteredResourceRequest: type: object - oneOf: + allOf: - properties: - id: + namespaceFqn: type: string - title: id - format: uuid - title: id - required: - - id - - properties: - name: + title: namespace_fqn + minLength: 1 + format: uri + namespaceId: type: string + title: namespace_id + format: uuid + - oneOf: + - type: object + properties: + id: + type: string + title: id + format: uuid + title: id + required: + - id + - type: object + properties: + name: + type: string + title: name + maxLength: 253 + description: | + rr_name_format // Registered Resource Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case. title: name - maxLength: 253 - description: |+ - Registered Resource Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case.: - ``` - size(this) > 0 ? this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') : true - ``` - - title: name - required: - - name + required: + - name title: GetRegisteredResourceRequest additionalProperties: false policy.registeredresources.GetRegisteredResourceResponse: @@ -1535,7 +1645,8 @@ components: policy.registeredresources.GetRegisteredResourceValueRequest: type: object oneOf: - - properties: + - type: object + properties: fqn: type: string title: fqn @@ -1544,7 +1655,8 @@ components: title: fqn required: - fqn - - properties: + - type: object + properties: id: type: string title: id @@ -1571,8 +1683,6 @@ components: type: string minLength: 1 format: uri - minItems: 1 - uniqueItems: true title: fqns minItems: 1 uniqueItems: true @@ -1607,13 +1717,9 @@ components: resourceId: type: string title: resource_id - description: |+ + description: | Optional - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID pagination: title: pagination description: Optional @@ -1636,10 +1742,31 @@ components: policy.registeredresources.ListRegisteredResourcesRequest: type: object properties: + namespaceId: + type: string + title: namespace_id + format: uuid + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri pagination: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.registeredresources.RegisteredResourcesSort' + title: sort + maxItems: 1 + description: |- + Optional - CONSTRAINT: max 1 item + Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC title: ListRegisteredResourcesRequest additionalProperties: false policy.registeredresources.ListRegisteredResourcesResponse: @@ -1655,6 +1782,25 @@ components: $ref: '#/components/schemas/policy.PageResponse' title: ListRegisteredResourcesResponse additionalProperties: false + policy.registeredresources.RegisteredResourcesSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.registeredresources.SortRegisteredResourcesType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: RegisteredResourcesSort + additionalProperties: false + policy.registeredresources.SortRegisteredResourcesType: + type: string + title: SortRegisteredResourcesType + enum: + - SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED + - SORT_REGISTERED_RESOURCES_TYPE_NAME + - SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT + - SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT policy.registeredresources.UpdateRegisteredResourceRequest: type: object properties: @@ -1667,13 +1813,9 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Optional - Registered Resource Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case.: - ``` - size(this) > 0 ? this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') : true - ``` - + rr_name_format // Registered Resource Name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored name will be normalized to lower case. metadata: title: metadata description: |- @@ -1705,13 +1847,9 @@ components: type: string title: value maxLength: 253 - description: |+ + description: | Optional - Registered Resource Value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case.: - ``` - size(this) > 0 ? this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') : true - ``` - + rr_value_format // Registered Resource Value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored value will be normalized to lower case. actionAttributeValues: type: array items: @@ -1739,63 +1877,6 @@ components: $ref: '#/components/schemas/policy.RegisteredResourceValue' title: UpdateRegisteredResourceValueResponse additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.registeredresources.RegisteredResourcesService diff --git a/docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml b/docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml index b7a687df31..7928a38a98 100644 --- a/docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml +++ b/docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml @@ -2,12 +2,12 @@ openapi: 3.1.0 info: title: policy.resourcemapping paths: - /policy.resourcemapping.ResourceMappingService/ListResourceMappingGroups: + /policy.resourcemapping.ResourceMappingService/CreateResourceMapping: post: tags: - policy.resourcemapping.ResourceMappingService - summary: ListResourceMappingGroups - operationId: policy.resourcemapping.ResourceMappingService.ListResourceMappingGroups + summary: CreateResourceMapping + operationId: policy.resourcemapping.ResourceMappingService.CreateResourceMapping parameters: - name: Connect-Protocol-Version in: header @@ -22,7 +22,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingGroupsRequest' + $ref: '#/components/schemas/policy.resourcemapping.CreateResourceMappingRequest' required: true responses: default: @@ -36,13 +36,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingGroupsResponse' - /policy.resourcemapping.ResourceMappingService/GetResourceMappingGroup: + $ref: '#/components/schemas/policy.resourcemapping.CreateResourceMappingResponse' + /policy.resourcemapping.ResourceMappingService/CreateResourceMappingGroup: post: tags: - policy.resourcemapping.ResourceMappingService - summary: GetResourceMappingGroup - operationId: policy.resourcemapping.ResourceMappingService.GetResourceMappingGroup + summary: CreateResourceMappingGroup + operationId: policy.resourcemapping.ResourceMappingService.CreateResourceMappingGroup parameters: - name: Connect-Protocol-Version in: header @@ -57,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.GetResourceMappingGroupRequest' + $ref: '#/components/schemas/policy.resourcemapping.CreateResourceMappingGroupRequest' required: true responses: default: @@ -71,13 +71,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.GetResourceMappingGroupResponse' - /policy.resourcemapping.ResourceMappingService/CreateResourceMappingGroup: + $ref: '#/components/schemas/policy.resourcemapping.CreateResourceMappingGroupResponse' + /policy.resourcemapping.ResourceMappingService/DeleteResourceMapping: post: tags: - policy.resourcemapping.ResourceMappingService - summary: CreateResourceMappingGroup - operationId: policy.resourcemapping.ResourceMappingService.CreateResourceMappingGroup + summary: DeleteResourceMapping + operationId: policy.resourcemapping.ResourceMappingService.DeleteResourceMapping parameters: - name: Connect-Protocol-Version in: header @@ -92,7 +92,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.CreateResourceMappingGroupRequest' + $ref: '#/components/schemas/policy.resourcemapping.DeleteResourceMappingRequest' required: true responses: default: @@ -106,13 +106,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.CreateResourceMappingGroupResponse' - /policy.resourcemapping.ResourceMappingService/UpdateResourceMappingGroup: + $ref: '#/components/schemas/policy.resourcemapping.DeleteResourceMappingResponse' + /policy.resourcemapping.ResourceMappingService/DeleteResourceMappingGroup: post: tags: - policy.resourcemapping.ResourceMappingService - summary: UpdateResourceMappingGroup - operationId: policy.resourcemapping.ResourceMappingService.UpdateResourceMappingGroup + summary: DeleteResourceMappingGroup + operationId: policy.resourcemapping.ResourceMappingService.DeleteResourceMappingGroup parameters: - name: Connect-Protocol-Version in: header @@ -127,7 +127,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.UpdateResourceMappingGroupRequest' + $ref: '#/components/schemas/policy.resourcemapping.DeleteResourceMappingGroupRequest' required: true responses: default: @@ -141,13 +141,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.UpdateResourceMappingGroupResponse' - /policy.resourcemapping.ResourceMappingService/DeleteResourceMappingGroup: + $ref: '#/components/schemas/policy.resourcemapping.DeleteResourceMappingGroupResponse' + /policy.resourcemapping.ResourceMappingService/GetResourceMapping: post: tags: - policy.resourcemapping.ResourceMappingService - summary: DeleteResourceMappingGroup - operationId: policy.resourcemapping.ResourceMappingService.DeleteResourceMappingGroup + summary: GetResourceMapping + operationId: policy.resourcemapping.ResourceMappingService.GetResourceMapping parameters: - name: Connect-Protocol-Version in: header @@ -162,7 +162,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.DeleteResourceMappingGroupRequest' + $ref: '#/components/schemas/policy.resourcemapping.GetResourceMappingRequest' required: true responses: default: @@ -176,13 +176,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.DeleteResourceMappingGroupResponse' - /policy.resourcemapping.ResourceMappingService/ListResourceMappings: + $ref: '#/components/schemas/policy.resourcemapping.GetResourceMappingResponse' + /policy.resourcemapping.ResourceMappingService/GetResourceMappingGroup: post: tags: - policy.resourcemapping.ResourceMappingService - summary: ListResourceMappings - operationId: policy.resourcemapping.ResourceMappingService.ListResourceMappings + summary: GetResourceMappingGroup + operationId: policy.resourcemapping.ResourceMappingService.GetResourceMappingGroup parameters: - name: Connect-Protocol-Version in: header @@ -197,7 +197,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingsRequest' + $ref: '#/components/schemas/policy.resourcemapping.GetResourceMappingGroupRequest' required: true responses: default: @@ -211,13 +211,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingsResponse' - /policy.resourcemapping.ResourceMappingService/ListResourceMappingsByGroupFqns: + $ref: '#/components/schemas/policy.resourcemapping.GetResourceMappingGroupResponse' + /policy.resourcemapping.ResourceMappingService/ListResourceMappingGroups: post: tags: - policy.resourcemapping.ResourceMappingService - summary: ListResourceMappingsByGroupFqns - operationId: policy.resourcemapping.ResourceMappingService.ListResourceMappingsByGroupFqns + summary: ListResourceMappingGroups + operationId: policy.resourcemapping.ResourceMappingService.ListResourceMappingGroups parameters: - name: Connect-Protocol-Version in: header @@ -232,7 +232,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingsByGroupFqnsRequest' + $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingGroupsRequest' required: true responses: default: @@ -246,13 +246,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingsByGroupFqnsResponse' - /policy.resourcemapping.ResourceMappingService/GetResourceMapping: + $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingGroupsResponse' + /policy.resourcemapping.ResourceMappingService/ListResourceMappings: post: tags: - policy.resourcemapping.ResourceMappingService - summary: GetResourceMapping - operationId: policy.resourcemapping.ResourceMappingService.GetResourceMapping + summary: ListResourceMappings + operationId: policy.resourcemapping.ResourceMappingService.ListResourceMappings parameters: - name: Connect-Protocol-Version in: header @@ -267,7 +267,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.GetResourceMappingRequest' + $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingsRequest' required: true responses: default: @@ -281,13 +281,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.GetResourceMappingResponse' - /policy.resourcemapping.ResourceMappingService/CreateResourceMapping: + $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingsResponse' + /policy.resourcemapping.ResourceMappingService/ListResourceMappingsByGroupFqns: post: tags: - policy.resourcemapping.ResourceMappingService - summary: CreateResourceMapping - operationId: policy.resourcemapping.ResourceMappingService.CreateResourceMapping + summary: ListResourceMappingsByGroupFqns + operationId: policy.resourcemapping.ResourceMappingService.ListResourceMappingsByGroupFqns parameters: - name: Connect-Protocol-Version in: header @@ -302,7 +302,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.CreateResourceMappingRequest' + $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingsByGroupFqnsRequest' required: true responses: default: @@ -316,7 +316,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.CreateResourceMappingResponse' + $ref: '#/components/schemas/policy.resourcemapping.ListResourceMappingsByGroupFqnsResponse' /policy.resourcemapping.ResourceMappingService/UpdateResourceMapping: post: tags: @@ -352,12 +352,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.resourcemapping.UpdateResourceMappingResponse' - /policy.resourcemapping.ResourceMappingService/DeleteResourceMapping: + /policy.resourcemapping.ResourceMappingService/UpdateResourceMappingGroup: post: tags: - policy.resourcemapping.ResourceMappingService - summary: DeleteResourceMapping - operationId: policy.resourcemapping.ResourceMappingService.DeleteResourceMapping + summary: UpdateResourceMappingGroup + operationId: policy.resourcemapping.ResourceMappingService.UpdateResourceMappingGroup parameters: - name: Connect-Protocol-Version in: header @@ -372,7 +372,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.DeleteResourceMappingRequest' + $ref: '#/components/schemas/policy.resourcemapping.UpdateResourceMappingGroupRequest' required: true responses: default: @@ -386,78 +386,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.resourcemapping.DeleteResourceMappingResponse' + $ref: '#/components/schemas/policy.resourcemapping.UpdateResourceMappingGroupResponse' components: schemas: - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.AttributeRuleTypeEnum: - type: string - title: AttributeRuleTypeEnum - enum: - - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED - - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF - - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF - - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY - policy.ConditionBooleanTypeEnum: - type: string - title: ConditionBooleanTypeEnum - enum: - - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED - - CONDITION_BOOLEAN_TYPE_ENUM_AND - - CONDITION_BOOLEAN_TYPE_ENUM_OR - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - policy.SubjectMappingOperatorEnum: - type: string - title: SubjectMappingOperatorEnum - enum: - - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED - - SUBJECT_MAPPING_OPERATOR_ENUM_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS common.Metadata: type: object properties: @@ -513,6 +444,82 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -525,8 +532,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -620,37 +627,65 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: + type: string + title: id + description: Generated uuid in database + name: type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard - properties: - id: - type: string - title: id - description: Generated uuid in database - name: - type: string - title: name - metadata: - title: metadata - $ref: '#/components/schemas/common.Metadata' + required: + - standard title: Action additionalProperties: false description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.Attribute: type: object properties: @@ -693,6 +728,12 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys associated with the attribute + allowTraversal: + title: allow_traversal + description: |- + Whether or not we will use the attribute definition during encryption + if the attribute value is missing. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Common metadata @@ -701,23 +742,14 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.AttributeRuleTypeEnum: + type: string + title: AttributeRuleTypeEnum + enum: + - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED + - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY policy.Condition: type: object properties: @@ -735,7 +767,6 @@ components: type: array items: type: string - minItems: 1 title: subject_external_values minItems: 1 description: |- @@ -751,6 +782,13 @@ components: * A Condition defines a rule of + policy.ConditionBooleanTypeEnum: + type: string + title: ConditionBooleanTypeEnum + enum: + - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED + - CONDITION_BOOLEAN_TYPE_ENUM_AND + - CONDITION_BOOLEAN_TYPE_ENUM_OR policy.ConditionGroup: type: object properties: @@ -787,18 +825,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -821,13 +872,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -890,12 +937,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: @@ -943,6 +984,10 @@ components: items: $ref: '#/components/schemas/policy.RequestContext' title: context + namespace: + title: namespace + description: The source namespace for this trigger, derived from the attribute value and action. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1027,7 +1072,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -1035,17 +1081,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -1106,6 +1149,10 @@ components: description: |- the common name for the group of resource mappings, which must be unique per namespace + fqn: + type: string + title: fqn + description: the fully qualified name of the resource mapping group metadata: title: metadata description: Common metadata @@ -1149,12 +1196,30 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.SubjectConditionSet: type: object properties: id: type: string title: id + namespace: + title: namespace + description: |- + the namespace containing this subject condition set + possible this is empty in the case a subject condition set + has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' subjectSets: type: array items: @@ -1192,6 +1257,13 @@ components: $ref: '#/components/schemas/policy.Action' title: actions description: The actions permitted by subjects in this mapping + namespace: + title: namespace + description: |- + the namespace containing this subject mapping + possible this is empty. If so that means + the Subject Mapping has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1200,6 +1272,14 @@ components: description: |- Subject Mapping: A Policy assigning Subject Set(s) to a permitted attribute value + action(s) combination + policy.SubjectMappingOperatorEnum: + type: string + title: SubjectMappingOperatorEnum + enum: + - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + - SUBJECT_MAPPING_OPERATOR_ENUM_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS policy.SubjectSet: type: object properties: @@ -1306,8 +1386,6 @@ components: type: array items: type: string - maxItems: 1000 - minItems: 1 title: terms maxItems: 1000 minItems: 1 @@ -1315,13 +1393,9 @@ components: groupId: type: string title: group_id - description: |+ + description: | Optional - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID metadata: title: metadata description: Optional @@ -1414,13 +1488,9 @@ components: namespaceId: type: string title: namespace_id - description: |+ + description: | Optional - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID pagination: title: pagination description: Optional @@ -1447,7 +1517,6 @@ components: type: array items: type: string - minItems: 1 title: fqns minItems: 1 description: |- @@ -1483,13 +1552,9 @@ components: groupId: type: string title: group_id - description: |+ + description: | Optional - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID pagination: title: pagination description: Optional @@ -1533,24 +1598,16 @@ components: namespaceId: type: string title: namespace_id - description: |+ + description: | Optional - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID name: type: string title: name maxLength: 253 - description: |+ + description: | Optional - Optional field must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored group name will be normalized to lower case.: - ``` - size(this) == 0 || this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + optional_name_format // Optional field must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored group name will be normalized to lower case. metadata: title: metadata description: Common metadata @@ -1579,31 +1636,22 @@ components: attributeValueId: type: string title: attribute_value_id - description: |+ + description: | Optional - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID terms: type: array items: type: string - maxItems: 1000 title: terms maxItems: 1000 description: Optional groupId: type: string title: group_id - description: |+ + description: | Optional - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID metadata: title: metadata description: |- @@ -1623,63 +1671,6 @@ components: $ref: '#/components/schemas/policy.ResourceMapping' title: UpdateResourceMappingResponse additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.resourcemapping.ResourceMappingService diff --git a/docs/openapi/policy/selectors.openapi.yaml b/docs/openapi/policy/selectors.openapi.yaml index e2d3c3a756..65cc330d4a 100644 --- a/docs/openapi/policy/selectors.openapi.yaml +++ b/docs/openapi/policy/selectors.openapi.yaml @@ -152,4 +152,16 @@ components: description: Total count of entire list title: PageResponse additionalProperties: false + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC + description: |- + Sorting direction shared across list APIs. + When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, + the endpoint's request message defines the default ordering; see the + specific List* request docs. security: [] diff --git a/docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml b/docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml index c48bebccee..10e2ebeb84 100644 --- a/docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml +++ b/docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml @@ -2,13 +2,12 @@ openapi: 3.1.0 info: title: policy.subjectmapping paths: - /policy.subjectmapping.SubjectMappingService/MatchSubjectMappings: + /policy.subjectmapping.SubjectMappingService/CreateSubjectConditionSet: post: tags: - policy.subjectmapping.SubjectMappingService - summary: MatchSubjectMappings - description: Find matching Subject Mappings for a given Subject - operationId: policy.subjectmapping.SubjectMappingService.MatchSubjectMappings + summary: CreateSubjectConditionSet + operationId: policy.subjectmapping.SubjectMappingService.CreateSubjectConditionSet parameters: - name: Connect-Protocol-Version in: header @@ -23,7 +22,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.MatchSubjectMappingsRequest' + $ref: '#/components/schemas/policy.subjectmapping.CreateSubjectConditionSetRequest' required: true responses: default: @@ -37,13 +36,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.MatchSubjectMappingsResponse' - /policy.subjectmapping.SubjectMappingService/ListSubjectMappings: + $ref: '#/components/schemas/policy.subjectmapping.CreateSubjectConditionSetResponse' + /policy.subjectmapping.SubjectMappingService/CreateSubjectMapping: post: tags: - policy.subjectmapping.SubjectMappingService - summary: ListSubjectMappings - operationId: policy.subjectmapping.SubjectMappingService.ListSubjectMappings + summary: CreateSubjectMapping + operationId: policy.subjectmapping.SubjectMappingService.CreateSubjectMapping parameters: - name: Connect-Protocol-Version in: header @@ -58,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.ListSubjectMappingsRequest' + $ref: '#/components/schemas/policy.subjectmapping.CreateSubjectMappingRequest' required: true responses: default: @@ -72,13 +71,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.ListSubjectMappingsResponse' - /policy.subjectmapping.SubjectMappingService/GetSubjectMapping: + $ref: '#/components/schemas/policy.subjectmapping.CreateSubjectMappingResponse' + /policy.subjectmapping.SubjectMappingService/DeleteAllUnmappedSubjectConditionSets: post: tags: - policy.subjectmapping.SubjectMappingService - summary: GetSubjectMapping - operationId: policy.subjectmapping.SubjectMappingService.GetSubjectMapping + summary: DeleteAllUnmappedSubjectConditionSets + operationId: policy.subjectmapping.SubjectMappingService.DeleteAllUnmappedSubjectConditionSets parameters: - name: Connect-Protocol-Version in: header @@ -93,7 +92,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.GetSubjectMappingRequest' + $ref: '#/components/schemas/policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsRequest' required: true responses: default: @@ -107,13 +106,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.GetSubjectMappingResponse' - /policy.subjectmapping.SubjectMappingService/CreateSubjectMapping: + $ref: '#/components/schemas/policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse' + /policy.subjectmapping.SubjectMappingService/DeleteSubjectConditionSet: post: tags: - policy.subjectmapping.SubjectMappingService - summary: CreateSubjectMapping - operationId: policy.subjectmapping.SubjectMappingService.CreateSubjectMapping + summary: DeleteSubjectConditionSet + operationId: policy.subjectmapping.SubjectMappingService.DeleteSubjectConditionSet parameters: - name: Connect-Protocol-Version in: header @@ -128,7 +127,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.CreateSubjectMappingRequest' + $ref: '#/components/schemas/policy.subjectmapping.DeleteSubjectConditionSetRequest' required: true responses: default: @@ -142,13 +141,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.CreateSubjectMappingResponse' - /policy.subjectmapping.SubjectMappingService/UpdateSubjectMapping: + $ref: '#/components/schemas/policy.subjectmapping.DeleteSubjectConditionSetResponse' + /policy.subjectmapping.SubjectMappingService/DeleteSubjectMapping: post: tags: - policy.subjectmapping.SubjectMappingService - summary: UpdateSubjectMapping - operationId: policy.subjectmapping.SubjectMappingService.UpdateSubjectMapping + summary: DeleteSubjectMapping + operationId: policy.subjectmapping.SubjectMappingService.DeleteSubjectMapping parameters: - name: Connect-Protocol-Version in: header @@ -163,7 +162,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.UpdateSubjectMappingRequest' + $ref: '#/components/schemas/policy.subjectmapping.DeleteSubjectMappingRequest' required: true responses: default: @@ -177,13 +176,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.UpdateSubjectMappingResponse' - /policy.subjectmapping.SubjectMappingService/DeleteSubjectMapping: + $ref: '#/components/schemas/policy.subjectmapping.DeleteSubjectMappingResponse' + /policy.subjectmapping.SubjectMappingService/GetSubjectConditionSet: post: tags: - policy.subjectmapping.SubjectMappingService - summary: DeleteSubjectMapping - operationId: policy.subjectmapping.SubjectMappingService.DeleteSubjectMapping + summary: GetSubjectConditionSet + operationId: policy.subjectmapping.SubjectMappingService.GetSubjectConditionSet parameters: - name: Connect-Protocol-Version in: header @@ -198,7 +197,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.DeleteSubjectMappingRequest' + $ref: '#/components/schemas/policy.subjectmapping.GetSubjectConditionSetRequest' required: true responses: default: @@ -212,13 +211,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.DeleteSubjectMappingResponse' - /policy.subjectmapping.SubjectMappingService/ListSubjectConditionSets: + $ref: '#/components/schemas/policy.subjectmapping.GetSubjectConditionSetResponse' + /policy.subjectmapping.SubjectMappingService/GetSubjectMapping: post: tags: - policy.subjectmapping.SubjectMappingService - summary: ListSubjectConditionSets - operationId: policy.subjectmapping.SubjectMappingService.ListSubjectConditionSets + summary: GetSubjectMapping + operationId: policy.subjectmapping.SubjectMappingService.GetSubjectMapping parameters: - name: Connect-Protocol-Version in: header @@ -233,7 +232,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.ListSubjectConditionSetsRequest' + $ref: '#/components/schemas/policy.subjectmapping.GetSubjectMappingRequest' required: true responses: default: @@ -247,13 +246,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.ListSubjectConditionSetsResponse' - /policy.subjectmapping.SubjectMappingService/GetSubjectConditionSet: + $ref: '#/components/schemas/policy.subjectmapping.GetSubjectMappingResponse' + /policy.subjectmapping.SubjectMappingService/ListSubjectConditionSets: post: tags: - policy.subjectmapping.SubjectMappingService - summary: GetSubjectConditionSet - operationId: policy.subjectmapping.SubjectMappingService.GetSubjectConditionSet + summary: ListSubjectConditionSets + operationId: policy.subjectmapping.SubjectMappingService.ListSubjectConditionSets parameters: - name: Connect-Protocol-Version in: header @@ -268,7 +267,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.GetSubjectConditionSetRequest' + $ref: '#/components/schemas/policy.subjectmapping.ListSubjectConditionSetsRequest' required: true responses: default: @@ -282,13 +281,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.GetSubjectConditionSetResponse' - /policy.subjectmapping.SubjectMappingService/CreateSubjectConditionSet: + $ref: '#/components/schemas/policy.subjectmapping.ListSubjectConditionSetsResponse' + /policy.subjectmapping.SubjectMappingService/ListSubjectMappings: post: tags: - policy.subjectmapping.SubjectMappingService - summary: CreateSubjectConditionSet - operationId: policy.subjectmapping.SubjectMappingService.CreateSubjectConditionSet + summary: ListSubjectMappings + operationId: policy.subjectmapping.SubjectMappingService.ListSubjectMappings parameters: - name: Connect-Protocol-Version in: header @@ -303,7 +302,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.CreateSubjectConditionSetRequest' + $ref: '#/components/schemas/policy.subjectmapping.ListSubjectMappingsRequest' required: true responses: default: @@ -317,13 +316,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.CreateSubjectConditionSetResponse' - /policy.subjectmapping.SubjectMappingService/UpdateSubjectConditionSet: + $ref: '#/components/schemas/policy.subjectmapping.ListSubjectMappingsResponse' + /policy.subjectmapping.SubjectMappingService/MatchSubjectMappings: post: tags: - policy.subjectmapping.SubjectMappingService - summary: UpdateSubjectConditionSet - operationId: policy.subjectmapping.SubjectMappingService.UpdateSubjectConditionSet + summary: MatchSubjectMappings + description: Find matching Subject Mappings for a given Subject + operationId: policy.subjectmapping.SubjectMappingService.MatchSubjectMappings parameters: - name: Connect-Protocol-Version in: header @@ -338,7 +338,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.UpdateSubjectConditionSetRequest' + $ref: '#/components/schemas/policy.subjectmapping.MatchSubjectMappingsRequest' required: true responses: default: @@ -352,13 +352,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.UpdateSubjectConditionSetResponse' - /policy.subjectmapping.SubjectMappingService/DeleteSubjectConditionSet: + $ref: '#/components/schemas/policy.subjectmapping.MatchSubjectMappingsResponse' + /policy.subjectmapping.SubjectMappingService/UpdateSubjectConditionSet: post: tags: - policy.subjectmapping.SubjectMappingService - summary: DeleteSubjectConditionSet - operationId: policy.subjectmapping.SubjectMappingService.DeleteSubjectConditionSet + summary: UpdateSubjectConditionSet + operationId: policy.subjectmapping.SubjectMappingService.UpdateSubjectConditionSet parameters: - name: Connect-Protocol-Version in: header @@ -373,7 +373,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.DeleteSubjectConditionSetRequest' + $ref: '#/components/schemas/policy.subjectmapping.UpdateSubjectConditionSetRequest' required: true responses: default: @@ -387,13 +387,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.DeleteSubjectConditionSetResponse' - /policy.subjectmapping.SubjectMappingService/DeleteAllUnmappedSubjectConditionSets: + $ref: '#/components/schemas/policy.subjectmapping.UpdateSubjectConditionSetResponse' + /policy.subjectmapping.SubjectMappingService/UpdateSubjectMapping: post: tags: - policy.subjectmapping.SubjectMappingService - summary: DeleteAllUnmappedSubjectConditionSets - operationId: policy.subjectmapping.SubjectMappingService.DeleteAllUnmappedSubjectConditionSets + summary: UpdateSubjectMapping + operationId: policy.subjectmapping.SubjectMappingService.UpdateSubjectMapping parameters: - name: Connect-Protocol-Version in: header @@ -408,7 +408,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsRequest' + $ref: '#/components/schemas/policy.subjectmapping.UpdateSubjectMappingRequest' required: true responses: default: @@ -422,78 +422,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse' + $ref: '#/components/schemas/policy.subjectmapping.UpdateSubjectMappingResponse' components: schemas: - common.MetadataUpdateEnum: - type: string - title: MetadataUpdateEnum - enum: - - METADATA_UPDATE_ENUM_UNSPECIFIED - - METADATA_UPDATE_ENUM_EXTEND - - METADATA_UPDATE_ENUM_REPLACE - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.AttributeRuleTypeEnum: - type: string - title: AttributeRuleTypeEnum - enum: - - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED - - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF - - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF - - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY - policy.ConditionBooleanTypeEnum: - type: string - title: ConditionBooleanTypeEnum - enum: - - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED - - CONDITION_BOOLEAN_TYPE_ENUM_AND - - CONDITION_BOOLEAN_TYPE_ENUM_OR - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - policy.SubjectMappingOperatorEnum: - type: string - title: SubjectMappingOperatorEnum - enum: - - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED - - SUBJECT_MAPPING_OPERATOR_ENUM_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS common.Metadata: type: object properties: @@ -549,6 +480,82 @@ components: title: value title: LabelsEntry additionalProperties: false + common.MetadataUpdateEnum: + type: string + title: MetadataUpdateEnum + enum: + - METADATA_UPDATE_ENUM_UNSPECIFIED + - METADATA_UPDATE_ENUM_EXTEND + - METADATA_UPDATE_ENUM_REPLACE + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -561,8 +568,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -656,37 +663,65 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: type: string + title: id + description: Generated uuid in database + name: + type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard - properties: - id: - type: string - title: id - description: Generated uuid in database - name: - type: string - title: name - metadata: - title: metadata - $ref: '#/components/schemas/common.Metadata' + required: + - standard title: Action additionalProperties: false description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.Attribute: type: object properties: @@ -729,6 +764,12 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys associated with the attribute + allowTraversal: + title: allow_traversal + description: |- + Whether or not we will use the attribute definition during encryption + if the attribute value is missing. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Common metadata @@ -737,23 +778,14 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.AttributeRuleTypeEnum: + type: string + title: AttributeRuleTypeEnum + enum: + - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED + - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY policy.Condition: type: object properties: @@ -771,7 +803,6 @@ components: type: array items: type: string - minItems: 1 title: subject_external_values minItems: 1 description: |- @@ -787,6 +818,13 @@ components: * A Condition defines a rule of + policy.ConditionBooleanTypeEnum: + type: string + title: ConditionBooleanTypeEnum + enum: + - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED + - CONDITION_BOOLEAN_TYPE_ENUM_AND + - CONDITION_BOOLEAN_TYPE_ENUM_OR policy.ConditionGroup: type: object properties: @@ -823,18 +861,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -857,13 +908,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -926,12 +973,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: @@ -979,6 +1020,10 @@ components: items: $ref: '#/components/schemas/policy.RequestContext' title: context + namespace: + title: namespace + description: The source namespace for this trigger, derived from the attribute value and action. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1063,7 +1108,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -1071,17 +1117,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -1142,6 +1185,10 @@ components: description: |- the common name for the group of resource mappings, which must be unique per namespace + fqn: + type: string + title: fqn + description: the fully qualified name of the resource mapping group metadata: title: metadata description: Common metadata @@ -1185,12 +1232,42 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC + description: |- + Sorting direction shared across list APIs. + When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, + the endpoint's request message defines the default ordering; see the + specific List* request docs. + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.SubjectConditionSet: type: object properties: id: type: string title: id + namespace: + title: namespace + description: |- + the namespace containing this subject condition set + possible this is empty in the case a subject condition set + has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' subjectSets: type: array items: @@ -1228,6 +1305,13 @@ components: $ref: '#/components/schemas/policy.Action' title: actions description: The actions permitted by subjects in this mapping + namespace: + title: namespace + description: |- + the namespace containing this subject mapping + possible this is empty. If so that means + the Subject Mapping has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1236,6 +1320,14 @@ components: description: |- Subject Mapping: A Policy assigning Subject Set(s) to a permitted attribute value + action(s) combination + policy.SubjectMappingOperatorEnum: + type: string + title: SubjectMappingOperatorEnum + enum: + - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + - SUBJECT_MAPPING_OPERATOR_ENUM_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS policy.SubjectProperty: type: object properties: @@ -1256,7 +1348,6 @@ components: authoritative source such as an IDP (Identity Provider) or User Store. Examples include such ADFS/LDAP, OKTA, etc. For now, a valid property must contain both a selector expression & a resulting value. - The external_selector_value is a specifier to select a value from a flattened external representation of an Entity (such as from idP/LDAP), and the external_value is the value selected by the external_selector_value on that @@ -1333,6 +1424,15 @@ components: subjectConditionSet: title: subject_condition_set $ref: '#/components/schemas/policy.subjectmapping.SubjectConditionSetCreate' + namespaceId: + type: string + title: namespace_id + format: uuid + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri title: CreateSubjectConditionSetRequest required: - subjectConditionSet @@ -1361,29 +1461,33 @@ components: $ref: '#/components/schemas/policy.Action' title: actions minItems: 1 - description: |+ + description: | Required The actions permitted by subjects in this mapping - Action name or ID must not be empty if provided: - ``` - this.all(item, item.name != '' || item.id != '') - ``` - + action_name_or_id_not_empty // Action name or ID must not be empty if provided existingSubjectConditionSetId: type: string title: existing_subject_condition_set_id - description: |+ + description: | Either of the following: Reuse existing SubjectConditionSet (NOTE: prioritized over new_subject_condition_set) - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID newSubjectConditionSet: title: new_subject_condition_set description: 'Create new SubjectConditionSet (NOTE: ignored if existing_subject_condition_set_id is provided)' $ref: '#/components/schemas/policy.subjectmapping.SubjectConditionSetCreate' + namespaceId: + type: string + title: namespace_id + format: uuid + description: |- + Optional + Namespace ID or FQN for the subject mapping + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri metadata: title: metadata description: Optional @@ -1497,10 +1601,31 @@ components: policy.subjectmapping.ListSubjectConditionSetsRequest: type: object properties: + namespaceId: + type: string + title: namespace_id + format: uuid + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri pagination: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.subjectmapping.SubjectConditionSetsSort' + title: sort + maxItems: 1 + description: |- + Optional - CONSTRAINT: max 1 item + Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC title: ListSubjectConditionSetsRequest additionalProperties: false policy.subjectmapping.ListSubjectConditionSetsResponse: @@ -1519,10 +1644,31 @@ components: policy.subjectmapping.ListSubjectMappingsRequest: type: object properties: + namespaceId: + type: string + title: namespace_id + format: uuid + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri pagination: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.subjectmapping.SubjectMappingsSort' + title: sort + maxItems: 1 + description: |- + Optional - CONSTRAINT: max 1 item + Sort defaults: + - direction UNSPECIFIED defaults to DESC for the specified field + - field UNSPECIFIED defaults to created_at with the specified direction + - both UNSPECIFIED or sort omitted defaults to created_at DESC title: ListSubjectMappingsRequest additionalProperties: false policy.subjectmapping.ListSubjectMappingsResponse: @@ -1562,6 +1708,20 @@ components: title: subject_mappings title: MatchSubjectMappingsResponse additionalProperties: false + policy.subjectmapping.SortSubjectConditionSetsType: + type: string + title: SortSubjectConditionSetsType + enum: + - SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED + - SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT + - SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT + policy.subjectmapping.SortSubjectMappingsType: + type: string + title: SortSubjectMappingsType + enum: + - SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED + - SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT + - SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT policy.subjectmapping.SubjectConditionSetCreate: type: object properties: @@ -1580,6 +1740,28 @@ components: $ref: '#/components/schemas/common.MetadataMutable' title: SubjectConditionSetCreate additionalProperties: false + policy.subjectmapping.SubjectConditionSetsSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.subjectmapping.SortSubjectConditionSetsType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: SubjectConditionSetsSort + additionalProperties: false + policy.subjectmapping.SubjectMappingsSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.subjectmapping.SortSubjectMappingsType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: SubjectMappingsSort + additionalProperties: false policy.subjectmapping.UpdateSubjectConditionSetRequest: type: object properties: @@ -1625,27 +1807,19 @@ components: subjectConditionSetId: type: string title: subject_condition_set_id - description: |+ + description: | Optional Replaces the existing SubjectConditionSet id with a new one - Optional field must be a valid UUID: - ``` - size(this) == 0 || this.matches('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') - ``` - + optional_uuid_format // Optional field must be a valid UUID actions: type: array items: $ref: '#/components/schemas/policy.Action' title: actions - description: |+ + description: | Optional Replaces entire list of actions permitted by subjects - Action name or ID must not be empty if provided: - ``` - this.size() == 0 || this.all(item, item.name != '' || item.id != '') - ``` - + action_name_or_id_not_empty // Action name or ID must not be empty if provided metadata: title: metadata description: Common metadata @@ -1664,63 +1838,6 @@ components: $ref: '#/components/schemas/policy.SubjectMapping' title: UpdateSubjectMappingResponse additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.subjectmapping.SubjectMappingService diff --git a/docs/openapi/policy/unsafe/unsafe.openapi.yaml b/docs/openapi/policy/unsafe/unsafe.openapi.yaml index 34c9d384c0..168c60f735 100644 --- a/docs/openapi/policy/unsafe/unsafe.openapi.yaml +++ b/docs/openapi/policy/unsafe/unsafe.openapi.yaml @@ -2,16 +2,12 @@ openapi: 3.1.0 info: title: policy.unsafe paths: - /policy.unsafe.UnsafeService/UnsafeUpdateNamespace: + /policy.unsafe.UnsafeService/UnsafeDeleteAttribute: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeUpdateNamespace - description: |- - --------------------------------------* - Namespace RPCs - --------------------------------------- - operationId: policy.unsafe.UnsafeService.UnsafeUpdateNamespace + summary: UnsafeDeleteAttribute + operationId: policy.unsafe.UnsafeService.UnsafeDeleteAttribute parameters: - name: Connect-Protocol-Version in: header @@ -26,7 +22,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateNamespaceRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteAttributeRequest' required: true responses: default: @@ -40,13 +36,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateNamespaceResponse' - /policy.unsafe.UnsafeService/UnsafeReactivateNamespace: + $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteAttributeResponse' + /policy.unsafe.UnsafeService/UnsafeDeleteAttributeValue: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeReactivateNamespace - operationId: policy.unsafe.UnsafeService.UnsafeReactivateNamespace + summary: UnsafeDeleteAttributeValue + operationId: policy.unsafe.UnsafeService.UnsafeDeleteAttributeValue parameters: - name: Connect-Protocol-Version in: header @@ -61,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateNamespaceRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteAttributeValueRequest' required: true responses: default: @@ -75,13 +71,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateNamespaceResponse' - /policy.unsafe.UnsafeService/UnsafeDeleteNamespace: + $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteAttributeValueResponse' + /policy.unsafe.UnsafeService/UnsafeDeleteKasKey: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeDeleteNamespace - operationId: policy.unsafe.UnsafeService.UnsafeDeleteNamespace + summary: UnsafeDeleteKasKey + description: |- + --------------------------------------* + Kas Key RPCs + --------------------------------------- + operationId: policy.unsafe.UnsafeService.UnsafeDeleteKasKey parameters: - name: Connect-Protocol-Version in: header @@ -96,7 +96,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteNamespaceRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteKasKeyRequest' required: true responses: default: @@ -110,17 +110,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteNamespaceResponse' - /policy.unsafe.UnsafeService/UnsafeUpdateAttribute: + $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteKasKeyResponse' + /policy.unsafe.UnsafeService/UnsafeDeleteNamespace: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeUpdateAttribute - description: |- - --------------------------------------* - Attribute RPCs - --------------------------------------- - operationId: policy.unsafe.UnsafeService.UnsafeUpdateAttribute + summary: UnsafeDeleteNamespace + operationId: policy.unsafe.UnsafeService.UnsafeDeleteNamespace parameters: - name: Connect-Protocol-Version in: header @@ -135,7 +131,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateAttributeRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteNamespaceRequest' required: true responses: default: @@ -149,7 +145,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateAttributeResponse' + $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteNamespaceResponse' /policy.unsafe.UnsafeService/UnsafeReactivateAttribute: post: tags: @@ -185,12 +181,12 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateAttributeResponse' - /policy.unsafe.UnsafeService/UnsafeDeleteAttribute: + /policy.unsafe.UnsafeService/UnsafeReactivateAttributeValue: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeDeleteAttribute - operationId: policy.unsafe.UnsafeService.UnsafeDeleteAttribute + summary: UnsafeReactivateAttributeValue + operationId: policy.unsafe.UnsafeService.UnsafeReactivateAttributeValue parameters: - name: Connect-Protocol-Version in: header @@ -205,7 +201,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteAttributeRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateAttributeValueRequest' required: true responses: default: @@ -219,17 +215,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteAttributeResponse' - /policy.unsafe.UnsafeService/UnsafeUpdateAttributeValue: + $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateAttributeValueResponse' + /policy.unsafe.UnsafeService/UnsafeReactivateNamespace: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeUpdateAttributeValue - description: |- - --------------------------------------* - Value RPCs - --------------------------------------- - operationId: policy.unsafe.UnsafeService.UnsafeUpdateAttributeValue + summary: UnsafeReactivateNamespace + operationId: policy.unsafe.UnsafeService.UnsafeReactivateNamespace parameters: - name: Connect-Protocol-Version in: header @@ -244,7 +236,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateAttributeValueRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateNamespaceRequest' required: true responses: default: @@ -258,13 +250,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateAttributeValueResponse' - /policy.unsafe.UnsafeService/UnsafeReactivateAttributeValue: + $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateNamespaceResponse' + /policy.unsafe.UnsafeService/UnsafeUpdateAttribute: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeReactivateAttributeValue - operationId: policy.unsafe.UnsafeService.UnsafeReactivateAttributeValue + summary: UnsafeUpdateAttribute + description: |- + --------------------------------------* + Attribute RPCs + --------------------------------------- + operationId: policy.unsafe.UnsafeService.UnsafeUpdateAttribute parameters: - name: Connect-Protocol-Version in: header @@ -279,7 +275,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateAttributeValueRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateAttributeRequest' required: true responses: default: @@ -293,13 +289,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeReactivateAttributeValueResponse' - /policy.unsafe.UnsafeService/UnsafeDeleteAttributeValue: + $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateAttributeResponse' + /policy.unsafe.UnsafeService/UnsafeUpdateAttributeValue: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeDeleteAttributeValue - operationId: policy.unsafe.UnsafeService.UnsafeDeleteAttributeValue + summary: UnsafeUpdateAttributeValue + description: |- + --------------------------------------* + Value RPCs + --------------------------------------- + operationId: policy.unsafe.UnsafeService.UnsafeUpdateAttributeValue parameters: - name: Connect-Protocol-Version in: header @@ -314,7 +314,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteAttributeValueRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateAttributeValueRequest' required: true responses: default: @@ -328,17 +328,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteAttributeValueResponse' - /policy.unsafe.UnsafeService/UnsafeDeleteKasKey: + $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateAttributeValueResponse' + /policy.unsafe.UnsafeService/UnsafeUpdateNamespace: post: tags: - policy.unsafe.UnsafeService - summary: UnsafeDeleteKasKey + summary: UnsafeUpdateNamespace description: |- --------------------------------------* - Kas Key RPCs + Namespace RPCs --------------------------------------- - operationId: policy.unsafe.UnsafeService.UnsafeDeleteKasKey + operationId: policy.unsafe.UnsafeService.UnsafeUpdateNamespace parameters: - name: Connect-Protocol-Version in: header @@ -353,7 +353,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteKasKeyRequest' + $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateNamespaceRequest' required: true responses: default: @@ -367,89 +367,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/policy.unsafe.UnsafeDeleteKasKeyResponse' + $ref: '#/components/schemas/policy.unsafe.UnsafeUpdateNamespaceResponse' components: schemas: - policy.Action.StandardAction: - type: string - title: StandardAction - enum: - - STANDARD_ACTION_UNSPECIFIED - - STANDARD_ACTION_DECRYPT - - STANDARD_ACTION_TRANSMIT - policy.Algorithm: - type: string - title: Algorithm - enum: - - ALGORITHM_UNSPECIFIED - - ALGORITHM_RSA_2048 - - ALGORITHM_RSA_4096 - - ALGORITHM_EC_P256 - - ALGORITHM_EC_P384 - - ALGORITHM_EC_P521 - description: Supported key algorithms. - policy.AttributeRuleTypeEnum: - type: string - title: AttributeRuleTypeEnum - enum: - - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED - - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF - - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF - - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY - policy.ConditionBooleanTypeEnum: - type: string - title: ConditionBooleanTypeEnum - enum: - - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED - - CONDITION_BOOLEAN_TYPE_ENUM_AND - - CONDITION_BOOLEAN_TYPE_ENUM_OR - policy.KasPublicKeyAlgEnum: - type: string - title: KasPublicKeyAlgEnum - enum: - - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 - - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 - policy.KeyMode: - type: string - title: KeyMode - enum: - - KEY_MODE_UNSPECIFIED - - KEY_MODE_CONFIG_ROOT_KEY - - KEY_MODE_PROVIDER_ROOT_KEY - - KEY_MODE_REMOTE - - KEY_MODE_PUBLIC_KEY_ONLY - description: Describes the management and operational mode of a cryptographic key. - policy.KeyStatus: - type: string - title: KeyStatus - enum: - - KEY_STATUS_UNSPECIFIED - - KEY_STATUS_ACTIVE - - KEY_STATUS_ROTATED - description: The status of the key - policy.SourceType: - type: string - title: SourceType - enum: - - SOURCE_TYPE_UNSPECIFIED - - SOURCE_TYPE_INTERNAL - - SOURCE_TYPE_EXTERNAL - description: |- - Describes whether this kas is managed by the organization or if they imported - the kas information from an external party. These two modes are necessary in order - to encrypt a tdf dek with an external parties kas public key. - policy.SubjectMappingOperatorEnum: - type: string - title: SubjectMappingOperatorEnum - enum: - - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED - - SUBJECT_MAPPING_OPERATOR_ENUM_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN - - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS common.Metadata: type: object properties: @@ -482,6 +402,75 @@ components: title: value title: LabelsEntry additionalProperties: false + connect-protocol-version: + type: number + title: Connect-Protocol-Version + enum: + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.BoolValue: type: boolean description: |- @@ -494,8 +483,8 @@ components: google.protobuf.Timestamp: type: string examples: - - 1s - - 1.000340012s + - "2023-01-15T01:30:15.01Z" + - "2024-12-25T12:00:00Z" format: date-time description: |- A Timestamp represents a point in time independent of any time zone or local @@ -589,37 +578,65 @@ components: ) to obtain a formatter capable of generating timestamps in this format. policy.Action: type: object - oneOf: + allOf: - properties: - custom: + id: + type: string + title: id + description: Generated uuid in database + name: type: string + title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + - oneOf: + - type: object + properties: + custom: + type: string + title: custom + description: Deprecated title: custom - description: Deprecated - title: custom - required: - - custom - - properties: - standard: + required: + - custom + - type: object + properties: + standard: + title: standard + description: Deprecated + $ref: '#/components/schemas/policy.Action.StandardAction' title: standard - description: Deprecated - $ref: '#/components/schemas/policy.Action.StandardAction' - title: standard - required: - - standard - properties: - id: - type: string - title: id - description: Generated uuid in database - name: - type: string - title: name - metadata: - title: metadata - $ref: '#/components/schemas/common.Metadata' + required: + - standard title: Action additionalProperties: false description: An action an entity can take + policy.Action.StandardAction: + type: string + title: StandardAction + enum: + - STANDARD_ACTION_UNSPECIFIED + - STANDARD_ACTION_DECRYPT + - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + - ALGORITHM_HPQT_XWING + - ALGORITHM_HPQT_SECP256R1_MLKEM768 + - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + description: Supported key algorithms. policy.AsymmetricKey: type: object properties: @@ -707,6 +724,12 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys associated with the attribute + allowTraversal: + title: allow_traversal + description: |- + Whether or not we will use the attribute definition during encryption + if the attribute value is missing. + $ref: '#/components/schemas/google.protobuf.BoolValue' metadata: title: metadata description: Common metadata @@ -715,23 +738,14 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false + policy.AttributeRuleTypeEnum: + type: string + title: AttributeRuleTypeEnum + enum: + - ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED + - ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + - ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + - ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY policy.Condition: type: object properties: @@ -749,7 +763,6 @@ components: type: array items: type: string - minItems: 1 title: subject_external_values minItems: 1 description: |- @@ -765,6 +778,13 @@ components: * A Condition defines a rule of + policy.ConditionBooleanTypeEnum: + type: string + title: ConditionBooleanTypeEnum + enum: + - CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED + - CONDITION_BOOLEAN_TYPE_ENUM_AND + - CONDITION_BOOLEAN_TYPE_ENUM_OR policy.ConditionGroup: type: object properties: @@ -815,18 +835,31 @@ components: alg: not: enum: - - 0 + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED title: alg description: |- A known algorithm type with any additional parameters encoded. - To start, these may be `rsa:2048` for encrypting ZTDF files and - `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' title: KasPublicKey additionalProperties: false description: |- Deprecated A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 policy.KasPublicKeySet: type: object properties: @@ -849,13 +882,9 @@ components: uri: type: string title: uri - description: |+ + description: | Address of a KAS instance - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. publicKey: title: public_key description: 'Deprecated: KAS can have multiple key pairs' @@ -883,6 +912,16 @@ components: title: KeyAccessServer additionalProperties: false description: Key Access Server Registry + policy.KeyMode: + type: string + title: KeyMode + enum: + - KEY_MODE_UNSPECIFIED + - KEY_MODE_CONFIG_ROOT_KEY + - KEY_MODE_PROVIDER_ROOT_KEY + - KEY_MODE_REMOTE + - KEY_MODE_PUBLIC_KEY_ONLY + description: Describes the management and operational mode of a cryptographic key. policy.KeyProviderConfig: type: object properties: @@ -905,6 +944,14 @@ components: $ref: '#/components/schemas/common.Metadata' title: KeyProviderConfig additionalProperties: false + policy.KeyStatus: + type: string + title: KeyStatus + enum: + - KEY_STATUS_UNSPECIFIED + - KEY_STATUS_ACTIVE + - KEY_STATUS_ROTATED + description: The status of the key policy.Namespace: type: object properties: @@ -940,12 +987,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: @@ -993,6 +1034,10 @@ components: items: $ref: '#/components/schemas/policy.RequestContext' title: context + namespace: + title: namespace + description: The source namespace for this trigger, derived from the attribute value and action. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1049,7 +1094,8 @@ components: policy.PublicKey: type: object oneOf: - - properties: + - type: object + properties: cached: title: cached description: public key with additional information. Current preferred version @@ -1057,17 +1103,14 @@ components: title: cached required: - cached - - properties: + - type: object + properties: remote: type: string title: remote - description: |+ + description: | kas public key url - optional since can also be retrieved via public key - URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: - ``` - this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') - ``` - + uri_format // URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes. title: remote required: - remote @@ -1138,6 +1181,10 @@ components: description: |- the common name for the group of resource mappings, which must be unique per namespace + fqn: + type: string + title: fqn + description: the fully qualified name of the resource mapping group metadata: title: metadata description: Common metadata @@ -1181,12 +1228,30 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. policy.SubjectConditionSet: type: object properties: id: type: string title: id + namespace: + title: namespace + description: |- + the namespace containing this subject condition set + possible this is empty in the case a subject condition set + has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' subjectSets: type: array items: @@ -1224,6 +1289,13 @@ components: $ref: '#/components/schemas/policy.Action' title: actions description: The actions permitted by subjects in this mapping + namespace: + title: namespace + description: |- + the namespace containing this subject mapping + possible this is empty. If so that means + the Subject Mapping has not been migrated to a namespace. + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1232,6 +1304,14 @@ components: description: |- Subject Mapping: A Policy assigning Subject Set(s) to a permitted attribute value + action(s) combination + policy.SubjectMappingOperatorEnum: + type: string + title: SubjectMappingOperatorEnum + enum: + - SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + - SUBJECT_MAPPING_OPERATOR_ENUM_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + - SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS policy.SubjectSet: type: object properties: @@ -1510,15 +1590,11 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Optional WARNING!! Updating the name of an Attribute will retroactively alter access to existing TDFs of the old and new Attribute name. - Attribute name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored attribute name will be normalized to lower case.: - ``` - size(this) > 0 ? this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') : true - ``` - + attribute_name_format // Attribute name must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored attribute name will be normalized to lower case. rule: title: rule description: |- @@ -1526,6 +1602,15 @@ components: WARNING!! Updating the rule of an Attribute will retroactively alter access to existing TDFs of the Attribute name. $ref: '#/components/schemas/policy.AttributeRuleTypeEnum' + allowTraversal: + title: allow_traversal + description: |- + Optional + WARNING!! + Updating allow_traversal allows TDF creation to be front-loaded, meaning a customer + can create encrypted content with an attribute definitions key mapping before + creating the attribute values needed to decrypt. + $ref: '#/components/schemas/google.protobuf.BoolValue' valuesOrder: type: array items: @@ -1562,13 +1647,9 @@ components: type: string title: value maxLength: 253 - description: |+ + description: | Required - Attribute Value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored attribute value will be normalized to lower case.: - ``` - this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') - ``` - + value_format // Attribute Value must be an alphanumeric string, allowing hyphens and underscores but not as the first or last character. The stored attribute value will be normalized to lower case. title: UnsafeUpdateAttributeValueRequest additionalProperties: false description: |- @@ -1594,13 +1675,9 @@ components: type: string title: name maxLength: 253 - description: |+ + description: | Required - Namespace must be a valid hostname. It should include at least one dot, with each segment (label) starting and ending with an alphanumeric character. Each label must be 1 to 63 characters long, allowing hyphens but not as the first or last character. The top-level domain (the last segment after the final dot) must consist of at least two alphabetic characters. The stored namespace will be normalized to lower case.: - ``` - this.matches('^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$') - ``` - + namespace_name_format // Namespace must be a valid hostname. It should include at least one dot, with each segment (label) starting and ending with an alphanumeric character. Each label must be 1 to 63 characters long, allowing hyphens but not as the first or last character. The top-level domain (the last segment after the final dot) must consist of at least two alphabetic characters. The stored namespace will be normalized to lower case. title: UnsafeUpdateNamespaceRequest additionalProperties: false description: |- @@ -1615,63 +1692,6 @@ components: $ref: '#/components/schemas/policy.Namespace' title: UnsafeUpdateNamespaceResponse additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: policy.unsafe.UnsafeService diff --git a/docs/openapi/wellknownconfiguration/wellknown_configuration.openapi.yaml b/docs/openapi/wellknownconfiguration/wellknown_configuration.openapi.yaml index dcac72a437..203cf0b27e 100644 --- a/docs/openapi/wellknownconfiguration/wellknown_configuration.openapi.yaml +++ b/docs/openapi/wellknownconfiguration/wellknown_configuration.openapi.yaml @@ -2,12 +2,28 @@ openapi: 3.1.0 info: title: wellknownconfiguration paths: - /.well-known/opentdf-configuration: - get: + /wellknownconfiguration.WellKnownService/GetWellKnownConfiguration: + post: tags: - wellknownconfiguration.WellKnownService summary: GetWellKnownConfiguration operationId: wellknownconfiguration.WellKnownService.GetWellKnownConfiguration + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/wellknownconfiguration.GetWellKnownConfigurationRequest' + required: true responses: default: description: Error @@ -23,16 +39,75 @@ paths: $ref: '#/components/schemas/wellknownconfiguration.GetWellKnownConfigurationResponse' components: schemas: - google.protobuf.NullValue: - type: string - title: NullValue + connect-protocol-version: + type: number + title: Connect-Protocol-Version enum: - - NULL_VALUE - description: |- - `NullValue` is a singleton enumeration to represent the null value for the - `Value` type union. - - The JSON representation for `NullValue` is JSON `null`. + - 1 + description: Define the version of the Connect protocol + const: 1 + connect-timeout-header: + type: number + title: Connect-Timeout-Ms + description: Define the timeout, in ms + connect.error: + type: object + properties: + code: + type: string + examples: + - not_found + enum: + - canceled + - unknown + - invalid_argument + - deadline_exceeded + - not_found + - already_exists + - permission_denied + - resource_exhausted + - failed_precondition + - aborted + - out_of_range + - unimplemented + - internal + - unavailable + - data_loss + - unauthenticated + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/connect.error_details.Any' + description: A list of messages that carry the error details. There is no limit on the number of messages. + title: Connect Error + additionalProperties: true + description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' + connect.error_details.Any: + type: object + properties: + type: + type: string + description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.' + value: + type: string + format: binary + description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field. + debug: + oneOf: + - type: object + title: Any + additionalProperties: true + description: Detailed error information. + discriminator: + propertyName: type + title: Debug + description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details. google.protobuf.ListValue: type: object properties: @@ -48,6 +123,16 @@ components: `ListValue` is a wrapper around a repeated field of values. The JSON representation for `ListValue` is JSON array. + google.protobuf.NullValue: + type: string + title: NullValue + enum: + - NULL_VALUE + description: |- + `NullValue` is a singleton enumeration to represent the null value for the + `Value` type union. + + The JSON representation for `NullValue` is JSON `null`. google.protobuf.Struct: type: object additionalProperties: @@ -122,63 +207,6 @@ components: $ref: '#/components/schemas/google.protobuf.Struct' title: ConfigurationEntry additionalProperties: false - connect-protocol-version: - type: number - title: Connect-Protocol-Version - enum: - - 1 - description: Define the version of the Connect protocol - const: 1 - connect-timeout-header: - type: number - title: Connect-Timeout-Ms - description: Define the timeout, in ms - connect.error: - type: object - properties: - code: - type: string - examples: - - not_found - enum: - - canceled - - unknown - - invalid_argument - - deadline_exceeded - - not_found - - already_exists - - permission_denied - - resource_exhausted - - failed_precondition - - aborted - - out_of_range - - unimplemented - - internal - - unavailable - - data_loss - - unauthenticated - description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - message: - type: string - description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - detail: - $ref: '#/components/schemas/google.protobuf.Any' - title: Connect Error - additionalProperties: true - description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation' - google.protobuf.Any: - type: object - properties: - type: - type: string - value: - type: string - format: binary - debug: - type: object - additionalProperties: true - additionalProperties: true - description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. security: [] tags: - name: wellknownconfiguration.WellKnownService diff --git a/examples/cmd/benchmark.go b/examples/cmd/benchmark.go index 106d0b2d23..8c4fbe80dd 100644 --- a/examples/cmd/benchmark.go +++ b/examples/cmd/benchmark.go @@ -1,4 +1,4 @@ -//nolint:forbidigo,nestif // We use Println here extensively because we are printing markdown. +//nolint:forbidigo // We use fmt.Printf here extensively because we are printing markdown. package cmd import ( @@ -15,13 +15,6 @@ import ( "github.com/spf13/cobra" ) -type TDFFormat string - -const ( - TDF3 TDFFormat = "tdf3" - NanoTDF TDFFormat = "nanotdf" -) - func gfmCellEscape(s string) string { // Escape pipe characters for GitHub Flavored Markdown tables pipes := strings.ReplaceAll(s, "|", "\\|") @@ -29,26 +22,7 @@ func gfmCellEscape(s string) string { return brs } -func (f *TDFFormat) String() string { - return string(*f) -} - -func (f *TDFFormat) Set(value string) error { - switch value { - case "tdf3", "nanotdf": - *f = TDFFormat(value) - return nil - default: - return errors.New("invalid TDF format") - } -} - -func (f *TDFFormat) Type() string { - return "TDFFormat" -} - type BenchmarkConfig struct { - TDFFormat TDFFormat ConcurrentRequests int RequestCount int RequestsPerSecond int @@ -69,7 +43,6 @@ func init() { benchmarkCmd.Flags().IntVar(&config.RequestCount, "count", 100, "Total number of requests") //nolint: mnd // This is output to the help with explanation benchmarkCmd.Flags().IntVar(&config.RequestsPerSecond, "rps", 50, "Requests per second limit") //nolint: mnd // This is output to the help with explanation benchmarkCmd.Flags().IntVar(&config.TimeoutSeconds, "timeout", 30, "Timeout in seconds") //nolint: mnd // This is output to the help with explanation - benchmarkCmd.Flags().Var(&config.TDFFormat, "tdf", "TDF format (tdf3 or nanotdf)") ExamplesCmd.AddCommand(benchmarkCmd) } @@ -96,67 +69,35 @@ func runBenchmark(cmd *cobra.Command, _ []string) error { }() dataAttributes := []string{"https://example.com/attr/attr1/value/value1"} - if config.TDFFormat == NanoTDF { - nanoTDFConfig, err := client.NewNanoTDFConfig() - if err != nil { - return err - } - err = nanoTDFConfig.SetAttributes(dataAttributes) - if err != nil { - return err - } - nanoTDFConfig.EnableECDSAPolicyBinding() - if insecurePlaintextConn || strings.HasPrefix(platformEndpoint, "http://") { - err = nanoTDFConfig.SetKasURL(fmt.Sprintf("http://%s/kas", "localhost:8080")) - } else { - err = nanoTDFConfig.SetKasURL(fmt.Sprintf("https://%s/kas", "localhost:8080")) - } - if err != nil { - return err - } - - _, err = client.CreateNanoTDF(out, in, *nanoTDFConfig) - if err != nil { - return err - } - - // if outputName != "-" { - // err = cat(cmd, outputName) - // if err != nil { - // return err - // } - // } + opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...), sdk.WithAutoconfigure(false)} + if insecurePlaintextConn || strings.HasPrefix(platformEndpoint, "http://") { + opts = append(opts, sdk.WithKasInformation( + sdk.KASInfo{ + URL: "http://localhost:8080", + PublicKey: "", + }), + ) } else { - opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...), sdk.WithAutoconfigure(false)} - if insecurePlaintextConn || strings.HasPrefix(platformEndpoint, "http://") { - opts = append(opts, sdk.WithKasInformation( - sdk.KASInfo{ - URL: "http://localhost:8080", - PublicKey: "", - }), - ) - } else { - opts = append(opts, sdk.WithKasInformation( - sdk.KASInfo{ - URL: "https://localhost:8080", - PublicKey: "", - }), - ) - } - tdf, err := client.CreateTDF( - out, in, - opts..., + opts = append(opts, sdk.WithKasInformation( + sdk.KASInfo{ + URL: "https://localhost:8080", + PublicKey: "", + }), ) - if err != nil { - return err - } + } + tdf, err := client.CreateTDF( + out, in, + opts..., + ) + if err != nil { + return err + } - manifestJSON, err := json.MarshalIndent(tdf.Manifest(), "", " ") - if err != nil { - return err - } - cmd.Println(string(manifestJSON)) + manifestJSON, err := json.MarshalIndent(tdf.Manifest(), "", " ") + if err != nil { + return err } + cmd.Println(string(manifestJSON)) var wg sync.WaitGroup // Queries (requests) channel @@ -178,24 +119,16 @@ func runBenchmark(cmd *cobra.Command, _ []string) error { } defer file.Close() - if config.TDFFormat == NanoTDF { - _, err = client.ReadNanoTDF(io.Discard, file) - if err != nil { - e <- fmt.Errorf("ReadNanoTDF error: %w", err) - return - } - } else { - tdfreader, err := client.LoadTDF(file) - if err != nil { - e <- fmt.Errorf("LoadTDF error: %w", err) - return - } + tdfreader, err := client.LoadTDF(file) + if err != nil { + e <- fmt.Errorf("LoadTDF error: %w", err) + return + } - _, err = io.Copy(io.Discard, tdfreader) - if err != nil && !errors.Is(err, io.EOF) { - e <- fmt.Errorf("read error: %w", err) - return - } + _, err = io.Copy(io.Discard, tdfreader) + if err != nil && !errors.Is(err, io.EOF) { + e <- fmt.Errorf("read error: %w", err) + return } a <- time.Since(start) @@ -238,11 +171,7 @@ func runBenchmark(cmd *cobra.Command, _ []string) error { } throughput := float64(successCount) / totalTime.Seconds() - format := config.TDFFormat - if format == "" { - format = TDF3 - } - fmt.Printf("## %s Benchmark Results:\n", strings.ToUpper(format.String())) + fmt.Printf("## %s Benchmark Results:\n", strings.ToUpper("tdf3")) fmt.Printf("| Metric | Value |\n") fmt.Printf("|-----------------------|---------------------------|\n") fmt.Printf("| Total Requests | %d |\n", config.RequestCount) diff --git a/examples/cmd/benchmark_bulk.go b/examples/cmd/benchmark_bulk.go index b20a0c5456..a6c8e7a0bb 100644 --- a/examples/cmd/benchmark_bulk.go +++ b/examples/cmd/benchmark_bulk.go @@ -1,4 +1,4 @@ -//nolint:forbidigo,nestif // We use Println here extensively because we are printing markdown. +//nolint:forbidigo // We use fmt.Printf here extensively because we are printing markdown. package cmd import ( @@ -24,7 +24,6 @@ func init() { } benchmarkCmd.Flags().IntVar(&config.RequestCount, "count", 100, "Total number of requests") //nolint: mnd // This is output to the help with explanation - benchmarkCmd.Flags().Var(&config.TDFFormat, "tdf", "TDF format (tdf3 or nanotdf)") ExamplesCmd.AddCommand(benchmarkCmd) } @@ -51,68 +50,35 @@ func runBenchmarkBulk(cmd *cobra.Command, _ []string) error { }() dataAttributes := []string{"https://example.com/attr/attr1/value/value1"} - if config.TDFFormat == NanoTDF { - nanoTDFConfig, err := client.NewNanoTDFConfig() - if err != nil { - return err - } - err = nanoTDFConfig.SetAttributes(dataAttributes) - if err != nil { - return err - } - nanoTDFConfig.EnableECDSAPolicyBinding() - // if plaintext or platform endpoint is http, set kas url to http, otherwise https - if insecurePlaintextConn || strings.HasPrefix(platformEndpoint, "http://") { - err = nanoTDFConfig.SetKasURL(fmt.Sprintf("http://%s/kas", "localhost:8080")) - } else { - err = nanoTDFConfig.SetKasURL(fmt.Sprintf("https://%s/kas", "localhost:8080")) - } - if err != nil { - return err - } - - _, err = client.CreateNanoTDF(out, in, *nanoTDFConfig) - if err != nil { - return err - } - - if outputName != "-" { - err = cat(cmd, outputName) - if err != nil { - return err - } - } + opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...), sdk.WithAutoconfigure(false)} + if insecurePlaintextConn || strings.HasPrefix(platformEndpoint, "http://") { + opts = append(opts, sdk.WithKasInformation( + sdk.KASInfo{ + URL: "http://localhost:8080", + PublicKey: "", + }), + ) } else { - opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...), sdk.WithAutoconfigure(false)} - if insecurePlaintextConn || strings.HasPrefix(platformEndpoint, "http://") { - opts = append(opts, sdk.WithKasInformation( - sdk.KASInfo{ - URL: "http://localhost:8080", - PublicKey: "", - }), - ) - } else { - opts = append(opts, sdk.WithKasInformation( - sdk.KASInfo{ - URL: "https://localhost:8080", - PublicKey: "", - }), - ) - } - tdf, err := client.CreateTDF( - out, in, - opts..., + opts = append(opts, sdk.WithKasInformation( + sdk.KASInfo{ + URL: "https://localhost:8080", + PublicKey: "", + }), ) - if err != nil { - return err - } + } + tdf, err := client.CreateTDF( + out, in, + opts..., + ) + if err != nil { + return err + } - manifestJSON, err := json.MarshalIndent(tdf.Manifest(), "", " ") - if err != nil { - return err - } - cmd.Println(string(manifestJSON)) + manifestJSON, err := json.MarshalIndent(tdf.Manifest(), "", " ") + if err != nil { + return err } + cmd.Println(string(manifestJSON)) var errors []error var requestFailure error @@ -131,15 +97,11 @@ func runBenchmarkBulk(cmd *cobra.Command, _ []string) error { requestFailure = fmt.Errorf("file seek error: %w", err) } - format := sdk.Nano var bulkTdfs []*sdk.BulkTDF - if config.TDFFormat == "tdf3" { - format = sdk.Standard - } for i := 0; i < config.RequestCount; i++ { bulkTdfs = append(bulkTdfs, &sdk.BulkTDF{Reader: bytes.NewReader(cipher), Writer: io.Discard}) } - err = client.BulkDecrypt(context.Background(), sdk.WithTDFs(bulkTdfs...), sdk.WithTDFType(format)) + err = client.BulkDecrypt(context.Background(), sdk.WithTDFs(bulkTdfs...), sdk.WithTDFType(sdk.Standard)) if err != nil { if errList, ok := sdk.FromBulkErrors(err); ok { errors = errList diff --git a/examples/cmd/decrypt.go b/examples/cmd/decrypt.go index 8105e8c84a..11279c7999 100644 --- a/examples/cmd/decrypt.go +++ b/examples/cmd/decrypt.go @@ -2,13 +2,13 @@ package cmd import ( - "bytes" "errors" "fmt" "io" "os" "path/filepath" + "github.com/opentdf/platform/lib/ocrypto" "github.com/opentdf/platform/sdk" "github.com/spf13/cobra" @@ -49,11 +49,23 @@ func decrypt(cmd *cobra.Command, args []string) error { if err != nil { return err } - _, err = client.ReadNanoTDF(os.Stdout, f) - fmt.Println() + opts := []sdk.TDFReaderOption{} + if alg != "" { + kt, err := ocrypto.ParseKeyType(alg) + if err != nil { + return err + } + opts = append(opts, sdk.WithSessionKeyType(kt)) + } + tdfReader, err := client.LoadTDF(f, opts...) + if err != nil { + return err + } + _, err = io.Copy(os.Stdout, tdfReader) if err != nil { return err } + fmt.Println() } } client.Close() @@ -66,48 +78,23 @@ func decrypt(cmd *cobra.Command, args []string) error { } defer file.Close() - var magic [3]byte - var isNano bool - n, err := io.ReadFull(file, magic[:]) - switch { - case err != nil: - return err - case n < 3: //nolint: mnd // All TDFs are more than 2 bytes - return errors.New("file too small; no magic number found") - case bytes.HasPrefix(magic[:], []byte("L1L")): - isNano = true - default: - isNano = false + opts := []sdk.TDFReaderOption{} + if alg != "" { + kt, err := ocrypto.ParseKeyType(alg) + if err != nil { + return err + } + opts = append(opts, sdk.WithSessionKeyType(kt)) } - _, err = file.Seek(0, 0) + tdfReader, err := client.LoadTDF(file, opts...) if err != nil { return err } - if !isNano { - opts := []sdk.TDFReaderOption{} - if alg != "" { - kt, err := keyTypeForKeyType(alg) - if err != nil { - return err - } - opts = append(opts, sdk.WithSessionKeyType(kt)) - } - tdfreader, err := client.LoadTDF(file, opts...) - if err != nil { - return err - } - - // Print decrypted string - _, err = io.Copy(os.Stdout, tdfreader) - if err != nil && !errors.Is(err, io.EOF) { - return err - } - } else { - _, err = client.ReadNanoTDF(os.Stdout, file) - if err != nil { - return err - } + // Print decrypted string + _, err = io.Copy(os.Stdout, tdfReader) + if err != nil && !errors.Is(err, io.EOF) { + return err } return nil } diff --git a/examples/cmd/encrypt.go b/examples/cmd/encrypt.go index ada8585f79..d6574335e1 100644 --- a/examples/cmd/encrypt.go +++ b/examples/cmd/encrypt.go @@ -1,14 +1,8 @@ -//nolint:forbidigo,nestif // Sample code package cmd import ( - "bytes" "encoding/json" - "errors" - "fmt" - "io" "os" - "path/filepath" "strings" "github.com/opentdf/platform/lib/ocrypto" @@ -17,15 +11,11 @@ import ( ) var ( - nanoFormat bool autoconfigure bool noKIDInKAO bool - noKIDInNano bool outputName string dataAttributes []string - collection int alg string - policyMode string ) func init() { @@ -36,14 +26,10 @@ func init() { Args: cobra.MinimumNArgs(1), } encryptCmd.Flags().StringSliceVarP(&dataAttributes, "data-attributes", "a", []string{"https://example.com/attr/attr1/value/value1"}, "space separated list of data attributes") - encryptCmd.Flags().BoolVar(&nanoFormat, "nano", false, "Output in nanoTDF format") encryptCmd.Flags().BoolVar(&autoconfigure, "autoconfigure", true, "Use attribute grants to select kases") encryptCmd.Flags().BoolVar(&noKIDInKAO, "no-kid-in-kao", false, "[deprecated] Disable storing key identifiers in TDF KAOs") - encryptCmd.Flags().BoolVar(&noKIDInNano, "no-kid-in-nano", true, "Disable storing key identifiers in nanoTDF KAS ResourceLocator") encryptCmd.Flags().StringVarP(&outputName, "output", "o", "sensitive.txt.tdf", "name or path of output file; - for stdout") encryptCmd.Flags().StringVarP(&alg, "key-encapsulation-algorithm", "A", "rsa:2048", "Key wrap algorithm algorithm:parameters") - encryptCmd.Flags().IntVarP(&collection, "collection", "c", 0, "number of nano's to create for collection. If collection >0 (default) then output will be _") - encryptCmd.Flags().StringVar(&policyMode, "policy-mode", "", "Store policy as encrypted instead of plaintext (nanoTDF only) [plaintext|encrypted]") ExamplesCmd.AddCommand(&encryptCmd) } @@ -63,31 +49,12 @@ func encrypt(cmd *cobra.Command, args []string) error { } out := os.Stdout - if outputName == "-" && collection > 0 { - return errors.New("cannot use stdout for collection") - } - - var writer []io.Writer - if outputName == "-" { - writer = append(writer, out) - } else { - dir, file := filepath.Split(outputName) - for i := 0; i < collection; i++ { - out, err = os.Create(filepath.Join(dir, fmt.Sprintf("%d_%s", i, file))) - if err != nil { - return err - } - writer = append(writer, out) - defer out.Close() - } - if collection == 0 { - out, err = os.Create(outputName) - writer = append(writer, out) - defer out.Close() - if err != nil { - return err - } + if outputName != "-" { + out, err = os.Create(outputName) + if err != nil { + return err } + defer out.Close() } baseKasURL := platformEndpoint @@ -95,112 +62,31 @@ func encrypt(cmd *cobra.Command, args []string) error { baseKasURL = "http://" + baseKasURL } - if !nanoFormat { - opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...)} - if !autoconfigure { - opts = append(opts, sdk.WithAutoconfigure(autoconfigure)) - opts = append(opts, sdk.WithKasInformation( - sdk.KASInfo{ - URL: baseKasURL, - PublicKey: "", - })) - } - if alg != "" { - kt, err := keyTypeForKeyType(alg) - if err != nil { - return err - } - opts = append(opts, sdk.WithWrappingKeyAlg(kt)) - } - tdf, err := client.CreateTDF(out, in, opts...) - if err != nil { - return err - } - - manifestJSON, err := json.MarshalIndent(tdf.Manifest(), "", " ") - if err != nil { - return err - } - cmd.Println(string(manifestJSON)) - } else { - nanoTDFConfig, err := client.NewNanoTDFConfig() - if err != nil { - return err - } - err = nanoTDFConfig.SetAttributes(dataAttributes) - if err != nil { - return err - } - nanoTDFConfig.EnableECDSAPolicyBinding() - if collection > 0 { - nanoTDFConfig.EnableCollection() - } - err = nanoTDFConfig.SetKasURL(baseKasURL + "/kas") - if err != nil { - return err - } - - // Handle policy mode if nanoTDF - switch policyMode { - case "": // default to encrypted - case "encrypted": - err = nanoTDFConfig.SetPolicyMode(sdk.NanoTDFPolicyModeEncrypted) - case "plaintext": - err = nanoTDFConfig.SetPolicyMode(sdk.NanoTDFPolicyModePlainText) - default: - err = fmt.Errorf("unsupported policy mode: %s", policyMode) - } + opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...)} + if !autoconfigure { + opts = append(opts, sdk.WithAutoconfigure(autoconfigure)) + opts = append(opts, sdk.WithKasInformation( + sdk.KASInfo{ + URL: baseKasURL, + PublicKey: "", + })) + } + if alg != "" { + kt, err := ocrypto.ParseKeyType(alg) if err != nil { return err } - - for i, writer := range writer { - input := plainText - if collection > 0 { - input = fmt.Sprintf("%d: %s", i, plainText) - } - in = strings.NewReader(input) - _, err = client.CreateNanoTDF(writer, in, *nanoTDFConfig) - if err != nil { - return err - } - } - - if outputName != "-" && collection == 0 { - err = cat(cmd, outputName) - if err != nil { - return err - } - } - } - return nil -} - -func keyTypeForKeyType(alg string) (ocrypto.KeyType, error) { - switch alg { - case string(ocrypto.RSA2048Key): - return ocrypto.RSA2048Key, nil - case string(ocrypto.EC256Key): - return ocrypto.EC256Key, nil - default: - // do not submit add ocrypto.UnknownKey - return ocrypto.RSA2048Key, fmt.Errorf("unsupported key type [%s]", alg) + opts = append(opts, sdk.WithWrappingKeyAlg(kt)) //nolint:staticcheck // Example code still needs to set wrapping algorithm } -} - -func cat(_ *cobra.Command, nTdfFile string) error { - f, err := os.Open(nTdfFile) + tdf, err := client.CreateTDF(out, in, opts...) if err != nil { return err } - buf := bytes.Buffer{} - _, err = buf.ReadFrom(f) + manifestJSON, err := json.MarshalIndent(tdf.Manifest(), "", " ") if err != nil { return err } - - fmt.Println(string(ocrypto.Base64Encode(buf.Bytes()))) - + cmd.Println(string(manifestJSON)) return nil } diff --git a/examples/cmd/examples.go b/examples/cmd/examples.go index 32d7f403a3..e8f1c158d8 100644 --- a/examples/cmd/examples.go +++ b/examples/cmd/examples.go @@ -12,12 +12,10 @@ import ( ) var ( - platformEndpoint string - clientCredentials string - tokenEndpoint string - storeCollectionHeaders bool - insecurePlaintextConn bool - insecureSkipVerify bool + platformEndpoint string + clientCredentials string + insecurePlaintextConn bool + insecureSkipVerify bool ) var ExamplesCmd = &cobra.Command{ @@ -29,8 +27,6 @@ func init() { f := ExamplesCmd.PersistentFlags() f.StringVarP(&clientCredentials, "creds", "", "opentdf-sdk:secret", "client id:secret credentials") f.StringVarP(&platformEndpoint, "platformEndpoint", "e", "https://localhost:8080", "Platform Endpoint") - f.StringVarP(&tokenEndpoint, "tokenEndpoint", "t", "http://localhost:8888/auth/realms/opentdf/protocol/openid-connect/token", "OAuth token endpoint") - f.BoolVar(&storeCollectionHeaders, "storeCollectionHeaders", false, "Store collection headers") f.BoolVar(&insecurePlaintextConn, "insecurePlaintextConn", false, "Use insecure plaintext connection") f.BoolVar(&insecureSkipVerify, "insecureSkipVerify", false, "Skip server certificate verification") } @@ -45,9 +41,6 @@ func newSDK() (*sdk.SDK, error) { if insecureSkipVerify { opts = append(opts, sdk.WithInsecureSkipVerifyConn()) } - if storeCollectionHeaders { - opts = append(opts, sdk.WithStoreCollectionHeaders()) - } if clientCredentials != "" { i := strings.Index(clientCredentials, ":") if i < 0 { @@ -55,16 +48,9 @@ func newSDK() (*sdk.SDK, error) { } opts = append(opts, sdk.WithClientCredentials(clientCredentials[:i], clientCredentials[i+1:], nil)) } - if tokenEndpoint != "" { - opts = append(opts, sdk.WithTokenEndpoint(tokenEndpoint)) - } if noKIDInKAO { opts = append(opts, sdk.WithNoKIDInKAO()) } - // double negative always gets me - if !noKIDInNano { - opts = append(opts, sdk.WithNoKIDInNano()) - } return sdk.New(platformEndpoint, opts...) } diff --git a/examples/cmd/isvalid.go b/examples/cmd/isvalid.go index 2b4cee0994..f4812a4451 100644 --- a/examples/cmd/isvalid.go +++ b/examples/cmd/isvalid.go @@ -64,14 +64,6 @@ func isValid(cmd *cobra.Command, in io.ReadSeeker) []typeInfo { return typeInfos } - isValidNano, err := sdk.IsValidNanoTdf(in) - typeInfos = append(typeInfos, typeInfo{Valid: isValidNano, Error: err, Type: "NanoTDF"}) - - if _, err := in.Seek(0, io.SeekStart); err != nil { - cmd.PrintErrf("Error seeking to start of file: %v\n", err) - return typeInfos - } - tdfType := sdk.GetTdfType(in) typeInfos = append(typeInfos, typeInfo{Valid: tdfType != sdk.Invalid, Error: nil, Type: tdfType.String()}) diff --git a/examples/cmd/isvalid_test.go b/examples/cmd/isvalid_test.go index 58b6278e2d..8adf00ab81 100644 --- a/examples/cmd/isvalid_test.go +++ b/examples/cmd/isvalid_test.go @@ -44,9 +44,6 @@ func (m *mockReadSeeker) Seek(offset int64, whence int) (int64, error) { return int64(newPos), nil } -//go:embed testdata/nano-valid.ntdf -var sampleNanoValid []byte - //go:embed testdata/tdf-filewatcher-old.tdf var sampleTDFFileWatcherOld []byte @@ -58,46 +55,34 @@ var sampleTDFValid []byte func TestIsValid(t *testing.T) { tests := []struct { - name string - data []byte - isValidTdf bool - isValidNano bool - tdfType sdk.TdfType + name string + data []byte + isValidTdf bool + tdfType sdk.TdfType }{ { - name: "Valid TDF3", - data: sampleTDFValid, - isValidTdf: true, - isValidNano: false, - tdfType: sdk.Standard, - }, - { - name: "Valid NanoTDF", - data: sampleNanoValid, - isValidTdf: false, - isValidNano: true, - tdfType: sdk.Nano, + name: "Valid TDF3", + data: sampleTDFValid, + isValidTdf: true, + tdfType: sdk.Standard, }, { - name: "Invalid All", - data: []byte("invalid data"), - isValidTdf: false, - isValidNano: false, - tdfType: sdk.Invalid, + name: "Invalid All", + data: []byte("invalid data"), + isValidTdf: false, + tdfType: sdk.Invalid, }, { - name: "Valid TDF3 (filewatcher)", - data: sampleTDFFileWatcherOld, - isValidTdf: true, - isValidNano: false, - tdfType: sdk.Standard, + name: "Valid TDF3 (filewatcher)", + data: sampleTDFFileWatcherOld, + isValidTdf: true, + tdfType: sdk.Standard, }, { - name: "Valid TDF3 (multikas)", - data: sampleTDFMultiKas, - isValidTdf: true, - isValidNano: false, - tdfType: sdk.Standard, + name: "Valid TDF3 (multikas)", + data: sampleTDFMultiKas, + isValidTdf: true, + tdfType: sdk.Standard, }, } @@ -107,10 +92,9 @@ func TestIsValid(t *testing.T) { cmd := &cobra.Command{} typeInfos := isValid(cmd, in) - assert.Len(t, typeInfos, 3) + assert.Len(t, typeInfos, 2) assert.Equal(t, tt.isValidTdf, typeInfos[0].Valid) - assert.Equal(t, tt.isValidNano, typeInfos[1].Valid) - assert.Equal(t, tt.tdfType.String(), typeInfos[2].Type) + assert.Equal(t, tt.tdfType.String(), typeInfos[1].Type) }) } } diff --git a/examples/cmd/kas.go b/examples/cmd/kas.go index 222c599167..955dd0dd60 100644 --- a/examples/cmd/kas.go +++ b/examples/cmd/kas.go @@ -30,7 +30,7 @@ func init() { return updateKas(cmd) }, } - // Note we currently only store one pk at a time. must be fixed for nano tests + // Note we currently only store one pk at a time. update.Flags().StringVarP(&algorithm, "algorithm", "", "", "algorithm used with the public key") update.Flags().StringVarP(&kas, "kas", "k", "", "kas uri") update.Flags().StringVarP(&key, "public-key", "", "", "public key value, e.g. $( lifetime ) type KeycloakData struct { @@ -72,13 +79,41 @@ type KeycloakConnectParams struct { AllowInsecureTLS bool } +// TokenManagerConfig allows configuring token refresh behavior +type TokenManagerConfig struct { + // TokenBuffer is duration before expiration to trigger preemptive refresh + // Default: 120s (2 minutes) + TokenBuffer time.Duration +} + +// TokenManager manages automatic token refresh for Keycloak operations +type TokenManager struct { + connectParams KeycloakConnectParams + client *gocloak.GoCloak + token *gocloak.JWT + expiresAt time.Time + tokenBuffer time.Duration + mu sync.Mutex +} + func SetupKeycloak(ctx context.Context, kcConnectParams KeycloakConnectParams) error { - // Create realm, if it does not exist. - client, token, err := keycloakLogin(ctx, &kcConnectParams) + return SetupKeycloakWithConfig(ctx, kcConnectParams, nil) +} + +func SetupKeycloakWithConfig(ctx context.Context, kcConnectParams KeycloakConnectParams, tmConfig *TokenManagerConfig) error { + // Create TokenManager + tm, err := NewTokenManager(ctx, &kcConnectParams, tmConfig) if err != nil { - return err + return fmt.Errorf("failed to create token manager: %w", err) } + // Get token and client + token, err := tm.GetToken(ctx) + if err != nil { + return fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + // Create realm realm, err := client.GetRealm(ctx, token.AccessToken, kcConnectParams.Realm) if err != nil { @@ -208,7 +243,7 @@ func SetupKeycloak(ctx context.Context, kcConnectParams KeycloakConnectParams) e } // Create OpenTDF Client - _, err = createClient(ctx, client, token, &kcConnectParams, gocloak.Client{ + _, err = createClient(ctx, tm, &kcConnectParams, gocloak.Client{ ClientID: gocloak.StringP(opentdfClientID), Enabled: gocloak.BoolP(true), Name: gocloak.StringP(opentdfClientID), @@ -250,7 +285,7 @@ func SetupKeycloak(ctx context.Context, kcConnectParams KeycloakConnectParams) e } // Create TDF SDK Client - sdkNumericID, err := createClient(ctx, client, token, &kcConnectParams, gocloak.Client{ + sdkNumericID, err := createClient(ctx, tm, &kcConnectParams, gocloak.Client{ ClientID: gocloak.StringP(opentdfSdkClientID), Enabled: gocloak.BoolP(true), // OptionalClientScopes: &[]string{"testscope"}, @@ -278,16 +313,16 @@ func SetupKeycloak(ctx context.Context, kcConnectParams KeycloakConnectParams) e } // Create TDF Entity Resolution Client - realmManagementClientID, err := getIDOfClient(ctx, client, token, &kcConnectParams, &realmMangementClientName) + realmManagementClientID, err := getIDOfClient(ctx, tm, &kcConnectParams, &realmMangementClientName) if err != nil { return err } - clientRolesToAdd, addErr := getClientRolesByList(ctx, &kcConnectParams, client, token, *realmManagementClientID, []string{"view-clients", "query-clients", "view-users", "query-users"}) + clientRolesToAdd, addErr := getClientRolesByList(ctx, &kcConnectParams, tm, *realmManagementClientID, []string{"view-clients", "query-clients", "view-users", "query-users"}) if addErr != nil { slog.Error("error getting client roles", slog.Any("error", err)) return err } - _, err = createClient(ctx, client, token, &kcConnectParams, gocloak.Client{ + _, err = createClient(ctx, tm, &kcConnectParams, gocloak.Client{ ClientID: gocloak.StringP(opentdfERSClientID), Enabled: gocloak.BoolP(true), Name: gocloak.StringP(opentdfERSClientID), @@ -301,7 +336,7 @@ func SetupKeycloak(ctx context.Context, kcConnectParams KeycloakConnectParams) e } // Create TDF Authorization Svc Client - _, err = createClient(ctx, client, token, &kcConnectParams, gocloak.Client{ + _, err = createClient(ctx, tm, &kcConnectParams, gocloak.Client{ ClientID: gocloak.StringP(opentdfAuthorizationClientID), Enabled: gocloak.BoolP(true), Name: gocloak.StringP(opentdfAuthorizationClientID), @@ -328,7 +363,7 @@ func SetupKeycloak(ctx context.Context, kcConnectParams KeycloakConnectParams) e Username: gocloak.StringP("sampleuser"), Attributes: &map[string][]string{"superhero_name": {"thor"}, "superhero_group": {"avengers"}}, } - _, err = createUser(ctx, client, token, &kcConnectParams, user) + _, err = createUser(ctx, tm, &kcConnectParams, user) if err != nil { panic("Oh no!, failed to create user :(") } @@ -345,6 +380,10 @@ func SetupKeycloak(ctx context.Context, kcConnectParams KeycloakConnectParams) e } func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, keycloakData KeycloakData) error { + return SetupCustomKeycloakWithConfig(ctx, kcParams, keycloakData, nil) +} + +func SetupCustomKeycloakWithConfig(ctx context.Context, kcParams KeycloakConnectParams, keycloakData KeycloakData, tmConfig *TokenManagerConfig) error { // for each realm to create for _, realmToCreate := range keycloakData.Realms { // login and try to create the realm @@ -360,13 +399,13 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke AllowInsecureTLS: true, } - err := createRealm(ctx, kcConnectParams, realmToCreate.RealmRepresentation) + // Create TokenManager for this realm + tm, err := NewTokenManager(ctx, &kcConnectParams, tmConfig) if err != nil { - return err + return fmt.Errorf("failed to create token manager: %w", err) } - // login to new realm - client, token, err := keycloakLogin(ctx, &kcConnectParams) + err = createRealmWithTokenManager(ctx, kcConnectParams, realmToCreate.RealmRepresentation, tm) if err != nil { return err } @@ -374,7 +413,7 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke // create the custom realm roles if realmToCreate.CustomRealmRoles != nil { for _, customRole := range realmToCreate.CustomRealmRoles { - err = createRealmRole(ctx, client, token, *realmToCreate.RealmRepresentation.Realm, customRole) + err = createRealmRole(ctx, tm, *realmToCreate.RealmRepresentation.Realm, customRole) if err != nil { return err } @@ -384,7 +423,7 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke // create the custom groups if realmToCreate.CustomGroups != nil { for _, customGroup := range realmToCreate.CustomGroups { - err = createGroup(ctx, client, token, *realmToCreate.RealmRepresentation.Realm, customGroup) + err = createGroup(ctx, tm, *realmToCreate.RealmRepresentation.Realm, customGroup) if err != nil { return err } @@ -394,23 +433,23 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke // create the clients if realmToCreate.Clients != nil { //nolint:nestif // need to create clients in order for _, customClient := range realmToCreate.Clients { - realmRoles, err := getRealmRolesByList(ctx, kcConnectParams.Realm, client, token, customClient.SaRealmRoles) + realmRoles, err := getRealmRolesByList(ctx, kcConnectParams.Realm, tm, customClient.SaRealmRoles) if err != nil { return err } clientRoleMap := make(map[string][]gocloak.Role) for clientID, roleString := range customClient.SaClientRoles { - longClientID, err := getIDOfClient(ctx, client, token, &kcConnectParams, &clientID) + longClientID, err := getIDOfClient(ctx, tm, &kcConnectParams, &clientID) if err != nil { return err } - roleList, err := getClientRolesByList(ctx, &kcConnectParams, client, token, *longClientID, roleString) + roleList, err := getClientRolesByList(ctx, &kcConnectParams, tm, *longClientID, roleString) if err != nil { return err } clientRoleMap[*longClientID] = roleList } - _, err = createClient(ctx, client, token, &kcConnectParams, customClient.Client, realmRoles, clientRoleMap) + _, err = createClient(ctx, tm, &kcConnectParams, customClient.Client, realmRoles, clientRoleMap) if err != nil { return err } @@ -424,7 +463,7 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke for i := 0; i < customClient.Copies; i++ { customClient.Client.ClientID = gocloak.StringP(fmt.Sprintf(padFormat, baseClientID, i)) customClient.Client.Name = gocloak.StringP(fmt.Sprintf(padFormat, baseClientName, i)) - _, err = createClient(ctx, client, token, &kcConnectParams, customClient.Client, realmRoles, clientRoleMap) + _, err = createClient(ctx, tm, &kcConnectParams, customClient.Client, realmRoles, clientRoleMap) if err != nil { return err } @@ -436,7 +475,7 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke if realmToCreate.CustomClientRoles != nil { for clientID, customRoles := range realmToCreate.CustomClientRoles { for _, customRole := range customRoles { - err = createClientRole(ctx, client, token, *realmToCreate.RealmRepresentation.Realm, clientID, customRole) + err = createClientRole(ctx, tm, *realmToCreate.RealmRepresentation.Realm, clientID, customRole) if err != nil { return err } @@ -447,7 +486,7 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke // create the users if realmToCreate.Users != nil { for _, customUser := range realmToCreate.Users { - _, err = createUser(ctx, client, token, &kcConnectParams, customUser.User) + _, err = createUser(ctx, tm, &kcConnectParams, customUser.User) if err != nil { return err } @@ -461,7 +500,7 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke for i := 0; i < customUser.Copies; i++ { customUser.Username = gocloak.StringP(fmt.Sprintf(padFormat, baseUserName, i)) customUser.Email = gocloak.StringP(fmt.Sprintf("%d-%s", i, baseEmail)) - _, err = createUser(ctx, client, token, &kcConnectParams, customUser.User) + _, err = createUser(ctx, tm, &kcConnectParams, customUser.User) if err != nil { return err } @@ -472,7 +511,7 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke // create token exchanges if realmToCreate.TokenExchanges != nil { for _, tokenExchange := range realmToCreate.TokenExchanges { - err := createTokenExchange(ctx, &kcConnectParams, tokenExchange.StartClientID, tokenExchange.TargetClientID) + err := createTokenExchangeWithTokenManager(ctx, &kcConnectParams, tm, tokenExchange.StartClientID, tokenExchange.TargetClientID) if err != nil { return err } @@ -482,6 +521,108 @@ func SetupCustomKeycloak(ctx context.Context, kcParams KeycloakConnectParams, ke return nil } +// NewTokenManager creates a new TokenManager with initial login +func NewTokenManager(ctx context.Context, connectParams *KeycloakConnectParams, config *TokenManagerConfig) (*TokenManager, error) { + if connectParams == nil { + return nil, errors.New("connectParams cannot be nil") + } + + // Set default token buffer if not provided + tokenBuffer := defaultTokenBufferSeconds * time.Second + if config != nil && config.TokenBuffer > 0 { + tokenBuffer = config.TokenBuffer + } + + tm := &TokenManager{ + connectParams: *connectParams, + tokenBuffer: tokenBuffer, + } + + // Perform initial login + if err := tm.refreshToken(ctx); err != nil { + return nil, fmt.Errorf("initial login failed: %w", err) + } + + return tm, nil +} + +// GetToken returns a valid token, refreshing if necessary +func (tm *TokenManager) GetToken(ctx context.Context) (*gocloak.JWT, error) { + tm.mu.Lock() + defer tm.mu.Unlock() + + // Check if token needs refresh + if tm.needsRefresh() { + slog.InfoContext(ctx, "keycloak token expired or expiring soon - refreshing") + if err := tm.refreshToken(ctx); err != nil { + return nil, fmt.Errorf("failed to refresh token: %w", err) + } + slog.InfoContext(ctx, "successfully refreshed keycloak token", + slog.Int("expires_in_seconds", tm.token.ExpiresIn)) + } + + return tm.token, nil +} + +// GetClient returns the GoCloak client +func (tm *TokenManager) GetClient() *gocloak.GoCloak { + tm.mu.Lock() + defer tm.mu.Unlock() + return tm.client +} + +// needsRefresh checks if the token needs to be refreshed +// Must be called with mutex locked +func (tm *TokenManager) needsRefresh() bool { + if tm.token == nil || tm.client == nil { + return true + } + return time.Now().After(tm.expiresAt.Add(-tm.tokenBuffer)) +} + +// refreshToken performs the actual token refresh +// Must be called with mutex locked +func (tm *TokenManager) refreshToken(ctx context.Context) error { + // Create client if needed + if tm.client == nil { + client := gocloak.NewClient(tm.connectParams.BasePath) + restyClient := client.RestyClient() + restyClient.SetTLSClientConfig(&tls.Config{ + InsecureSkipVerify: tm.connectParams.AllowInsecureTLS, //nolint:gosec // need insecure TLS option for testing and development + }) + tm.client = client + } + + // Get new token from master realm + // Note: Admin tokens are ALWAYS obtained from the "master" realm in Keycloak, + // regardless of which realm is being managed. The token has admin permissions + // across all realms. This is standard Keycloak authentication behavior. + token, err := tm.client.LoginAdmin(ctx, tm.connectParams.Username, tm.connectParams.Password, "master") + if err != nil { + return fmt.Errorf("keycloak login failed: %w", err) + } + + tm.token = token + // Use the token's expiration time if available, otherwise calculate from ExpiresIn + if token.ExpiresIn > 0 { + tm.expiresAt = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second) + + // Adaptive buffer: if token lifetime is shorter than buffer, use 50% of lifetime instead + tokenLifetime := time.Duration(token.ExpiresIn) * time.Second + if tm.tokenBuffer >= tokenLifetime { + tm.tokenBuffer = tokenLifetime / adaptiveBufferDivisor + slog.InfoContext(ctx, "adjusted token buffer for short token lifetime", + slog.Duration("token_lifetime", tokenLifetime), + slog.Duration("adjusted_buffer", tm.tokenBuffer)) + } + } else { + // Fallback to a reasonable default if ExpiresIn is not set + tm.expiresAt = time.Now().Add(defaultFallbackExpiryMinutes * time.Minute) + } + + return nil +} + func keycloakLogin(ctx context.Context, connectParams *KeycloakConnectParams) (*gocloak.GoCloak, *gocloak.JWT, error) { client := gocloak.NewClient(connectParams.BasePath) restyClient := client.RestyClient() @@ -496,29 +637,29 @@ func keycloakLogin(ctx context.Context, connectParams *KeycloakConnectParams) (* return client, token, err } -func createRealm(ctx context.Context, kcConnectParams KeycloakConnectParams, realm gocloak.RealmRepresentation) error { - // Create realm, if it does not exist. - client, token, err := keycloakLogin(ctx, &kcConnectParams) +func createRealmWithTokenManager(ctx context.Context, kcConnectParams KeycloakConnectParams, realm gocloak.RealmRepresentation, tm *TokenManager) error { + // Get fresh token + token, err := tm.GetToken(ctx) if err != nil { - return err + return fmt.Errorf("failed to get token: %w", err) } + client := tm.GetClient() // Create realm r, err := client.GetRealm(ctx, token.AccessToken, *realm.Realm) - var kcErr *gocloak.APIError - if errors.As(err, &kcErr) { - switch kcErr.Code { - case http.StatusNotFound: - // yes - case http.StatusConflict: - slog.Info("realm already exists, skipping create", slog.String("realm", *realm.Realm)) - default: - return err - } - } else if err != nil { - if kcErr.Code == http.StatusConflict { - slog.Info("realm already exists, skipping create", slog.String("realm", *realm.Realm)) - } else if kcErr.Code != http.StatusNotFound { + if err != nil { + var kcErr *gocloak.APIError + if errors.As(err, &kcErr) { + switch kcErr.Code { + case http.StatusNotFound: + // Realm doesn't exist, we'll create it below + case http.StatusConflict: + slog.Info("realm already exists, skipping create", slog.String("realm", *realm.Realm)) + default: + return err + } + } else { + // Non-Keycloak error return err } } @@ -557,11 +698,19 @@ func createRealm(ctx context.Context, kcConnectParams KeycloakConnectParams, rea return nil } -func createGroup(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT, realmName string, group gocloak.Group) error { +func createGroup(ctx context.Context, tm *TokenManager, realmName string, group gocloak.Group) error { if group.Name == nil { return errors.New("group does not have name") } - _, err := client.CreateGroup(ctx, token.AccessToken, realmName, group) + + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + + _, err = client.CreateGroup(ctx, token.AccessToken, realmName, group) if err != nil { kcErr := err.(*gocloak.APIError) //nolint:errcheck,errorlint,forcetypeassert // kc error checked below if kcErr.Code == http.StatusConflict { @@ -577,11 +726,19 @@ func createGroup(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JW return nil } -func createRealmRole(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT, realmName string, role gocloak.Role) error { +func createRealmRole(ctx context.Context, tm *TokenManager, realmName string, role gocloak.Role) error { if role.Name == nil { return errors.New("realm role does not have name") } - _, err := client.CreateRealmRole(ctx, token.AccessToken, realmName, role) + + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + + _, err = client.CreateRealmRole(ctx, token.AccessToken, realmName, role) if err != nil { kcErr := err.(*gocloak.APIError) //nolint:errcheck,errorlint,forcetypeassert // kc error checked below if kcErr.Code == http.StatusConflict { @@ -597,10 +754,18 @@ func createRealmRole(ctx context.Context, client *gocloak.GoCloak, token *gocloa return nil } -func createClientRole(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT, realmName string, clientID string, role gocloak.Role) error { +func createClientRole(ctx context.Context, tm *TokenManager, realmName string, clientID string, role gocloak.Role) error { if role.Name == nil { return errors.New("client role does not have name") } + + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + results, err := client.GetClients(ctx, token.AccessToken, realmName, gocloak.GetClientsParams{ClientID: &clientID}) if err != nil || len(results) == 0 { slog.Error("error getting client", @@ -630,11 +795,18 @@ func createClientRole(ctx context.Context, client *gocloak.GoCloak, token *goclo return nil } -func createClient(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT, connectParams *KeycloakConnectParams, newClient gocloak.Client, realmRoles []gocloak.Role, clientRoles map[string][]gocloak.Role) (string, error) { +func createClient(ctx context.Context, tm *TokenManager, connectParams *KeycloakConnectParams, newClient gocloak.Client, realmRoles []gocloak.Role, clientRoles map[string][]gocloak.Role) (string, error) { + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return "", fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + var longClientID string clientID := *newClient.ClientID - longClientID, err := client.CreateClient(ctx, token.AccessToken, connectParams.Realm, newClient) + longClientID, err = client.CreateClient(ctx, token.AccessToken, connectParams.Realm, newClient) if err != nil { switch kcErrCode(err) { case http.StatusConflict: @@ -719,29 +891,40 @@ func createClient(ctx context.Context, client *gocloak.GoCloak, token *gocloak.J return longClientID, nil } -func createUser(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT, connectParams *KeycloakConnectParams, newUser gocloak.User) (*string, error) { //nolint:unparam // return var to be used in future +func createUser(ctx context.Context, tm *TokenManager, connectParams *KeycloakConnectParams, newUser gocloak.User) (*string, error) { //nolint:unparam // return var to be used in future + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + username := *newUser.Username longUserID, err := client.CreateUser(ctx, token.AccessToken, connectParams.Realm, newUser) if err != nil { - switch kcErrCode(err) { - case http.StatusConflict: - slog.Warn("user already exists", slog.String("username", username)) - users, err := client.GetUsers(ctx, token.AccessToken, connectParams.Realm, gocloak.GetUsersParams{Username: newUser.Username}) - if err != nil { - return nil, err - } - if len(users) == 1 { - longUserID = *users[0].ID - } else { - err = fmt.Errorf("error, %s user not found", username) - return nil, err - } - default: + if kcErrCode(err) != http.StatusConflict { slog.Error("error creating user", slog.String("username", username), slog.Any("error", err)) return nil, err } + + slog.Warn("user already exists", slog.String("username", username)) + users, err := client.GetUsers(ctx, token.AccessToken, connectParams.Realm, gocloak.GetUsersParams{ + Username: newUser.Username, + Exact: gocloak.BoolP(true), + }) + if err != nil { + return nil, err + } + switch len(users) { + case 1: + longUserID = *users[0].ID + case 0: + return nil, fmt.Errorf("error, %s user not found", username) + default: + return nil, fmt.Errorf("error, multiple users found with username %s", username) + } } else { //nolint:sloglint // allow existing emojis slog.Info("✅ user created", @@ -751,7 +934,7 @@ func createUser(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT // assign realm roles to user // retrieve the roles by name if newUser.RealmRoles != nil { - roles, err := getRealmRolesByList(ctx, connectParams.Realm, client, token, *newUser.RealmRoles) + roles, err := getRealmRolesByList(ctx, connectParams.Realm, tm, *newUser.RealmRoles) if err != nil { return nil, err } @@ -775,7 +958,7 @@ func createUser(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT } idOfClient := results[0].ID - clientRoles, err := getClientRolesByList(ctx, connectParams, client, token, *idOfClient, roles) + clientRoles, err := getClientRolesByList(ctx, connectParams, tm, *idOfClient, roles) if err != nil { slog.Error("error getting client roles", slog.Any("error", err)) return nil, err @@ -799,7 +982,14 @@ func createUser(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT return &longUserID, nil } -func getRealmRolesByList(ctx context.Context, realmName string, client *gocloak.GoCloak, token *gocloak.JWT, rolesToAdd []string) ([]gocloak.Role, error) { +func getRealmRolesByList(ctx context.Context, realmName string, tm *TokenManager, rolesToAdd []string) ([]gocloak.Role, error) { + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + var roles []gocloak.Role for _, roleName := range rolesToAdd { role, err := client.GetRealmRole( @@ -818,7 +1008,14 @@ func getRealmRolesByList(ctx context.Context, realmName string, client *gocloak. return roles, nil } -func getClientRolesByList(ctx context.Context, connectParams *KeycloakConnectParams, client *gocloak.GoCloak, token *gocloak.JWT, idClient string, roles []string) ([]gocloak.Role, error) { +func getClientRolesByList(ctx context.Context, connectParams *KeycloakConnectParams, tm *TokenManager, idClient string, roles []string) ([]gocloak.Role, error) { + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + var notFoundRoles []string var clientRoles []gocloak.Role @@ -845,7 +1042,14 @@ searchRole: return clientRoles, nil } -func getIDOfClient(ctx context.Context, client *gocloak.GoCloak, token *gocloak.JWT, connectParams *KeycloakConnectParams, clientName *string) (*string, error) { +func getIDOfClient(ctx context.Context, tm *TokenManager, connectParams *KeycloakConnectParams, clientName *string) (*string, error) { + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + results, err := client.GetClients(ctx, token.AccessToken, connectParams.Realm, gocloak.GetClientsParams{ClientID: clientName}) if err != nil || len(results) == 0 { slog.Error("error getting realm management client", slog.Any("error", err)) @@ -856,12 +1060,24 @@ func getIDOfClient(ctx context.Context, client *gocloak.GoCloak, token *gocloak. } func createTokenExchange(ctx context.Context, connectParams *KeycloakConnectParams, startClientID string, targetClientID string) error { - client, token, err := keycloakLogin(ctx, connectParams) + // Create TokenManager and delegate to TokenManager version + tm, err := NewTokenManager(ctx, connectParams, nil) if err != nil { - return err + return fmt.Errorf("failed to create token manager: %w", err) } + return createTokenExchangeWithTokenManager(ctx, connectParams, tm, startClientID, targetClientID) +} + +func createTokenExchangeWithTokenManager(ctx context.Context, connectParams *KeycloakConnectParams, tm *TokenManager, startClientID string, targetClientID string) error { + // Get fresh token + token, err := tm.GetToken(ctx) + if err != nil { + return fmt.Errorf("failed to get token: %w", err) + } + client := tm.GetClient() + // Step 1- enable permissions for target client - idForTargetClientID, err := getIDOfClient(ctx, client, token, connectParams, &targetClientID) + idForTargetClientID, err := getIDOfClient(ctx, tm, connectParams, &targetClientID) if err != nil { return err } @@ -882,7 +1098,7 @@ func createTokenExchange(ctx context.Context, connectParams *KeycloakConnectPara slog.Debug("step 2 - get realm mgmt client id") realmMangementClientName := "realm-management" - realmManagementClientID, err := getIDOfClient(ctx, client, token, connectParams, &realmMangementClientName) + realmManagementClientID, err := getIDOfClient(ctx, tm, connectParams, &realmMangementClientName) if err != nil { return err } diff --git a/lib/fixtures/keycloak_token_manager_test.go b/lib/fixtures/keycloak_token_manager_test.go new file mode 100644 index 0000000000..c202b04ad8 --- /dev/null +++ b/lib/fixtures/keycloak_token_manager_test.go @@ -0,0 +1,257 @@ +package fixtures + +import ( + "context" + "testing" + "time" +) + +// TestTokenManager_InitialLogin tests that a new TokenManager successfully performs initial login +func TestTokenManager_InitialLogin(t *testing.T) { + // Skip this test in CI as it requires a real Keycloak instance + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + ctx := context.Background() + connectParams := &KeycloakConnectParams{ + BasePath: "http://localhost:8888/auth", + Username: "admin", + Password: "changeme", + Realm: "master", + AllowInsecureTLS: true, + } + + tm, err := NewTokenManager(ctx, connectParams, nil) + if err != nil { + t.Fatalf("Failed to create TokenManager: %v", err) + } + + if tm.token == nil { + t.Fatal("Token should not be nil after initial login") + } + + if tm.client == nil { + t.Fatal("Client should not be nil after initial login") + } + + if tm.expiresAt.IsZero() { + t.Fatal("ExpiresAt should be set after initial login") + } + + if tm.tokenBuffer != 120*time.Second { + t.Errorf("Expected default token buffer of 120s, got %v", tm.tokenBuffer) + } +} + +// TestTokenManager_CustomTokenBuffer tests that custom token buffer is applied +func TestTokenManager_CustomTokenBuffer(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + ctx := context.Background() + connectParams := &KeycloakConnectParams{ + BasePath: "http://localhost:8888/auth", + Username: "admin", + Password: "changeme", + Realm: "master", + AllowInsecureTLS: true, + } + + customBuffer := 60 * time.Second + config := &TokenManagerConfig{ + TokenBuffer: customBuffer, + } + + tm, err := NewTokenManager(ctx, connectParams, config) + if err != nil { + t.Fatalf("Failed to create TokenManager: %v", err) + } + + if tm.tokenBuffer != customBuffer { + t.Errorf("Expected token buffer of %v, got %v", customBuffer, tm.tokenBuffer) + } +} + +// TestTokenManager_GetToken tests that GetToken returns a valid token +func TestTokenManager_GetToken(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + ctx := context.Background() + connectParams := &KeycloakConnectParams{ + BasePath: "http://localhost:8888/auth", + Username: "admin", + Password: "changeme", + Realm: "master", + AllowInsecureTLS: true, + } + + tm, err := NewTokenManager(ctx, connectParams, nil) + if err != nil { + t.Fatalf("Failed to create TokenManager: %v", err) + } + + token, err := tm.GetToken(ctx) + if err != nil { + t.Fatalf("Failed to get token: %v", err) + } + + if token == nil { + t.Fatal("Token should not be nil") + } + + if token.AccessToken == "" { + t.Fatal("AccessToken should not be empty") + } +} + +// TestTokenManager_GetClient tests that GetClient returns a valid client +func TestTokenManager_GetClient(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + ctx := context.Background() + connectParams := &KeycloakConnectParams{ + BasePath: "http://localhost:8888/auth", + Username: "admin", + Password: "changeme", + Realm: "master", + AllowInsecureTLS: true, + } + + tm, err := NewTokenManager(ctx, connectParams, nil) + if err != nil { + t.Fatalf("Failed to create TokenManager: %v", err) + } + + client := tm.GetClient() + if client == nil { + t.Fatal("Client should not be nil") + } +} + +// TestTokenManager_PreemptiveRefresh tests that tokens are refreshed before expiration +func TestTokenManager_PreemptiveRefresh(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + ctx := context.Background() + connectParams := &KeycloakConnectParams{ + BasePath: "http://localhost:8888/auth", + Username: "admin", + Password: "changeme", + Realm: "master", + AllowInsecureTLS: true, + } + + // Use a token buffer that is longer than typical token lifetimes to exercise the refresh logic. + // With a 1-hour buffer, tokens with lifetimes shorter than 1 hour will trigger preemptive refresh. + // If the Keycloak instance has tokens that live longer than 1 hour, this test will not observe a refresh, + // which is expected behavior (no refresh needed if token is still valid beyond the buffer). + config := &TokenManagerConfig{ + TokenBuffer: 1 * time.Hour, // Buffer longer than typical token lifetime + } + + tm, err := NewTokenManager(ctx, connectParams, config) + if err != nil { + t.Fatalf("Failed to create TokenManager: %v", err) + } + + firstToken := tm.token.AccessToken + firstExpiresAt := tm.expiresAt + + // Get token should trigger refresh due to long buffer (if token lifetime < 1 hour) + _, err = tm.GetToken(ctx) + if err != nil { + t.Fatalf("Failed to get token: %v", err) + } + + // Check if token was refreshed + secondToken := tm.token.AccessToken + secondExpiresAt := tm.expiresAt + + if firstToken == secondToken { + // This is not necessarily a failure - it means the token lifetime is longer than our buffer + t.Logf("Token was not refreshed. This is expected if token lifetime > 1 hour. Token expires at: %v", firstExpiresAt) + } else { + // Token was refreshed as expected + t.Logf("Token was successfully refreshed. Old expiry: %v, New expiry: %v", firstExpiresAt, secondExpiresAt) + } +} + +// TestTokenManager_ConcurrentAccess tests thread safety of TokenManager +func TestTokenManager_ConcurrentAccess(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + ctx := context.Background() + connectParams := &KeycloakConnectParams{ + BasePath: "http://localhost:8888/auth", + Username: "admin", + Password: "changeme", + Realm: "master", + AllowInsecureTLS: true, + } + + tm, err := NewTokenManager(ctx, connectParams, nil) + if err != nil { + t.Fatalf("Failed to create TokenManager: %v", err) + } + + // Spawn multiple goroutines to access token concurrently + done := make(chan error, 10) + for i := 0; i < 10; i++ { + go func() { + token, err := tm.GetToken(ctx) + if err != nil { + done <- err + return + } + if token == nil { + done <- err + return + } + done <- nil + }() + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + if err := <-done; err != nil { + t.Errorf("Concurrent access failed: %v", err) + } + } +} + +// TestTokenManager_RefreshFailure tests error handling when refresh fails +func TestTokenManager_RefreshFailure(t *testing.T) { + ctx := context.Background() + connectParams := &KeycloakConnectParams{ + BasePath: "http://invalid-keycloak-url:9999/auth", + Username: "admin", + Password: "wrongpassword", + Realm: "master", + AllowInsecureTLS: true, + } + + _, err := NewTokenManager(ctx, connectParams, nil) + if err == nil { + t.Fatal("Expected error when creating TokenManager with invalid credentials") + } +} + +// TestTokenManager_NilConnectParams tests error handling for nil connect params +func TestTokenManager_NilConnectParams(t *testing.T) { + ctx := context.Background() + + _, err := NewTokenManager(ctx, nil, nil) + if err == nil { + t.Fatal("Expected error when creating TokenManager with nil connect params") + } +} diff --git a/lib/flattening/go.mod b/lib/flattening/go.mod index 0ea7a056d6..7e709fd35a 100644 --- a/lib/flattening/go.mod +++ b/lib/flattening/go.mod @@ -1,8 +1,8 @@ module github.com/opentdf/platform/lib/flattening -go 1.23 +go 1.25.0 -require github.com/stretchr/testify v1.10.0 +require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/lib/flattening/go.sum b/lib/flattening/go.sum index 98ba14e231..9b33b481a4 100644 --- a/lib/flattening/go.sum +++ b/lib/flattening/go.sum @@ -14,8 +14,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/lib/identifier/CHANGELOG.md b/lib/identifier/CHANGELOG.md index 3b636040f1..bf2eaf178d 100644 --- a/lib/identifier/CHANGELOG.md +++ b/lib/identifier/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [0.4.0](https://github.com/opentdf/platform/compare/lib/identifier/v0.3.0...lib/identifier/v0.4.0) (2026-04-21) + + +### Bug Fixes + +* **deps:** bump the external group across 3 directories with 2 updates ([#3153](https://github.com/opentdf/platform/issues/3153)) ([c832d89](https://github.com/opentdf/platform/commit/c832d89f0a61abbdf0969184437e172789466f5c)) + +## [0.3.0](https://github.com/opentdf/platform/compare/lib/identifier/v0.2.0...lib/identifier/v0.3.0) (2026-03-13) + + +### ⚠ BREAKING CHANGES + +* **policy:** namespace Registered Resources ([#3111](https://github.com/opentdf/platform/issues/3111)) + +### Features + +* **policy:** namespace Registered Resources ([#3111](https://github.com/opentdf/platform/issues/3111)) ([6db1883](https://github.com/opentdf/platform/commit/6db188380d3c44f578b6170f123cb9cb1597f4d8)) + + +### Bug Fixes + +* **ci:** Upgrade toolchain version to 1.25.8 ([#3116](https://github.com/opentdf/platform/issues/3116)) ([e1b7882](https://github.com/opentdf/platform/commit/e1b78822c0380a106e6eec05af78dc1fc9e5701f)) +* Go 1.25 ([#3053](https://github.com/opentdf/platform/issues/3053)) ([65eb7c3](https://github.com/opentdf/platform/commit/65eb7c3d5fe1892de1e4fabb9b3b7894742c3f02)) + ## [0.2.0](https://github.com/opentdf/platform/compare/lib/identifier/v0.1.0...lib/identifier/v0.2.0) (2025-09-26) diff --git a/lib/identifier/go.mod b/lib/identifier/go.mod index db0d6e5ab9..f8bac60c76 100644 --- a/lib/identifier/go.mod +++ b/lib/identifier/go.mod @@ -1,8 +1,8 @@ module github.com/opentdf/platform/lib/identifier -go 1.23 +go 1.25.0 -require github.com/stretchr/testify v1.10.0 +require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/lib/identifier/go.sum b/lib/identifier/go.sum index 713a0b4f0a..c4c1710c47 100644 --- a/lib/identifier/go.sum +++ b/lib/identifier/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/lib/identifier/registered_resource_value.go b/lib/identifier/registered_resource_value.go index f286853afd..5866903ce1 100644 --- a/lib/identifier/registered_resource_value.go +++ b/lib/identifier/registered_resource_value.go @@ -7,51 +7,80 @@ import ( ) type FullyQualifiedRegisteredResourceValue struct { - Name string - Value string + Namespace string + Name string + Value string } -// protovalidate already validates the FQN format in the service request -// for parsing purposes, we can just look for any non-whitespace characters -// e.g. should be in format of "https://reg_res//value/" +// New FQN format: https:///reg_res//value/ var registeredResourceValueFqnRegex = regexp.MustCompile( - `^https:\/\/reg_res\/(?[^\/]+)\/value\/(?[^\/]+)$`, + `^https:\/\/(?P[^\/]+)\/reg_res\/(?P[^\/]+)\/value\/(?P[^\/]+)$`, ) -// parseRegisteredResourceValueFqn parses a registered resource value FQN string into a FullyQualifiedRegisteredResourceValue struct. -// The FQN must be in the format: https://reg_res//value/ -func parseRegisteredResourceValueFqn(fqn string) (*FullyQualifiedRegisteredResourceValue, error) { - matches := registeredResourceValueFqnRegex.FindStringSubmatch(fqn) +// Legacy FQN format: https://reg_res//value/ +var legacyRegisteredResourceValueFqnRegex = regexp.MustCompile( + `^https:\/\/reg_res\/(?P[^\/]+)\/value\/(?P[^\/]+)$`, +) - // Check if we have matches first +// matchFqnParts attempts to match fqn against re, extracts named groups, lowercases them, +// and validates name/value with validObjectNameRegex. Returns nil if the regex doesn't match. +func matchFqnParts(re *regexp.Regexp, fqn string, groups []string) (map[string]string, error) { + matches := re.FindStringSubmatch(fqn) if len(matches) == 0 { - return nil, fmt.Errorf("%w: FQN must be in format https://reg_res//value/", ErrInvalidFQNFormat) + return nil, nil //nolint:nilnil // nil means no match, not an error } + result := make(map[string]string, len(groups)) + for _, g := range groups { + idx := re.SubexpIndex(g) + if idx == -1 || idx >= len(matches) { + return nil, fmt.Errorf("%w: missing group %s", ErrInvalidFQNFormat, g) + } + result[g] = strings.ToLower(matches[idx]) + } + if namespace, ok := result["namespace"]; ok { + if !validNamespaceRegex.MatchString(namespace) { + return nil, fmt.Errorf("%w: invalid namespace format %s", ErrInvalidFQNFormat, namespace) + } + } + name, value := result["name"], result["value"] + if !validObjectNameRegex.MatchString(name) || !validObjectNameRegex.MatchString(value) { + return nil, fmt.Errorf("%w: found name %s with value %s", ErrInvalidFQNFormat, name, value) + } + return result, nil +} - nameIdx := registeredResourceValueFqnRegex.SubexpIndex("name") - valueIdx := registeredResourceValueFqnRegex.SubexpIndex("value") - - if nameIdx == -1 || valueIdx == -1 || len(matches) <= nameIdx || len(matches) <= valueIdx { - return nil, fmt.Errorf("%w: valid FQN format of https://reg_res//value/ must be provided", ErrInvalidFQNFormat) +// parseRegisteredResourceValueFqn parses a registered resource value FQN string into a FullyQualifiedRegisteredResourceValue struct. +// Supports both the new format: https:///reg_res//value/ +// and the legacy format: https://reg_res//value/ +func parseRegisteredResourceValueFqn(fqn string) (*FullyQualifiedRegisteredResourceValue, error) { + // Try new format: https:///reg_res//value/ + if parts, err := matchFqnParts(registeredResourceValueFqnRegex, fqn, []string{"namespace", "name", "value"}); err != nil { + return nil, err + } else if parts != nil { + return &FullyQualifiedRegisteredResourceValue{Namespace: parts["namespace"], Name: parts["name"], Value: parts["value"]}, nil } - name := strings.ToLower(matches[nameIdx]) - value := strings.ToLower(matches[valueIdx]) - isValid := validObjectNameRegex.MatchString(name) && validObjectNameRegex.MatchString(value) - if !isValid { - return nil, fmt.Errorf("%w: found name %s with value %s", ErrInvalidFQNFormat, name, value) + // Try legacy format: https://reg_res//value/ + if parts, err := matchFqnParts(legacyRegisteredResourceValueFqnRegex, fqn, []string{"name", "value"}); err != nil { + return nil, err + } else if parts != nil { + return &FullyQualifiedRegisteredResourceValue{Name: parts["name"], Value: parts["value"]}, nil } - return &FullyQualifiedRegisteredResourceValue{ - Name: name, - Value: value, - }, nil + return nil, fmt.Errorf("%w: FQN must be in format https:///reg_res//value/", ErrInvalidFQNFormat) } // Implementing FullyQualified interface for FullyQualifiedRegisteredResourceValue func (rrv *FullyQualifiedRegisteredResourceValue) FQN() string { builder := strings.Builder{} - builder.WriteString("https://reg_res/") + if rrv.Namespace != "" { + builder.WriteString("https://") + builder.WriteString(rrv.Namespace) + builder.WriteString("/reg_res/") + } else { + // Legacy format for backward compatibility + builder.WriteString("https://reg_res/") + } builder.WriteString(rrv.Name) builder.WriteString("/value/") builder.WriteString(rrv.Value) @@ -59,6 +88,9 @@ func (rrv *FullyQualifiedRegisteredResourceValue) FQN() string { } func (rrv *FullyQualifiedRegisteredResourceValue) Validate() error { + if rrv.Namespace != "" && !validNamespaceRegex.MatchString(rrv.Namespace) { + return fmt.Errorf("%w: invalid namespace format %s", ErrInvalidFQNFormat, rrv.Namespace) + } if !validObjectNameRegex.MatchString(rrv.Name) { return fmt.Errorf("%w: invalid resource name format %s", ErrInvalidFQNFormat, rrv.Name) } diff --git a/lib/identifier/registered_resource_value_test.go b/lib/identifier/registered_resource_value_test.go index 8b3873b6de..d7c83f8a5a 100644 --- a/lib/identifier/registered_resource_value_test.go +++ b/lib/identifier/registered_resource_value_test.go @@ -8,48 +8,67 @@ import ( func TestRegisteredResourceValueFQN(t *testing.T) { tests := []struct { - name string - resName string - value string - want string + name string + namespace string + resName string + value string + want string }{ { - name: "basic example", - resName: "resource", - value: "value", - want: "https://reg_res/resource/value/value", + name: "namespaced basic example", + namespace: "example.com", + resName: "resource", + value: "value", + want: "https://example.com/reg_res/resource/value/value", }, { - name: "with hyphens", - resName: "test-resource", - value: "test-value", - want: "https://reg_res/test-resource/value/test-value", + name: "namespaced with hyphens", + namespace: "example.com", + resName: "test-resource", + value: "test-value", + want: "https://example.com/reg_res/test-resource/value/test-value", }, { - name: "with underscores", - resName: "test_resource", - value: "test_value", - want: "https://reg_res/test_resource/value/test_value", + name: "namespaced with underscores", + namespace: "example.com", + resName: "test_resource", + value: "test_value", + want: "https://example.com/reg_res/test_resource/value/test_value", }, { - name: "with numbers", - resName: "resource123", - value: "value456", - want: "https://reg_res/resource123/value/value456", + name: "namespaced with numbers", + namespace: "example.com", + resName: "resource123", + value: "value456", + want: "https://example.com/reg_res/resource123/value/value456", + }, + { + name: "namespaced lower case", + namespace: "EXAMPLE.COM", + resName: "RESOURCE", + value: "VALUE", + want: "https://example.com/reg_res/resource/value/value", }, { - name: "lower case", - resName: "RESOURCE", - value: "VALUE", + name: "legacy no namespace", + resName: "resource", + value: "value", want: "https://reg_res/resource/value/value", }, + { + name: "legacy with hyphens", + resName: "test-resource", + value: "test-value", + want: "https://reg_res/test-resource/value/test-value", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rrv := &FullyQualifiedRegisteredResourceValue{ - Name: tt.resName, - Value: tt.value, + Namespace: tt.namespace, + Name: tt.resName, + Value: tt.value, } got := rrv.FQN() require.Equal(t, tt.want, got) @@ -148,47 +167,91 @@ func TestRegisteredResourceValueValidate(t *testing.T) { func TestParseRegisteredResourceValueFqn(t *testing.T) { tests := []struct { - name string - fqn string - wantName string - wantValue string - wantErr bool + name string + fqn string + wantNamespace string + wantName string + wantValue string + wantErr bool }{ + // New format tests + { + name: "valid namespaced basic", + fqn: "https://example.com/reg_res/valid/value/test", + wantNamespace: "example.com", + wantName: "valid", + wantValue: "test", + wantErr: false, + }, + { + name: "valid namespaced with hyphens", + fqn: "https://example.com/reg_res/test-resource/value/test-value", + wantNamespace: "example.com", + wantName: "test-resource", + wantValue: "test-value", + wantErr: false, + }, + { + name: "valid namespaced with underscores", + fqn: "https://example.com/reg_res/test_resource/value/test_value", + wantNamespace: "example.com", + wantName: "test_resource", + wantValue: "test_value", + wantErr: false, + }, { - name: "valid basic", + name: "valid namespaced with numbers", + fqn: "https://example.com/reg_res/resource123/value/value456", + wantNamespace: "example.com", + wantName: "resource123", + wantValue: "value456", + wantErr: false, + }, + { + name: "valid namespaced lower case", + fqn: "https://EXAMPLE.COM/reg_res/RESOURce/value/valUE", + wantNamespace: "example.com", + wantName: "resource", + wantValue: "value", + wantErr: false, + }, + // Legacy format tests + { + name: "valid legacy basic", fqn: "https://reg_res/valid/value/test", wantName: "valid", wantValue: "test", wantErr: false, }, { - name: "valid with hyphens", + name: "valid legacy with hyphens", fqn: "https://reg_res/test-resource/value/test-value", wantName: "test-resource", wantValue: "test-value", wantErr: false, }, { - name: "valid with underscores", + name: "valid legacy with underscores", fqn: "https://reg_res/test_resource/value/test_value", wantName: "test_resource", wantValue: "test_value", wantErr: false, }, { - name: "valid with numbers", + name: "valid legacy with numbers", fqn: "https://reg_res/resource123/value/value456", wantName: "resource123", wantValue: "value456", wantErr: false, }, { - name: "valid lower case", + name: "valid legacy lower case", fqn: "https://reg_res/RESOURce/value/valUE", wantName: "resource", wantValue: "value", wantErr: false, }, + // Invalid format tests { name: "empty string", fqn: "", @@ -199,29 +262,29 @@ func TestParseRegisteredResourceValueFqn(t *testing.T) { fqn: "invalid", wantErr: true, }, - { - name: "wrong prefix", - fqn: "https://registered/valid/value/test", - wantErr: true, - }, { name: "missing parts", - fqn: "https://reg_res/valid", + fqn: "https://example.com/reg_res/valid", wantErr: true, }, { name: "missing value segment", - fqn: "https://reg_res/valid/value", + fqn: "https://example.com/reg_res/valid/value", wantErr: true, }, { name: "wrong protocol", - fqn: "http://reg_res/test/value/something", + fqn: "http://example.com/reg_res/test/value/something", wantErr: true, }, { name: "extra prefix", - fqn: "somethinghttps://reg_res/test/value/something", + fqn: "somethinghttps://example.com/reg_res/test/value/something", + wantErr: true, + }, + { + name: "invalid namespace format", + fqn: "https://not_a_valid_namespace/reg_res/test/value/something", wantErr: true, }, } @@ -236,6 +299,7 @@ func TestParseRegisteredResourceValueFqn(t *testing.T) { if tt.wantErr { return } + require.Equal(t, tt.wantNamespace, got.Namespace) require.Equal(t, tt.wantName, got.Name) require.Equal(t, tt.wantValue, got.Value) }) @@ -245,29 +309,44 @@ func TestParseRegisteredResourceValueFqn(t *testing.T) { func TestRegisteredResourceValueRoundTrip(t *testing.T) { // Test round trip from struct to FQN to parse and back tests := []struct { - name string - resName string - value string + name string + namespace string + resName string + value string }{ { - name: "basic example", - resName: "resource", - value: "value", + name: "namespaced basic example", + namespace: "example.com", + resName: "resource", + value: "value", }, { - name: "with hyphens", - resName: "test-resource", - value: "test-value", + name: "namespaced with hyphens", + namespace: "example.com", + resName: "test-resource", + value: "test-value", }, { - name: "with underscores", - resName: "test_resource", - value: "test_value", + name: "namespaced with underscores", + namespace: "my.namespace.org", + resName: "test_resource", + value: "test_value", }, { - name: "with numbers", - resName: "resource123", - value: "value456", + name: "namespaced with numbers", + namespace: "example.com", + resName: "resource123", + value: "value456", + }, + { + name: "legacy basic example", + resName: "resource", + value: "value", + }, + { + name: "legacy with hyphens", + resName: "test-resource", + value: "test-value", }, } @@ -275,8 +354,9 @@ func TestRegisteredResourceValueRoundTrip(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Create original registered resource value original := &FullyQualifiedRegisteredResourceValue{ - Name: tt.resName, - Value: tt.value, + Namespace: tt.namespace, + Name: tt.resName, + Value: tt.value, } // Get FQN @@ -287,6 +367,7 @@ func TestRegisteredResourceValueRoundTrip(t *testing.T) { require.NoError(t, err) // Check the parsed values match original + require.Equal(t, original.Namespace, parsed.Namespace) require.Equal(t, original.Name, parsed.Name) require.Equal(t, original.Value, parsed.Value) diff --git a/lib/ocrypto/BENCHMARK_REPORT.md b/lib/ocrypto/BENCHMARK_REPORT.md new file mode 100644 index 0000000000..8c01339a51 --- /dev/null +++ b/lib/ocrypto/BENCHMARK_REPORT.md @@ -0,0 +1,168 @@ +# Benchmark Report: Hybrid Post-Quantum Key Wrapping Performance + +**Platform:** Apple M4, darwin/arm64, Go 1.25.9 +**Date:** 2026-04-29 +**Methodology:** `go test -bench=. -benchmem -count=5` (median of 5 runs) + +> **Note:** Wrap and unwrap benchmarks mirror the actual TDF code paths: +> - **Wrap** follows `sdk/tdf.go` (`generateWrapKeyWithRSA`, `generateWrapKeyWithEC`, `generateWrapKeyWithHybrid`) +> - **Unwrap** follows `service/internal/security/standard_crypto.go:Decrypt()` +> +> This includes PEM parsing, ephemeral keygen, ECDH, HKDF, AES-GCM, and ASN.1 marshaling — not simplified library-level `WrapDEK()` / `UnwrapDEK()` calls. + +## How to Run + +```bash +# Full benchmark suite (use -count=5 for statistical significance) +cd lib/ocrypto && go test -bench=. -benchmem -count=5 -timeout=10m + +# Quick single-count run +cd lib/ocrypto && go test -bench=. -benchmem -count=1 -timeout=5m + +# Specific benchmark groups +cd lib/ocrypto && go test -bench=BenchmarkKeyGeneration -benchmem +cd lib/ocrypto && go test -bench=BenchmarkWrapDEK -benchmem +cd lib/ocrypto && go test -bench=BenchmarkUnwrapDEK -benchmem +cd lib/ocrypto && go test -bench=BenchmarkHybridSubOps -benchmem + +# Wrapped key size comparison table +cd lib/ocrypto && go test -v -run TestWrappedKeySizeComparison +``` + +## Results + +### Key Generation + +| Scheme | Time | B/op | allocs/op | vs EC P-256 | +|--------|-----:|-----:|----------:|-------------| +| RSA-2048 | 47.7 ms | 652 KB | 5,929 | ~6,400x slower | +| EC P-256 | 7.4 us | 984 B | 16 | baseline | +| EC P-384 | 71.3 us | 1.2 KB | 19 | ~9.6x slower | +| X-Wing | 43.8 us | 9.8 KB | 9 | ~5.9x slower | +| P256+ML-KEM-768 | 34.8 us | 11.4 KB | 13 | ~4.7x slower | +| P384+ML-KEM-1024 | 113.8 us | 17.9 KB | 16 | ~15x slower | + +**Takeaway:** RSA-2048 key generation is orders of magnitude slower than everything else (~48ms). All hybrid schemes generate keys in under 115us. EC P-256 is fastest at ~7us; EC P-384 keygen is ~10x slower than P-256 due to the larger field size. + +### Wrap DEK (32-byte AES-256 key) + +These benchmarks follow the exact TDF wrapping paths: +- **RSA:** `FromPublicPEM` -> `Encrypt` (OAEP) +- **EC:** `NewECKeyPair` -> `ComputeECDHKey` -> `CalculateHKDF` -> `AES-GCM Encrypt` +- **Hybrid:** `PubKeyFromPem` -> `Encapsulate` -> `CalculateHKDF` -> `AES-GCM Encrypt` -> `ASN.1 Marshal` + +| Scheme | Time | Wrapped Size | B/op | allocs/op | vs EC P-256 | +|--------|-----:|-------------:|-----:|----------:|-------------| +| RSA-2048 | 25.5 us | 256 B | 4.1 KB | 33 | 0.5x (faster) | +| EC P-256 | 54.5 us | 60 B | 12.0 KB | 158 | baseline | +| EC P-384 | 449.3 us | 60 B | 14.3 KB | 189 | ~8.2x slower | +| X-Wing | 77.4 us | 1,190 B | 16.4 KB | 42 | ~1.4x slower | +| P256+ML-KEM-768 | 75.2 us | 1,223 B | 18.7 KB | 59 | ~1.4x slower | +| P384+ML-KEM-1024 | 369.9 us | 1,735 B | 27.0 KB | 68 | ~6.8x slower | + +**Takeaway:** P256+ML-KEM-768 wrapping (~75us) is only ~1.4x slower than EC P-256 (~55us) — the ephemeral EC keygen + ECDH in the EC path narrows the gap significantly. RSA wrap is fastest since it's just OAEP padding. The two P-384-based schemes are the slowest (EC P-384 ~449us, P384+ML-KEM-1024 ~370us) — the P-384 ECDH operation alone dominates EC P-384's wrap cost since each call re-generates an ephemeral key. + +### Unwrap DEK + +These benchmarks follow the KAS unwrap paths: +- **RSA:** pre-loaded `AsymDecryption.Decrypt` (key parsed at startup) +- **EC:** `NewSaltedECDecryptor(cachedKey, TDFSalt)` -> `DecryptWithEphemeralKey` +- **Hybrid:** `PrivateKeyFromPem` -> `UnwrapDEK` (PEM parsed each call) + +| Scheme | Time | B/op | allocs/op | vs EC P-256 | +|--------|-----:|-----:|----------:|-------------| +| RSA-2048 | 737.3 us | 560 B | 8 | ~26x slower | +| EC P-256 | 28.4 us | 4.1 KB | 40 | baseline | +| EC P-384 | 230.5 us | 4.6 KB | 55 | ~8.1x slower | +| X-Wing | 90.4 us | 12.4 KB | 37 | ~3.2x slower | +| P256+ML-KEM-768 | 96.3 us | 13.8 KB | 51 | ~3.4x slower | +| P384+ML-KEM-1024 | 400.2 us | 20.1 KB | 60 | ~14x slower | + +**Takeaway:** RSA unwrap is the slowest operation in the entire suite (~737us) due to private key exponentiation. P256+ML-KEM-768 unwraps in ~96us — fast enough for real-time use. EC P-384 unwrap (~231us) is ~8x slower than P-256 because of the more expensive curve operations. Hybrid unwraps include PEM parsing overhead that could be optimized by caching parsed keys (as EC already does). + +### Wrap + Unwrap Round-Trip Summary + +| Scheme | Wrap + Unwrap | Quantum Safe? | +|--------|-------------:|:-------------:| +| RSA-2048 | 763 us | No | +| EC P-256 | 83 us | No | +| EC P-384 | 680 us | No | +| X-Wing | 168 us | Yes | +| P256+ML-KEM-768 | 172 us | Yes | +| P384+ML-KEM-1024 | 770 us | Yes | + +## Analysis: Where Time Is Spent + +The `BenchmarkHybridSubOps` benchmarks break down hybrid wrap operations into their constituent parts: + +### X-Wing Sub-Operations + +| Operation | Time | % of Wrap | +|-----------|-----:|----------:| +| Encapsulate (X25519 + ML-KEM-768) | 71.6 us | 92.5% | +| HKDF key derivation | 0.49 us | 0.6% | +| AES-GCM encrypt (32B DEK) | 0.37 us | 0.5% | +| ASN.1 marshal | 0.52 us | 0.7% | +| PEM parsing + overhead | ~4.4 us | 5.7% | + +### P256+ML-KEM-768 Sub-Operations + +| Operation | Time | % of Wrap | +|-----------|-----:|----------:| +| Encapsulate (ECDH P-256 + ML-KEM-768) | 70.0 us | 93.1% | +| HKDF key derivation | 0.51 us | 0.7% | +| AES-GCM encrypt (32B DEK) | 0.37 us | 0.5% | +| ASN.1 marshal | 0.51 us | 0.7% | +| PEM parsing + overhead | ~3.8 us | 5.1% | + +### P384+ML-KEM-1024 Sub-Operations + +| Operation | Time | % of Wrap | +|-----------|-----:|----------:| +| Encapsulate (ECDH P-384 + ML-KEM-1024) | 359.9 us | 97.3% | +| HKDF key derivation | 0.51 us | 0.1% | +| AES-GCM encrypt (32B DEK) | 0.37 us | 0.1% | +| ASN.1 marshal | 0.54 us | 0.1% | +| PEM parsing + overhead | ~8.6 us | 2.3% | + +**Conclusion:** KEM encapsulation dominates all hybrid schemes at 93-97% of total time. HKDF, AES-GCM, and ASN.1 marshaling are all sub-microsecond and negligible. The P-384 elliptic curve ECDH is ~5x slower than P-256, which is why P384+ML-KEM-1024 is significantly slower than P256+ML-KEM-768. + +## Manifest Size Impact + +| Scheme | Wrapped Key | Public Key (PEM) | Base64 Wrapped | Notes | +|--------|------------:|-----------------:|---------------:|-------| +| RSA-2048 | 256 B | 451 B | 344 B | No ephemeral key in manifest | +| EC P-256 | 60 B | 178 B | 80 B | + ephemeral key (91 B) in manifest | +| EC P-384 | 60 B | 215 B | 80 B | + ephemeral key (120 B) in manifest | +| X-Wing | 1,190 B | 1,714 B | 1,588 B | All in single ASN.1 blob | +| P256+ML-KEM-768 | 1,223 B | 1,785 B | 1,632 B | All in single ASN.1 blob | +| P384+ML-KEM-1024 | 1,735 B | 2,347 B | 2,316 B | All in single ASN.1 blob | + +> Base64 overhead = ceil(raw_bytes * 4/3). TDF manifests store wrapped keys as base64. + +Hybrid schemes produce wrapped keys that are ~20x larger than EC P-256 (1.2-1.7 KB vs 60 B). For a TDF with a single recipient, this adds ~1-2 KB to the manifest. For multi-recipient TDFs, the overhead scales linearly per recipient. + +## Trade-offs Summary + +| Concern | RSA-2048 | EC P-256 | EC P-384 | X-Wing | P256+ML-KEM-768 | P384+ML-KEM-1024 | +|---------|----------|----------|----------|--------|-----------------|-------------------| +| Quantum resistance | None | None | None | Yes (hybrid) | Yes (hybrid) | Yes (hybrid) | +| Key generation | 48 ms (slow) | 7.4 us (fastest) | 71 us | 44 us | 35 us | 114 us | +| Wrap latency | 26 us | 55 us | 449 us | 77 us | 75 us | 370 us | +| Unwrap latency | 737 us (slow) | 28 us | 231 us | 90 us | 96 us | 400 us | +| Round-trip | 763 us | 83 us | 680 us | 168 us | 172 us | 770 us | +| Wrapped key size | 256 B | 60 B | 60 B | 1,190 B | 1,223 B | 1,735 B | +| Standards basis | PKCS#1 | ECIES | ECIES | IETF draft | NIST SP 800-227 | NIST SP 800-227 | + +### Recommendations + +- **P256+ML-KEM-768** is the best all-around hybrid choice: NIST-standardized, fastest hybrid round-trip (~172us), and moderate size overhead (1.2 KB wrapped keys). Only ~1.4x slower than EC P-256 for wrapping. +- **P384+ML-KEM-1024** provides a higher classical security level (Cat 3 classical / Cat 5 PQ) at the cost of ~4-5x more latency. Use when policy requires P-384 or equivalent classical strength. +- **X-Wing** offers a simpler construction (X25519 + ML-KEM-768) but is based on an IETF draft rather than a NIST standard. Performance is comparable to P256+ML-KEM-768. +- **EC P-256** remains the fastest and smallest option for environments where quantum resistance is not yet required. +- **EC P-384** is significantly more expensive than P-256 (~8-10x for both wrap and unwrap) without quantum protection — prefer P384+ML-KEM-1024 if the latency budget already covers P-384, since it adds PQ resistance for similar cost. +- **RSA-2048** has the worst unwrap performance (~737us) and should be considered legacy. + +### Optimization Opportunities + +- **Hybrid unwrap PEM caching:** The KAS currently parses hybrid private key PEM on every unwrap call. Caching the parsed key (as EC already does) would save ~5-10us per unwrap. diff --git a/lib/ocrypto/CHANGELOG.md b/lib/ocrypto/CHANGELOG.md index 1c77c63959..b5c19b0f69 100644 --- a/lib/ocrypto/CHANGELOG.md +++ b/lib/ocrypto/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog +## [0.12.0](https://github.com/opentdf/platform/compare/lib/ocrypto/v0.11.0...lib/ocrypto/v0.12.0) (2026-05-27) + + +### Bug Fixes + +* **core:** Uses strict FIPS AES-GCM ([#3507](https://github.com/opentdf/platform/issues/3507)) ([a0bb218](https://github.com/opentdf/platform/commit/a0bb218762a178b4b925e401f90ee0e76ea865c5)) +* **deps:** bump the external group across 1 directory with 2 updates ([#3519](https://github.com/opentdf/platform/issues/3519)) ([0d0c57b](https://github.com/opentdf/platform/commit/0d0c57b2743a9ffae6e5cb7242520721c6864152)) + +## [0.11.0](https://github.com/opentdf/platform/compare/lib/ocrypto/v0.10.0...lib/ocrypto/v0.11.0) (2026-05-26) + + +### Features + +* **core:** add hybrid NIST EC + ML-KEM key wrapping support ([#3276](https://github.com/opentdf/platform/issues/3276)) ([1209acc](https://github.com/opentdf/platform/commit/1209acc2f8ae24af121f6a2892817c20ebb14d25)) + + +### Bug Fixes + +* **ci:** Upgrade toolchain version to 1.25.8 ([#3116](https://github.com/opentdf/platform/issues/3116)) ([e1b7882](https://github.com/opentdf/platform/commit/e1b78822c0380a106e6eec05af78dc1fc9e5701f)) +* **deps:** bump the external group across 3 directories with 2 updates ([#3153](https://github.com/opentdf/platform/issues/3153)) ([c832d89](https://github.com/opentdf/platform/commit/c832d89f0a61abbdf0969184437e172789466f5c)) + +## [0.10.0](https://github.com/opentdf/platform/compare/lib/ocrypto/v0.9.0...lib/ocrypto/v0.10.0) (2026-02-13) + + +### Bug Fixes + +* Go 1.25 ([#3053](https://github.com/opentdf/platform/issues/3053)) ([65eb7c3](https://github.com/opentdf/platform/commit/65eb7c3d5fe1892de1e4fabb9b3b7894742c3f02)) +* **kas:** dont hardcode P-256 curve ([#3073](https://github.com/opentdf/platform/issues/3073)) ([826d857](https://github.com/opentdf/platform/commit/826d857cf11a1e83108e45773d794c334c2b2e09)) +* **policy:** reject unencrypted private keys for modes 1/2 ([#3072](https://github.com/opentdf/platform/issues/3072)) ([e2dc6d8](https://github.com/opentdf/platform/commit/e2dc6d8d1e1d35ce6a241bce2a23fa2d128511fa)) + +## [0.9.0](https://github.com/opentdf/platform/compare/lib/ocrypto/v0.8.0...lib/ocrypto/v0.9.0) (2026-01-26) + + +### ⚠ BREAKING CHANGES + +* remove nanotdf support ([#3013](https://github.com/opentdf/platform/issues/3013)) + +### Bug Fixes + +* remove nanotdf support ([#3013](https://github.com/opentdf/platform/issues/3013)) ([90ff7ce](https://github.com/opentdf/platform/commit/90ff7ce50754a1f37ba1cc530507c1f6e15930a0)) + ## [0.8.0](https://github.com/opentdf/platform/compare/lib/ocrypto/v0.7.0...lib/ocrypto/v0.8.0) (2025-12-19) diff --git a/lib/ocrypto/HYBRID_NIST_KEY_WRAPPING.md b/lib/ocrypto/HYBRID_NIST_KEY_WRAPPING.md new file mode 100644 index 0000000000..e6cbf4fe74 --- /dev/null +++ b/lib/ocrypto/HYBRID_NIST_KEY_WRAPPING.md @@ -0,0 +1,308 @@ +# NIST EC + ML-KEM Hybrid Key Wrapping + +## Overview + +This document describes the hybrid post-quantum key wrapping scheme used in TDF (Trusted Data Format) that combines classical elliptic curve cryptography (ECDH) with post-quantum lattice-based cryptography (ML-KEM) to protect data encryption keys (split keys). + +Two variants are supported. Hybrid security is bounded by the stronger of the two underlying primitives against each adversary class, so the post-quantum strength is set by ML-KEM and the classical strength is set by ECDH. + +| Variant | Classical (ECDH) | Post-quantum (ML-KEM) | +|---------|------------------|-----------------------| +| P-256 + ML-KEM-768 | NIST Category 1 | NIST Category 3 | +| P-384 + ML-KEM-1024 | NIST Category 3 | NIST Category 5 | + +References: +- NIST PQC Call for Proposals §4.A.5 (category definitions): https://csrc.nist.gov/csrc/media/projects/post-quantum-cryptography/documents/call-for-proposals-final-dec-2016.pdf +- FIPS 203 (ML-KEM-768 = Cat 3, ML-KEM-1024 = Cat 5): https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.203.pdf +- NIST SP 800-57 Part 1 Rev. 5 Table 2 (P-256 = 128-bit ≈ Cat 1, P-384 = 192-bit ≈ Cat 3): https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-57pt1r5.pdf + +Core implementation: `lib/ocrypto/hybrid_nist.go` + +## Key Format + +### Combined Public Key + +The KAS (Key Access Server) hosts a combined public key in PEM format. The raw bytes inside the PEM are a simple concatenation of the EC and ML-KEM public keys: + +``` +[ EC Public Key (uncompressed point) | ML-KEM Public Key ] +``` + +| Variant | EC Public Key Size | ML-KEM Public Key Size | Combined Size | PEM Block Type | +|---------|-------------------|----------------------|---------------|----------------| +| P-256 + ML-KEM-768 | 65 bytes | 1184 bytes | 1249 bytes | `SECP256R1 MLKEM768 PUBLIC KEY` | +| P-384 + ML-KEM-1024 | 97 bytes | 1568 bytes | 1665 bytes | `SECP384R1 MLKEM1024 PUBLIC KEY` | + +### Combined Private Key + +The KAS holds a combined private key, also a concatenation: + +``` +[ EC Private Key (raw scalar) | ML-KEM Private Key ] +``` + +The ML-KEM portion is stored in the 64-byte seed form (`d || z`) defined by FIPS 203 §7.1, not the expanded ~2400/3168-byte decapsulation key. The seed is what `crypto/mlkem` (Go 1.25) emits via `Bytes()` and consumes via `NewDecapsulationKey768` / `NewDecapsulationKey1024`. The constant `mlkemSeedSize = 64` in `hybrid_nist.go` and the size checks in `decodeSizedPEMBlock` enforce this layout. + +| Variant | EC Private Key Size | ML-KEM Seed Size | Combined Size | PEM Block Type | +|---------|--------------------|------------------|---------------|----------------| +| P-256 + ML-KEM-768 | 32 bytes | 64 bytes | 96 bytes | `SECP256R1 MLKEM768 PRIVATE KEY` | +| P-384 + ML-KEM-1024 | 48 bytes | 64 bytes | 112 bytes | `SECP384R1 MLKEM1024 PRIVATE KEY` | + +### How the Client Obtains the Public Key + +The client obtains the combined public key from KAS in one of two ways: + +1. **Fetched at runtime (autoconfigure)**: The SDK calls the KAS `PublicKey` gRPC endpoint, specifying the hybrid algorithm. KAS returns the combined PEM. The response is cached for 5 minutes. +2. **Provided manually**: The caller supplies the public key PEM via `WithKasInformation(...)` when configuring the SDK. + +## Wrap (Encrypt the Split Key) + +Function: `hybridNISTWrapDEK` (`hybrid_nist.go`, line 339) + +This is performed on the **client side** during TDF encryption. + +### Step 1 - Split the Public Key + +The combined public key bytes are split at the known EC public key size boundary: + +``` +ecPubBytes = publicKeyRaw[:ecPubSize] // 65 bytes for P-256, 97 bytes for P-384 +mlkemPubBytes = publicKeyRaw[ecPubSize:] // 1184 bytes for ML-KEM-768, 1568 bytes for ML-KEM-1024 +``` + +### Step 2 - ECDH (Classical Key Agreement) + +An ephemeral EC key pair is generated on the same curve as the KAS static key: + +1. Generate ephemeral EC key pair: `ephemeral_private, ephemeral_public` +2. Compute ECDH shared secret: `ecdhSecret = ECDH(ephemeral_private, KAS_ec_public)` +3. Retain `ephemeral_public` bytes for inclusion in the output + +This is a standard elliptic curve Diffie-Hellman operation. The ephemeral key provides forward secrecy - even if the KAS static key is later compromised, past wrapped keys remain protected. + +### Step 3 - ML-KEM Encapsulate (Post-Quantum KEM) + +ML-KEM (Module Lattice-based Key Encapsulation Mechanism, formerly known as Kyber) is a KEM, not a key exchange. The encapsulation operation takes only the public key and produces two outputs: + +``` +(mlkemSecret, mlkemCiphertext) = ML-KEM.Encapsulate(KAS_mlkem_public) +``` + +- `mlkemSecret` (32 bytes): A shared secret known to the encapsulator +- `mlkemCiphertext` (1088 bytes for ML-KEM-768, 1568 bytes for ML-KEM-1024): An opaque ciphertext that only the ML-KEM private key holder can decapsulate to recover the same shared secret + +Internally, `EncapsulateTo`: +1. Generates random coins (entropy) +2. Uses the ML-KEM public key (a matrix over a polynomial ring) to encrypt those random coins into the ciphertext +3. Derives the shared secret from both the random coins and the ciphertext + +No ephemeral ML-KEM key pair is generated by the client. The ciphertext itself serves as the "ephemeral" artifact sent to KAS. + +### Step 4 - Combine Secrets + +The two shared secrets from the classical and post-quantum operations are concatenated: + +``` +combinedSecret = ecdhSecret || mlkemSecret +``` + +This is a simple byte concatenation. The security property is that an attacker must break **both** ECDH and ML-KEM to recover the combined secret. If quantum computers break ECDH but ML-KEM remains secure, the combined secret is still protected (and vice versa). + +### Step 5 - Key Derivation (HKDF) + +A 32-byte AES-256 wrapping key is derived from the combined secret using HKDF-SHA256: + +``` +wrapKey = HKDF-SHA256( + IKM: combinedSecret, // ecdhSecret || mlkemSecret + salt: SHA256("TDF"), // 32-byte fixed salt + info: // empty by default +) -> 32 bytes +``` + +Function: `deriveHybridNISTWrapKey` (`hybrid_nist.go`, line 479) + +The salt is a hardcoded SHA-256 digest of the ASCII string `"TDF"`, shared across all TDF key wrapping schemes. + +### Step 6 - AES-GCM Encrypt the Split Key + +The split key (the actual data encryption key shard) is encrypted using AES-256-GCM: + +``` +encryptedDEK = AES-256-GCM.Encrypt(key=wrapKey, plaintext=splitKey) +``` + +The output format is: + +``` +[ nonce (12 bytes) | ciphertext | authentication tag (16 bytes) ] +``` + +The nonce is randomly generated. The authentication tag provides integrity verification. + +### Step 7 - Package as ASN.1 DER + +The ephemeral EC public key, ML-KEM ciphertext, and encrypted DEK are packaged into an ASN.1 DER structure: + +```asn1 +HybridNISTWrappedKey ::= SEQUENCE { + hybridCiphertext [0] OCTET STRING, -- ephemeralECPub || mlkemCiphertext + encryptedDEK [1] OCTET STRING -- AES-GCM nonce + ciphertext + tag +} +``` + +Where `hybridCiphertext` is: + +``` +[ ephemeral EC public key (65 or 97 bytes) | ML-KEM ciphertext (1088 or 1568 bytes) ] +``` + +| Variant | Ephemeral EC Pub | ML-KEM Ciphertext | hybridCiphertext Size | +|---------|-----------------|-------------------|----------------------| +| P-256 + ML-KEM-768 | 65 bytes | 1088 bytes | 1153 bytes | +| P-384 + ML-KEM-1024 | 97 bytes | 1568 bytes | 1665 bytes | + +This DER blob is then base64-encoded and stored as the `wrappedKey` field in the TDF manifest's Key Access Object, with `keyType` set to `"hybrid-wrapped"`. + +## Unwrap (Decrypt the Split Key) + +Function: `hybridNISTUnwrapDEK` (`hybrid_nist.go`, line 406) + +This is performed on the **KAS server side** when a client sends a rewrap request. KAS holds the combined private key on disk, loaded at startup. + +### Step 1 - Parse the ASN.1 DER + +The base64-decoded DER blob is unmarshalled: + +``` +ASN.1 Unmarshal -> HybridNISTWrappedKey { + hybridCiphertext: [ephemeralECPub | mlkemCiphertext] + encryptedDEK: [nonce | ciphertext | tag] +} +``` + +### Step 2 - Split the Hybrid Ciphertext + +The `hybridCiphertext` is split at the known EC public key size boundary: + +``` +ephemeralECPub = hybridCiphertext[:ecPubSize] // 65 or 97 bytes +mlkemCiphertext = hybridCiphertext[ecPubSize:] // 1088 or 1568 bytes +``` + +### Step 3 - Split the Private Key + +The combined private key is split at the known EC private key size boundary: + +``` +ecPrivBytes = privateKeyRaw[:ecPrivSize] // 32 or 48 bytes +mlkemPrivBytes = privateKeyRaw[ecPrivSize:] // 2400 or 3168 bytes +``` + +### Step 4 - ECDH (Reconstruct Classical Shared Secret) + +KAS uses its static EC private key with the client's ephemeral EC public key: + +``` +ecdhSecret = ECDH(KAS_ec_private, ephemeral_ec_public) +``` + +This produces the same `ecdhSecret` that the client computed in Wrap Step 2, because `ECDH(a, g^b) == ECDH(b, g^a)`. + +### Step 5 - ML-KEM Decapsulate (Reconstruct Post-Quantum Shared Secret) + +KAS uses its ML-KEM private key to decapsulate the ciphertext: + +``` +mlkemSecret = ML-KEM.Decapsulate(KAS_mlkem_private, mlkemCiphertext) +``` + +The ML-KEM private key contains the secret trapdoor in the lattice structure. Decapsulation recovers the random coins from the ciphertext and derives the same 32-byte shared secret that the client obtained during encapsulation. + +### Step 6 - Combine Secrets + +Identical to Wrap Step 4: + +``` +combinedSecret = ecdhSecret || mlkemSecret +``` + +### Step 7 - Key Derivation (HKDF) + +Identical to Wrap Step 5: + +``` +wrapKey = HKDF-SHA256( + IKM: combinedSecret, + salt: SHA256("TDF"), + info: +) -> 32 bytes +``` + +Both sides derive the same `wrapKey` because both sides have the same `ecdhSecret` and `mlkemSecret`. + +### Step 8 - AES-GCM Decrypt the Split Key + +``` +splitKey = AES-256-GCM.Decrypt(key=wrapKey, ciphertext=encryptedDEK) +``` + +The AES-GCM decryption verifies the authentication tag. If the tag does not match (indicating tampering or a wrong key), decryption fails. + +KAS now has the original split key. It enforces policy checks, and if the requesting client is authorized, returns the split key (protected by the session transport layer). + +## Security Properties + +### Hybrid Security Guarantee + +The combined secret is derived from both ECDH and ML-KEM. An attacker must break **both** to recover the wrap key: + +- If a quantum computer breaks ECDH (recovers `ecdhSecret` from the ephemeral public key), ML-KEM still protects the combined secret +- If a classical vulnerability is found in ML-KEM (recovers `mlkemSecret` from the ciphertext), ECDH still protects the combined secret + +### Forward Secrecy + +- The ephemeral EC key pair is generated fresh for each wrap operation, providing forward secrecy on the classical side +- ML-KEM encapsulation generates fresh randomness for each operation, providing forward secrecy on the post-quantum side + +### What is NOT in the Combiner + +Unlike the X-Wing KEM (which uses SHA3-256 and mixes in public keys, ciphertexts, and a domain label), this NIST hybrid implementation: + +- Does **not** mix the ephemeral EC public key into the KDF +- Does **not** mix the ML-KEM ciphertext into the KDF +- Does **not** mix the static public keys into the KDF +- Does **not** use a domain separation label in the HKDF info + +The two raw shared secrets are simply concatenated and passed through HKDF. This is a common and accepted pattern for hybrid KEM composition, though it provides less identity binding than the X-Wing combiner approach. + +## Comparison with Other Key Wrapping Schemes in TDF + +| Aspect | RSA (`wrapped`) | EC (`ec-wrapped`) | NIST Hybrid (`hybrid-wrapped`) | X-Wing (`hybrid-wrapped`) | +|--------|-----------------|-------------------|-------------------------------|--------------------------| +| Classical | RSA-2048 | ECDH (P-256/384/521) | ECDH (P-256/384) | X25519 | +| Post-Quantum | None | None | ML-KEM-768/1024 | ML-KEM-768 | +| Combiner | N/A | HKDF only | Concatenation + HKDF | SHA3-256 (spec-defined, inside circl library) + HKDF | +| Output Format | Base64(RSA ciphertext) | Base64(AES-GCM ciphertext) | Base64(ASN.1 DER) | Base64(ASN.1 DER) | +| Ephemeral Key | None | EC ephemeral | EC ephemeral + ML-KEM ciphertext | X-Wing ciphertext (contains both) | +| Identity Binding | N/A | No | No | Yes (public keys mixed into SHA3-256) | + +## Manifest Example + +A Key Access Object in the TDF manifest for hybrid wrapping: + +```json +{ + "type": "hybrid-wrapped", + "url": "https://kas.example.com", + "kid": "hybrid-key-1", + "sid": "split-1", + "wrappedKey": "", + "policyBinding": { + "alg": "HS256", + "hash": "" + } +} +``` + +Note: Unlike `ec-wrapped`, the `ephemeralPublicKey` field is **not** used in the manifest for hybrid wrapping. The ephemeral EC public key is embedded inside the `wrappedKey` ASN.1 structure alongside the ML-KEM ciphertext. diff --git a/lib/ocrypto/aes_gcm.go b/lib/ocrypto/aes_gcm.go index 6866587050..f0af776c7e 100644 --- a/lib/ocrypto/aes_gcm.go +++ b/lib/ocrypto/aes_gcm.go @@ -16,15 +16,18 @@ const DefaultNonceSize = 16 const GcmStandardNonceSize = 12 +// ErrUnsupportedAESGCMConfiguration is returned for AES-GCM options that Go strict FIPS mode does not allow. +var ErrUnsupportedAESGCMConfiguration = errors.New("unsupported AES-GCM configuration") + // NewAESGcm creates and returns a new AesGcm. func NewAESGcm(key []byte) (AesGcm, error) { if len(key) == 0 { - return AesGcm{}, errors.New("invalid key size for gcm encryption") + return AesGcm{}, ErrInvalidKeyData } block, err := aes.NewCipher(key) if err != nil { - return AesGcm{}, fmt.Errorf("aes.NewCipher failed: %w", err) + return AesGcm{}, fmt.Errorf("%w: %w", ErrInvalidKeyData, err) } return AesGcm{block: block}, nil @@ -33,115 +36,37 @@ func NewAESGcm(key []byte) (AesGcm, error) { // Encrypt encrypts data with symmetric key. // NOTE: This method use nonce of 12 bytes and auth tag as aes block size(16 bytes). func (aesGcm AesGcm) Encrypt(data []byte) ([]byte, error) { - nonce, err := RandomBytes(GcmStandardNonceSize) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCMWithNonceSize(aesGcm.block, GcmStandardNonceSize) + gcm, err := cipher.NewGCMWithRandomNonce(aesGcm.block) if err != nil { - return nil, fmt.Errorf("cipher.NewGCMWithNonceSize failed: %w", err) + return nil, fmt.Errorf("cipher.NewGCMWithRandomNonce failed: %w", err) } - cipherText := gcm.Seal(nonce, nonce, data, nil) + cipherText := gcm.Seal(nil, nil, data, nil) return cipherText, nil } func (aesGcm AesGcm) EncryptInPlace(data []byte) ([]byte, []byte, error) { - nonce, err := RandomBytes(GcmStandardNonceSize) + gcm, err := cipher.NewGCMWithRandomNonce(aesGcm.block) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("cipher.NewGCMWithRandomNonce failed: %w", err) } - gcm, err := cipher.NewGCMWithNonceSize(aesGcm.block, GcmStandardNonceSize) - if err != nil { - return nil, nil, fmt.Errorf("cipher.NewGCMWithNonceSize failed: %w", err) - } - - cipherText := gcm.Seal(data[:0], nonce, data, nil) + sealed := gcm.Seal(nil, nil, data, nil) + nonce, cipherText := sealed[:GcmStandardNonceSize], sealed[GcmStandardNonceSize:] return cipherText, nonce, nil } -// EncryptWithIV encrypts data with symmetric key. -// NOTE: This method use default auth tag as aes block size(16 bytes) -// and expects iv of 16 bytes. -func (aesGcm AesGcm) EncryptWithIV(iv, data []byte) ([]byte, error) { - gcm, err := cipher.NewGCMWithNonceSize(aesGcm.block, len(iv)) - if err != nil { - return nil, fmt.Errorf("cipher.NewGCMWithNonceSize failed: %w", err) - } - - cipherText := gcm.Seal(iv, iv, data, nil) - return cipherText, nil -} - -// EncryptWithIVAndTagSize encrypts data with symmetric key. -// NOTE: This method expects gcm standard nonce size(12) of iv. -func (aesGcm AesGcm) EncryptWithIVAndTagSize(iv, data []byte, authTagSize int) ([]byte, error) { - if len(iv) != GcmStandardNonceSize { - return nil, errors.New("invalid nonce size, expects GcmStandardNonceSize") - } - - gcm, err := cipher.NewGCMWithTagSize(aesGcm.block, authTagSize) - if err != nil { - return nil, fmt.Errorf("cipher.NewGCMWithTagSize failed: %w", err) - } - - cipherText := gcm.Seal(iv, iv, data, nil) - return cipherText, nil -} - -// Decrypt decrypts data with symmetric key. -// NOTE: This method use nonce of 12 bytes and auth tag as aes block size(16 bytes) -// also expects IV as preamble of data. -func (aesGcm AesGcm) Decrypt(data []byte) ([]byte, error) { // extract nonce and cipherText - nonce, cipherText := data[:GcmStandardNonceSize], data[GcmStandardNonceSize:] - - gcm, err := cipher.NewGCMWithNonceSize(aesGcm.block, GcmStandardNonceSize) - if err != nil { - return nil, fmt.Errorf("cipher.NewGCMWithNonceSize failed: %w", err) - } - - plainData, err := gcm.Open(nil, nonce, cipherText, nil) - if err != nil { - return nil, fmt.Errorf("gcm.Open failed: %w", err) - } - - return plainData, nil -} - -// DecryptWithTagSize decrypts data with symmetric key. -// NOTE: This method expects gcm standard nonce size(12) of iv. -func (aesGcm AesGcm) DecryptWithTagSize(data []byte, authTagSize int) ([]byte, error) { - // extract nonce and cipherText - nonce, cipherText := data[:GcmStandardNonceSize], data[GcmStandardNonceSize:] - - gcm, err := cipher.NewGCMWithTagSize(aesGcm.block, authTagSize) - if err != nil { - return nil, fmt.Errorf("cipher.NewGCMWithTagSize failed: %w", err) - } - - plainData, err := gcm.Open(nil, nonce, cipherText, nil) - if err != nil { - return nil, fmt.Errorf("gcm.Open failed: %w", err) - } - - return plainData, nil -} - -// DecryptWithIVAndTagSize decrypts data with symmetric key. -// NOTE: This method expects gcm standard nonce size(12) of iv. -func (aesGcm AesGcm) DecryptWithIVAndTagSize(iv, data []byte, authTagSize int) ([]byte, error) { - if len(iv) != GcmStandardNonceSize { - return nil, errors.New("invalid nonce size, expects GcmStandardNonceSize") +// Decrypt decrypts data with a 12-byte nonce prefix and a 16-byte AES-GCM authentication tag. +func (aesGcm AesGcm) Decrypt(data []byte) ([]byte, error) { + if len(data) < GcmStandardNonceSize+aes.BlockSize { + return nil, ErrInvalidCiphertext } - - gcm, err := cipher.NewGCMWithTagSize(aesGcm.block, authTagSize) + gcm, err := cipher.NewGCMWithRandomNonce(aesGcm.block) if err != nil { - return nil, fmt.Errorf("cipher.NewGCMWithTagSize failed: %w", err) + return nil, fmt.Errorf("cipher.NewGCMWithRandomNonce failed: %w", err) } - plainData, err := gcm.Open(nil, iv, data, nil) + plainData, err := gcm.Open(nil, nil, data, nil) if err != nil { return nil, fmt.Errorf("gcm.Open failed: %w", err) } diff --git a/lib/ocrypto/aes_gcm_test.go b/lib/ocrypto/aes_gcm_test.go index faaf6c6a4a..fc1708104b 100644 --- a/lib/ocrypto/aes_gcm_test.go +++ b/lib/ocrypto/aes_gcm_test.go @@ -1,7 +1,9 @@ package ocrypto import ( + "crypto/aes" "encoding/hex" + "errors" "strings" "testing" ) @@ -60,88 +62,82 @@ widely adopted for its performance`, } func TestCreateAesGcm_EncryptWithDefaults(t *testing.T) { - gcmEncryptionTests := []struct { - symmetricKey string - iv string - plainText string - cipherText string - }{ - { - "66af5c10753139c6161d0f0eee125bbc9545d6704d64890e396c5c8d4f4820d4", - "29a8b044b5b6ce00e18bc6fc78ff50c6", - "virtru", - "29a8b044b5b6ce00e18bc6fc78ff50c6b3cf733137d865892e5af63dcbca08086ba1ac82aae2", - }, - { - "120fba31c537d99ade0a0a8c8e6df535f7de86fb6e1d5948317b4596982a5e1b", - "ec9074bc6c6b81d6520f5a7425f8977a", - "", - "ec9074bc6c6b81d6520f5a7425f8977adabe8b28dd100eea2f58d71e3644b43d", - }, - { - "9895f395913a3cfd974ea53c0735030c7df4602d699c986afdc5fdd10071c0a5", - "5142d90e8499f597802ca68cddb25ec1", - `In cryptography, Galois/Counter Mode (GCM)[1] is a mode of operation -for symmetric-key cryptographic block ciphers which is -widely adopted for its performance`, - `5142d90e8499f597802ca68cddb25ec101c1e44df776bfca60ed217e06421c7b945adaf328984 -9406ca5b7046c886050fe72cc0ebc429f683f9cfe3a47613e2ca8a812ef9b75d361c32d042124d3dc5d84c757225 -21df65ed7829327b5adda0ae020a778b909328a48311cc705d4c0a8b83f49430aa80febba73e27e99b3006d6e768 -a092d5b9dc894e7a634235198b1a986a3624912dec108ef03055b319f59f25fc579eb08f01820ea19edc7f9896129 -c572c36440ed80fd61fc71df37`, - }, + key, _ := hex.DecodeString("66af5c10753139c6161d0f0eee125bbc9545d6704d64890e396c5c8d4f4820d4") + aesGcm, err := NewAESGcm(key) + if err != nil { + t.Fatalf("Fail to create AesGcm: %v", err) } - for _, test := range gcmEncryptionTests { - key, _ := hex.DecodeString(test.symmetricKey) - nonce, _ := hex.DecodeString(test.iv) + plainText := []byte("virtru") + cipherText, err := aesGcm.Encrypt(plainText) + if err != nil { + t.Fatalf("Fail to encrypt: %v", err) + } - aesGcm, err := NewAESGcm(key) - if err != nil { - t.Fatalf("Fail to create AesGcm: %v", err) - } + if len(cipherText) != len(plainText)+GcmStandardNonceSize+aes.BlockSize { + t.Fatalf("unexpected ciphertext length: got %d", len(cipherText)) + } - cipherText, err := aesGcm.EncryptWithIV(nonce, []byte(test.plainText)) - if err != nil { - t.Fatalf("Fail to encrypt with iv: %v", err) - } + decipherText, err := aesGcm.Decrypt(cipherText) + if err != nil { + t.Fatalf("Fail to decrypt: %v", err) + } - actualCipherText, _ := hex.DecodeString(strings.ReplaceAll(test.cipherText, "\n", "")) - if string(actualCipherText) != string(cipherText) { - t.Fatalf("encrypt test fail: actual:%s, expected:%s", - string(actualCipherText), string(cipherText)) - } + if string(plainText) != string(decipherText) { + t.Errorf("gcm decryption test don't match: expected %v, got %v", string(plainText), string(decipherText)) } } -func TestCreateAESGcm_WithDifferentAuthTags(t *testing.T) { - plainText := "Virtru" +func TestCreateAESGcm_EncryptInPlace(t *testing.T) { key, _ := hex.DecodeString("66af5c10753139c6161d0f0eee125bbc9545d6704d64890e396c5c8d4f4820d4") aesGcm, err := NewAESGcm(key) if err != nil { t.Fatalf("Fail to create AesGcm: %v", err) } - nonce, err := RandomBytes(GcmStandardNonceSize) - if err != nil { - t.Fatalf("Fail to grenerate nonce %v", err) + plainText := []byte("Virtru") + tests := []struct { + name string + data []byte + }{ + { + name: "exact capacity", + data: append([]byte{}, plainText...), + }, + { + name: "spare capacity", + data: func() []byte { + buf := make([]byte, len(plainText), len(plainText)+GcmStandardNonceSize+aes.BlockSize) + copy(buf, plainText) + return buf + }(), + }, } - authTagsForNanoTDF := []int{12, 13, 14, 15, 16} - for _, authTag := range authTagsForNanoTDF { - cipherText, err := aesGcm.EncryptWithIVAndTagSize(nonce, []byte(plainText), authTag) - if err != nil { - t.Fatalf("Fail to encrypt with auth tag:%d err:%v", authTag, err) - } - - decipherText, err := aesGcm.DecryptWithTagSize(cipherText, authTag) - if err != nil { - t.Fatalf("Fail to decrypt with auth tag:%d err:%v", authTag, err) - } - - if plainText != string(decipherText) { - t.Errorf("gcm decryption test don't match: expected %v, got %v", plainText, string(decipherText)) - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cipherText, nonce, err := aesGcm.EncryptInPlace(test.data) + if err != nil { + t.Fatalf("Fail to encrypt in place: %v", err) + } + + if len(nonce) != GcmStandardNonceSize { + t.Fatalf("unexpected nonce length: got %d", len(nonce)) + } + if len(cipherText) != len(plainText)+aes.BlockSize { + t.Fatalf("unexpected ciphertext length: got %d", len(cipherText)) + } + + sealed := append(append([]byte{}, nonce...), cipherText...) + decipherText, err := aesGcm.Decrypt(sealed) + if err != nil { + t.Fatalf("Fail to decrypt ciphertext: %v", err) + } + + if string(plainText) != string(decipherText) { + t.Errorf("gcm decryption test don't match: expected %v, got %v", string(plainText), string(decipherText)) + } + }) } } @@ -173,3 +169,62 @@ func BenchmarkAESGcm_ForTDF3(b *testing.B) { b.Errorf("gcm decryption test don't match") } } + +func TestNewAESGcm_EmptyKey(t *testing.T) { + _, err := NewAESGcm([]byte{}) + if err == nil { + t.Fatal("expected error for empty key, got nil") + } + if !errors.Is(err, ErrInvalidKeyData) { + t.Errorf("expected ErrInvalidKeyData, got %v", err) + } +} + +func TestNewAESGcm_InvalidKeySize(t *testing.T) { + // AES only supports 16, 24, or 32 byte keys + invalidKeys := [][]byte{ + {0x01}, // 1 byte + {0x01, 0x02, 0x03}, // 3 bytes + make([]byte, 15), // 15 bytes + make([]byte, 17), // 17 bytes + make([]byte, 31), // 31 bytes + make([]byte, 33), // 33 bytes + } + + for _, key := range invalidKeys { + _, err := NewAESGcm(key) + if err == nil { + t.Errorf("expected error for %d-byte key, got nil", len(key)) + } + if !errors.Is(err, ErrInvalidKeyData) { + t.Errorf("expected ErrInvalidKeyData for %d-byte key, got %v", len(key), err) + } + } +} + +func TestDecrypt_EmptyData(t *testing.T) { + key, _ := hex.DecodeString("66af5c10753139c6161d0f0eee125bbc9545d6704d64890e396c5c8d4f4820d4") + aesGcm, _ := NewAESGcm(key) + + _, err := aesGcm.Decrypt([]byte{}) + if err == nil { + t.Fatal("expected error for empty data, got nil") + } + if !errors.Is(err, ErrInvalidCiphertext) { + t.Errorf("expected ErrInvalidCiphertext, got %v", err) + } +} + +func TestDecrypt_TooShortData(t *testing.T) { + key, _ := hex.DecodeString("66af5c10753139c6161d0f0eee125bbc9545d6704d64890e396c5c8d4f4820d4") + aesGcm, _ := NewAESGcm(key) + + // Data shorter than GcmStandardNonceSize (12 bytes) + _, err := aesGcm.Decrypt([]byte{0x01, 0x02, 0x03}) + if err == nil { + t.Fatal("expected error for short data, got nil") + } + if !errors.Is(err, ErrInvalidCiphertext) { + t.Errorf("expected ErrInvalidCiphertext, got %v", err) + } +} diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index 426f859723..1cbfbfc943 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -43,6 +43,14 @@ func FromPrivatePEMWithSalt(privateKeyInPem string, salt, info []byte) (PrivateK if block == nil { return AsymDecryption{}, errors.New("failed to parse PEM formatted private key") } + switch block.Type { + case PEMBlockXWingPrivateKey: + return NewSaltedXWingDecryptor(block.Bytes, salt, info) + case PEMBlockP256MLKEM768PrivateKey: + return NewSaltedP256MLKEM768Decryptor(block.Bytes, salt, info) + case PEMBlockP384MLKEM1024PrivateKey: + return NewSaltedP384MLKEM1024Decryptor(block.Bytes, salt, info) + } priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) switch { @@ -171,24 +179,22 @@ func (e ECDecryptor) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, er return nil, fmt.Errorf("hkdf failure: %w", err) } - // Encrypt data with derived key using aes-gcm + if len(data) < GcmStandardNonceSize+aes.BlockSize { + return nil, errors.New("ciphertext too short") + } + + // Decrypt data with derived key using AES-GCM. block, err := aes.NewCipher(derivedKey) if err != nil { return nil, fmt.Errorf("aes.NewCipher failure: %w", err) } - gcm, err := cipher.NewGCM(block) + gcm, err := cipher.NewGCMWithRandomNonce(block) if err != nil { - return nil, fmt.Errorf("cipher.NewGCM failure: %w", err) - } - - nonceSize := gcm.NonceSize() - if len(data) < nonceSize { - return nil, errors.New("ciphertext too short") + return nil, fmt.Errorf("cipher.NewGCMWithRandomNonce failure: %w", err) } - nonce, ciphertext := data[:nonceSize], data[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + plaintext, err := gcm.Open(nil, nil, data, nil) if err != nil { return nil, fmt.Errorf("gcm.Open failure: %w", err) } diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index b0136ddb45..2a030c3f32 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -23,8 +23,9 @@ import ( type SchemeType string const ( - RSA SchemeType = "wrapped" - EC SchemeType = "ec-wrapped" + RSA SchemeType = "wrapped" + EC SchemeType = "ec-wrapped" + Hybrid SchemeType = "hybrid-wrapped" ) type PublicKeyEncryptor interface { @@ -69,6 +70,19 @@ func FromPublicPEM(publicKeyInPem string) (PublicKeyEncryptor, error) { } func FromPublicPEMWithSalt(publicKeyInPem string, salt, info []byte) (PublicKeyEncryptor, error) { + block, _ := pem.Decode([]byte(publicKeyInPem)) + if block == nil { + return nil, errors.New("failed to parse PEM formatted public key") + } + switch block.Type { + case PEMBlockXWingPublicKey: + return NewXWingEncryptor(block.Bytes, salt, info) + case PEMBlockP256MLKEM768PublicKey: + return NewP256MLKEM768Encryptor(block.Bytes, salt, info) + case PEMBlockP384MLKEM1024PublicKey: + return NewP384MLKEM1024Encryptor(block.Bytes, salt, info) + } + pub, err := getPublicPart(publicKeyInPem) if err != nil { return nil, err @@ -98,6 +112,7 @@ func newECIES(pub *ecdh.PublicKey, salt, info []byte) (ECEncryptor, error) { } // NewAsymEncryption creates and returns a new AsymEncryption. +// // Deprecated: Use FromPublicPEM instead. func NewAsymEncryption(publicKeyInPem string) (AsymEncryption, error) { pub, err := getPublicPart(publicKeyInPem) @@ -254,17 +269,12 @@ func (e ECEncryptor) Encrypt(data []byte) ([]byte, error) { return nil, fmt.Errorf("aes.NewCipher failed: %w", err) } - gcm, err := cipher.NewGCM(block) + gcm, err := cipher.NewGCMWithRandomNonce(block) if err != nil { - return nil, fmt.Errorf("cipher.NewGCM failed: %w", err) - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, fmt.Errorf("nonce generation failed: %w", err) + return nil, fmt.Errorf("cipher.NewGCMWithRandomNonce failed: %w", err) } - ciphertext := gcm.Seal(nonce, nonce, data, nil) + ciphertext := gcm.Seal(nil, nil, data, nil) return ciphertext, nil } diff --git a/lib/ocrypto/benchmark_test.go b/lib/ocrypto/benchmark_test.go new file mode 100644 index 0000000000..b86b3075e7 --- /dev/null +++ b/lib/ocrypto/benchmark_test.go @@ -0,0 +1,816 @@ +package ocrypto + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/asn1" + "fmt" + "testing" +) + +// Sink variables to prevent compiler from optimizing away results. +var ( + sinkBytes []byte + errSink error +) + +// testDEK is a 32-byte AES-256 key used as the payload for wrap/unwrap benchmarks. +var testDEK = []byte("0123456789abcdef0123456789abcdef") + +func BenchmarkKeyGeneration(b *testing.B) { + b.Run("RSA-2048", func(b *testing.B) { + for b.Loop() { + _, errSink = NewRSAKeyPair(2048) + } + }) + b.Run("EC-P256", func(b *testing.B) { + for b.Loop() { + _, errSink = NewECKeyPair(ECCModeSecp256r1) + } + }) + b.Run("EC-P384", func(b *testing.B) { + for b.Loop() { + _, errSink = NewECKeyPair(ECCModeSecp384r1) + } + }) + b.Run("XWing", func(b *testing.B) { + for b.Loop() { + _, errSink = NewXWingKeyPair() + } + }) + b.Run("P256_MLKEM768", func(b *testing.B) { + for b.Loop() { + _, errSink = NewP256MLKEM768KeyPair() + } + }) + b.Run("P384_MLKEM1024", func(b *testing.B) { + for b.Loop() { + _, errSink = NewP384MLKEM1024KeyPair() + } + }) +} + +// benchTDFSalt matches tdf.go:tdfSalt() — SHA-256("TDF"). +func benchTDFSalt() []byte { + digest := sha256.New() + digest.Write([]byte("TDF")) + return digest.Sum(nil) +} + +// BenchmarkWrapDEK mirrors the actual TDF key-wrapping paths in sdk/tdf.go: +// - RSA: FromPublicPEM -> Encrypt (generateWrapKeyWithRSA) +// - EC: NewECKeyPair -> ComputeECDHKey -> HKDF -> AES-GCM (generateWrapKeyWithEC) +// - Hybrid: PubKeyFromPem -> Encapsulate -> HKDF -> AES-GCM -> ASN.1 (generateWrapKeyWithHybrid) +func BenchmarkWrapDEK(b *testing.B) { + salt := benchTDFSalt() + + // RSA-2048: setup KAS public key + rsaKP, err := NewRSAKeyPair(2048) + if err != nil { + b.Fatal(err) + } + rsaPubPEM, err := rsaKP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + + // EC P-256: setup KAS public key PEM + ecKP, err := NewECKeyPair(ECCModeSecp256r1) + if err != nil { + b.Fatal(err) + } + ecKASPubPEM, err := ecKP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + + // EC P-384: setup KAS public key PEM + ec384KP, err := NewECKeyPair(ECCModeSecp384r1) + if err != nil { + b.Fatal(err) + } + ec384KASPubPEM, err := ec384KP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + + // X-Wing: setup KAS public key PEM + xwingKP, err := NewXWingKeyPair() + if err != nil { + b.Fatal(err) + } + xwingPubPEM, err := xwingKP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + + // P256+MLKEM768: setup KAS public key PEM + p256KP, err := NewP256MLKEM768KeyPair() + if err != nil { + b.Fatal(err) + } + p256PubPEM, err := p256KP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + + // P384+MLKEM1024: setup KAS public key PEM + p384KP, err := NewP384MLKEM1024KeyPair() + if err != nil { + b.Fatal(err) + } + p384PubPEM, err := p384KP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + + // RSA: tdf.go calls FromPublicPEM -> Encrypt + b.Run("RSA-2048", func(b *testing.B) { + for b.Loop() { + enc, err := FromPublicPEM(rsaPubPEM) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = enc.Encrypt(testDEK) + } + b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") + }) + + // EC: tdf.go generates ephemeral EC keypair, computes ECDH, derives via HKDF, AES-GCM wraps + b.Run("EC-P256", func(b *testing.B) { + for b.Loop() { + ephKP, err := NewECKeyPair(ECCModeSecp256r1) + if err != nil { + b.Fatal(err) + } + ephPrivPEM, err := ephKP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ecdhKey, err := ComputeECDHKey([]byte(ephPrivPEM), []byte(ecKASPubPEM)) + if err != nil { + b.Fatal(err) + } + sessionKey, err := CalculateHKDF(salt, ecdhKey) + if err != nil { + b.Fatal(err) + } + gcm, err := NewAESGcm(sessionKey) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = gcm.Encrypt(testDEK) + } + b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") + }) + + b.Run("EC-P384", func(b *testing.B) { + for b.Loop() { + ephKP, err := NewECKeyPair(ECCModeSecp384r1) + if err != nil { + b.Fatal(err) + } + ephPrivPEM, err := ephKP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ecdhKey, err := ComputeECDHKey([]byte(ephPrivPEM), []byte(ec384KASPubPEM)) + if err != nil { + b.Fatal(err) + } + sessionKey, err := CalculateHKDF(salt, ecdhKey) + if err != nil { + b.Fatal(err) + } + gcm, err := NewAESGcm(sessionKey) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = gcm.Encrypt(testDEK) + } + b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") + }) + + // X-Wing: tdf.go parses PEM, calls Encapsulate, HKDF, AES-GCM, then ASN.1 marshal + b.Run("XWing", func(b *testing.B) { + for b.Loop() { + pubKey, err := XWingPubKeyFromPem([]byte(xwingPubPEM)) + if err != nil { + b.Fatal(err) + } + ss, ct, err := XWingEncapsulate(pubKey) + if err != nil { + b.Fatal(err) + } + wrapKey, err := CalculateHKDF(salt, ss) + if err != nil { + b.Fatal(err) + } + gcm, err := NewAESGcm(wrapKey) + if err != nil { + b.Fatal(err) + } + encDEK, err := gcm.Encrypt(testDEK) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = asn1.Marshal(HybridNISTWrappedKey{ + HybridCiphertext: ct, + EncryptedDEK: encDEK, + }) + } + b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") + }) + + // P256+MLKEM768: same flow as X-Wing with different Encapsulate/PEM parse + b.Run("P256_MLKEM768", func(b *testing.B) { + for b.Loop() { + pubKey, err := P256MLKEM768PubKeyFromPem([]byte(p256PubPEM)) + if err != nil { + b.Fatal(err) + } + ss, ct, err := P256MLKEM768Encapsulate(pubKey) + if err != nil { + b.Fatal(err) + } + wrapKey, err := CalculateHKDF(salt, ss) + if err != nil { + b.Fatal(err) + } + gcm, err := NewAESGcm(wrapKey) + if err != nil { + b.Fatal(err) + } + encDEK, err := gcm.Encrypt(testDEK) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = asn1.Marshal(HybridNISTWrappedKey{ + HybridCiphertext: ct, + EncryptedDEK: encDEK, + }) + } + b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") + }) + + // P384+MLKEM1024: same flow with P384 variant + b.Run("P384_MLKEM1024", func(b *testing.B) { + for b.Loop() { + pubKey, err := P384MLKEM1024PubKeyFromPem([]byte(p384PubPEM)) + if err != nil { + b.Fatal(err) + } + ss, ct, err := P384MLKEM1024Encapsulate(pubKey) + if err != nil { + b.Fatal(err) + } + wrapKey, err := CalculateHKDF(salt, ss) + if err != nil { + b.Fatal(err) + } + gcm, err := NewAESGcm(wrapKey) + if err != nil { + b.Fatal(err) + } + encDEK, err := gcm.Encrypt(testDEK) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = asn1.Marshal(HybridNISTWrappedKey{ + HybridCiphertext: ct, + EncryptedDEK: encDEK, + }) + } + b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") + }) +} + +// BenchmarkUnwrapDEK mirrors the actual KAS unwrap paths in +// service/internal/security/standard_crypto.go:Decrypt(): +// - RSA: pre-loaded AsymDecryption.Decrypt (key already parsed) +// - EC: ECPrivateKeyFromPem (cached) -> NewSaltedECDecryptor(TDFSalt) -> DecryptWithEphemeralKey +// - Hybrid: PrivateKeyFromPem -> UnwrapDEK (PEM parsed each time in current KAS code) +func BenchmarkUnwrapDEK(b *testing.B) { + salt := benchTDFSalt() + + // RSA-2048: KAS pre-loads the AsymDecryption at startup + rsaKP, err := NewRSAKeyPair(2048) + if err != nil { + b.Fatal(err) + } + rsaPubPEM, err := rsaKP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + rsaPrivPEM, err := rsaKP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + rsaEnc, err := NewAsymEncryption(rsaPubPEM) + if err != nil { + b.Fatal(err) + } + rsaWrapped, err := rsaEnc.Encrypt(testDEK) + if err != nil { + b.Fatal(err) + } + rsaDec, err := NewAsymDecryption(rsaPrivPEM) + if err != nil { + b.Fatal(err) + } + + // EC P-256: KAS caches the parsed private key, creates decryptor per request + ecKASKP, err := NewECKeyPair(ECCModeSecp256r1) + if err != nil { + b.Fatal(err) + } + ecKASPubPEM, err := ecKASKP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ecKASPrivPEM, err := ecKASKP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + // Wrap using the TDF path: ephemeral keygen + ECDH + HKDF + AES-GCM + ecEphKP, err := NewECKeyPair(ECCModeSecp256r1) + if err != nil { + b.Fatal(err) + } + ecEphPrivPEM, err := ecEphKP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ecEphPubPEM, err := ecEphKP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ecdhKey, err := ComputeECDHKey([]byte(ecEphPrivPEM), []byte(ecKASPubPEM)) + if err != nil { + b.Fatal(err) + } + ecSessionKey, err := CalculateHKDF(salt, ecdhKey) + if err != nil { + b.Fatal(err) + } + ecGCM, err := NewAESGcm(ecSessionKey) + if err != nil { + b.Fatal(err) + } + ecWrapped, err := ecGCM.Encrypt(testDEK) + if err != nil { + b.Fatal(err) + } + // KAS receives the ephemeral public key as DER (parsed from PEM in the manifest). + // DecryptWithEphemeralKey first tries x509.ParsePKIXPublicKey (DER), then compressed. + ecEphPubECDH, err := ECPubKeyFromPem([]byte(ecEphPubPEM)) + if err != nil { + b.Fatal(err) + } + ecEphDER, err := x509.MarshalPKIXPublicKey(ecEphPubECDH) + if err != nil { + b.Fatal(err) + } + // KAS parses private key once (cached in StandardECCrypto) + ecKASPrivKey, err := ECPrivateKeyFromPem([]byte(ecKASPrivPEM)) + if err != nil { + b.Fatal(err) + } + + // EC P-384: same flow as P-256, just on a different curve + ec384KASKP, err := NewECKeyPair(ECCModeSecp384r1) + if err != nil { + b.Fatal(err) + } + ec384KASPubPEM, err := ec384KASKP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ec384KASPrivPEM, err := ec384KASKP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ec384EphKP, err := NewECKeyPair(ECCModeSecp384r1) + if err != nil { + b.Fatal(err) + } + ec384EphPrivPEM, err := ec384EphKP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ec384EphPubPEM, err := ec384EphKP.PublicKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + ec384DhKey, err := ComputeECDHKey([]byte(ec384EphPrivPEM), []byte(ec384KASPubPEM)) + if err != nil { + b.Fatal(err) + } + ec384SessionKey, err := CalculateHKDF(salt, ec384DhKey) + if err != nil { + b.Fatal(err) + } + ec384GCM, err := NewAESGcm(ec384SessionKey) + if err != nil { + b.Fatal(err) + } + ec384Wrapped, err := ec384GCM.Encrypt(testDEK) + if err != nil { + b.Fatal(err) + } + ec384EphPubECDH, err := ECPubKeyFromPem([]byte(ec384EphPubPEM)) + if err != nil { + b.Fatal(err) + } + ec384EphDER, err := x509.MarshalPKIXPublicKey(ec384EphPubECDH) + if err != nil { + b.Fatal(err) + } + ec384KASPrivKey, err := ECPrivateKeyFromPem([]byte(ec384KASPrivPEM)) + if err != nil { + b.Fatal(err) + } + + // X-Wing: KAS parses PEM each call, then calls UnwrapDEK + xwingKP, err := NewXWingKeyPair() + if err != nil { + b.Fatal(err) + } + xwingPrivPEM, err := xwingKP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + xwingWrapped, err := XWingWrapDEK(xwingKP.publicKey, testDEK) + if err != nil { + b.Fatal(err) + } + + // P256+MLKEM768: KAS parses PEM each call, then calls UnwrapDEK + p256KP, err := NewP256MLKEM768KeyPair() + if err != nil { + b.Fatal(err) + } + p256PrivPEM, err := p256KP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + p256Wrapped, err := P256MLKEM768WrapDEK(p256KP.publicKey, testDEK) + if err != nil { + b.Fatal(err) + } + + // P384+MLKEM1024: KAS parses PEM each call, then calls UnwrapDEK + p384KP, err := NewP384MLKEM1024KeyPair() + if err != nil { + b.Fatal(err) + } + p384PrivPEM, err := p384KP.PrivateKeyInPemFormat() + if err != nil { + b.Fatal(err) + } + p384Wrapped, err := P384MLKEM1024WrapDEK(p384KP.publicKey, testDEK) + if err != nil { + b.Fatal(err) + } + + // RSA: KAS has pre-loaded AsymDecryption, just calls Decrypt + b.Run("RSA-2048", func(b *testing.B) { + for b.Loop() { + sinkBytes, errSink = rsaDec.Decrypt(rsaWrapped) + } + }) + + // EC: KAS creates NewSaltedECDecryptor(cachedSK, TDFSalt, nil) -> DecryptWithEphemeralKey + b.Run("EC-P256", func(b *testing.B) { + for b.Loop() { + dec, err := NewSaltedECDecryptor(ecKASPrivKey, salt, nil) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = dec.DecryptWithEphemeralKey(ecWrapped, ecEphDER) + } + }) + + b.Run("EC-P384", func(b *testing.B) { + for b.Loop() { + dec, err := NewSaltedECDecryptor(ec384KASPrivKey, salt, nil) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = dec.DecryptWithEphemeralKey(ec384Wrapped, ec384EphDER) + } + }) + + // X-Wing: KAS parses PEM then calls UnwrapDEK + b.Run("XWing", func(b *testing.B) { + for b.Loop() { + privKey, err := XWingPrivateKeyFromPem([]byte(xwingPrivPEM)) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = XWingUnwrapDEK(privKey, xwingWrapped) + } + }) + + // P256+MLKEM768: KAS parses PEM then calls UnwrapDEK + b.Run("P256_MLKEM768", func(b *testing.B) { + for b.Loop() { + privKey, err := P256MLKEM768PrivateKeyFromPem([]byte(p256PrivPEM)) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = P256MLKEM768UnwrapDEK(privKey, p256Wrapped) + } + }) + + // P384+MLKEM1024: KAS parses PEM then calls UnwrapDEK + b.Run("P384_MLKEM1024", func(b *testing.B) { + for b.Loop() { + privKey, err := P384MLKEM1024PrivateKeyFromPem([]byte(p384PrivPEM)) + if err != nil { + b.Fatal(err) + } + sinkBytes, errSink = P384MLKEM1024UnwrapDEK(privKey, p384Wrapped) + } + }) +} + +func BenchmarkHybridSubOps(b *testing.B) { + // Setup X-Wing + xwingKP, err := NewXWingKeyPair() + if err != nil { + b.Fatal(err) + } + xwingSS, xwingCt, err := XWingEncapsulate(xwingKP.publicKey) + if err != nil { + b.Fatal(err) + } + + // Setup P256+MLKEM768 + p256KP, err := NewP256MLKEM768KeyPair() + if err != nil { + b.Fatal(err) + } + p256SS, p256Ct, err := P256MLKEM768Encapsulate(p256KP.publicKey) + if err != nil { + b.Fatal(err) + } + + // Setup P384+MLKEM1024 + p384KP, err := NewP384MLKEM1024KeyPair() + if err != nil { + b.Fatal(err) + } + p384SS, p384Ct, err := P384MLKEM1024Encapsulate(p384KP.publicKey) + if err != nil { + b.Fatal(err) + } + + salt := defaultTDFSalt() + + // Pre-derive a wrap key for AES-GCM benchmarks + wrapKey, err := deriveXWingWrapKey(xwingSS, salt, nil) + if err != nil { + b.Fatal(err) + } + + b.Run("XWing/Encapsulate", func(b *testing.B) { + for b.Loop() { + sinkBytes, sinkBytes, errSink = XWingEncapsulate(xwingKP.publicKey) + } + }) + b.Run("XWing/HKDF", func(b *testing.B) { + for b.Loop() { + sinkBytes, errSink = deriveXWingWrapKey(xwingSS, salt, nil) + } + }) + b.Run("XWing/AES-GCM-Encrypt", func(b *testing.B) { + gcm, err := NewAESGcm(wrapKey) + if err != nil { + b.Fatal(err) + } + for b.Loop() { + sinkBytes, errSink = gcm.Encrypt(testDEK) + } + }) + b.Run("XWing/ASN1-Marshal", func(b *testing.B) { + wrapped := XWingWrappedKey{XWingCiphertext: xwingCt, EncryptedDEK: testDEK} + for b.Loop() { + sinkBytes, errSink = asn1.Marshal(wrapped) + } + }) + + // P256+MLKEM768 sub-ops + p256WrapKey, err := deriveHybridNISTWrapKey(p256SS, salt, nil) + if err != nil { + b.Fatal(err) + } + b.Run("P256_MLKEM768/Encapsulate", func(b *testing.B) { + for b.Loop() { + sinkBytes, sinkBytes, errSink = P256MLKEM768Encapsulate(p256KP.publicKey) + } + }) + b.Run("P256_MLKEM768/HKDF", func(b *testing.B) { + for b.Loop() { + sinkBytes, errSink = deriveHybridNISTWrapKey(p256SS, salt, nil) + } + }) + b.Run("P256_MLKEM768/AES-GCM-Encrypt", func(b *testing.B) { + gcm, err := NewAESGcm(p256WrapKey) + if err != nil { + b.Fatal(err) + } + for b.Loop() { + sinkBytes, errSink = gcm.Encrypt(testDEK) + } + }) + b.Run("P256_MLKEM768/ASN1-Marshal", func(b *testing.B) { + wrapped := HybridNISTWrappedKey{HybridCiphertext: p256Ct, EncryptedDEK: testDEK} + for b.Loop() { + sinkBytes, errSink = asn1.Marshal(wrapped) + } + }) + + // P384+MLKEM1024 sub-ops + p384WrapKey, err := deriveHybridNISTWrapKey(p384SS, salt, nil) + if err != nil { + b.Fatal(err) + } + b.Run("P384_MLKEM1024/Encapsulate", func(b *testing.B) { + for b.Loop() { + sinkBytes, sinkBytes, errSink = P384MLKEM1024Encapsulate(p384KP.publicKey) + } + }) + b.Run("P384_MLKEM1024/HKDF", func(b *testing.B) { + for b.Loop() { + sinkBytes, errSink = deriveHybridNISTWrapKey(p384SS, salt, nil) + } + }) + b.Run("P384_MLKEM1024/AES-GCM-Encrypt", func(b *testing.B) { + gcm, err := NewAESGcm(p384WrapKey) + if err != nil { + b.Fatal(err) + } + for b.Loop() { + sinkBytes, errSink = gcm.Encrypt(testDEK) + } + }) + b.Run("P384_MLKEM1024/ASN1-Marshal", func(b *testing.B) { + wrapped := HybridNISTWrappedKey{HybridCiphertext: p384Ct, EncryptedDEK: testDEK} + for b.Loop() { + sinkBytes, errSink = asn1.Marshal(wrapped) + } + }) +} + +func TestWrappedKeySizeComparison(t *testing.T) { + type sizeResult struct { + scheme string + wrappedLen int + pubKeyLen int + notes string + } + + var results []sizeResult + + // RSA-2048 + rsaKP, err := NewRSAKeyPair(2048) + if err != nil { + t.Fatal(err) + } + rsaPubPEM, err := rsaKP.PublicKeyInPemFormat() + if err != nil { + t.Fatal(err) + } + rsaEnc, err := NewAsymEncryption(rsaPubPEM) + if err != nil { + t.Fatal(err) + } + rsaWrapped, err := rsaEnc.Encrypt(testDEK) + if err != nil { + t.Fatal(err) + } + results = append(results, sizeResult{ + scheme: "RSA-2048", + wrappedLen: len(rsaWrapped), + pubKeyLen: len(rsaPubPEM), + notes: "No ephemeral key", + }) + + // EC P-256 + ecKP, err := NewECKeyPair(ECCModeSecp256r1) + if err != nil { + t.Fatal(err) + } + ecPubPEM, err := ecKP.PublicKeyInPemFormat() + if err != nil { + t.Fatal(err) + } + ecEnc, err := FromPublicPEM(ecPubPEM) + if err != nil { + t.Fatal(err) + } + ecWrapped, err := ecEnc.Encrypt(testDEK) + if err != nil { + t.Fatal(err) + } + ecEphemeral := ecEnc.EphemeralKey() + results = append(results, sizeResult{ + scheme: "EC P-256", + wrappedLen: len(ecWrapped), + pubKeyLen: len(ecPubPEM), + notes: fmt.Sprintf("+ ephemeral key (%d bytes)", len(ecEphemeral)), + }) + + // EC P-384 + ec384KP, err := NewECKeyPair(ECCModeSecp384r1) + if err != nil { + t.Fatal(err) + } + ec384PubPEM, err := ec384KP.PublicKeyInPemFormat() + if err != nil { + t.Fatal(err) + } + ec384Enc, err := FromPublicPEM(ec384PubPEM) + if err != nil { + t.Fatal(err) + } + ec384Wrapped, err := ec384Enc.Encrypt(testDEK) + if err != nil { + t.Fatal(err) + } + ec384Ephemeral := ec384Enc.EphemeralKey() + results = append(results, sizeResult{ + scheme: "EC P-384", + wrappedLen: len(ec384Wrapped), + pubKeyLen: len(ec384PubPEM), + notes: fmt.Sprintf("+ ephemeral key (%d bytes)", len(ec384Ephemeral)), + }) + + // X-Wing + xwingKP, err := NewXWingKeyPair() + if err != nil { + t.Fatal(err) + } + xwingPubPEM, err := xwingKP.PublicKeyInPemFormat() + if err != nil { + t.Fatal(err) + } + xwingWrapped, err := XWingWrapDEK(xwingKP.publicKey, testDEK) + if err != nil { + t.Fatal(err) + } + results = append(results, sizeResult{ + scheme: "X-Wing", + wrappedLen: len(xwingWrapped), + pubKeyLen: len(xwingPubPEM), + notes: "All in ASN.1 blob", + }) + + // P256+MLKEM768 + p256KP, err := NewP256MLKEM768KeyPair() + if err != nil { + t.Fatal(err) + } + p256PubPEM, err := p256KP.PublicKeyInPemFormat() + if err != nil { + t.Fatal(err) + } + p256Wrapped, err := P256MLKEM768WrapDEK(p256KP.publicKey, testDEK) + if err != nil { + t.Fatal(err) + } + results = append(results, sizeResult{ + scheme: "P256+MLKEM768", + wrappedLen: len(p256Wrapped), + pubKeyLen: len(p256PubPEM), + notes: "All in ASN.1 blob", + }) + + // P384+MLKEM1024 + p384KP, err := NewP384MLKEM1024KeyPair() + if err != nil { + t.Fatal(err) + } + p384PubPEM, err := p384KP.PublicKeyInPemFormat() + if err != nil { + t.Fatal(err) + } + p384Wrapped, err := P384MLKEM1024WrapDEK(p384KP.publicKey, testDEK) + if err != nil { + t.Fatal(err) + } + results = append(results, sizeResult{ + scheme: "P384+MLKEM1024", + wrappedLen: len(p384Wrapped), + pubKeyLen: len(p384PubPEM), + notes: "All in ASN.1 blob", + }) + + // Print table + t.Logf("\n%-20s %20s %20s %s", "Scheme", "Wrapped Key (bytes)", "Public Key (bytes)", "Notes") + t.Logf("%-20s %20s %20s %s", "------", "-------------------", "------------------", "-----") + for _, r := range results { + t.Logf("%-20s %20d %20d %s", r.scheme, r.wrappedLen, r.pubKeyLen, r.notes) + } +} diff --git a/lib/ocrypto/ec_decrypt_compressed_test.go b/lib/ocrypto/ec_decrypt_compressed_test.go new file mode 100644 index 0000000000..45a94e795f --- /dev/null +++ b/lib/ocrypto/ec_decrypt_compressed_test.go @@ -0,0 +1,133 @@ +package ocrypto + +import ( + "crypto/ecdh" + "crypto/ecdsa" + "crypto/x509" + "errors" + "testing" +) + +func TestECDecryptWithCompressedEphemeralKey(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + mode ECCMode + } + + tests := []testCase{ + {name: "P256", mode: ECCModeSecp256r1}, + {name: "P384", mode: ECCModeSecp384r1}, + {name: "P521", mode: ECCModeSecp521r1}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + receiverKeys, err := NewECKeyPair(test.mode) + if err != nil { + t.Fatalf("NewECKeyPair failed: %v", err) + } + + pubPEM, err := receiverKeys.PublicKeyInPemFormat() + if err != nil { + t.Fatalf("PublicKeyInPemFormat failed: %v", err) + } + + privPEM, err := receiverKeys.PrivateKeyInPemFormat() + if err != nil { + t.Fatalf("PrivateKeyInPemFormat failed: %v", err) + } + + salt := []byte("test-salt") + encryptor, err := FromPublicPEMWithSalt(pubPEM, salt, nil) + if err != nil { + t.Fatalf("FromPublicPEMWithSalt failed: %v", err) + } + + plaintext := []byte("test payload for ec decrypt") + ciphertext, err := encryptor.Encrypt(plaintext) + if err != nil { + t.Fatalf("Encrypt failed: %v", err) + } + + ephemeralDER := encryptor.EphemeralKey() + if len(ephemeralDER) == 0 { + t.Fatal("EphemeralKey returned empty data") + } + + compressed, err := compressEphemeralKeyFromDER(ephemeralDER) + if err != nil { + t.Fatalf("compressEphemeralKeyFromDER failed: %v", err) + } + + decryptor, err := FromPrivatePEMWithSalt(privPEM, salt, nil) + if err != nil { + t.Fatalf("FromPrivatePEMWithSalt failed: %v", err) + } + + ecDecryptor, ok := decryptor.(ECDecryptor) + if !ok { + t.Fatalf("unexpected decryptor type: %T", decryptor) + } + + decrypted, err := ecDecryptor.DecryptWithEphemeralKey(ciphertext, compressed) + if err != nil { + t.Fatalf("DecryptWithEphemeralKey failed: %v", err) + } + + if string(decrypted) != string(plaintext) { + t.Fatalf("unexpected plaintext: got %q want %q", decrypted, plaintext) + } + }) + } +} + +func compressEphemeralKeyFromDER(der []byte) ([]byte, error) { + pub, err := x509.ParsePKIXPublicKey(der) + if err != nil { + return nil, err + } + + switch pub := pub.(type) { + case *ecdsa.PublicKey: + ecdhPub, err := pub.ECDH() + if err != nil { + return nil, err + } + return compressUncompressedPoint(ecdhPub.Bytes()) + case *ecdh.PublicKey: + return compressUncompressedPoint(pub.Bytes()) + default: + return nil, errors.New("unsupported public key type") + } +} + +func compressUncompressedPoint(uncompressed []byte) ([]byte, error) { + if len(uncompressed) == 0 || uncompressed[0] != 4 { + return nil, errors.New("unexpected uncompressed key format") + } + + if (len(uncompressed)-1)%2 != 0 { + return nil, errors.New("invalid uncompressed key length") + } + + coordSize := (len(uncompressed) - 1) / 2 + x := uncompressed[1 : 1+coordSize] + y := uncompressed[1+coordSize:] + if len(y) != coordSize { + return nil, errors.New("invalid coordinate sizes") + } + + prefix := byte(2) + if y[coordSize-1]&1 == 1 { + prefix = 3 + } + + compressed := make([]byte, 1+coordSize) + compressed[0] = prefix + copy(compressed[1:], x) + return compressed, nil +} diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index 040b367f34..70e30cc8df 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -29,6 +29,19 @@ const ( EC521Key KeyType = "ec:secp521r1" ) +// ParseKeyType validates a string as a known KeyType, returning an error for +// unrecognized values. +func ParseKeyType(alg string) (KeyType, error) { + switch KeyType(alg) { + case RSA2048Key, RSA4096Key, + EC256Key, EC384Key, EC521Key, + HybridXWingKey, HybridSecp256r1MLKEM768Key, HybridSecp384r1MLKEM1024Key: + return KeyType(alg), nil + default: + return "", fmt.Errorf("unrecognized key type: %s", alg) + } +} + const ( ECCModeSecp256r1 ECCMode = 0 ECCModeSecp384r1 ECCMode = 1 @@ -64,6 +77,8 @@ func NewKeyPair(kt KeyType) (KeyPair, error) { return nil, err } return NewECKeyPair(mode) + case HybridSecp256r1MLKEM768Key, HybridSecp384r1MLKEM1024Key, HybridXWingKey: + return NewHybridKeyPair(kt) default: return nil, fmt.Errorf("unsupported key type: %v", kt) } @@ -104,9 +119,9 @@ func GetECCurveFromECCMode(mode ECCMode) (elliptic.Curve, error) { c = elliptic.P521() case ECCModeSecp256k1: // TODO FIXME - unsupported? - return nil, errors.New("unsupported nanoTDF ecc mode") + return nil, errors.New("unsupported ECC mode") default: - return nil, fmt.Errorf("unsupported nanoTDF ecc mode %d", mode) + return nil, fmt.Errorf("unsupported ECC mode %d", mode) } return c, nil @@ -421,7 +436,7 @@ func UncompressECPubKey(curve elliptic.Curve, compressedPubKey []byte) (*ecdsa.P } // Creating ecdsa.PublicKey from *big.Int ephemeralECDSAPublicKey := &ecdsa.PublicKey{ - Curve: elliptic.P256(), + Curve: curve, X: x, Y: y, } diff --git a/lib/ocrypto/ec_key_pair_test.go b/lib/ocrypto/ec_key_pair_test.go index 5c4cd630af..18e875cb52 100644 --- a/lib/ocrypto/ec_key_pair_test.go +++ b/lib/ocrypto/ec_key_pair_test.go @@ -68,7 +68,7 @@ func TestECKeyPair(t *testing.T) { } } -func TestNanoTDFRewrapKeyGenerate(t *testing.T) { +func TestECRewrapKeyGenerate(t *testing.T) { kasECKeyPair, err := NewECKeyPair(ECCModeSecp256r1) require.NoError(t, err, "fail on NewECKeyPair") @@ -92,7 +92,7 @@ func TestNanoTDFRewrapKeyGenerate(t *testing.T) { // slat digest := sha256.New() - digest.Write([]byte("L1L")) + digest.Write([]byte("TDF")) kasSymmetricKey, err := CalculateHKDF(digest.Sum(nil), kasECDHKey) require.NoError(t, err, "fail to calculate HKDF key") diff --git a/lib/ocrypto/errors.go b/lib/ocrypto/errors.go new file mode 100644 index 0000000000..ddf3e90c55 --- /dev/null +++ b/lib/ocrypto/errors.go @@ -0,0 +1,9 @@ +package ocrypto + +import "errors" + +// ErrInvalidKeyData is returned when key data is invalid (empty, nil, or wrong size) +var ErrInvalidKeyData = errors.New("invalid key data") + +// ErrInvalidCiphertext is returned when ciphertext or input data is invalid (empty, wrong size, etc.) +var ErrInvalidCiphertext = errors.New("invalid ciphertext") diff --git a/lib/ocrypto/go.mod b/lib/ocrypto/go.mod index 1e8219a891..98d5837da9 100644 --- a/lib/ocrypto/go.mod +++ b/lib/ocrypto/go.mod @@ -1,12 +1,11 @@ module github.com/opentdf/platform/lib/ocrypto -go 1.24.0 - -toolchain go1.24.11 +go 1.25.0 require ( - github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.45.0 + github.com/cloudflare/circl v1.6.3 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.52.0 ) require ( @@ -14,6 +13,7 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect + golang.org/x/sys v0.45.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/lib/ocrypto/go.sum b/lib/ocrypto/go.sum index 63574bef4b..f0476a5fee 100644 --- a/lib/ocrypto/go.sum +++ b/lib/ocrypto/go.sum @@ -1,3 +1,5 @@ +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -14,10 +16,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/lib/ocrypto/hybrid_common.go b/lib/ocrypto/hybrid_common.go new file mode 100644 index 0000000000..4f4ee05417 --- /dev/null +++ b/lib/ocrypto/hybrid_common.go @@ -0,0 +1,75 @@ +package ocrypto + +import ( + "crypto/sha256" + "encoding/pem" + "fmt" +) + +// HybridWrapDEK parses the recipient's hybrid public key PEM, encapsulates +// against it using the scheme implied by ktype, and returns the ASN.1-encoded +// wrapped DEK envelope used in `hybrid-wrapped` manifests. It dispatches across +// both the X-Wing and NIST EC + ML-KEM families so SDK call sites do not need +// to repeat the algorithm switch. +// +// The HKDF salt is the default TDF salt; callers that need a non-default salt +// should call the per-scheme `*WrapDEK` helpers directly. +func HybridWrapDEK(ktype KeyType, kasPublicKeyPEM string, dek []byte) ([]byte, error) { + switch ktype { //nolint:exhaustive // only handle hybrid types + case HybridXWingKey: + pubKey, err := XWingPubKeyFromPem([]byte(kasPublicKeyPEM)) + if err != nil { + return nil, fmt.Errorf("X-Wing public key: %w", err) + } + return XWingWrapDEK(pubKey, dek) + case HybridSecp256r1MLKEM768Key: + pubKey, err := P256MLKEM768PubKeyFromPem([]byte(kasPublicKeyPEM)) + if err != nil { + return nil, fmt.Errorf("P-256+ML-KEM-768 public key: %w", err) + } + return P256MLKEM768WrapDEK(pubKey, dek) + case HybridSecp384r1MLKEM1024Key: + pubKey, err := P384MLKEM1024PubKeyFromPem([]byte(kasPublicKeyPEM)) + if err != nil { + return nil, fmt.Errorf("P-384+ML-KEM-1024 public key: %w", err) + } + return P384MLKEM1024WrapDEK(pubKey, dek) + default: + return nil, fmt.Errorf("unsupported hybrid key type: %s", ktype) + } +} + +// defaultTDFSalt returns the salt used for HKDF derivation in all TDF hybrid +// key wrapping schemes (X-Wing and NIST EC + ML-KEM). Defined here rather than +// in a per-scheme file so that any change applies uniformly across schemes. +func defaultTDFSalt() []byte { + digest := sha256.New() + digest.Write([]byte("TDF")) + return digest.Sum(nil) +} + +// rawToPEM wraps a fixed-size byte slice in a PEM block of the given type. Used +// by both X-Wing and NIST hybrid key serialization. +func rawToPEM(blockType string, raw []byte, expectedSize int) (string, error) { + if len(raw) != expectedSize { + return "", fmt.Errorf("invalid %s size: got %d want %d", blockType, len(raw), expectedSize) + } + + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: blockType, + Bytes: raw, + }) + if pemBytes == nil { + return "", fmt.Errorf("failed to encode %s to PEM", blockType) + } + + return string(pemBytes), nil +} + +// cloneOrNil returns a copy of data, or nil if data is empty. +func cloneOrNil(data []byte) []byte { + if len(data) == 0 { + return nil + } + return append([]byte(nil), data...) +} diff --git a/lib/ocrypto/hybrid_nist.go b/lib/ocrypto/hybrid_nist.go new file mode 100644 index 0000000000..ee0a575ac5 --- /dev/null +++ b/lib/ocrypto/hybrid_nist.go @@ -0,0 +1,519 @@ +package ocrypto + +import ( + "crypto/ecdh" + "crypto/mlkem" + "crypto/rand" + "crypto/sha256" + "encoding/asn1" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +const ( + HybridSecp256r1MLKEM768Key KeyType = "hpqt:secp256r1-mlkem768" + HybridSecp384r1MLKEM1024Key KeyType = "hpqt:secp384r1-mlkem1024" +) + +// ML-KEM seed size (d || z) used by crypto/mlkem for private key serialization. +const mlkemSeedSize = 64 + +// Sizes for P-256 + ML-KEM-768 hybrid. +const ( + P256MLKEM768ECPublicKeySize = 65 // uncompressed P-256 point + P256MLKEM768ECPrivateKeySize = 32 // P-256 scalar + P256MLKEM768MLKEMPubKeySize = 1184 // mlkem768 encapsulation key + P256MLKEM768MLKEMPrivKeySize = mlkemSeedSize + P256MLKEM768MLKEMCtSize = 1088 // mlkem768 ciphertext + + P256MLKEM768PublicKeySize = P256MLKEM768ECPublicKeySize + P256MLKEM768MLKEMPubKeySize // 1249 + P256MLKEM768PrivateKeySize = P256MLKEM768ECPrivateKeySize + P256MLKEM768MLKEMPrivKeySize // 96 + P256MLKEM768CiphertextSize = P256MLKEM768ECPublicKeySize + P256MLKEM768MLKEMCtSize // 1153 + + PEMBlockP256MLKEM768PublicKey = "SECP256R1 MLKEM768 PUBLIC KEY" + PEMBlockP256MLKEM768PrivateKey = "SECP256R1 MLKEM768 PRIVATE KEY" +) + +// Sizes for P-384 + ML-KEM-1024 hybrid. +const ( + P384MLKEM1024ECPublicKeySize = 97 // uncompressed P-384 point + P384MLKEM1024ECPrivateKeySize = 48 // P-384 scalar + P384MLKEM1024MLKEMPubKeySize = 1568 // mlkem1024 encapsulation key + P384MLKEM1024MLKEMPrivKeySize = mlkemSeedSize + P384MLKEM1024MLKEMCtSize = 1568 // mlkem1024 ciphertext + + P384MLKEM1024PublicKeySize = P384MLKEM1024ECPublicKeySize + P384MLKEM1024MLKEMPubKeySize // 1665 + P384MLKEM1024PrivateKeySize = P384MLKEM1024ECPrivateKeySize + P384MLKEM1024MLKEMPrivKeySize // 112 + P384MLKEM1024CiphertextSize = P384MLKEM1024ECPublicKeySize + P384MLKEM1024MLKEMCtSize // 1665 + + PEMBlockP384MLKEM1024PublicKey = "SECP384R1 MLKEM1024 PUBLIC KEY" + PEMBlockP384MLKEM1024PrivateKey = "SECP384R1 MLKEM1024 PRIVATE KEY" +) + +// AES-256 key size used for wrap key derivation. +const hybridNISTWrapKeySize = 32 + +// HybridNISTWrappedKey is the ASN.1 envelope stored in wrapped_key. +type HybridNISTWrappedKey struct { + HybridCiphertext []byte `asn1:"tag:0"` + EncryptedDEK []byte `asn1:"tag:1"` +} + +// hybridNISTParams captures the curve-specific parameters for a NIST hybrid scheme. +type hybridNISTParams struct { + curve ecdh.Curve + ecPubSize int + ecPrivSize int + mlkemPubSize int + mlkemPrivSize int + mlkemCtSize int + pubPEMBlock string + privPEMBlock string + keyType KeyType +} + +var p256mlkem768Params = hybridNISTParams{ + curve: ecdh.P256(), + ecPubSize: P256MLKEM768ECPublicKeySize, + ecPrivSize: P256MLKEM768ECPrivateKeySize, + mlkemPubSize: P256MLKEM768MLKEMPubKeySize, + mlkemPrivSize: P256MLKEM768MLKEMPrivKeySize, + mlkemCtSize: P256MLKEM768MLKEMCtSize, + pubPEMBlock: PEMBlockP256MLKEM768PublicKey, + privPEMBlock: PEMBlockP256MLKEM768PrivateKey, + keyType: HybridSecp256r1MLKEM768Key, +} + +var p384mlkem1024Params = hybridNISTParams{ + curve: ecdh.P384(), + ecPubSize: P384MLKEM1024ECPublicKeySize, + ecPrivSize: P384MLKEM1024ECPrivateKeySize, + mlkemPubSize: P384MLKEM1024MLKEMPubKeySize, + mlkemPrivSize: P384MLKEM1024MLKEMPrivKeySize, + mlkemCtSize: P384MLKEM1024MLKEMCtSize, + pubPEMBlock: PEMBlockP384MLKEM1024PublicKey, + privPEMBlock: PEMBlockP384MLKEM1024PrivateKey, + keyType: HybridSecp384r1MLKEM1024Key, +} + +// HybridNISTKeyPair holds a hybrid EC + ML-KEM keypair as raw bytes. +type HybridNISTKeyPair struct { + publicKey []byte + privateKey []byte + params *hybridNISTParams +} + +// HybridNISTEncryptor implements PublicKeyEncryptor for NIST hybrid schemes. +type HybridNISTEncryptor struct { + publicKey []byte + salt []byte + info []byte + params *hybridNISTParams +} + +// HybridNISTDecryptor implements PrivateKeyDecryptor for NIST hybrid schemes. +type HybridNISTDecryptor struct { + privateKey []byte + salt []byte + info []byte + params *hybridNISTParams +} + +// IsHybridKeyType returns true if the key type is a hybrid post-quantum type. +func IsHybridKeyType(kt KeyType) bool { + switch kt { //nolint:exhaustive // only handle hybrid types + case HybridXWingKey, HybridSecp256r1MLKEM768Key, HybridSecp384r1MLKEM1024Key: + return true + default: + return false + } +} + +// NewHybridKeyPair creates a key pair for the given hybrid key type. +func NewHybridKeyPair(kt KeyType) (KeyPair, error) { + switch kt { //nolint:exhaustive // only handle hybrid types + case HybridXWingKey: + return NewXWingKeyPair() + case HybridSecp256r1MLKEM768Key: + return NewP256MLKEM768KeyPair() + case HybridSecp384r1MLKEM1024Key: + return NewP384MLKEM1024KeyPair() + default: + return nil, fmt.Errorf("unsupported hybrid key type: %v", kt) + } +} + +func NewP256MLKEM768KeyPair() (HybridNISTKeyPair, error) { + return newHybridNISTKeyPair(&p256mlkem768Params, func() ([]byte, []byte, error) { + dk, err := mlkem.GenerateKey768() + if err != nil { + return nil, nil, err + } + return dk.EncapsulationKey().Bytes(), dk.Bytes(), nil + }) +} + +func NewP384MLKEM1024KeyPair() (HybridNISTKeyPair, error) { + return newHybridNISTKeyPair(&p384mlkem1024Params, func() ([]byte, []byte, error) { + dk, err := mlkem.GenerateKey1024() + if err != nil { + return nil, nil, err + } + return dk.EncapsulationKey().Bytes(), dk.Bytes(), nil + }) +} + +func newHybridNISTKeyPair(p *hybridNISTParams, genMLKEM func() (pub, priv []byte, err error)) (HybridNISTKeyPair, error) { + ecPriv, err := p.curve.GenerateKey(rand.Reader) + if err != nil { + return HybridNISTKeyPair{}, fmt.Errorf("ECDH key generation failed: %w", err) + } + ecPub := ecPriv.PublicKey().Bytes() // uncompressed point + ecPrivBytes := ecPriv.Bytes() // raw scalar + + mlkemPub, mlkemPriv, err := genMLKEM() + if err != nil { + return HybridNISTKeyPair{}, fmt.Errorf("ML-KEM key generation failed: %w", err) + } + + pubKey := make([]byte, 0, p.ecPubSize+p.mlkemPubSize) + pubKey = append(pubKey, ecPub...) + pubKey = append(pubKey, mlkemPub...) + + privKey := make([]byte, 0, p.ecPrivSize+p.mlkemPrivSize) + privKey = append(privKey, ecPrivBytes...) + privKey = append(privKey, mlkemPriv...) + + return HybridNISTKeyPair{ + publicKey: pubKey, + privateKey: privKey, + params: p, + }, nil +} + +func (k HybridNISTKeyPair) PublicKeyInPemFormat() (string, error) { + return rawToPEM(k.params.pubPEMBlock, k.publicKey, k.params.ecPubSize+k.params.mlkemPubSize) +} + +func (k HybridNISTKeyPair) PrivateKeyInPemFormat() (string, error) { + return rawToPEM(k.params.privPEMBlock, k.privateKey, k.params.ecPrivSize+k.params.mlkemPrivSize) +} + +func (k HybridNISTKeyPair) GetKeyType() KeyType { + return k.params.keyType +} + +func P256MLKEM768PubKeyFromPem(data []byte) ([]byte, error) { + return decodeSizedPEMBlock(data, PEMBlockP256MLKEM768PublicKey, P256MLKEM768PublicKeySize) +} + +func P256MLKEM768PrivateKeyFromPem(data []byte) ([]byte, error) { + return decodeSizedPEMBlock(data, PEMBlockP256MLKEM768PrivateKey, P256MLKEM768PrivateKeySize) +} + +func P384MLKEM1024PubKeyFromPem(data []byte) ([]byte, error) { + return decodeSizedPEMBlock(data, PEMBlockP384MLKEM1024PublicKey, P384MLKEM1024PublicKeySize) +} + +func P384MLKEM1024PrivateKeyFromPem(data []byte) ([]byte, error) { + return decodeSizedPEMBlock(data, PEMBlockP384MLKEM1024PrivateKey, P384MLKEM1024PrivateKeySize) +} + +func NewP256MLKEM768Encryptor(publicKey, salt, info []byte) (*HybridNISTEncryptor, error) { + return newHybridNISTEncryptor(&p256mlkem768Params, publicKey, salt, info) +} + +func NewP384MLKEM1024Encryptor(publicKey, salt, info []byte) (*HybridNISTEncryptor, error) { + return newHybridNISTEncryptor(&p384mlkem1024Params, publicKey, salt, info) +} + +func newHybridNISTEncryptor(p *hybridNISTParams, publicKey, salt, info []byte) (*HybridNISTEncryptor, error) { + expectedSize := p.ecPubSize + p.mlkemPubSize + if len(publicKey) != expectedSize { + return nil, fmt.Errorf("invalid %s public key size: got %d want %d", p.keyType, len(publicKey), expectedSize) + } + return &HybridNISTEncryptor{ + publicKey: append([]byte(nil), publicKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + params: p, + }, nil +} + +func (e *HybridNISTEncryptor) Encrypt(data []byte) ([]byte, error) { + return hybridNISTWrapDEK(e.params, e.publicKey, data, e.salt, e.info) +} + +func (e *HybridNISTEncryptor) PublicKeyInPemFormat() (string, error) { + return rawToPEM(e.params.pubPEMBlock, e.publicKey, e.params.ecPubSize+e.params.mlkemPubSize) +} + +func (e *HybridNISTEncryptor) Type() SchemeType { return Hybrid } +func (e *HybridNISTEncryptor) KeyType() KeyType { return e.params.keyType } +func (e *HybridNISTEncryptor) EphemeralKey() []byte { return nil } + +func (e *HybridNISTEncryptor) Metadata() (map[string]string, error) { + return make(map[string]string), nil +} + +func NewP256MLKEM768Decryptor(privateKey []byte) (*HybridNISTDecryptor, error) { + return NewSaltedP256MLKEM768Decryptor(privateKey, defaultTDFSalt(), nil) +} + +func NewSaltedP256MLKEM768Decryptor(privateKey, salt, info []byte) (*HybridNISTDecryptor, error) { + return newHybridNISTDecryptor(&p256mlkem768Params, privateKey, salt, info) +} + +func NewP384MLKEM1024Decryptor(privateKey []byte) (*HybridNISTDecryptor, error) { + return NewSaltedP384MLKEM1024Decryptor(privateKey, defaultTDFSalt(), nil) +} + +func NewSaltedP384MLKEM1024Decryptor(privateKey, salt, info []byte) (*HybridNISTDecryptor, error) { + return newHybridNISTDecryptor(&p384mlkem1024Params, privateKey, salt, info) +} + +func newHybridNISTDecryptor(p *hybridNISTParams, privateKey, salt, info []byte) (*HybridNISTDecryptor, error) { + expectedSize := p.ecPrivSize + p.mlkemPrivSize + if len(privateKey) != expectedSize { + return nil, fmt.Errorf("invalid %s private key size: got %d want %d", p.keyType, len(privateKey), expectedSize) + } + return &HybridNISTDecryptor{ + privateKey: append([]byte(nil), privateKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + params: p, + }, nil +} + +func (d *HybridNISTDecryptor) Decrypt(data []byte) ([]byte, error) { + return hybridNISTUnwrapDEK(d.params, d.privateKey, data, d.salt, d.info) +} + +func P256MLKEM768WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { + return hybridNISTWrapDEK(&p256mlkem768Params, publicKeyRaw, dek, defaultTDFSalt(), nil) +} + +func P256MLKEM768UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { + return hybridNISTUnwrapDEK(&p256mlkem768Params, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) +} + +func P384MLKEM1024WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { + return hybridNISTWrapDEK(&p384mlkem1024Params, publicKeyRaw, dek, defaultTDFSalt(), nil) +} + +func P384MLKEM1024UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { + return hybridNISTUnwrapDEK(&p384mlkem1024Params, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) +} + +// hybridNISTEncapsulate performs hybrid encapsulation: +// 1. Generates an ephemeral EC key and computes ECDH shared secret +// 2. Encapsulates ML-KEM to produce a post-quantum shared secret +// 3. Combines both secrets (ECDH || ML-KEM) +// 4. Builds hybrid ciphertext (ephemeral EC point || ML-KEM ciphertext) +// +// Returns (combinedSecret, hybridCiphertext) without applying KDF or encryption. +func hybridNISTEncapsulate(p *hybridNISTParams, publicKeyRaw []byte) ([]byte, []byte, error) { + expectedPubSize := p.ecPubSize + p.mlkemPubSize + if len(publicKeyRaw) != expectedPubSize { + return nil, nil, fmt.Errorf("invalid %s public key size: got %d want %d", p.keyType, len(publicKeyRaw), expectedPubSize) + } + + ecPubBytes := publicKeyRaw[:p.ecPubSize] + mlkemPubBytes := publicKeyRaw[p.ecPubSize:] + + // ECDH: generate ephemeral key, compute shared secret + ecPub, err := p.curve.NewPublicKey(ecPubBytes) + if err != nil { + return nil, nil, fmt.Errorf("invalid EC public key: %w", err) + } + ephemeral, err := p.curve.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("ECDH ephemeral key generation failed: %w", err) + } + ecdhSecret, err := ephemeral.ECDH(ecPub) + if err != nil { + return nil, nil, fmt.Errorf("ECDH failed: %w", err) + } + ephemeralPub := ephemeral.PublicKey().Bytes() + + // ML-KEM: encapsulate + var mlkemSecret, mlkemCt []byte + switch p.keyType { //nolint:exhaustive // only NIST hybrid types + case HybridSecp256r1MLKEM768Key: + ek, ekErr := mlkem.NewEncapsulationKey768(mlkemPubBytes) + if ekErr != nil { + return nil, nil, fmt.Errorf("mlkem768 encapsulation key: %w", ekErr) + } + mlkemSecret, mlkemCt = ek.Encapsulate() + case HybridSecp384r1MLKEM1024Key: + ek, ekErr := mlkem.NewEncapsulationKey1024(mlkemPubBytes) + if ekErr != nil { + return nil, nil, fmt.Errorf("mlkem1024 encapsulation key: %w", ekErr) + } + mlkemSecret, mlkemCt = ek.Encapsulate() + default: + return nil, nil, fmt.Errorf("unsupported ML-KEM key type: %s", p.keyType) + } + + // Combine secrets: ECDH || ML-KEM + combinedSecret := make([]byte, 0, len(ecdhSecret)+len(mlkemSecret)) + combinedSecret = append(combinedSecret, ecdhSecret...) + combinedSecret = append(combinedSecret, mlkemSecret...) + + // Build hybrid ciphertext: ephemeral EC point || ML-KEM ciphertext + hybridCt := make([]byte, 0, len(ephemeralPub)+len(mlkemCt)) + hybridCt = append(hybridCt, ephemeralPub...) + hybridCt = append(hybridCt, mlkemCt...) + + return combinedSecret, hybridCt, nil +} + +// P256MLKEM768Encapsulate performs P-256 ECDH + ML-KEM-768 hybrid encapsulation. +func P256MLKEM768Encapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { + return hybridNISTEncapsulate(&p256mlkem768Params, publicKeyRaw) +} + +// P384MLKEM1024Encapsulate performs P-384 ECDH + ML-KEM-1024 hybrid encapsulation. +func P384MLKEM1024Encapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { + return hybridNISTEncapsulate(&p384mlkem1024Params, publicKeyRaw) +} + +func hybridNISTWrapDEK(p *hybridNISTParams, publicKeyRaw, dek, salt, info []byte) ([]byte, error) { + combinedSecret, hybridCt, err := hybridNISTEncapsulate(p, publicKeyRaw) + if err != nil { + return nil, err + } + + // Derive AES-256 wrap key via HKDF + wrapKey, err := deriveHybridNISTWrapKey(combinedSecret, salt, info) + if err != nil { + return nil, err + } + + // AES-GCM encrypt DEK + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + encryptedDEK, err := gcm.Encrypt(dek) + if err != nil { + return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) + } + + wrappedDER, err := asn1.Marshal(HybridNISTWrappedKey{ + HybridCiphertext: hybridCt, + EncryptedDEK: encryptedDEK, + }) + if err != nil { + return nil, fmt.Errorf("asn1.Marshal failed: %w", err) + } + + return wrappedDER, nil +} + +func hybridNISTUnwrapDEK(p *hybridNISTParams, privateKeyRaw, wrappedDER, salt, info []byte) ([]byte, error) { + expectedPrivSize := p.ecPrivSize + p.mlkemPrivSize + if len(privateKeyRaw) != expectedPrivSize { + return nil, fmt.Errorf("invalid %s private key size: got %d want %d", p.keyType, len(privateKeyRaw), expectedPrivSize) + } + + var wrapped HybridNISTWrappedKey + rest, err := asn1.Unmarshal(wrappedDER, &wrapped) + if err != nil { + return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) + } + if len(rest) != 0 { + return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) + } + + expectedCtSize := p.ecPubSize + p.mlkemCtSize + if len(wrapped.HybridCiphertext) != expectedCtSize { + return nil, fmt.Errorf("invalid %s ciphertext size: got %d want %d", + p.keyType, len(wrapped.HybridCiphertext), expectedCtSize) + } + + // Split hybrid ciphertext + ephemeralPubBytes := wrapped.HybridCiphertext[:p.ecPubSize] + mlkemCtBytes := wrapped.HybridCiphertext[p.ecPubSize:] + + // Split private key + ecPrivBytes := privateKeyRaw[:p.ecPrivSize] + mlkemPrivBytes := privateKeyRaw[p.ecPrivSize:] + + // ECDH: reconstruct shared secret + ecPriv, err := p.curve.NewPrivateKey(ecPrivBytes) + if err != nil { + return nil, fmt.Errorf("invalid EC private key: %w", err) + } + ephemeralPub, err := p.curve.NewPublicKey(ephemeralPubBytes) + if err != nil { + return nil, fmt.Errorf("invalid ephemeral EC public key: %w", err) + } + ecdhSecret, err := ecPriv.ECDH(ephemeralPub) + if err != nil { + return nil, fmt.Errorf("ECDH failed: %w", err) + } + + // ML-KEM: decapsulate. Implicit rejection (FIPS 203 §6.3) means a wrong-key + // ciphertext yields a pseudorandom shared secret without an error here; + // authentication is enforced by the AES-GCM decrypt below. + var mlkemSecret []byte + switch p.keyType { //nolint:exhaustive // only NIST hybrid types + case HybridSecp256r1MLKEM768Key: + dk, dkErr := mlkem.NewDecapsulationKey768(mlkemPrivBytes) + if dkErr != nil { + return nil, fmt.Errorf("mlkem768 decapsulation key: %w", dkErr) + } + mlkemSecret, err = dk.Decapsulate(mlkemCtBytes) + case HybridSecp384r1MLKEM1024Key: + dk, dkErr := mlkem.NewDecapsulationKey1024(mlkemPrivBytes) + if dkErr != nil { + return nil, fmt.Errorf("mlkem1024 decapsulation key: %w", dkErr) + } + mlkemSecret, err = dk.Decapsulate(mlkemCtBytes) + default: + return nil, fmt.Errorf("unsupported ML-KEM key type: %s", p.keyType) + } + if err != nil { + return nil, fmt.Errorf("ML-KEM decapsulate failed: %w", err) + } + + // Combine secrets: ECDH || ML-KEM + combinedSecret := make([]byte, 0, len(ecdhSecret)+len(mlkemSecret)) + combinedSecret = append(combinedSecret, ecdhSecret...) + combinedSecret = append(combinedSecret, mlkemSecret...) + + // Derive AES-256 wrap key via HKDF + wrapKey, err := deriveHybridNISTWrapKey(combinedSecret, salt, info) + if err != nil { + return nil, err + } + + // AES-GCM decrypt DEK + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + plaintext, err := gcm.Decrypt(wrapped.EncryptedDEK) + if err != nil { + return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) + } + + return plaintext, nil +} + +func deriveHybridNISTWrapKey(combinedSecret, salt, info []byte) ([]byte, error) { + if len(salt) == 0 { + salt = defaultTDFSalt() + } + + hkdfObj := hkdf.New(sha256.New, combinedSecret, salt, info) + derivedKey := make([]byte, hybridNISTWrapKeySize) + if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { + return nil, fmt.Errorf("hkdf failure: %w", err) + } + + return derivedKey, nil +} diff --git a/lib/ocrypto/hybrid_nist_test.go b/lib/ocrypto/hybrid_nist_test.go new file mode 100644 index 0000000000..e7c9808aae --- /dev/null +++ b/lib/ocrypto/hybrid_nist_test.go @@ -0,0 +1,240 @@ +package ocrypto + +import ( + "encoding/asn1" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestP256MLKEM768KeyPairAndPEM(t *testing.T) { + keyPair, err := NewP256MLKEM768KeyPair() + require.NoError(t, err) + + publicPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privatePEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + publicKey, err := P256MLKEM768PubKeyFromPem([]byte(publicPEM)) + require.NoError(t, err) + privateKey, err := P256MLKEM768PrivateKeyFromPem([]byte(privatePEM)) + require.NoError(t, err) + + assert.Len(t, publicKey, P256MLKEM768PublicKeySize) + assert.Len(t, privateKey, P256MLKEM768PrivateKeySize) + assert.Equal(t, HybridSecp256r1MLKEM768Key, keyPair.GetKeyType()) +} + +func TestP384MLKEM1024KeyPairAndPEM(t *testing.T) { + keyPair, err := NewP384MLKEM1024KeyPair() + require.NoError(t, err) + + publicPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privatePEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + publicKey, err := P384MLKEM1024PubKeyFromPem([]byte(publicPEM)) + require.NoError(t, err) + privateKey, err := P384MLKEM1024PrivateKeyFromPem([]byte(privatePEM)) + require.NoError(t, err) + + assert.Len(t, publicKey, P384MLKEM1024PublicKeySize) + assert.Len(t, privateKey, P384MLKEM1024PrivateKeySize) + assert.Equal(t, HybridSecp384r1MLKEM1024Key, keyPair.GetKeyType()) +} + +func TestNewKeyPairP256MLKEM768(t *testing.T) { + keyPair, err := NewP256MLKEM768KeyPair() + require.NoError(t, err) + assert.Equal(t, HybridSecp256r1MLKEM768Key, keyPair.GetKeyType()) +} + +func TestNewKeyPairP384MLKEM1024(t *testing.T) { + keyPair, err := NewP384MLKEM1024KeyPair() + require.NoError(t, err) + assert.Equal(t, HybridSecp384r1MLKEM1024Key, keyPair.GetKeyType()) +} + +func TestP256MLKEM768WrapUnwrapRoundTrip(t *testing.T) { + keyPair, err := NewP256MLKEM768KeyPair() + require.NoError(t, err) + + dek := []byte("0123456789abcdef0123456789abcdef") + wrapped, err := P256MLKEM768WrapDEK(keyPair.publicKey, dek) + require.NoError(t, err) + + plaintext, err := P256MLKEM768UnwrapDEK(keyPair.privateKey, wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintext) +} + +func TestP384MLKEM1024WrapUnwrapRoundTrip(t *testing.T) { + keyPair, err := NewP384MLKEM1024KeyPair() + require.NoError(t, err) + + dek := []byte("0123456789abcdef0123456789abcdef") + wrapped, err := P384MLKEM1024WrapDEK(keyPair.publicKey, dek) + require.NoError(t, err) + + plaintext, err := P384MLKEM1024UnwrapDEK(keyPair.privateKey, wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintext) +} + +func TestP256MLKEM768WrapUnwrapWrongKeyFails(t *testing.T) { + keyPair, err := NewP256MLKEM768KeyPair() + require.NoError(t, err) + wrongKeyPair, err := NewP256MLKEM768KeyPair() + require.NoError(t, err) + + wrapped, err := P256MLKEM768WrapDEK(keyPair.publicKey, []byte("top secret dek")) + require.NoError(t, err) + + _, err = P256MLKEM768UnwrapDEK(wrongKeyPair.privateKey, wrapped) + require.Error(t, err) + // Wrong-key failure must surface through AES-GCM authentication, not a + // parse/size mismatch — ML-KEM uses implicit rejection so DecapsulateTo + // returns a pseudorandom secret rather than an error. + assert.ErrorContains(t, err, "AES-GCM decrypt failed") +} + +func TestP384MLKEM1024WrapUnwrapWrongKeyFails(t *testing.T) { + keyPair, err := NewP384MLKEM1024KeyPair() + require.NoError(t, err) + wrongKeyPair, err := NewP384MLKEM1024KeyPair() + require.NoError(t, err) + + wrapped, err := P384MLKEM1024WrapDEK(keyPair.publicKey, []byte("top secret dek")) + require.NoError(t, err) + + _, err = P384MLKEM1024UnwrapDEK(wrongKeyPair.privateKey, wrapped) + require.Error(t, err) + assert.ErrorContains(t, err, "AES-GCM decrypt failed") +} + +func TestHybridNISTWrappedKeyASN1RoundTrip(t *testing.T) { + original := HybridNISTWrappedKey{ + HybridCiphertext: []byte("hybrid-ciphertext-data"), + EncryptedDEK: []byte("encrypted-dek-data"), + } + + der, err := asn1.Marshal(original) + require.NoError(t, err) + + var decoded HybridNISTWrappedKey + rest, err := asn1.Unmarshal(der, &decoded) + require.NoError(t, err) + assert.Empty(t, rest) + assert.Equal(t, original, decoded) +} + +func TestP256MLKEM768PEMDispatch(t *testing.T) { + keyPair, err := NewP256MLKEM768KeyPair() + require.NoError(t, err) + + publicPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privatePEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + encryptor, err := FromPublicPEMWithSalt(publicPEM, []byte("salt"), []byte("info")) + require.NoError(t, err) + + decryptor, err := FromPrivatePEMWithSalt(privatePEM, []byte("salt"), []byte("info")) + require.NoError(t, err) + + nistEncryptor, ok := encryptor.(*HybridNISTEncryptor) + require.True(t, ok) + assert.Equal(t, Hybrid, nistEncryptor.Type()) + assert.Equal(t, HybridSecp256r1MLKEM768Key, nistEncryptor.KeyType()) + assert.Nil(t, nistEncryptor.EphemeralKey()) + + metadata, err := nistEncryptor.Metadata() + require.NoError(t, err) + assert.Empty(t, metadata) + + nistDecryptor, ok := decryptor.(*HybridNISTDecryptor) + require.True(t, ok) + + wrapped, err := nistEncryptor.Encrypt([]byte("dispatch-dek")) + require.NoError(t, err) + + plaintext, err := nistDecryptor.Decrypt(wrapped) + require.NoError(t, err) + assert.Equal(t, []byte("dispatch-dek"), plaintext) +} + +func TestP384MLKEM1024PEMDispatch(t *testing.T) { + keyPair, err := NewP384MLKEM1024KeyPair() + require.NoError(t, err) + + publicPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privatePEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + encryptor, err := FromPublicPEMWithSalt(publicPEM, []byte("salt"), []byte("info")) + require.NoError(t, err) + + decryptor, err := FromPrivatePEMWithSalt(privatePEM, []byte("salt"), []byte("info")) + require.NoError(t, err) + + nistEncryptor, ok := encryptor.(*HybridNISTEncryptor) + require.True(t, ok) + assert.Equal(t, Hybrid, nistEncryptor.Type()) + assert.Equal(t, HybridSecp384r1MLKEM1024Key, nistEncryptor.KeyType()) + assert.Nil(t, nistEncryptor.EphemeralKey()) + + nistDecryptor, ok := decryptor.(*HybridNISTDecryptor) + require.True(t, ok) + + wrapped, err := nistEncryptor.Encrypt([]byte("dispatch-dek-384")) + require.NoError(t, err) + + plaintext, err := nistDecryptor.Decrypt(wrapped) + require.NoError(t, err) + assert.Equal(t, []byte("dispatch-dek-384"), plaintext) +} + +func TestP256MLKEM768Encapsulate(t *testing.T) { + keyPair, err := NewP256MLKEM768KeyPair() + require.NoError(t, err) + + pubKey, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + + pubKeyRaw, err := P256MLKEM768PubKeyFromPem([]byte(pubKey)) + require.NoError(t, err) + + combinedSecret, hybridCt, err := P256MLKEM768Encapsulate(pubKeyRaw) + require.NoError(t, err) + assert.NotEmpty(t, combinedSecret) + assert.Len(t, hybridCt, P256MLKEM768CiphertextSize) +} + +func TestP384MLKEM1024Encapsulate(t *testing.T) { + keyPair, err := NewP384MLKEM1024KeyPair() + require.NoError(t, err) + + pubKey, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + + pubKeyRaw, err := P384MLKEM1024PubKeyFromPem([]byte(pubKey)) + require.NoError(t, err) + + combinedSecret, hybridCt, err := P384MLKEM1024Encapsulate(pubKeyRaw) + require.NoError(t, err) + assert.NotEmpty(t, combinedSecret) + assert.Len(t, hybridCt, P384MLKEM1024CiphertextSize) +} + +func TestIsHybridKeyTypeIncludesNewTypes(t *testing.T) { + assert.True(t, IsHybridKeyType(HybridXWingKey)) + assert.True(t, IsHybridKeyType(HybridSecp256r1MLKEM768Key)) + assert.True(t, IsHybridKeyType(HybridSecp384r1MLKEM1024Key)) + assert.False(t, IsHybridKeyType(EC256Key)) + assert.False(t, IsHybridKeyType(RSA2048Key)) +} diff --git a/lib/ocrypto/interfaces.go b/lib/ocrypto/interfaces.go index b9d74ae18d..3ff276b8d1 100644 --- a/lib/ocrypto/interfaces.go +++ b/lib/ocrypto/interfaces.go @@ -27,7 +27,8 @@ type ProtectedKey interface { VerifyBinding(ctx context.Context, policy, policyBinding []byte) error // Export returns the raw key data, optionally encrypting it with the provided encapsulator - // Deprecated: Use the Encapsulator's Encapsulate method instead + // + // Deprecated: Use the Encapsulator's Encapsulate method instead. Export(encapsulator Encapsulator) ([]byte, error) // DecryptAESGCM decrypts encrypted policies and metadata diff --git a/lib/ocrypto/key_material.go b/lib/ocrypto/key_material.go new file mode 100644 index 0000000000..44230a1e3e --- /dev/null +++ b/lib/ocrypto/key_material.go @@ -0,0 +1,29 @@ +package ocrypto + +import ( + "crypto/x509" + "encoding/pem" + "strings" +) + +// IsPEMOrDERPrivateKey reports whether data appears to be an unencrypted private key +// in PEM or DER format. It does not attempt decryption or key unwrapping. +func IsPEMOrDERPrivateKey(data []byte) bool { + for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) { + if strings.Contains(block.Type, "PRIVATE KEY") { + return true + } + } + + if _, err := x509.ParsePKCS8PrivateKey(data); err == nil { + return true + } + if _, err := x509.ParsePKCS1PrivateKey(data); err == nil { + return true + } + if _, err := x509.ParseECPrivateKey(data); err == nil { + return true + } + + return false +} diff --git a/lib/ocrypto/key_material_test.go b/lib/ocrypto/key_material_test.go new file mode 100644 index 0000000000..741d96ec06 --- /dev/null +++ b/lib/ocrypto/key_material_test.go @@ -0,0 +1,48 @@ +package ocrypto + +import ( + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsPEMOrDERPrivateKey(t *testing.T) { + privateKeyFiles := []string{ + "sample-rsa-2048-01-private.pem", + "sample-ec-secp256r1-01-private.pem", + } + + for _, filename := range privateKeyFiles { + t.Run("pem-private-"+filename, func(t *testing.T) { + pemData := readTestData(t, filename) + require.True(t, IsPEMOrDERPrivateKey(pemData)) + }) + } + + t.Run("pem-public", func(t *testing.T) { + pemData := readTestData(t, "sample-rsa-2048-01-public.pem") + require.False(t, IsPEMOrDERPrivateKey(pemData)) + }) + + t.Run("der-private", func(t *testing.T) { + pemData := readTestData(t, "sample-rsa-2048-01-private.pem") + block, _ := pem.Decode(pemData) + require.NotNil(t, block) + require.True(t, IsPEMOrDERPrivateKey(block.Bytes)) + }) + + t.Run("random-bytes", func(t *testing.T) { + require.False(t, IsPEMOrDERPrivateKey([]byte("not a key"))) + }) +} + +func readTestData(t *testing.T, filename string) []byte { + t.Helper() + path := filepath.Join("testdata", filename) + data, err := os.ReadFile(path) + require.NoError(t, err) + return data +} diff --git a/lib/ocrypto/protected_key.go b/lib/ocrypto/protected_key.go index e885a67b55..1c2070a1a8 100644 --- a/lib/ocrypto/protected_key.go +++ b/lib/ocrypto/protected_key.go @@ -2,6 +2,7 @@ package ocrypto import ( "context" + "crypto/aes" "crypto/hmac" "crypto/sha256" "errors" @@ -34,6 +35,9 @@ func NewAESProtectedKey(rawKey []byte) (*AESProtectedKey, error) { // Pre-initialize the AES-GCM cipher for performance aesGcm, err := NewAESGcm(keyCopy) if err != nil { + if errors.Is(err, ErrInvalidKeyData) { + return nil, fmt.Errorf("invalid key data provided: %w", err) + } return nil, fmt.Errorf("failed to initialize AES-GCM cipher: %w", err) } @@ -45,9 +49,22 @@ func NewAESProtectedKey(rawKey []byte) (*AESProtectedKey, error) { // DecryptAESGCM decrypts data using AES-GCM with the protected key func (k *AESProtectedKey) DecryptAESGCM(iv []byte, body []byte, tagSize int) ([]byte, error) { + if len(iv) != GcmStandardNonceSize { + return nil, fmt.Errorf("invalid ciphertext or IV: %w", ErrInvalidCiphertext) + } + if tagSize != aes.BlockSize { + return nil, fmt.Errorf("AES-GCM tag size %d is not supported: %w", tagSize, ErrUnsupportedAESGCMConfiguration) + } + // Use the pre-initialized AES-GCM cipher for better performance - decryptedData, err := k.aesGcm.DecryptWithIVAndTagSize(iv, body, tagSize) + sealed := make([]byte, 0, len(iv)+len(body)) + sealed = append(sealed, iv...) + sealed = append(sealed, body...) + decryptedData, err := k.aesGcm.Decrypt(sealed) if err != nil { + if errors.Is(err, ErrInvalidCiphertext) { + return nil, fmt.Errorf("invalid ciphertext or IV: %w", err) + } return nil, fmt.Errorf("AES-GCM decryption failed: %w", err) } @@ -55,7 +72,8 @@ func (k *AESProtectedKey) DecryptAESGCM(iv []byte, body []byte, tagSize int) ([] } // Export returns the raw key data, optionally encrypting it with the provided Encapsulator -// Deprecated: Use the Encapsulator's Encapsulate method instead +// +// Deprecated: Use the Encapsulator's Encapsulate method instead. func (k *AESProtectedKey) Export(encapsulator Encapsulator) ([]byte, error) { if encapsulator == nil { // Return error if encapsulator is nil diff --git a/lib/ocrypto/protected_key_test.go b/lib/ocrypto/protected_key_test.go index 30b32c578c..e56211e647 100644 --- a/lib/ocrypto/protected_key_test.go +++ b/lib/ocrypto/protected_key_test.go @@ -51,11 +51,41 @@ func TestAESProtectedKey_DecryptAESGCM(t *testing.T) { assert.Equal(t, plaintext, decrypted) } +func TestAESProtectedKey_DecryptAESGCM_InvalidCiphertext(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + require.NoError(t, err) + + protectedKey, err := NewAESProtectedKey(key) + require.NoError(t, err) + + // Test with invalid IV size (should be 12 bytes) + _, err = protectedKey.DecryptAESGCM([]byte{0x01, 0x02}, []byte("test"), 16) + require.ErrorIs(t, err, ErrInvalidCiphertext) +} + +func TestAESProtectedKey_DecryptAESGCM_UnsupportedTagSize(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + require.NoError(t, err) + + protectedKey, err := NewAESProtectedKey(key) + require.NoError(t, err) + + _, err = protectedKey.DecryptAESGCM(make([]byte, GcmStandardNonceSize), []byte("test"), 12) + require.ErrorIs(t, err, ErrUnsupportedAESGCMConfiguration) +} + func TestAESProtectedKey_DecryptAESGCM_InvalidKey(t *testing.T) { // Empty key should fail _, err := NewAESProtectedKey([]byte{}) - require.Error(t, err) - assert.ErrorIs(t, err, ErrEmptyKeyData) + require.ErrorIs(t, err, ErrEmptyKeyData) +} + +func TestNewAESProtectedKey_InvalidKeyData(t *testing.T) { + // Test with key that causes NewAESGcm to return ErrInvalidKeyData + _, err := NewAESProtectedKey([]byte{}) + require.ErrorIs(t, err, ErrEmptyKeyData) } func TestAESProtectedKey_Export_NoEncapsulator(t *testing.T) { @@ -64,7 +94,6 @@ func TestAESProtectedKey_Export_NoEncapsulator(t *testing.T) { require.NoError(t, err) exported, err := protectedKey.Export(nil) - require.Error(t, err) require.ErrorContains(t, err, "encapsulator cannot be nil") assert.Nil(t, exported) } diff --git a/lib/ocrypto/xwing.go b/lib/ocrypto/xwing.go new file mode 100644 index 0000000000..1ea9d5ab25 --- /dev/null +++ b/lib/ocrypto/xwing.go @@ -0,0 +1,260 @@ +package ocrypto + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/asn1" + "encoding/pem" + "fmt" + "io" + + "github.com/cloudflare/circl/kem/xwing" + "golang.org/x/crypto/hkdf" +) + +const ( + HybridXWingKey KeyType = "hpqt:xwing" + + XWingPublicKeySize = xwing.PublicKeySize + XWingPrivateKeySize = xwing.PrivateKeySize + XWingCiphertextSize = xwing.CiphertextSize + + PEMBlockXWingPublicKey = "XWING PUBLIC KEY" + PEMBlockXWingPrivateKey = "XWING PRIVATE KEY" +) + +type XWingWrappedKey struct { + XWingCiphertext []byte `asn1:"tag:0"` + EncryptedDEK []byte `asn1:"tag:1"` +} + +type XWingKeyPair struct { + publicKey []byte + privateKey []byte +} + +type XWingEncryptor struct { + publicKey []byte + salt []byte + info []byte +} + +type XWingDecryptor struct { + privateKey []byte + salt []byte + info []byte +} + +func NewXWingKeyPair() (XWingKeyPair, error) { + sk, pk, err := xwing.GenerateKeyPair(rand.Reader) + if err != nil { + return XWingKeyPair{}, fmt.Errorf("xwing.GenerateKeyPair failed: %w", err) + } + + publicKey := make([]byte, XWingPublicKeySize) + privateKey := make([]byte, XWingPrivateKeySize) + pk.Pack(publicKey) + sk.Pack(privateKey) + + return XWingKeyPair{ + publicKey: publicKey, + privateKey: privateKey, + }, nil +} + +func (k XWingKeyPair) PublicKeyInPemFormat() (string, error) { + return rawToPEM(PEMBlockXWingPublicKey, k.publicKey, XWingPublicKeySize) +} + +func (k XWingKeyPair) PrivateKeyInPemFormat() (string, error) { + return rawToPEM(PEMBlockXWingPrivateKey, k.privateKey, XWingPrivateKeySize) +} + +func (k XWingKeyPair) GetKeyType() KeyType { + return HybridXWingKey +} + +func XWingPubKeyFromPem(data []byte) ([]byte, error) { + return decodeSizedPEMBlock(data, PEMBlockXWingPublicKey, XWingPublicKeySize) +} + +func XWingPrivateKeyFromPem(data []byte) ([]byte, error) { + return decodeSizedPEMBlock(data, PEMBlockXWingPrivateKey, XWingPrivateKeySize) +} + +func NewXWingEncryptor(publicKey, salt, info []byte) (*XWingEncryptor, error) { + if len(publicKey) != XWingPublicKeySize { + return nil, fmt.Errorf("invalid X-Wing public key size: got %d want %d", len(publicKey), XWingPublicKeySize) + } + + return &XWingEncryptor{ + publicKey: append([]byte(nil), publicKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + }, nil +} + +func (e *XWingEncryptor) Encrypt(data []byte) ([]byte, error) { + return xwingWrapDEK(e.publicKey, data, e.salt, e.info) +} + +func (e *XWingEncryptor) PublicKeyInPemFormat() (string, error) { + return rawToPEM(PEMBlockXWingPublicKey, e.publicKey, XWingPublicKeySize) +} + +func (e *XWingEncryptor) Type() SchemeType { + return Hybrid +} + +func (e *XWingEncryptor) KeyType() KeyType { + return HybridXWingKey +} + +func (e *XWingEncryptor) EphemeralKey() []byte { + return nil +} + +func (e *XWingEncryptor) Metadata() (map[string]string, error) { + return make(map[string]string), nil +} + +func NewXWingDecryptor(privateKey []byte) (*XWingDecryptor, error) { + return NewSaltedXWingDecryptor(privateKey, defaultTDFSalt(), nil) +} + +func NewSaltedXWingDecryptor(privateKey, salt, info []byte) (*XWingDecryptor, error) { + if len(privateKey) != XWingPrivateKeySize { + return nil, fmt.Errorf("invalid X-Wing private key size: got %d want %d", len(privateKey), XWingPrivateKeySize) + } + + return &XWingDecryptor{ + privateKey: append([]byte(nil), privateKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + }, nil +} + +func (d *XWingDecryptor) Decrypt(data []byte) ([]byte, error) { + return xwingUnwrapDEK(d.privateKey, data, d.salt, d.info) +} + +func XWingWrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { + return xwingWrapDEK(publicKeyRaw, dek, defaultTDFSalt(), nil) +} + +func XWingUnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { + return xwingUnwrapDEK(privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) +} + +// XWingEncapsulate performs the X-Wing KEM encapsulation, returning the shared +// secret and ciphertext without applying KDF or encryption. +func XWingEncapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { + if len(publicKeyRaw) != XWingPublicKeySize { + return nil, nil, fmt.Errorf("invalid X-Wing public key size: got %d want %d", len(publicKeyRaw), XWingPublicKeySize) + } + + sharedSecret, ciphertext, err := xwing.Encapsulate(publicKeyRaw, nil) + if err != nil { + return nil, nil, fmt.Errorf("xwing.Encapsulate failed: %w", err) + } + + return sharedSecret, ciphertext, nil +} + +func xwingWrapDEK(publicKeyRaw, dek, salt, info []byte) ([]byte, error) { + sharedSecret, ciphertext, err := XWingEncapsulate(publicKeyRaw) + if err != nil { + return nil, err + } + + wrapKey, err := deriveXWingWrapKey(sharedSecret, salt, info) + if err != nil { + return nil, err + } + + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + + encryptedDEK, err := gcm.Encrypt(dek) + if err != nil { + return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) + } + + wrappedDER, err := asn1.Marshal(XWingWrappedKey{ + XWingCiphertext: ciphertext, + EncryptedDEK: encryptedDEK, + }) + if err != nil { + return nil, fmt.Errorf("asn1.Marshal failed: %w", err) + } + + return wrappedDER, nil +} + +func xwingUnwrapDEK(privateKeyRaw, wrappedDER, salt, info []byte) ([]byte, error) { + if len(privateKeyRaw) != XWingPrivateKeySize { + return nil, fmt.Errorf("invalid X-Wing private key size: got %d want %d", len(privateKeyRaw), XWingPrivateKeySize) + } + + var wrappedKey XWingWrappedKey + rest, err := asn1.Unmarshal(wrappedDER, &wrappedKey) + if err != nil { + return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) + } + if len(rest) != 0 { + return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) + } + if len(wrappedKey.XWingCiphertext) != XWingCiphertextSize { + return nil, fmt.Errorf("invalid X-Wing ciphertext size: got %d want %d", len(wrappedKey.XWingCiphertext), XWingCiphertextSize) + } + + sharedSecret := xwing.Decapsulate(wrappedKey.XWingCiphertext, privateKeyRaw) + + wrapKey, err := deriveXWingWrapKey(sharedSecret, salt, info) + if err != nil { + return nil, err + } + + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + + plaintext, err := gcm.Decrypt(wrappedKey.EncryptedDEK) + if err != nil { + return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) + } + + return plaintext, nil +} + +func deriveXWingWrapKey(sharedSecret, salt, info []byte) ([]byte, error) { + if len(salt) == 0 { + salt = defaultTDFSalt() + } + + hkdfObj := hkdf.New(sha256.New, sharedSecret, salt, info) + derivedKey := make([]byte, xwing.SharedKeySize) + if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { + return nil, fmt.Errorf("hkdf failure: %w", err) + } + + return derivedKey, nil +} + +func decodeSizedPEMBlock(data []byte, blockType string, expectedSize int) ([]byte, error) { + block, _ := pem.Decode(data) + if block == nil { + return nil, fmt.Errorf("failed to parse PEM formatted %s", blockType) + } + if block.Type != blockType { + return nil, fmt.Errorf("unexpected PEM block type: got %s want %s", block.Type, blockType) + } + if len(block.Bytes) != expectedSize { + return nil, fmt.Errorf("invalid %s size: got %d want %d", blockType, len(block.Bytes), expectedSize) + } + + return append([]byte(nil), block.Bytes...), nil +} diff --git a/lib/ocrypto/xwing_test.go b/lib/ocrypto/xwing_test.go new file mode 100644 index 0000000000..c0bf783ba9 --- /dev/null +++ b/lib/ocrypto/xwing_test.go @@ -0,0 +1,129 @@ +package ocrypto + +import ( + "encoding/asn1" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestXWingKeyPairAndPEM(t *testing.T) { + keyPair, err := NewXWingKeyPair() + require.NoError(t, err) + + publicPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privatePEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + publicKey, err := XWingPubKeyFromPem([]byte(publicPEM)) + require.NoError(t, err) + privateKey, err := XWingPrivateKeyFromPem([]byte(privatePEM)) + require.NoError(t, err) + + assert.Len(t, publicKey, XWingPublicKeySize) + assert.Len(t, privateKey, XWingPrivateKeySize) + assert.Equal(t, HybridXWingKey, keyPair.GetKeyType()) +} + +func TestNewKeyPairXWing(t *testing.T) { + keyPair, err := NewXWingKeyPair() + require.NoError(t, err) + assert.Equal(t, HybridXWingKey, keyPair.GetKeyType()) +} + +func TestXWingWrapUnwrapRoundTrip(t *testing.T) { + keyPair, err := NewXWingKeyPair() + require.NoError(t, err) + + dek := []byte("0123456789abcdef0123456789abcdef") + wrapped, err := XWingWrapDEK(keyPair.publicKey, dek) + require.NoError(t, err) + + plaintext, err := XWingUnwrapDEK(keyPair.privateKey, wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintext) +} + +func TestXWingWrapUnwrapWrongKeyFails(t *testing.T) { + keyPair, err := NewXWingKeyPair() + require.NoError(t, err) + wrongKeyPair, err := NewXWingKeyPair() + require.NoError(t, err) + + wrapped, err := XWingWrapDEK(keyPair.publicKey, []byte("top secret dek")) + require.NoError(t, err) + + _, err = XWingUnwrapDEK(wrongKeyPair.privateKey, wrapped) + require.Error(t, err) + assert.Contains(t, err.Error(), "AES-GCM decrypt failed") +} + +func TestXWingWrappedKeyASN1RoundTrip(t *testing.T) { + original := XWingWrappedKey{ + XWingCiphertext: []byte("ciphertext"), + EncryptedDEK: []byte("encrypted-dek"), + } + + der, err := asn1.Marshal(original) + require.NoError(t, err) + + var decoded XWingWrappedKey + rest, err := asn1.Unmarshal(der, &decoded) + require.NoError(t, err) + assert.Empty(t, rest) + assert.Equal(t, original, decoded) +} + +func TestXWingPEMDispatch(t *testing.T) { + keyPair, err := NewXWingKeyPair() + require.NoError(t, err) + + publicPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privatePEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + encryptor, err := FromPublicPEMWithSalt(publicPEM, []byte("salt"), []byte("info")) + require.NoError(t, err) + + decryptor, err := FromPrivatePEMWithSalt(privatePEM, []byte("salt"), []byte("info")) + require.NoError(t, err) + + xwingEncryptor, ok := encryptor.(*XWingEncryptor) + require.True(t, ok) + assert.Equal(t, Hybrid, xwingEncryptor.Type()) + assert.Equal(t, HybridXWingKey, xwingEncryptor.KeyType()) + assert.Nil(t, xwingEncryptor.EphemeralKey()) + + metadata, err := xwingEncryptor.Metadata() + require.NoError(t, err) + assert.Empty(t, metadata) + + xwingDecryptor, ok := decryptor.(*XWingDecryptor) + require.True(t, ok) + + wrapped, err := xwingEncryptor.Encrypt([]byte("dispatch-dek")) + require.NoError(t, err) + + plaintext, err := xwingDecryptor.Decrypt(wrapped) + require.NoError(t, err) + assert.Equal(t, []byte("dispatch-dek"), plaintext) +} + +func TestXWingEncapsulate(t *testing.T) { + keyPair, err := NewXWingKeyPair() + require.NoError(t, err) + + sharedSecret, ciphertext, err := XWingEncapsulate(keyPair.publicKey) + require.NoError(t, err) + assert.Len(t, sharedSecret, 32) + assert.Len(t, ciphertext, XWingCiphertextSize) +} + +func TestXWingEncapsulateInvalidKeySize(t *testing.T) { + _, _, err := XWingEncapsulate([]byte("too-short")) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid X-Wing public key size") +} diff --git a/opentdf-dev.yaml b/opentdf-dev.yaml index 6f7754f7c9..5a90cfa605 100644 --- a/opentdf-dev.yaml +++ b/opentdf-dev.yaml @@ -22,6 +22,7 @@ services: registered_kas_uri: http://localhost:8080 # Should match what you have registered for *this* KAS in the policy db. preview: ec_tdf_enabled: false + hybrid_tdf_enabled: false key_management: false root_key: a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1 # For local development testing only keyring: @@ -75,6 +76,10 @@ server: groups_claim: # realm_access.roles # Claim the represents the idP client ID client_id_claim: # azp + # Optional external role provider (name is resolved via StartOptions) + # roles_provider: + # name: external + # config: {} # provider-specific (any object) ## Extends the builtin policy extension: | g, opentdf-admin, role:admin diff --git a/opentdf-example.yaml b/opentdf-example.yaml index a0b97da826..9bc9f90ef2 100644 --- a/opentdf-example.yaml +++ b/opentdf-example.yaml @@ -123,4 +123,16 @@ server: alg: ec:secp256r1 private: /keys/kas-ec-private.pem cert: /keys/kas-ec-cert.pem + - kid: x1 + alg: hpqt:xwing + private: /keys/kas-xwing-private.pem + cert: /keys/kas-xwing-public.pem + - kid: h1 + alg: hpqt:secp256r1-mlkem768 + private: /keys/kas-p256mlkem768-private.pem + cert: /keys/kas-p256mlkem768-public.pem + - kid: h2 + alg: hpqt:secp384r1-mlkem1024 + private: /keys/kas-p384mlkem1024-private.pem + cert: /keys/kas-p384mlkem1024-public.pem port: 8080 diff --git a/opentdf-kas-mode.yaml b/opentdf-kas-mode.yaml index ebcfb6f0c2..4bdd67e376 100644 --- a/opentdf-kas-mode.yaml +++ b/opentdf-kas-mode.yaml @@ -33,6 +33,12 @@ services: - kid: r1 alg: rsa:2048 legacy: true + - kid: x1 + alg: hpqt:xwing + - kid: h1 + alg: hpqt:secp256r1-mlkem768 + - kid: h2 + alg: hpqt:secp384r1-mlkem1024 server: public_hostname: localhost tls: @@ -123,4 +129,16 @@ server: alg: ec:secp256r1 private: kas-ec-private.pem cert: kas-ec-cert.pem + - kid: x1 + alg: hpqt:xwing + private: kas-xwing-private.pem + cert: kas-xwing-public.pem + - kid: h1 + alg: hpqt:secp256r1-mlkem768 + private: kas-p256mlkem768-private.pem + cert: kas-p256mlkem768-public.pem + - kid: h2 + alg: hpqt:secp384r1-mlkem1024 + private: kas-p384mlkem1024-private.pem + cert: kas-p384mlkem1024-public.pem port: 8181 diff --git a/otdfctl/CHANGELOG.md b/otdfctl/CHANGELOG.md new file mode 100644 index 0000000000..046a31b481 --- /dev/null +++ b/otdfctl/CHANGELOG.md @@ -0,0 +1,437 @@ +# Changelog + +## [0.32.0](https://github.com/opentdf/platform/compare/otdfctl/v0.31.0...otdfctl/v0.32.0) (2026-05-19) + + +### Features + +* **cli:** Add better unit testing. ([#3378](https://github.com/opentdf/platform/issues/3378)) ([3ad33dc](https://github.com/opentdf/platform/commit/3ad33dc8adde0d110a64978f61358f728e6cbe0d)) +* **cli:** Add interactive review for prune plans ([#3421](https://github.com/opentdf/platform/issues/3421)) ([c11680b](https://github.com/opentdf/platform/commit/c11680b8d5718a2b119bb079f26e72ba064af065)) +* **cli:** Add prune confirmation. ([#3469](https://github.com/opentdf/platform/issues/3469)) ([c6d47ec](https://github.com/opentdf/platform/commit/c6d47ec800f82ddb7d912d9c7de4b4c1c2b55284)) +* **cli:** Add prune planner. ([#3411](https://github.com/opentdf/platform/issues/3411)) ([3e294e6](https://github.com/opentdf/platform/commit/3e294e63cac669830ec3159cce788f1692c3b27e)) +* **cli:** Add prune summary information ([#3456](https://github.com/opentdf/platform/issues/3456)) ([c900c53](https://github.com/opentdf/platform/commit/c900c53b39ed6a737716a163e66eec6c71cea60d)) +* **cli:** add sensitive flag annotation to DocFlag ([#3457](https://github.com/opentdf/platform/issues/3457)) ([98f48d2](https://github.com/opentdf/platform/commit/98f48d2ef87740ef564e6b79eaf03593684d51bc)) +* **cli:** Confirm and execute pruning of legacy objects ([#3458](https://github.com/opentdf/platform/issues/3458)) ([24c09dd](https://github.com/opentdf/platform/commit/24c09dd6318f713e16106a23c9e623176db011c8)) +* **cli:** Print report on failure ([#3365](https://github.com/opentdf/platform/issues/3365)) ([05a4473](https://github.com/opentdf/platform/commit/05a4473cf291e0837f215398b4212244bcfb2210)) +* **cli:** Sort parameters. ([#3478](https://github.com/opentdf/platform/issues/3478)) ([73ad878](https://github.com/opentdf/platform/commit/73ad878b819b2723c50e4398ef9f1663eb519735)) +* **policy:** Add FQN to RegisteredResourceValues ([#3446](https://github.com/opentdf/platform/issues/3446)) ([3199583](https://github.com/opentdf/platform/commit/3199583c4a6454ac7eabe1260a142e5c5ff067ad)) +* **policy:** Add resource mapping group FQNs ([#3447](https://github.com/opentdf/platform/issues/3447)) ([6a0b3c6](https://github.com/opentdf/platform/commit/6a0b3c63795cf79b4d87d561464101c7cd2cf351)) + + +### Bug Fixes + +* **cli:** Prune was not classifying multi-namespaced RRs properly. ([#3488](https://github.com/opentdf/platform/issues/3488)) ([eae8645](https://github.com/opentdf/platform/commit/eae86452b0cf0b88a103f473f51051e2b6e7d717)) +* **cli:** support json profile output ([#3448](https://github.com/opentdf/platform/issues/3448)) ([61f194c](https://github.com/opentdf/platform/commit/61f194c90af3b67d7a183daa92175174c69dfff6)) +* **deps:** bump github.com/opentdf/platform/lib/identifier from 0.3.0 to 0.4.0 in /otdfctl ([#3367](https://github.com/opentdf/platform/issues/3367)) ([aa23179](https://github.com/opentdf/platform/commit/aa23179f9a25235d1f3a26ebccf63503fa0cc53d)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.27.0 to 0.28.0 in /otdfctl ([#3419](https://github.com/opentdf/platform/issues/3419)) ([c80374f](https://github.com/opentdf/platform/commit/c80374f59f9af121679100cb50550e0bb899c0bb)) +* **deps:** bump github.com/opentdf/platform/sdk from 0.16.0 to 0.17.0 in /otdfctl ([#3397](https://github.com/opentdf/platform/issues/3397)) ([bb9fcd6](https://github.com/opentdf/platform/commit/bb9fcd6e99745d1d960b1a56ce91bc977b87e7ba)) +* **deps:** bump go.opentelemetry.io/otel from 1.40.0 to 1.41.0 in /otdfctl ([#3400](https://github.com/opentdf/platform/issues/3400)) ([5631c37](https://github.com/opentdf/platform/commit/5631c3709ef5cd8ecb771a4842a76bd4e248b9dd)) +* **deps:** bump module protocol/go to v0.30.0 throughout ([#3459](https://github.com/opentdf/platform/issues/3459)) ([8eaa502](https://github.com/opentdf/platform/commit/8eaa502b0f949ddbe18a5a1dac0931b92eec2351)) + +## [0.31.0](https://github.com/opentdf/platform/compare/otdfctl/v0.30.0...otdfctl/v0.31.0) (2026-04-22) + + +### Features + +* **cli:** Add e2e tests and fix bugs. ([#3353](https://github.com/opentdf/platform/issues/3353)) ([213d843](https://github.com/opentdf/platform/commit/213d843cedc38be9a7c255d73c7d66ffbb4fdc53)) +* **cli:** Add E2E tests for namespaced policy. ([#3363](https://github.com/opentdf/platform/issues/3363)) ([e46f07d](https://github.com/opentdf/platform/commit/e46f07d7126a66f92251ee6a37ff59bc3d60e165)) +* **cli:** Interactive confirmation ([#3360](https://github.com/opentdf/platform/issues/3360)) ([cd931d9](https://github.com/opentdf/platform/commit/cd931d9a46083151d9cd2a2efea4bd447c43f5cc)) +* **cli:** migrate otdfctl into platform monorepo ([#3205](https://github.com/opentdf/platform/issues/3205)) ([5177bec](https://github.com/opentdf/platform/commit/5177bec0a2f67aa1395e45a1b8a72570910f6208)) + + +### Bug Fixes + +* **deps:** bump github.com/opentdf/platform/sdk from 0.15.0 to 0.16.0 in /otdfctl ([#3357](https://github.com/opentdf/platform/issues/3357)) ([d829184](https://github.com/opentdf/platform/commit/d829184487f539dde2c57c0f781d4fe1a65bd63a)) + +## [0.30.0](https://github.com/opentdf/otdfctl/compare/v0.29.0...v0.30.0) (2026-03-31) + + +### Features + +* **core:** Add optional namespace flag for subject mappings and condtion sets ([#779](https://github.com/opentdf/otdfctl/issues/779)) ([9e849c4](https://github.com/opentdf/otdfctl/commit/9e849c4c80d8ba6f32a24a4be161dfe26d28cdde)) +* **core:** add scope support for client creds ([#752](https://github.com/opentdf/otdfctl/issues/752)) ([9ca9e43](https://github.com/opentdf/otdfctl/commit/9ca9e43394c67813a5fd1f506d174fbf33dcc492)) +* **core:** migrate registered resources ([#772](https://github.com/opentdf/otdfctl/issues/772)) ([2b49a7d](https://github.com/opentdf/otdfctl/commit/2b49a7deceaf9f227a6548e59c7c6e8cbf100b17)) +* **core:** optional namespace in actions commands and re-enable actions/RR tests ([#775](https://github.com/opentdf/otdfctl/issues/775)) ([29a2eb1](https://github.com/opentdf/otdfctl/commit/29a2eb13c8201f5ca059478169479dc5fea9de4e)) +* **core:** support namespaced registered resources ([#767](https://github.com/opentdf/otdfctl/issues/767)) ([4d786b5](https://github.com/opentdf/otdfctl/commit/4d786b5103580afae21ab6811e426f0b25eb6b3a)) + + +### Bug Fixes + +* **ci:** Temporarily skip namespaced-actions impacted BATS cases ([#773](https://github.com/opentdf/otdfctl/issues/773)) ([633728a](https://github.com/opentdf/otdfctl/commit/633728af0d7ce9cc6a1231a07315eafd23971d56)) +* **core:** bump toolchain to go 1.24.13 ([#747](https://github.com/opentdf/otdfctl/issues/747)) ([6804b93](https://github.com/opentdf/otdfctl/commit/6804b93c848bb56398bfdff09a78e1458493f5e7)) +* **core:** disable RR E2E tests ([#768](https://github.com/opentdf/otdfctl/issues/768)) ([0821b8c](https://github.com/opentdf/otdfctl/commit/0821b8c933b97a9550fdac47449adf8b533cb04a)) +* **core:** make namespacing registered resources optional ([#785](https://github.com/opentdf/otdfctl/issues/785)) ([8e6eb31](https://github.com/opentdf/otdfctl/commit/8e6eb3141feb7bc9c1a4e8cb7131cbf89f559eba)) +* **core:** refactor `ListAttributesValues` to use `Get` ([#769](https://github.com/opentdf/otdfctl/issues/769)) ([a82f7b7](https://github.com/opentdf/otdfctl/commit/a82f7b74a9fefd2dc59b019e9ad2877e25ac731a)) +* **core:** unsafe update result output values order ([#759](https://github.com/opentdf/otdfctl/issues/759)) ([baeba0f](https://github.com/opentdf/otdfctl/commit/baeba0f078fdad1b5fff018ff7b097eda794c703)) + +## [0.29.0](https://github.com/opentdf/otdfctl/compare/v0.28.0...v0.29.0) (2026-01-28) + + +### ⚠ BREAKING CHANGES + +* **core:** remove NanoTDF support ([#736](https://github.com/opentdf/otdfctl/issues/736)) + +### Features + +* **core:** Add allow_traversal to attribute defs. ([#739](https://github.com/opentdf/otdfctl/issues/739)) ([63d71b0](https://github.com/opentdf/otdfctl/commit/63d71b0c0dc93545f50f921f613880f99482c959)) + + +### Bug Fixes + +* **core:** obligations commands id and fqn flag exclusivity ([#731](https://github.com/opentdf/otdfctl/issues/731)) ([77ebbb4](https://github.com/opentdf/otdfctl/commit/77ebbb49d26b0b96abd254e9e177187504f7abb5)), closes [#728](https://github.com/opentdf/otdfctl/issues/728) +* **core:** remove NanoTDF support ([#736](https://github.com/opentdf/otdfctl/issues/736)) ([9528821](https://github.com/opentdf/otdfctl/commit/9528821ed1d2e728afa44af29e4d4ebf72030039)) + +## [0.28.0](https://github.com/opentdf/otdfctl/compare/v0.27.0...v0.28.0) (2025-12-16) + + +### ⚠ BREAKING CHANGES + +* **core:** Store output format to profile. ([#719](https://github.com/opentdf/otdfctl/issues/719)) + +### Features + +* **core:** Output to stdout, log to stderr. ([#716](https://github.com/opentdf/otdfctl/issues/716)) ([4f6e1e4](https://github.com/opentdf/otdfctl/commit/4f6e1e4883c2e1d5215835cd8893e51b01e6c358)) +* **core:** pass default slogger into SDK init ([#721](https://github.com/opentdf/otdfctl/issues/721)) ([c6bc084](https://github.com/opentdf/otdfctl/commit/c6bc084bf9856075a50f70168b88dd2c188488cc)) +* **core:** Store output format to profile. ([#719](https://github.com/opentdf/otdfctl/issues/719)) ([400ecec](https://github.com/opentdf/otdfctl/commit/400ecec5af8f9b96c716310b76bb493d3124748f)) + + +### Bug Fixes + +* **core:** Fix log-level flag ([#714](https://github.com/opentdf/otdfctl/issues/714)) ([84f191b](https://github.com/opentdf/otdfctl/commit/84f191b8c64ca06b692f855f0144ac9bcd2f56b9)) +* **core:** Print errors and messages with JSON if enabled in printer ([#724](https://github.com/opentdf/otdfctl/issues/724)) ([ce0256b](https://github.com/opentdf/otdfctl/commit/ce0256bf888745cb25e09e1a608b620824f73139)) + +## [0.27.0](https://github.com/opentdf/otdfctl/compare/v0.26.0...v0.27.0) (2025-12-03) + + +### ⚠ BREAKING CHANGES + +* **core:** Return pagination in responses. ([#684](https://github.com/opentdf/otdfctl/issues/684)) + +### Features + +* **core:** Filesystem as profile store ([#705](https://github.com/opentdf/otdfctl/issues/705)) ([47df5da](https://github.com/opentdf/otdfctl/commit/47df5dac8a0b3474f6ab145288886d0bd7031053)) + + +### Bug Fixes + +* **core:** Ensure IDs are displayed for keys. ([#681](https://github.com/opentdf/otdfctl/issues/681)) ([c5c9989](https://github.com/opentdf/otdfctl/commit/c5c9989f2a7ae9e8865e03ae761df830a4c54b15)) +* **core:** first set of manual lint fixes ([#700](https://github.com/opentdf/otdfctl/issues/700)) ([1f89120](https://github.com/opentdf/otdfctl/commit/1f89120a5590ac5b6aa5c64d51f3947cd3f29bb1)) +* **core:** Fix obligation smoke test ([#687](https://github.com/opentdf/otdfctl/issues/687)) ([c8e9b20](https://github.com/opentdf/otdfctl/commit/c8e9b2004f21ac87a0a0d4bce9c546796928ba2d)) +* **core:** improve TLS error handling UX when connecting to platform ([#708](https://github.com/opentdf/otdfctl/issues/708)) ([373df89](https://github.com/opentdf/otdfctl/commit/373df89875235f6f3acfc0bf9680ac7b057e04e0)) +* **core:** lint fixes that can be automatically resolved ([#699](https://github.com/opentdf/otdfctl/issues/699)) ([a2aedcb](https://github.com/opentdf/otdfctl/commit/a2aedcbc3fd3d1b31aed15de63bffb86e9b4c597)) +* **core:** many manually resolved lint fixes ([#701](https://github.com/opentdf/otdfctl/issues/701)) ([bb998cf](https://github.com/opentdf/otdfctl/commit/bb998cfd20cf1123c66daf6a237a648f4bf144e8)) +* **core:** restructure cmd package to resolve remaining lint issues ([#702](https://github.com/opentdf/otdfctl/issues/702)) ([5d677e1](https://github.com/opentdf/otdfctl/commit/5d677e1bda9abdf1c88b21c19a1b7b423368fb32)) +* **core:** Return pagination in responses. ([#684](https://github.com/opentdf/otdfctl/issues/684)) ([666ac2f](https://github.com/opentdf/otdfctl/commit/666ac2f57b532000cc715d04f1312800dd560049)) +* **main:** Add SCS creation in setup_file ([#693](https://github.com/opentdf/otdfctl/issues/693)) ([33ae971](https://github.com/opentdf/otdfctl/commit/33ae9712944e5175a047c8e1eba2cbe08a021955)) +* **main:** Subject mapping tests ([#691](https://github.com/opentdf/otdfctl/issues/691)) ([6c137c0](https://github.com/opentdf/otdfctl/commit/6c137c04b8c407cd9117c0828657eb9e5c3c2819)) +* **main:** Update flaky subject mapping tests ([#694](https://github.com/opentdf/otdfctl/issues/694)) ([b3cd4df](https://github.com/opentdf/otdfctl/commit/b3cd4df40b5e7e315ff4109f34b8d5d8ffd36c0f)) +* **main:** Use assertion helpers ([#692](https://github.com/opentdf/otdfctl/issues/692)) ([2956244](https://github.com/opentdf/otdfctl/commit/2956244d6d9947e7d09b20070f31dd0cff7f1d5b)) + +## [0.26.0](https://github.com/opentdf/otdfctl/compare/v0.25.0...v0.26.0) (2025-10-22) + + +### Features + +* **core:** Add list obligation triggers. ([#677](https://github.com/opentdf/otdfctl/issues/677)) ([ac3bd5e](https://github.com/opentdf/otdfctl/commit/ac3bd5e55d0101d005b4bc8c6e24b6595d4ff859)) +* **core:** Append required obligations to error output ([#673](https://github.com/opentdf/otdfctl/issues/673)) ([7eae582](https://github.com/opentdf/otdfctl/commit/7eae58246047176d68a27022ecb862822a573794)) + + +### Bug Fixes + +* **core:** Provider config manager table field empty ([#668](https://github.com/opentdf/otdfctl/issues/668)) ([89871f6](https://github.com/opentdf/otdfctl/commit/89871f6fde4d5d1b6cfca375723424bedcefc1f2)) +* **core:** Use fqn populated on obligation value. ([#679](https://github.com/opentdf/otdfctl/issues/679)) ([7dd626e](https://github.com/opentdf/otdfctl/commit/7dd626ecb380725ef4c16fc98b44d3c861cd8244)) +* validate --public-key-pem value on key creation ([#678](https://github.com/opentdf/otdfctl/issues/678)) ([b1e69ef](https://github.com/opentdf/otdfctl/commit/b1e69efd5b8a22499ac95a1b9bc08f08b415e0a3)) + +## [0.25.0](https://github.com/opentdf/otdfctl/compare/v0.24.0...v0.25.0) (2025-10-06) + + +### Features + +* add support for provider manager column ([#660](https://github.com/opentdf/otdfctl/issues/660)) ([fe4e50b](https://github.com/opentdf/otdfctl/commit/fe4e50ba9c1773f0b12622a924a4317ccdbe2ed6)) +* **core:** Add legacy flag to import and list. ([#641](https://github.com/opentdf/otdfctl/issues/641)) ([ffd0dc0](https://github.com/opentdf/otdfctl/commit/ffd0dc0fc84ef0cee3b896fc939d1c244da5728d)) +* **core:** Add obligation triggers ([#656](https://github.com/opentdf/otdfctl/issues/656)) ([8f6087f](https://github.com/opentdf/otdfctl/commit/8f6087fd2531628dda64eb8b0133830c6a21f9f6)) +* **core:** Adds policy-mode encrypt param ([#633](https://github.com/opentdf/otdfctl/issues/633)) ([9e83016](https://github.com/opentdf/otdfctl/commit/9e830168a38c0803396fd5c4c188fa62c0ccf5a0)) +* **core:** Create/Update triggers via obligation values. ([#658](https://github.com/opentdf/otdfctl/issues/658)) ([2a2f0c6](https://github.com/opentdf/otdfctl/commit/2a2f0c6c87ff9e7a543e4c4634d0be96dfe9e8e3)) +* **core:** obligations defs + vals CRUD ([#639](https://github.com/opentdf/otdfctl/issues/639)) ([3a3df0d](https://github.com/opentdf/otdfctl/commit/3a3df0d7b862fc56635b18d73af784ecd3066ae2)) + + +### Bug Fixes + +* **core:** add missing port flag ([#638](https://github.com/opentdf/otdfctl/issues/638)) ([c9bb4e5](https://github.com/opentdf/otdfctl/commit/c9bb4e50d0690cecda5ded7a41f572a10d18f6a6)) +* **core:** Clarifies not_found in attrs ([#649](https://github.com/opentdf/otdfctl/issues/649)) ([d46bd0f](https://github.com/opentdf/otdfctl/commit/d46bd0f3c60bc8b7f47789d93f842271791cf824)) +* **core:** render kas-registry key list-mappings table rows ([#663](https://github.com/opentdf/otdfctl/issues/663)) ([fb39718](https://github.com/opentdf/otdfctl/commit/fb39718aa23a626186dcb54d9115b837b66a9b79)) + +## [0.24.0](https://github.com/opentdf/otdfctl/compare/v0.23.0...v0.24.0) (2025-07-29) + + +### Features + +* **core:** Delete kas keys ([#627](https://github.com/opentdf/otdfctl/issues/627)) ([e2acb67](https://github.com/opentdf/otdfctl/commit/e2acb670b66ffb8d8c889a240f784c1a02ec42b5)) +* **core:** expose registered resources commands ([#631](https://github.com/opentdf/otdfctl/issues/631)) ([18530b8](https://github.com/opentdf/otdfctl/commit/18530b8c623c67afcd7515ccd2bacb9d2de14fef)) +* **core:** Key mappings command ([#623](https://github.com/opentdf/otdfctl/issues/623)) ([28403c6](https://github.com/opentdf/otdfctl/commit/28403c600e0fff9404d6be79207330046237b5d4)) +* **core:** Registered Resources - action attribute values update confirmation ([#620](https://github.com/opentdf/otdfctl/issues/620)) ([2ad0b9e](https://github.com/opentdf/otdfctl/commit/2ad0b9e9260785ac5bd7603b0d7f95b8957cba11)) + +## [0.23.0](https://github.com/opentdf/otdfctl/compare/v0.22.0...v0.23.0) (2025-07-01) + + +### Features + +* **core:** Import keys. ([#617](https://github.com/opentdf/otdfctl/issues/617)) ([4dc69e6](https://github.com/opentdf/otdfctl/commit/4dc69e6eaf2cdb23116b97ca2448bbbd57346f49)) + +## [0.22.0](https://github.com/opentdf/otdfctl/compare/v0.21.0...v0.22.0) (2025-06-24) + + +### ⚠ BREAKING CHANGES + +* remove the ability to assign grants ([#604](https://github.com/opentdf/otdfctl/issues/604)) + +### Features + +* **core:** dynamic port allocation ([#606](https://github.com/opentdf/otdfctl/issues/606)) ([75552e1](https://github.com/opentdf/otdfctl/commit/75552e187eef204b03b1d13d55920fa43ec3cf30)) +* **core:** Uncomment code and pull in new protos. ([#594](https://github.com/opentdf/otdfctl/issues/594)) ([2883e50](https://github.com/opentdf/otdfctl/commit/2883e5060ca1f9d22f9a9500293fc407e7f4bcfd)) +* **core:** Unhide key commands. ([#607](https://github.com/opentdf/otdfctl/issues/607)) ([a3660d9](https://github.com/opentdf/otdfctl/commit/a3660d9e8271e3fd179e6521eab02a2b096a01db)) +* remove the ability to assign grants ([#604](https://github.com/opentdf/otdfctl/issues/604)) ([c9f0d82](https://github.com/opentdf/otdfctl/commit/c9f0d822747a62a6253c441ede144238715da50b)) + + +### Bug Fixes + +* add more Deprecated text to kas-grants ([#605](https://github.com/opentdf/otdfctl/issues/605)) ([2106d2f](https://github.com/opentdf/otdfctl/commit/2106d2f5189de49fe05b94025228474ffdb026ae)) +* **ci:** Trigger for release-please (testing) ([#580](https://github.com/opentdf/otdfctl/issues/580)) ([5cd33f9](https://github.com/opentdf/otdfctl/commit/5cd33f9f9b5fb66b2cc9c0c795bd84cf10630298)) +* **core:** Change base key name so tests run last. ([#611](https://github.com/opentdf/otdfctl/issues/611)) ([464b179](https://github.com/opentdf/otdfctl/commit/464b179a3134890943d8319bdee41cbad9078d64)) +* **core:** Move key management under policy. ([#597](https://github.com/opentdf/otdfctl/issues/597)) ([d657e96](https://github.com/opentdf/otdfctl/commit/d657e96cab3afc516437ae08321ab45aff376460)) +* disable kas-registry --public-keys and --publickey-remote flags ([#603](https://github.com/opentdf/otdfctl/issues/603)) ([279bbbd](https://github.com/opentdf/otdfctl/commit/279bbbd8ced14765c97ae3928421d38737ac0a8d)) +* enforce hex encoded wrapping-key ([#581](https://github.com/opentdf/otdfctl/issues/581)) ([416e215](https://github.com/opentdf/otdfctl/commit/416e215abf0c910aa4d18dc84729f89ea578fd4d)) +* **main:** Use cmd.Context for resource mapping group commands ([#592](https://github.com/opentdf/otdfctl/issues/592)) ([b5d8b6f](https://github.com/opentdf/otdfctl/commit/b5d8b6f6c335483873cec90363d94e0196d18b14)) + +## [0.21.0](https://github.com/opentdf/otdfctl/compare/v0.20.0...v0.21.0) (2025-05-29) + + +### Features + +* Add initial Dependency Review configuration ([#551](https://github.com/opentdf/otdfctl/issues/551)) ([b622666](https://github.com/opentdf/otdfctl/commit/b6226660c1d75e133a8ead456efcab74de4b4fc0)) +* **core:** Add base key cmds ([#563](https://github.com/opentdf/otdfctl/issues/563)) ([edfd6c0](https://github.com/opentdf/otdfctl/commit/edfd6c08dc9b84f2cbfc79643ccc266a45ce58fd)) +* **core:** DSPX-18 clean up Go context usage to follow best practices ([#558](https://github.com/opentdf/otdfctl/issues/558)) ([a2c9f8b](https://github.com/opentdf/otdfctl/commit/a2c9f8b13cbab740b46262f70aecc82a94f3d788)) +* **core:** DSPX-608 - Deprecate public_client_id ([#555](https://github.com/opentdf/otdfctl/issues/555)) ([8d396bd](https://github.com/opentdf/otdfctl/commit/8d396bd022126524d9d20daa03ec6ca262cf4406)) +* **core:** DSPX-608 - require clientID for login ([#553](https://github.com/opentdf/otdfctl/issues/553)) ([580172e](https://github.com/opentdf/otdfctl/commit/580172e1861b54366f4914a141e459fe3221a16d)) +* **core:** DSPX-896 add registered resources CRUD ([#559](https://github.com/opentdf/otdfctl/issues/559)) ([8e7475e](https://github.com/opentdf/otdfctl/commit/8e7475ef8aab91d28ab7efd320af13dc5ab53d3b)) +* **core:** KAS allowlist options ([#539](https://github.com/opentdf/otdfctl/issues/539)) ([af7978f](https://github.com/opentdf/otdfctl/commit/af7978f86ced38543b31b792e008654071333789)) +* **core:** key management operations ([#533](https://github.com/opentdf/otdfctl/issues/533)) ([d4f6aaa](https://github.com/opentdf/otdfctl/commit/d4f6aaac3f6fc1b50fbc988e5d34a32de0ed9f64)) +* **main:** add actions CRUD and e2e tests ([#523](https://github.com/opentdf/otdfctl/issues/523)) ([2fb9ec7](https://github.com/opentdf/otdfctl/commit/2fb9ec7336da5731b868da94f0bbd5b2f226ede1)) +* **main:** refactor actions within existing CLI policy object CRUD ([#543](https://github.com/opentdf/otdfctl/issues/543)) ([9ab1a58](https://github.com/opentdf/otdfctl/commit/9ab1a58418643ea709aefb08e3f5ca8bd06235f4)) +* **core:** Resource mapping groups ([#567](https://github.com/opentdf/otdfctl/issues/567)) ([03fa307](https://github.com/opentdf/otdfctl/commit/03fa307b3ab91f25baeb74e30fde6eeec6d479a1)) +* **core:** Update key mgmt flags to consistent format ([#570](https://github.com/opentdf/otdfctl/issues/570)) ([#846f96c](https://github.com/opentdf/otdfctl/commit/846f96cb9adfe03e355c9e64b559f1c11d84a86f)) +* **core:** Rotate Key ([#572](https://github.com/opentdf/otdfctl/issues/572)) ([afd0043](https://github.com/opentdf/otdfctl/commit/afd0043f1ea66f0b371a95b556320551f73749bb)) + + +### Bug Fixes + +* **ci:** ci job should run on changes to GHA ([#530](https://github.com/opentdf/otdfctl/issues/530)) ([1d296ca](https://github.com/opentdf/otdfctl/commit/1d296ca8fac889a6e776ad381df999a2fcf9d6ce)) +* **main:** Pass the full url when building the sdk object ([#544](https://github.com/opentdf/otdfctl/issues/544)) ([8b836f0](https://github.com/opentdf/otdfctl/commit/8b836f0fa3aa414c3ab19d830f4d1f833d3ae61d)) + +## [0.20.0](https://github.com/opentdf/otdfctl/compare/v0.19.0...v0.20.0) (2025-04-08) + + +### Features + +* **core:** add aliases for profile command ([#510](https://github.com/opentdf/otdfctl/issues/510)) ([45c633d](https://github.com/opentdf/otdfctl/commit/45c633da6b00b04a8c92686521d25144048ac62c)) +* **core:** Add support for WithTargetMode encrypt option ([#519](https://github.com/opentdf/otdfctl/issues/519)) ([a0ab213](https://github.com/opentdf/otdfctl/commit/a0ab2136be0b1d39e16a7522210f493fd797089d)) + + +### Bug Fixes + +* **core:** bump jwt dep and remove outdated version ([#520](https://github.com/opentdf/otdfctl/issues/520)) ([77bb9ca](https://github.com/opentdf/otdfctl/commit/77bb9ca9a0741ab7b920cc00f264a021064b117c)) + +## [0.19.0](https://github.com/opentdf/otdfctl/compare/v0.18.0...v0.19.0) (2025-03-05) + + +### Features + +* **core:** support for ec-wrapping ([#499](https://github.com/opentdf/otdfctl/issues/499)) ([e839445](https://github.com/opentdf/otdfctl/commit/e839445181c89447d9a2374d54ce5ea4c3f46320)) + + +### Bug Fixes + +* **core:** mark new algorithm flags experimental ([#501](https://github.com/opentdf/otdfctl/issues/501)) ([95e00bf](https://github.com/opentdf/otdfctl/commit/95e00bf3daa8eb05196a5839488a4718c2230210)) + +## [0.18.0](https://github.com/opentdf/otdfctl/compare/v0.17.1...v0.18.0) (2025-02-25) + + +### Features + +* Assertion verification ([#452](https://github.com/opentdf/otdfctl/issues/452)) ([5a8fe0d](https://github.com/opentdf/otdfctl/commit/5a8fe0d64088b74c95d3376e4a2a5a47d680d9c0)) +* **core:** Adding examples docs, mainly policy commands ([#461](https://github.com/opentdf/otdfctl/issues/461)) ([04c1743](https://github.com/opentdf/otdfctl/commit/04c17439bb5f68fb5d44ba96cb457ce9ca072250)) +* **core:** bump SDK and consume new platform connection validation ([#493](https://github.com/opentdf/otdfctl/issues/493)) ([1106b54](https://github.com/opentdf/otdfctl/commit/1106b54e73f9ceb711ff19d15cd08bf1cebbb29f)) +* **core:** Shows SDK version and spec info ([#474](https://github.com/opentdf/otdfctl/issues/474)) ([5a685c4](https://github.com/opentdf/otdfctl/commit/5a685c4e36cf524c4f594fac42cfec30f62a6e83)) + +## [0.17.1](https://github.com/opentdf/otdfctl/compare/v0.17.0...v0.17.1) (2024-12-09) + + +### Bug Fixes + +* **core:** kasr creation JSON example ([#453](https://github.com/opentdf/otdfctl/issues/453)) ([192c7b2](https://github.com/opentdf/otdfctl/commit/192c7b2975a4ab6f648ab7924e20e70535ce04b2)) + +## [0.17.0](https://github.com/opentdf/otdfctl/compare/v0.16.0...v0.17.0) (2024-12-05) + + +### Features + +* **core:** pagination of LIST commands ([#447](https://github.com/opentdf/otdfctl/issues/447)) ([673a064](https://github.com/opentdf/otdfctl/commit/673a06424d30e706798b9a1fa1bbfd9b4601e765)) +* **core:** subject condition set prune ([#439](https://github.com/opentdf/otdfctl/issues/439)) ([c4c8b8b](https://github.com/opentdf/otdfctl/commit/c4c8b8b276b2189df74e6cf30e14abac9369d97e)) + + +### Bug Fixes + +* **core:** kas registry get should allow -i 'id' flag shorthand ([#434](https://github.com/opentdf/otdfctl/issues/434)) ([bed3701](https://github.com/opentdf/otdfctl/commit/bed3701d89510ee78c3aed43b1a072e41ee3873f)) +* **core:** sm list should provide value fqn instead of just value string ([#438](https://github.com/opentdf/otdfctl/issues/438)) ([9a7cb72](https://github.com/opentdf/otdfctl/commit/9a7cb7242e0e39ccc2b54425028638fa0c5e3f9f)) + +## [0.16.0](https://github.com/opentdf/otdfctl/compare/v0.15.0...v0.16.0) (2024-11-20) + + +### Features + +* assertion verification disable ([#419](https://github.com/opentdf/otdfctl/issues/419)) ([acf5702](https://github.com/opentdf/otdfctl/commit/acf57028f1481f432b6b0c3c7a3e2c2261ac739f)) +* **core:** add `subject-mappings match` to CLI ([#413](https://github.com/opentdf/otdfctl/issues/413)) ([bc56c19](https://github.com/opentdf/otdfctl/commit/bc56c199a73b12b8c90045d1b6f9cc6fdec16c54)) +* **core:** add optional name to kas registry CRUD commands ([#429](https://github.com/opentdf/otdfctl/issues/429)) ([f675d86](https://github.com/opentdf/otdfctl/commit/f675d86c83205232db407d6609e80fa865a3998e)) +* **core:** adds assertions to encrypt subcommand ([#408](https://github.com/opentdf/otdfctl/issues/408)) ([8f0e906](https://github.com/opentdf/otdfctl/commit/8f0e906c1dfe99fe6aa5f2ff43d02f0da90474cf)) +* **core:** adds storeFile to save encrypted profiles to disk and updates auth to propagate tlsNoVerify ([#420](https://github.com/opentdf/otdfctl/issues/420)) ([f709e01](https://github.com/opentdf/otdfctl/commit/f709e014bf3f82a2808eae5df76b3667730c36ef)) +* refactor encrypt and decrypt + CLI examples ([#418](https://github.com/opentdf/otdfctl/issues/418)) ([e681823](https://github.com/opentdf/otdfctl/commit/e681823ad54ddf70f4aa2215438d69a3d02cf6eb)) +* support --with-access-token for auth ([#409](https://github.com/opentdf/otdfctl/issues/409)) ([856efa4](https://github.com/opentdf/otdfctl/commit/856efa4d61bb24b05f3a98943b94600ff77536fa)) + + +### Bug Fixes + +* **core:** dev selectors employ flattening from platform instead of jq ([#411](https://github.com/opentdf/otdfctl/issues/411)) ([57966ff](https://github.com/opentdf/otdfctl/commit/57966ffadcc61e1611869171bd3fc85723492fb7)) +* **core:** improve readability of TDF methods ([#424](https://github.com/opentdf/otdfctl/issues/424)) ([a88d386](https://github.com/opentdf/otdfctl/commit/a88d386b3dfe6e7bf210c632c92eb54069c1c5b8)) +* **core:** remove trailing slashes on host/platformEndpoint ([#415](https://github.com/opentdf/otdfctl/issues/415)) ([2ffd3c7](https://github.com/opentdf/otdfctl/commit/2ffd3c7707aa5c610f952d3499a7bfc76e8feca8)), closes [#414](https://github.com/opentdf/otdfctl/issues/414) +* **core:** revert profiles file system storage last commit ([#427](https://github.com/opentdf/otdfctl/issues/427)) ([79f2079](https://github.com/opentdf/otdfctl/commit/79f2079342bfbf210e07ce7cc6714deafea12b29)) +* updates sdk to 0.3.19 with GetTdfType fixes ([#425](https://github.com/opentdf/otdfctl/issues/425)) ([0a9adfe](https://github.com/opentdf/otdfctl/commit/0a9adfe416b966b09db4b9ee60fa379db93ede76)) + +## [0.15.0](https://github.com/opentdf/otdfctl/compare/v0.14.0...v0.15.0) (2024-10-15) + + +### Features + +* **core:** DSP-51 - deprecate PublicKey local field ([#400](https://github.com/opentdf/otdfctl/issues/400)) ([1955800](https://github.com/opentdf/otdfctl/commit/1955800fcd63c4d5044517ec0355a82c0e687f1b)) +* **core:** Update Resource Mapping delete to use get before delete for cli output ([#398](https://github.com/opentdf/otdfctl/issues/398)) ([79f2a42](https://github.com/opentdf/otdfctl/commit/79f2a423380cbd3f4a7805c4ec35d4657a9c0d5c)) + + +### Bug Fixes + +* **core:** build with latest opentdf releases ([#404](https://github.com/opentdf/otdfctl/issues/404)) ([969b82b](https://github.com/opentdf/otdfctl/commit/969b82b5cf90405002ac2da4a31b022dca9dfa37)) + +## [0.14.0](https://github.com/opentdf/otdfctl/compare/v0.13.0...v0.14.0) (2024-10-01) + + +### Features + +* **ci:** add e2e tests for subject mappings, support for --force delete ([#388](https://github.com/opentdf/otdfctl/issues/388)) ([c1f544b](https://github.com/opentdf/otdfctl/commit/c1f544b1079f52bfccb96c4c9e0b579a6854ad58)) +* **ci:** add tests for subject condition sets, and --force delete flag ([#389](https://github.com/opentdf/otdfctl/issues/389)) ([c6d2abc](https://github.com/opentdf/otdfctl/commit/c6d2abcd4afe78d92fd285e5c77fecdfe806ed5d)), closes [#331](https://github.com/opentdf/otdfctl/issues/331) +* **ci:** e2e attribute definitions tests ([#384](https://github.com/opentdf/otdfctl/issues/384)) ([2894391](https://github.com/opentdf/otdfctl/commit/28943915f19e0fb565cfb38cfebdd6fde21c019a)), closes [#327](https://github.com/opentdf/otdfctl/issues/327) +* **core:** export manual functions for CLI wrappers to consume ([#397](https://github.com/opentdf/otdfctl/issues/397)) ([aa0bf95](https://github.com/opentdf/otdfctl/commit/aa0bf95a39dfc0aec4155e498a2096cbd158efdd)) +* **core:** resource mappings LIST fix, delete --force support, and e2e tests ([#387](https://github.com/opentdf/otdfctl/issues/387)) ([326e74b](https://github.com/opentdf/otdfctl/commit/326e74b37d0abfb4ad50deadaa1ed46ecf9f8a5d)), closes [#386](https://github.com/opentdf/otdfctl/issues/386) + + +### Bug Fixes + +* **core:** remove duplicate titling of help manual ([#391](https://github.com/opentdf/otdfctl/issues/391)) ([cb8db69](https://github.com/opentdf/otdfctl/commit/cb8db69ec4df42c7f230fbd87142bfbcd2d3940f)) + +## [0.13.0](https://github.com/opentdf/otdfctl/compare/v0.12.2...v0.13.0) (2024-09-12) + + +### Features + +* add cli test mode and profile tests ([#313](https://github.com/opentdf/otdfctl/issues/313)) ([e0bc183](https://github.com/opentdf/otdfctl/commit/e0bc1836e8b5f14c87b5d572ad7937924c76d860)) +* **ci:** make e2e test workflow reusable ([#365](https://github.com/opentdf/otdfctl/issues/365)) ([d94408c](https://github.com/opentdf/otdfctl/commit/d94408cc2898d46b3444e874c035ff2bffe451f4)) +* **ci:** namespaces e2e tests and test suite improvements ([#351](https://github.com/opentdf/otdfctl/issues/351)) ([ce28555](https://github.com/opentdf/otdfctl/commit/ce285554866bf89ee8aa2df4a4b426548a58b59a)) +* **ci:** reusable platform composite action in e2e tests ([#369](https://github.com/opentdf/otdfctl/issues/369)) ([f7d5a1c](https://github.com/opentdf/otdfctl/commit/f7d5a1c07304bee14dfc92fa81bd65389e76d9f6)) +* **core:** add ecdsa-binding encrypt flag ([#360](https://github.com/opentdf/otdfctl/issues/360)) ([8702ec0](https://github.com/opentdf/otdfctl/commit/8702ec007b6d1354b6c0366e6b375f26216dfde1)) +* **core:** adds missing long manual output docs ([#362](https://github.com/opentdf/otdfctl/issues/362)) ([8e1390f](https://github.com/opentdf/otdfctl/commit/8e1390f20c17a5900c586f94384af76ffd9a2844)), closes [#359](https://github.com/opentdf/otdfctl/issues/359) +* **core:** kas-grants list ([#346](https://github.com/opentdf/otdfctl/issues/346)) ([7f51282](https://github.com/opentdf/otdfctl/commit/7f512825eab814e3c130e3fe4e8ed85ecbe2d146)), closes [#253](https://github.com/opentdf/otdfctl/issues/253) + + +### Bug Fixes + +* **ci:** e2e workflow should be fully reusable ([#368](https://github.com/opentdf/otdfctl/issues/368)) ([cc1e2b9](https://github.com/opentdf/otdfctl/commit/cc1e2b938fb0c8c4cf64d735f2961f7c9cae79fa)) +* **ci:** enhance lint config and resolve all lint issues ([#363](https://github.com/opentdf/otdfctl/issues/363)) ([5c1dbf1](https://github.com/opentdf/otdfctl/commit/5c1dbf1f5e441ca0ebd8cfcca145a77b623f3638)) +* **core:** GOOS, error message fixes ([#378](https://github.com/opentdf/otdfctl/issues/378)) ([623a82a](https://github.com/opentdf/otdfctl/commit/623a82ad3c1ed698a83eed54cf15a4f552096728)), closes [#380](https://github.com/opentdf/otdfctl/issues/380) +* **core:** metadata rendering cleanup ([#293](https://github.com/opentdf/otdfctl/issues/293)) ([ed21f81](https://github.com/opentdf/otdfctl/commit/ed21f81863450fd6167106711392e713a43c55be)) +* **core:** wire attribute value FQNs to encrypt ([#370](https://github.com/opentdf/otdfctl/issues/370)) ([21f9b80](https://github.com/opentdf/otdfctl/commit/21f9b80cdee7d695a308937b08dbc768d11fbbd5)) +* refactor to support varying print output ([#350](https://github.com/opentdf/otdfctl/issues/350)) ([d6932f3](https://github.com/opentdf/otdfctl/commit/d6932f30d9f653e46b32761a3257f3555ef0a6eb)) + +## [0.12.2](https://github.com/opentdf/otdfctl/compare/v0.12.1...v0.12.2) (2024-08-27) + + +### Bug Fixes + +* **core:** improve KASR docs and add spellcheck GHA to pipeline ([#323](https://github.com/opentdf/otdfctl/issues/323)) ([a77cf30](https://github.com/opentdf/otdfctl/commit/a77cf30dc8077d034cb4c9df8cc94712b1a17dff)), closes [#335](https://github.com/opentdf/otdfctl/issues/335) [#337](https://github.com/opentdf/otdfctl/issues/337) +* create new http client to ignore tls verification ([#324](https://github.com/opentdf/otdfctl/issues/324)) ([4d4afb7](https://github.com/opentdf/otdfctl/commit/4d4afb7e5b6411bb08a92bc53181ac5730ca1992)) + +## [0.12.1](https://github.com/opentdf/otdfctl/compare/v0.12.0...v0.12.1) (2024-08-26) + + +### Bug Fixes + +* **core:** remove documentation that cached kas pubkey is base64 ([#320](https://github.com/opentdf/otdfctl/issues/320)) ([fce8f44](https://github.com/opentdf/otdfctl/commit/fce8f44f767f35ccc4863f88d46e7ffcbd80f37a)), closes [#321](https://github.com/opentdf/otdfctl/issues/321) + +## [0.12.0](https://github.com/opentdf/otdfctl/compare/v0.11.4...v0.12.0) (2024-08-23) + + +### Features + +* **ci:** attr e2e tests with mixed casing ([#315](https://github.com/opentdf/otdfctl/issues/315)) ([50ce712](https://github.com/opentdf/otdfctl/commit/50ce712eab38f6686611e2b306bda5cacd55c28e)) +* **core:** kasr cached keys to deprecate local ([#318](https://github.com/opentdf/otdfctl/issues/318)) ([5419cc3](https://github.com/opentdf/otdfctl/commit/5419cc39e143eb484f836ca1ee671d626d5e2c60)), closes [#317](https://github.com/opentdf/otdfctl/issues/317) + +## [0.11.4](https://github.com/opentdf/otdfctl/compare/v0.11.3...v0.11.4) (2024-08-22) + + +### Bug Fixes + +* update workflow permissions ([#310](https://github.com/opentdf/otdfctl/issues/310)) ([3979fe8](https://github.com/opentdf/otdfctl/commit/3979fe85c9ab6511376d98b672cbfebddbf9bb84)) + +## [0.11.3](https://github.com/opentdf/otdfctl/compare/v0.11.2...v0.11.3) (2024-08-22) + + +### Bug Fixes + +* **core:** do not import unused fmt ([#306](https://github.com/opentdf/otdfctl/issues/306)) ([0dc552d](https://github.com/opentdf/otdfctl/commit/0dc552d3d6814f910c04d5f8cefa35404b4945f5)) +* **core:** nil panic on set-default ([#304](https://github.com/opentdf/otdfctl/issues/304)) ([92bbfa3](https://github.com/opentdf/otdfctl/commit/92bbfa32ae42b73b68551c2f9d3551d357bc5922)) +* **core:** warn and do now allow deletion of default profile ([#308](https://github.com/opentdf/otdfctl/issues/308)) ([fdd8167](https://github.com/opentdf/otdfctl/commit/fdd8167e8e2b22d652b48d796a756f86398bfd3c)) +* make file not building correctly ([#307](https://github.com/opentdf/otdfctl/issues/307)) ([64eb821](https://github.com/opentdf/otdfctl/commit/64eb82170fdcc50396194271be358bf9c9d43049)) + +## [0.11.2](https://github.com/opentdf/otdfctl/compare/v0.11.1...v0.11.2) (2024-08-22) + + +### Bug Fixes + +* disable tagging ([#302](https://github.com/opentdf/otdfctl/issues/302)) ([2b5db85](https://github.com/opentdf/otdfctl/commit/2b5db852ed0088e61f1180500135cd1865f9798b)) + +## [0.11.1](https://github.com/opentdf/otdfctl/compare/v0.11.0...v0.11.1) (2024-08-22) + + +### Bug Fixes + +* release-please tweak ([#300](https://github.com/opentdf/otdfctl/issues/300)) ([29fc836](https://github.com/opentdf/otdfctl/commit/29fc8360ae0b701aefe70b25d1838f442fd7eb8d)) + +## [0.11.0](https://github.com/opentdf/otdfctl/compare/v0.10.0...v0.11.0) (2024-08-22) + + +### Features + +* move git checkout before tagging ([#298](https://github.com/opentdf/otdfctl/issues/298)) ([1114e25](https://github.com/opentdf/otdfctl/commit/1114e25a90946e85622c8ff7a7befbf18beb4ba1)) + +## [0.10.0](https://github.com/opentdf/otdfctl/compare/v0.9.4...v0.10.0) (2024-08-22) + + +### Features + +* add profile support for cli ([#289](https://github.com/opentdf/otdfctl/issues/289)) ([15700f3](https://github.com/opentdf/otdfctl/commit/15700f3375196595e4a0ea3a7a6dea4da06d8612)) +* **core:** add scaffolding and POC for auth code flow ([#144](https://github.com/opentdf/otdfctl/issues/144)) ([03ecbfb](https://github.com/opentdf/otdfctl/commit/03ecbfb4f689f4a9f161a5a03d80efd50f728780)) +* **core:** support kas grants to namespaces ([#292](https://github.com/opentdf/otdfctl/issues/292)) ([f2c6689](https://github.com/opentdf/otdfctl/commit/f2c6689d2f775b1aed907d553c42d87c8464e6c7)), closes [#269](https://github.com/opentdf/otdfctl/issues/269) +* improve auth with client credentials ([#286](https://github.com/opentdf/otdfctl/issues/286)) ([9c4968f](https://github.com/opentdf/otdfctl/commit/9c4968f48d1ba23a61ed5c8ad23a109bf141ba56)) +* improve auth with client credentials ([#296](https://github.com/opentdf/otdfctl/issues/296)) ([0f533c7](https://github.com/opentdf/otdfctl/commit/0f533c7278a53ddd90656b3c7efcaee1c5bfd957)) + + +### Bug Fixes + +* **core:** bump platform deps ([#276](https://github.com/opentdf/otdfctl/issues/276)) ([e4ced99](https://github.com/opentdf/otdfctl/commit/e4ced996ae336b9db6db88906683f6600a2e5bf4)) +* reduce prints ([#277](https://github.com/opentdf/otdfctl/issues/277)) ([8b5734a](https://github.com/opentdf/otdfctl/commit/8b5734a18636071566fd8c4cfc808f3f240a02a5)) diff --git a/otdfctl/Makefile b/otdfctl/Makefile new file mode 100644 index 0000000000..c6fc513e4c --- /dev/null +++ b/otdfctl/Makefile @@ -0,0 +1,95 @@ +# We're going to be using this Makefile as a sort of task runner, for all sorts of operations in this project + +# first we'll grab the current version from our ENV VAR (added by our CI) - see here: https://github.com/marketplace/actions/version-increment +BINARY_NAME := otdfctl +CURR_VERSION := ${SEM_VER} +COMMIT_SHA := ${COMMIT_SHA} +BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') + +GO_MOD_LINE = $(shell head -n 1 go.mod | cut -c 8-) +GO_MOD_NAME = $(word 1,$(subst /, ,$(GO_MOD_LINE))) +APP_CFG = $(GO_MOD_LINE)/pkg/config + +GO_BUILD_FLAGS=-ldflags " \ + -X $(APP_CFG).Version=${CURR_VERSION} \ + -X $(APP_CFG).CommitSha=${COMMIT_SHA} \ + -X $(APP_CFG).BuildTime=${BUILD_TIME} \ +" +GO_BUILD_PREFIX=$(TARGET_DIR)/$(BINARY_NAME)-${CURR_VERSION} + +# If commit sha is not available try git +ifndef COMMIT_SHA + COMMIT_SHA := $(shell git rev-parse HEAD) +endif + +# If current version is not available try git +ifndef CURR_VERSION + CURR_VERSION := $(shell git describe --tags --always) +endif + +# Default target executed when no arguments are given to make. +# NOTE: .PHONY is used to indicate that the target is not a file (e.g. there is no file called 'build-darwin-amd64', instead the .PHONY directive tells make that the proceeding target is a command to be executed, not a file to be generated) +.PHONY: all +all: run +.DEFAULT_GOAL := run + +# Target directory for compiled binaries +TARGET_DIR=target + +# Output directory for the zipped artifacts +OUTPUT_DIR=output + +# Build commands for each platform (extra hyphen used in windows to avoid issues with the .exe extension) +PLATFORMS := \ + darwin-amd64 \ + darwin-arm64 \ + linux-amd64 \ + linux-arm \ + linux-arm64 \ + windows-amd64-.exe \ + windows-arm-.exe \ + windows-arm64-.exe + +build: test clean $(addprefix build-,$(PLATFORMS)) zip-builds verify-checksums + +build-%: + GOOS=$(word 1,$(subst -, ,$*)) \ + GOARCH=$(word 2,$(subst -, ,$*)) \ + go build $(GO_BUILD_FLAGS) \ + -o $(GO_BUILD_PREFIX)-$(word 1,$(subst -, ,$*))-$(word 2,$(subst -, ,$*))$(word 3,$(subst -, ,$*)) + +zip-builds: + ./scripts/zip-builds.sh $(BINARY_NAME)-$(CURR_VERSION) $(TARGET_DIR) $(OUTPUT_DIR) + +verify-checksums: + ./scripts/verify-checksums.sh $(OUTPUT_DIR) $(BINARY_NAME)-$(CURR_VERSION)_checksums.txt + +# Target for running the project (adjust as necessary for your project) +.PHONY: run +run: + go run . + +# Target for testing the project +.PHONY: test +test: + go test -v ./... + +.PHONY: build-test +build-test: + go build \ + -ldflags "\ + -X $(APP_CFG).TestMode=true \ + -X $(APP_CFG).Version=${CURR_VERSION}-testbuild \ + -X $(APP_CFG).CommitSha=${COMMIT_SHA} \ + -X $(APP_CFG).BuildTime=${BUILD_TIME} \ + " \ + -o $(BINARY_NAME)_testbuild + +.PHONY: test-bats +test-bats: build-test + ./e2e/resize_terminal.sh && bats ./e2e + +# Target for cleaning up the target directory +.PHONY: clean +clean: + rm -rf $(TARGET_DIR) diff --git a/otdfctl/README.md b/otdfctl/README.md new file mode 100644 index 0000000000..15e3c0d592 --- /dev/null +++ b/otdfctl/README.md @@ -0,0 +1,146 @@ +# otdfctl: cli to manage OpenTDF Platform + +This command line interface is used to manage OpenTDF Platform. + +The main goals are to: + +- simplify setup +- facilitate migration +- aid in configuration management + +## TODO list + +- [ ] Add support for json input as piped input +- [ ] Add help level handler for each command +- [ ] Add support for `--verbose` persistent flag +- [ ] Helper functions to support common tasks like pretty printing and json output + +## Usage + +The CLI is configured via profiles. Use `otdfctl profile create ` (and optionally `--set-default`) to define how the CLI should connect to your platform instance. + +Load up the platform (see its [README](https://github.com/opentdf/platform?tab=readme-ov-file#run) for instructions). + +## Development + +### CLI + +The CLI is built using [cobra](https://cobra.dev/). + +The primary function is to support CRUD operations using commands as arguments and flags as the values. + +The output format (currently `styled` or `json`) is stored with each profile (via `otdfctl profile create --output-format ` or `otdfctl profile set-output-format `) and can still be overridden per command with the `--json` flag. + +#### To add a command + +1. Capture the flag value and validate the values + 1. Alt support JSON input as piped input +2. Run the handler which is located in `pkg/handlers` and pass the values as arguments +3. Handle any errors and return the result in a lite TUI format + +### TUI + +> [!CAUTION] +> This is a work in progress please avoid touching until framework is defined + +The TUI will be used to create an interactive experience for the user. + +## Documentation + +Documentation drives the CLI in this project. This can be found in `/docs/man` and is used in the +CLI via the `man.Docs.GetDoc()` function. + +## Testing + +The CLI is equipped with a test mode that can be enabled by building the CLI with `config.TestMode = true`. +For convenience, the CLI can be built with `make build-test`. + +**Test Mode features**: + +- Use the in-memory keyring provider for user profiles +- Enable provisioning profiles for testing via `OTDFCTL_TEST_PROFILE` environment variable + +### BATS + +> [!NOTE] +> Bat Automated Test System (bats) is a TAP-compliant testing framework for Bash. It provides a simple way to verify that the UNIX programs you write behave as expected. + +BATS is used to test the CLI from an end-to-end perspective. To run the tests you will need to ensure the following +prerequisites are met: + +- bats is installed on your system + - Follow bats-core advice [here](https://github.com/bats-core/homebrew-bats-core?tab=readme-ov-file#homebrew-bats-core) +- The platform is running and provisioned with basic keycloak clients/users + - See the [platform README](https://github.com/opentdf/platform) for instructions + +To run the tests you can either run `make test-bats` or execute specific test suites with `bats e2e/.bats`. + +#### Terminal Size + +Some tests for output rendered in the terminal will vary in behavior depending on terminal size. + +Terminal size when testing: + +1. set to standard defaults if running `make test-bats` +2. can be set manually by mouse in terminal where tests are triggered +3. can be set by argument `./e2e/resize_terminal.sh < rows height > < columns width >` +4. can be set by environment variable, i.e. `export TEST_TERMINAL_WIDTH="200"` (200 is columns width) + +#### TestRail Integration (Optional) + +This project supports optional integration with TestRail for uploading BATS test results. + +##### 1. Prerequisites + +- TestRail account with API access enabled +- `jq`, `curl` installed + +##### 2. Setup + +1. Copy and configure TestRail connection: +`cp testrail.config.example.json testrail.config.json` + + Edit `testrail.config.json` with: + - `url`: Your TestRail instance URL + - `projectId`: Your TestRail project ID + - `tapFile`: Path to BATS TAP results file + +2. Copy and configure test mapping: + `cp testname-to-testrail-id.example.json testname-to-testrail-id.json` + +Fill in the mapping between test names (exactly as they appear in TAP output) and your TestRail case IDs. +Can be flat JSON: +```json +{ + "test_name_1": "C12345", + "test_name_2": "C67890" +} +``` +Or nested for better organization: +```json +{ + "group_1": { + "test_name_1": "C12345", + "test_name_2": "C67890" + }, + "group_2": { + "test_name_3": "C54321" + } +} +``` + +3. Set TestRail credentials via environment variables +```bash + export TESTRAIL_USER=you@example.com + export TESTRAIL_PASS=your_api_key +``` + +##### 3. Run Tests and Upload Results + +1. Run BATS with TAP report output (e2e folder): `bats --tap bats-tests/ > e2e/bats-results.tap` +Alternatively, get the TAP test report from the CI pipeline artifacts. +2. Upload results to TestRail: +`TESTRAIL_CLI_RUN_NAME=*optional-testrail-run-name* ./testrail-integration/upload-bats-test-results-to-testrail.sh` + + + diff --git a/otdfctl/adr/0000-use-adr-dir-for-adr.md b/otdfctl/adr/0000-use-adr-dir-for-adr.md new file mode 100644 index 0000000000..2505901830 --- /dev/null +++ b/otdfctl/adr/0000-use-adr-dir-for-adr.md @@ -0,0 +1,44 @@ +--- +status: accepted +date: 2024-08-29 +decision: Use ADRs in the `adr` directory of the repo to document architectural decisions +author: '@jakedoublev' +deciders: ['@ryanulit', '@jrschumacher'] +--- + +# Use a ADR storage format that make diffs easier to read + +## Context and Problem Statement + +We've been using Github Issues to document ADR decisions, but it's hard to read the diffs when changes are made. We need a better way to store and manage ADRs. ADRs sometimes get updated and it's hard to track the changes and decision using the edit history dropdown or the comments section. + +## Decision Drivers + +- **Low barrier of entry**: A primary goal of our ADR process is to ensure decisions are captured. +- **Ease of management**: Make it easy to manage the ADRs. +- **Ensure appropriate tracking and review**: Make it easy to track and review the changes in the ADRs. + +## Considered Options + +1. Use Github Issues +2. Use Github Discussions +3. Use a shared ADR repository +4. Use an `adr` directory in the repo + +## Decision Outcome + +It was decided to use an `adr` directory in the repo to store ADRs. This approach provides a low barrier of entry for developers to document decisions and ensures that the decisions are tracked and reviewed appropriately. + +Additionally, this change does not impact other teams or repositories, and it is easy to manage and maintain. We can experiment with this decision and if it works promote it to other repositories. + +### Consequences + +- **Positive**: + - Low barrier of entry for developers to document decisions. + - Easy to manage and maintain. + - Ensures appropriate tracking and review of decisions via git history and code review. +- **Negative**: + - Requires developers to be aware of the ADR process and where to find the ADRs. + - May require additional tooling to manage and maintain the ADRs. + - May require additional training for developers to understand the ADR process and how to use it effectively. + diff --git a/otdfctl/adr/0001-printing-with-json.md b/otdfctl/adr/0001-printing-with-json.md new file mode 100644 index 0000000000..7f8d835976 --- /dev/null +++ b/otdfctl/adr/0001-printing-with-json.md @@ -0,0 +1,39 @@ +--- +status: accepted +date: 2024-08-29 +decision: Encapsulate printing to ensure consistent output format +author: '@jrschumacher' +deciders: ['@jakedoublev', '@ryanulit', '@suchak1'] +--- + +# Consistent output format for printing JSON and pretty-print + +## Context and Problem Statement + +We need to develop a printer that can globally determine when to print in pretty-print format versus JSON format. This decision is crucial to ensure consistent and appropriate output formatting across different use cases and environments. + +## Decision Drivers + +- **Consistency**: Ensure uniform output format across the application. +- **Flexibility**: Ability to switch between pretty-print and JSON formats based on context. +- **Ease of Implementation**: Simplicity in implementing and maintaining the solution. + +## Considered Options + +1. Keep existing code as is +2. Move the printing into a global function that has context about the CLI flags to drive output format + +## Decision Outcome + +It was decided to encapsulate printing to ensure there is consistent output format. This function will have context about the CLI flags to drive the output format. This approach provides the flexibility to switch between pretty-print and JSON formats based on the context. + +### Consequences + +- **Positive**: + - Provides flexibility to switch formats without changing the code. + - Ensures consistent output format across different environments. + - Simplifies the implementation and maintenance process. + +- **Negative**: + - Requires careful management of configuration settings. + - Potential for misconfiguration leading to incorrect output format when developers use `fmt` directly. diff --git a/otdfctl/cmd/auth/auth.go b/otdfctl/cmd/auth/auth.go new file mode 100644 index 0000000000..69f42012cf --- /dev/null +++ b/otdfctl/cmd/auth/auth.go @@ -0,0 +1,36 @@ +package auth + +import ( + "runtime" + + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +var ( + authCmd = man.Docs.GetCommand("auth", man.WithHiddenFlags( + "with-client-creds", + "with-client-creds-file", + )) + + Cmd = &authCmd.Command +) + +func InitCommands() { + authCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + // not supported on linux + if runtime.GOOS == "linux" { + cli.ExitWithWarning( + "Warning: Keyring storage is not available on Linux. Please use the `--with-client-creds` flag or the" + + "`--with-client-creds-file` flag to provide client credentials securely.", + ) + } + } + + Cmd.AddCommand(newLoginCmd()) + Cmd.AddCommand(newLogoutCmd()) + Cmd.AddCommand(newClientCredentialsCmd()) + Cmd.AddCommand(newClearClientCredentialsCmd()) + Cmd.AddCommand(newPrintAccessTokenCmd()) +} diff --git a/otdfctl/cmd/auth/clearCachedCredentials.go b/otdfctl/cmd/auth/clearCachedCredentials.go new file mode 100644 index 0000000000..05d532af82 --- /dev/null +++ b/otdfctl/cmd/auth/clearCachedCredentials.go @@ -0,0 +1,12 @@ +package auth + +import ( + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +// newClearClientCredentialsCmd creates and configures the clear-client-credentials command. +func newClearClientCredentialsCmd() *cobra.Command { + doc := man.Docs.GetCommand("auth/clear-client-credentials") + return &doc.Command +} diff --git a/otdfctl/cmd/auth/clientCredentials.go b/otdfctl/cmd/auth/clientCredentials.go new file mode 100644 index 0000000000..499779d138 --- /dev/null +++ b/otdfctl/cmd/auth/clientCredentials.go @@ -0,0 +1,78 @@ +package auth + +import ( + "fmt" + "strings" + + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/auth" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/spf13/cobra" +) + +func clientCredentialsRun(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + cp := common.InitProfile(c) + + var clientID string + var clientSecret string + + if len(args) > 0 { + clientID = args[0] + } + if len(args) > 1 { + clientSecret = args[1] + } + + if clientID == "" { + clientID = cli.AskForInput("Enter client id: ") + } + if clientSecret == "" { + clientSecret = cli.AskForSecret("Enter client secret: ") + } + var scopes []string + if cmd.Flags().Changed("scopes") { + flagScopes, err := cmd.Flags().GetStringSlice("scopes") + if err != nil { + c.ExitWithError("Failed to read scopes flag", err) + } + scopes = make([]string, 0, len(flagScopes)) + for _, scope := range flagScopes { + scopes = append(scopes, strings.TrimSpace(scope)) + } + } + + // Set the client credentials + err := cp.SetAuthCredentials(profiles.AuthCredentials{ + AuthType: profiles.AuthTypeClientCredentials, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopes, + }) + if err != nil { + c.ExitWithError("Failed to set client credentials", err) + } + + // Validate the client credentials + if err := auth.ValidateProfileAuthCredentials(cmd.Context(), cp); err != nil { + c.ExitWithError("An error occurred during login. Please check your credentials and try again", err) + } + + c.ExitWithMessage(fmt.Sprintf("Client credentials set for profile [%s]", cp.Name()), cli.ExitCodeSuccess) +} + +// newClientCredentialsCmd creates and configures the client-credentials command. +func newClientCredentialsCmd() *cobra.Command { + doc := man.Docs.GetCommand("auth/client-credentials", + man.WithRun(clientCredentialsRun), + man.WithHiddenFlags("with-client-creds", "with-client-creds-file"), + ) + doc.Flags().StringSlice( + doc.GetDocFlag("scopes").Name, + []string{}, + doc.GetDocFlag("scopes").Description, + ) + return &doc.Command +} diff --git a/otdfctl/cmd/auth/login.go b/otdfctl/cmd/auth/login.go new file mode 100644 index 0000000000..c39bbf05f7 --- /dev/null +++ b/otdfctl/cmd/auth/login.go @@ -0,0 +1,66 @@ +package auth + +import ( + "fmt" + + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/auth" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/spf13/cobra" +) + +func codeLogin(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + cp := common.InitProfile(c) + clientID := c.FlagHelper.GetRequiredString("client-id") + port := c.FlagHelper.GetOptionalString("port") + tok, err := auth.LoginWithPKCE( + cmd.Context(), + cp.GetEndpoint(), + clientID, + c.FlagHelper.GetOptionalBool("tls-no-verify"), + port, + ) + if err != nil { + c.ExitWithError("could not authenticate", err) + } + + // Set the auth credentials to profile + if err := cp.SetAuthCredentials(profiles.AuthCredentials{ + AuthType: profiles.AuthTypeAccessToken, + AccessToken: profiles.AuthCredentialsAccessToken{ + ClientID: clientID, + AccessToken: tok.AccessToken, + Expiration: tok.Expiry.Unix(), + RefreshToken: tok.RefreshToken, + }, + }); err != nil { + c.ExitWithError("failed to set auth credentials", err) + } + c.ExitWithMessage(fmt.Sprintf("Code login complete for profile: [%s]", cp.Name()), cli.ExitCodeSuccess) +} + +// newLoginCmd creates and configures the login command with all flags. +func newLoginCmd() *cobra.Command { + doc := man.Docs.GetCommand("auth/login", man.WithRun(codeLogin)) + + // Register flags + doc.Flags().StringP( + doc.GetDocFlag("client-id").Name, + doc.GetDocFlag("client-id").Shorthand, + doc.GetDocFlag("client-id").Default, + doc.GetDocFlag("client-id").Description, + ) + + // intentionally a string flag to support an empty port which represents a dynamic port + doc.Flags().StringP( + doc.GetDocFlag("port").Name, + doc.GetDocFlag("port").Shorthand, + doc.GetDocFlag("port").Default, + doc.GetDocFlag("port").Description, + ) + + return &doc.Command +} diff --git a/otdfctl/cmd/auth/logout.go b/otdfctl/cmd/auth/logout.go new file mode 100644 index 0000000000..dda2d0fc7a --- /dev/null +++ b/otdfctl/cmd/auth/logout.go @@ -0,0 +1,42 @@ +package auth + +import ( + "fmt" + + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/auth" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/spf13/cobra" +) + +func logout(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + cp := common.InitProfile(c) + + // we can only revoke access tokens stored for the code login flow, not client credentials + creds := cp.GetAuthCredentials() + if creds.AuthType == profiles.AuthTypeAccessToken { + if err := auth.RevokeAccessToken( + cmd.Context(), + cp.GetEndpoint(), + creds.AccessToken.ClientID, + creds.AccessToken.RefreshToken, + c.FlagHelper.GetOptionalBool("tls-no-verify"), + ); err != nil { + c.ExitWithError("An error occurred while revoking the access token", err) + } + } + + if err := cp.SetAuthCredentials(profiles.AuthCredentials{}); err != nil { + c.ExitWithError("An error occurred while logging out", err) + } + c.ExitWithMessage(fmt.Sprintf("Profile: [%s], logged out", cp.Name()), cli.ExitCodeSuccess) +} + +// newLogoutCmd creates and configures the logout command. +func newLogoutCmd() *cobra.Command { + doc := man.Docs.GetCommand("auth/logout", man.WithRun(logout)) + return &doc.Command +} diff --git a/otdfctl/cmd/auth/printAccessToken.go b/otdfctl/cmd/auth/printAccessToken.go new file mode 100644 index 0000000000..aebd504757 --- /dev/null +++ b/otdfctl/cmd/auth/printAccessToken.go @@ -0,0 +1,38 @@ +package auth + +import ( + "fmt" + "os" + + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/auth" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/spf13/cobra" +) + +func printAccessTokenRun(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + cp := common.InitProfile(c) + + ac := cp.GetAuthCredentials() + switch ac.AuthType { + case profiles.AuthTypeClientCredentials: + case profiles.AuthTypeAccessToken: + default: + c.ExitWithError("Invalid auth type", nil) + } + tok, err := auth.GetTokenWithProfile(cmd.Context(), cp) + if err != nil { + cli.ExitWithError("Failed to get token", err) + } + + c.ExitWith(fmt.Sprintf("Access Token: %s\n", tok.AccessToken), tok, cli.ExitCodeSuccess, os.Stdout) +} + +// newPrintAccessTokenCmd creates and configures the print-access-token command. +func newPrintAccessTokenCmd() *cobra.Command { + doc := man.Docs.GetCommand("auth/print-access-token", man.WithRun(printAccessTokenRun)) + return &doc.Command +} diff --git a/otdfctl/cmd/common/common.go b/otdfctl/cmd/common/common.go new file mode 100644 index 0000000000..f3532c1387 --- /dev/null +++ b/otdfctl/cmd/common/common.go @@ -0,0 +1,228 @@ +package common + +import ( + "crypto/tls" + "errors" + "fmt" + "log/slog" + + "github.com/evertras/bubble-table/table" + osprofiles "github.com/jrschumacher/go-osprofiles" + "github.com/opentdf/platform/otdfctl/pkg/auth" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/config" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/opentdf/platform/sdk" + "github.com/spf13/cobra" +) + +var profileOutputFormat = profiles.OutputStyled + +func shouldUseProfileJSONOutput() bool { + return profileOutputFormat == profiles.OutputJSON +} + +func applyOutputFormatPreference(c *cli.Cli, store *profiles.OtdfctlProfileStore) { + if store == nil { + return + } + + profileOutputFormat = profiles.NormalizeOutputFormat(store.GetOutputFormat()) + if shouldUseProfileJSONOutput() { + c.SetJSONOutput(true) + } +} + +// InitProfile initializes the profile store and loads the profile specified in the flags +// if onlyNew is set to true, a new profile will be created and returned +// returns the profile and the current profile store +func InitProfile(c *cli.Cli) *profiles.OtdfctlProfileStore { + var err error + profileName := c.FlagHelper.GetOptionalString("profile") + + hasKeyringStore, err := osprofiles.HasGlobalStore(config.AppName, osprofiles.WithKeyringStore()) + if err != nil { + slog.Warn("could not determine whether any profiles were stored on the keyring, defaulting to filesystem", + slog.Any("error", err), + ) + } + if hasKeyringStore { + slog.Debug("keyring store still active, migrating profiles to filesystem") + err := profiles.Migrate(profiles.ProfileDriverFileSystem, profiles.ProfileDriverKeyring) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Error during profile migration from %s, to %s. %s cannot continue with profiles being stored within %s, please use the `profile migrate` command to manually migrate profiles", profiles.ProfileDriverKeyring, profiles.ProfileDriverFileSystem, config.AppName, profiles.ProfileDriverKeyring), err) + } + } + + profiler, err := profiles.CreateProfiler(profiles.ProfileDriverFileSystem) + if err != nil { + cli.ExitWithError("Error creating profiler", err) + } + + defaultProfileName := osprofiles.GetGlobalConfig(profiler).GetDefaultProfile() + if len(defaultProfileName) == 0 { + c.ExitWithWarning(fmt.Sprintf("No default profile set. Use `%s profile create ` to create a default profile.", config.AppName)) + } + + if profileName == "" { + profileName = defaultProfileName + } + + slog.Debug("using profile", slog.String("profile", profileName)) + + // load profile + store, err := profiles.LoadOtdfctlProfileStore(profiles.ProfileDriverFileSystem, profileName) + if err != nil { + c.ExitWithError("Failed to load profile: "+profileName, err) + } + + applyOutputFormatPreference(c, store) + + return store +} + +// instantiates a new handler with authentication via client credentials +// TODO make this a preRun hook +// +//nolint:nestif // separate refactor [https://github.com/opentdf/otdfctl/issues/383] +func NewHandler(c *cli.Cli) handlers.Handler { + // if global flags are set then validate and create a temporary profile in memory + var cp *profiles.OtdfctlProfileStore + + // Non-profile flags + host := c.FlagHelper.GetOptionalString("host") + tlsNoVerify := c.FlagHelper.GetOptionalBool("tls-no-verify") + withClientCreds := c.FlagHelper.GetOptionalString("with-client-creds") + withClientCredsFile := c.FlagHelper.GetOptionalString("with-client-creds-file") + withAccessToken := c.FlagHelper.GetOptionalString("with-access-token") + var inMemoryProfile bool + + authFlags := []string{"--with-access-token", "--with-client-creds", "--with-client-creds-file"} + nonProfileFlags := append([]string{"--host", "--tls-no-verify"}, authFlags...) + hasNonProfileFlags := host != "" || tlsNoVerify || withClientCreds != "" || withClientCredsFile != "" || withAccessToken != "" + + //nolint:nestif // nested if statements are necessary for validation + if hasNonProfileFlags { + err := fmt.Errorf("when using global flags %s, profiles will not be used and all required flags must be set", cli.PrettyList(nonProfileFlags)) + + // host must be set + if host == "" { + cli.ExitWithError("Host must be set", err) + } + + authFlagsCounter := 0 + if withAccessToken != "" { + authFlagsCounter++ + } + if withClientCreds != "" { + authFlagsCounter++ + } + if withClientCredsFile != "" { + authFlagsCounter++ + } + if authFlagsCounter == 0 { + cli.ExitWithError(fmt.Sprintf("One of %s must be set", cli.PrettyList(authFlags)), err) + } else if authFlagsCounter > 1 { + cli.ExitWithError(fmt.Sprintf("Only one of %s must be set", cli.PrettyList(authFlags)), err) + } + + inMemoryProfile = true + config := profiles.ProfileConfig{ + Name: "temp", + Endpoint: host, + TLSNoVerify: tlsNoVerify, + } + cp, err = profiles.NewOtdfctlProfileStore(profiles.ProfileDriverMemory, &config, true) + if err != nil { + cli.ExitWithError("Failed to initialize in-memory profile", err) + } + + // get credentials from flags + if withAccessToken != "" { + claims, err := auth.ParseClaimsJWT(withAccessToken) + if err != nil { + cli.ExitWithError("Failed to get access token", err) + } + + if err := cp.SetAuthCredentials(profiles.AuthCredentials{ + AuthType: profiles.AuthTypeAccessToken, + AccessToken: profiles.AuthCredentialsAccessToken{ + AccessToken: withAccessToken, + Expiration: claims.Expiration, + }, + }); err != nil { + cli.ExitWithError("Failed to set access token", err) + } + } else { + var cc auth.ClientCredentials + if withClientCreds != "" { + cc, err = auth.GetClientCredsFromJSON([]byte(withClientCreds)) + } else if withClientCredsFile != "" { + cc, err = auth.GetClientCredsFromFile(withClientCredsFile) + } + if err != nil { + cli.ExitWithError("Failed to get client credentials", err) + } + + // add credentials to the temporary profile + if err := cp.SetAuthCredentials(profiles.AuthCredentials{ + AuthType: profiles.AuthTypeClientCredentials, + ClientID: cc.ClientID, + ClientSecret: cc.ClientSecret, + Scopes: cc.Scopes, + }); err != nil { + cli.ExitWithError("Failed to set client credentials", err) + } + + applyOutputFormatPreference(c, cp) + } + } else { + cp = InitProfile(c) + } + + if err := auth.ValidateProfileAuthCredentials(c.Context(), cp); err != nil { + endpoint := cp.GetEndpoint() + var certErr *tls.CertificateVerificationError + if errors.As(err, &certErr) { + cli.ExitWithError(fmt.Sprintf("Failed to validate TLS certificates served at '%s'. Caution: if host is correct and insecure certificates should be dangerously trusted, use '--tls-no-verify'", endpoint), nil) + } + if errors.Is(err, sdk.ErrPlatformUnreachable) { + cli.ExitWithError(fmt.Sprintf("Failed to connect to the platform. Is the platform accepting connections at '%s'?", endpoint), nil) + } + if errors.Is(err, sdk.ErrPlatformConfigFailed) { + cli.ExitWithError(fmt.Sprintf("Failed to get the platform configuration. Is the platform serving a well-known configuration at '%s'?", endpoint), nil) + } + if inMemoryProfile { + cli.ExitWithError("Failed to authenticate with flag-provided client credentials.", err) + } + if errors.Is(err, auth.ErrProfileCredentialsNotFound) { + cli.ExitWithWarning("Profile missing credentials. Please login or add client credentials.") + } + + if errors.Is(err, auth.ErrAccessTokenExpired) { + cli.ExitWithWarning("Access token expired. Please login or add flag-provided credentials.") + } + if errors.Is(err, auth.ErrAccessTokenNotFound) { + cli.ExitWithWarning("No access token found. Please login or add flag-provided credentials.") + } + cli.ExitWithError("Failed to get access token.", err) + } + + h, err := handlers.New(handlers.WithProfile(cp)) + if err != nil { + cli.ExitWithError("Unexpected error", err) + } + return h +} + +// HandleSuccess prints a success message according to the configured format (styled table or JSON) +func HandleSuccess(command *cobra.Command, id string, t table.Model, policyObject interface{}) { + c := cli.New(command, []string{}) + jsonFlag := c.Flags.GetOptionalBool("json") + if jsonFlag || shouldUseProfileJSONOutput() { + c.SetJSONOutput(true) + c.ExitWithJSON(policyObject, cli.ExitCodeSuccess) + } + cli.PrintSuccessTable(command, id, t) +} diff --git a/otdfctl/cmd/config/config.go b/otdfctl/cmd/config/config.go new file mode 100644 index 0000000000..73b2b01c05 --- /dev/null +++ b/otdfctl/cmd/config/config.go @@ -0,0 +1,28 @@ +package config + +import ( + "github.com/opentdf/platform/otdfctl/pkg/man" +) + +var ( + outputDoc = man.Docs.GetCommand("config/output") + configDoc = man.Docs.GetCommand("config", man.WithSubcommands(outputDoc)) + Cmd = &configDoc.Command +) + +const ( + cfgDeprecationNotice = "use profile commands" + cfgOutputDeprecationMsg = "use profile set-output-format instead" +) + +func InitCommands() { + // Mark the entire config command as deprecated so users migrate to profiles. + Cmd.Deprecated = cfgDeprecationNotice + outputDoc.Deprecated = cfgOutputDeprecationMsg + + outputDoc.Flags().String( + outputDoc.GetDocFlag("format").Name, + outputDoc.GetDocFlag("format").Default, + outputDoc.GetDocFlag("format").Description, + ) +} diff --git a/otdfctl/cmd/dev/dev.go b/otdfctl/cmd/dev/dev.go new file mode 100644 index 0000000000..d80bdd9dde --- /dev/null +++ b/otdfctl/cmd/dev/dev.go @@ -0,0 +1,66 @@ +//nolint:forbidigo // print statements need flexibility +package dev + +import ( + "fmt" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +var ( + // Command holding playground-style development + devCmd = man.Docs.GetCommand("dev") + + Cmd = &devCmd.Command +) + +func designSystemRun(cmd *cobra.Command, args []string) { + fmt.Print("Design system\n=============\n\n") + + printDSComponent("Table", renderDSTable()) + + printDSComponent("Messages", renderDSMessages()) +} + +func printDSComponent(title string, component string) { + fmt.Printf("%s\n", title) + fmt.Print("-----\n\n") + fmt.Printf("%s\n", component) + fmt.Print("\n\n") +} + +func renderDSTable() string { + tbl := cli.NewTable( + table.NewFlexColumn("one", "One", cli.FlexColumnWidthOne), + table.NewFlexColumn("two", "Two", cli.FlexColumnWidthOne), + table.NewFlexColumn("three", "Three", cli.FlexColumnWidthOne), + ).WithRows([]table.Row{ + table.NewRow(table.RowData{ + "one": "1", + "two": "2", + "three": "3", + }), + table.NewRow(table.RowData{ + "one": "4", + "two": "5", + "three": "6", + }), + }) + return tbl.View() +} + +func renderDSMessages() string { + return cli.SuccessMessage("Success message") + "\n" + cli.ErrorMessage("Error message", nil) +} + +func InitCommands() { + designCmd := man.Docs.GetCommand("dev/design-system", + man.WithRun(designSystemRun), + ) + devCmd.AddCommand(&designCmd.Command) + + initSelectorsCommands() +} diff --git a/otdfctl/cmd/dev/selectors.go b/otdfctl/cmd/dev/selectors.go new file mode 100644 index 0000000000..a7a7ed9057 --- /dev/null +++ b/otdfctl/cmd/dev/selectors.go @@ -0,0 +1,97 @@ +package dev + +import ( + "fmt" + + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +var selectors []string + +func selectorsGen(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + subject := c.Flags.GetRequiredString("subject") + + flattened, err := handlers.FlattenSubjectContext(subject) + if err != nil { + cli.ExitWithError("Failed to parse subject context keys and values", err) + } + + rows := [][]string{} + for _, item := range flattened { + rows = append(rows, []string{item.Key, fmt.Sprintf("%v", item.Value)}) + } + + t := cli.NewTabular(rows...) + cli.PrintSuccessTable(cmd, "", t) +} + +func selectorsTest(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + subject := c.Flags.GetRequiredString("subject") + selectors = c.Flags.GetStringSlice("selector", selectors, cli.FlagsStringSliceOptions{Min: 1}) + + flattened, err := handlers.FlattenSubjectContext(subject) + if err != nil { + cli.ExitWithError("Failed to process subject context keys and values", err) + } + + rows := [][]string{} + for _, item := range flattened { + for _, selector := range selectors { + if selector == item.Key { + rows = append(rows, []string{item.Key, fmt.Sprintf("%v", item.Value)}) + } + } + } + + t := cli.NewTabular(rows...) + cli.PrintSuccessTable(cmd, "", t) +} + +// initSelectorsCommands sets up the selectors subcommand and its children. +// Called from dev.go InitCommands. +func initSelectorsCommands() { + genCmd := man.Docs.GetCommand("dev/selectors/generate", + man.WithRun(selectorsGen), + ) + genCmd.Flags().StringP( + genCmd.GetDocFlag("subject").Name, + genCmd.GetDocFlag("subject").Shorthand, + genCmd.GetDocFlag("subject").Default, + genCmd.GetDocFlag("subject").Description, + ) + + testCmd := man.Docs.GetCommand("dev/selectors/test", + man.WithRun(selectorsTest), + ) + testCmd.Flags().StringP( + testCmd.GetDocFlag("subject").Name, + testCmd.GetDocFlag("subject").Shorthand, + testCmd.GetDocFlag("subject").Default, + testCmd.GetDocFlag("subject").Description, + ) + testCmd.Flags().StringSliceVarP( + &selectors, + testCmd.GetDocFlag("selector").Name, + testCmd.GetDocFlag("selector").Shorthand, + []string{}, + testCmd.GetDocFlag("selector").Description, + ) + + devSelectors := man.Docs.GetCommand("dev/selectors", + man.WithSubcommands(genCmd, testCmd), + ) + + Cmd.AddCommand(&devSelectors.Command) +} diff --git a/otdfctl/cmd/execute.go b/otdfctl/cmd/execute.go new file mode 100644 index 0000000000..32dc8ec7c3 --- /dev/null +++ b/otdfctl/cmd/execute.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "errors" + "os" + + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/spf13/cobra" +) + +type ExecuteConfig struct { + mountTo *cobra.Command + renameCmd *cobra.Command + cmdName string +} +type ExecuteOptFunc func(c ExecuteConfig) ExecuteConfig + +func WithMountTo(cmd *cobra.Command, renameCmd *cobra.Command) ExecuteOptFunc { + if cmd == nil { + panic("cmd is nil") + } + + return func(c ExecuteConfig) ExecuteConfig { + c.cmdName = cmd.Use + if renameCmd.Use != "" { + c.cmdName = renameCmd.Use + } + c.mountTo = cmd + c.renameCmd = renameCmd + return c + } +} + +func Execute(opts ...ExecuteOptFunc) { + c := ExecuteConfig{} + for _, opt := range opts { + c = opt(c) + } + + if c.mountTo != nil { + err := MountRoot(c.mountTo, c.renameCmd) + if err != nil { + os.Exit(cli.ExitCodeError) + } + } else { + err := RootCmd.Execute() + if err != nil { + os.Exit(cli.ExitCodeError) + } + } +} + +func MountRoot(newRoot *cobra.Command, cmd *cobra.Command) error { + if newRoot == nil { + return errors.New("newRoot is nil") + } + + if cmd != nil { + RootCmd.Use = cmd.Use + RootCmd.Short = cmd.Short + RootCmd.Long = cmd.Long + } + + newRoot.AddCommand(RootCmd) + return nil +} diff --git a/otdfctl/cmd/execute_test.go b/otdfctl/cmd/execute_test.go new file mode 100644 index 0000000000..770649ad12 --- /dev/null +++ b/otdfctl/cmd/execute_test.go @@ -0,0 +1,83 @@ +package cmd + +import ( + "os" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_MountRoot(t *testing.T) { + r, w, _ := os.Pipe() + origStdout := os.Stdout + os.Stdout = w + + rootCmd := cobra.Command{ + Use: "new-root", + Short: "new-root short", + Long: "new-root long", + } + + err := MountRoot(&rootCmd, nil) + require.NoError(t, err) + + assert.Equal(t, "new-root", rootCmd.Use) + assert.Equal(t, "new-root short", rootCmd.Short) + assert.Equal(t, "new-root long", rootCmd.Long) + + err = rootCmd.Execute() + require.NoError(t, err) + buf := make([]byte, 1024) + n, err := r.Read(buf) + require.NoError(t, err) + + // Ensure the old root is added with the existing name + assert.Contains(t, string(buf[:n]), "otdfctl") + + os.Stdout = origStdout +} + +func Test_MountRootWithRename(t *testing.T) { + r, w, _ := os.Pipe() + origStdout := os.Stdout + os.Stdout = w + + rootCmd := cobra.Command{ + Use: "new-root", + Short: "new-root short", + Long: "new-root long", + } + + err := MountRoot(&rootCmd, &cobra.Command{ + Use: "rename-otdfctl", + Short: "rename-otdfctl short", + Long: "rename-otdfctl long", + }) + require.NoError(t, err) + + assert.Equal(t, "new-root", rootCmd.Use) + assert.Equal(t, "new-root short", rootCmd.Short) + assert.Equal(t, "new-root long", rootCmd.Long) + + err = rootCmd.Execute() + require.NoError(t, err) + buf := make([]byte, 1024) + n, err := r.Read(buf) + require.NoError(t, err) + + // Ensure the old root is added as a subcommand and renamed + assert.Contains(t, string(buf[:n]), "rename-otdfctl") + + os.Stdout = origStdout +} + +func Test_MountRootError(t *testing.T) { + require.Error(t, MountRoot(nil, nil)) + require.Error(t, MountRoot(nil, &cobra.Command{ + Use: "rename-otdfctl", + Short: "rename-otdfctl short", + Long: "rename-otdfctl long", + })) +} diff --git a/otdfctl/cmd/interactive.go b/otdfctl/cmd/interactive.go new file mode 100644 index 0000000000..105c8458e2 --- /dev/null +++ b/otdfctl/cmd/interactive.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/tui" + "github.com/spf13/cobra" +) + +// newInteractiveCmd creates and configures the interactive command. +func newInteractiveCmd() *cobra.Command { + doc := man.Docs.GetCommand("interactive", + man.WithRun(func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + //nolint:errcheck // error does not need to be checked + tui.StartTea(h) + }), + ) + return &doc.Command +} diff --git a/otdfctl/cmd/migrate/migrate.go b/otdfctl/cmd/migrate/migrate.go new file mode 100644 index 0000000000..8f9673dc5b --- /dev/null +++ b/otdfctl/cmd/migrate/migrate.go @@ -0,0 +1,35 @@ +package migrate + +import ( + "github.com/opentdf/platform/otdfctl/cmd/migrate/prune" + "github.com/opentdf/platform/otdfctl/pkg/man" +) + +var ( + migrateDoc = man.Docs.GetDoc("migrate") + + Cmd = &migrateDoc.Command +) + +func InitCommands() { + Cmd.PersistentFlags().BoolP( + migrateDoc.GetDocFlag("commit").Name, + migrateDoc.GetDocFlag("commit").Shorthand, + migrateDoc.GetDocFlag("commit").DefaultAsBool(), + migrateDoc.GetDocFlag("commit").Description, + ) + + Cmd.PersistentFlags().BoolP( + migrateDoc.GetDocFlag("interactive").Name, + migrateDoc.GetDocFlag("interactive").Shorthand, + migrateDoc.GetDocFlag("interactive").DefaultAsBool(), + migrateDoc.GetDocFlag("interactive").Description, + ) + + prune.InitCommands() + + Cmd.AddCommand( + migrateNamespacedPolicyCmd(), + prune.Cmd, + ) +} diff --git a/otdfctl/cmd/migrate/namespaced_policy.go b/otdfctl/cmd/migrate/namespaced_policy.go new file mode 100644 index 0000000000..1a852ba855 --- /dev/null +++ b/otdfctl/cmd/migrate/namespaced_policy.go @@ -0,0 +1,103 @@ +package migrate + +import ( + "errors" + + otdfctl "github.com/opentdf/platform/otdfctl/cmd/common" + namespacedpolicy "github.com/opentdf/platform/otdfctl/migrations/namespacedpolicy" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +func migrateNamespacedPolicyCmd() *cobra.Command { + doc := man.Docs.GetCommand("migrate/namespaced-policy", man.WithRun(migrateNamespacedPolicy)) + doc.Args = cobra.NoArgs + doc.Flags().StringP( + doc.GetDocFlag("scope").Name, + doc.GetDocFlag("scope").Shorthand, + doc.GetDocFlag("scope").Default, + doc.GetDocFlag("scope").Description, + ) + + return &doc.Command +} + +func migrateNamespacedPolicy(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + scopeCSV := c.Flags.GetRequiredString("scope") + prompter := &namespacedpolicy.HuhPrompter{} + + commit, err := cmd.InheritedFlags().GetBool("commit") + if err != nil { + cli.ExitWithError("could not read --commit flag", err) + } + interactive, err := cmd.InheritedFlags().GetBool("interactive") + if err != nil { + cli.ExitWithError("could not read --interactive flag", err) + } + + h := otdfctl.NewHandler(c) + defer h.Close() + + var plannerOpts []namespacedpolicy.Option + if interactive { + plannerOpts = append(plannerOpts, namespacedpolicy.WithInteractiveReviewer(namespacedpolicy.NewHuhInteractiveReviewer(&h, prompter))) + } + + planner, err := namespacedpolicy.NewMigrationPlanner(&h, scopeCSV, plannerOpts...) + if err != nil { + cli.ExitWithError("could not create namespaced-policy planner", err) + } + + plan, err := planner.Plan(cmd.Context()) + if err != nil { + cli.ExitWithError("could not build namespaced-policy plan", err) + } + + if commit { + executeNamespacedPolicyCommit(cmd, h, plan, interactive, prompter) + } + + if _, err := cmd.OutOrStdout().Write([]byte(namespacedpolicy.RenderNamespacedPolicySummary(plan, commit) + "\n")); err != nil { + cli.ExitWithError("could not write namespaced-policy summary", err) + } +} + +func confirmNamespacedPolicyCommit(cmd *cobra.Command, plan *namespacedpolicy.MigrationPlan, interactive bool, prompter namespacedpolicy.InteractivePrompter) error { + if !interactive { + return nil + } + if err := namespacedpolicy.ConfirmNamespacedPolicyBackup(cmd.Context(), prompter); err != nil { + return err + } + if err := namespacedpolicy.ConfirmMigrationPlan(cmd.Context(), plan, prompter); err != nil { + return err + } + return nil +} + +func executeNamespacedPolicyCommit(cmd *cobra.Command, h namespacedpolicy.ExecutorHandler, plan *namespacedpolicy.MigrationPlan, interactive bool, prompter namespacedpolicy.InteractivePrompter) { + if err := confirmNamespacedPolicyCommit(cmd, plan, interactive, prompter); err != nil { + if errors.Is(err, namespacedpolicy.ErrNamespacedPolicyBackupNotConfirmed) || errors.Is(err, namespacedpolicy.ErrInteractiveReviewAborted) { + writeNamespacedPolicySummary(cmd, plan, false, "aborted") + } + cli.ExitWithError("could not review namespaced-policy commit", err) + } + + executor, err := namespacedpolicy.NewMigrationExecutor(h) + if err != nil { + cli.ExitWithError("could not create namespaced-policy executor", err) + } + + if err := executor.ExecuteMigration(cmd.Context(), plan); err != nil { + writeNamespacedPolicySummary(cmd, plan, true, "failure") + cli.ExitWithError("could not execute namespaced-policy commit", err) + } +} + +func writeNamespacedPolicySummary(cmd *cobra.Command, plan *namespacedpolicy.MigrationPlan, commit bool, result string) { + if _, err := cmd.OutOrStdout().Write([]byte(namespacedpolicy.RenderNamespacedPolicySummaryWithResult(plan, commit, result) + "\n")); err != nil { + cli.ExitWithError("could not write namespaced-policy summary", err) + } +} diff --git a/otdfctl/cmd/migrate/prune/namespaced_policy.go b/otdfctl/cmd/migrate/prune/namespaced_policy.go new file mode 100644 index 0000000000..086483abdc --- /dev/null +++ b/otdfctl/cmd/migrate/prune/namespaced_policy.go @@ -0,0 +1,109 @@ +package prune + +import ( + "errors" + + otdfctl "github.com/opentdf/platform/otdfctl/cmd/common" + namespacedpolicy "github.com/opentdf/platform/otdfctl/migrations/namespacedpolicy" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +func pruneNamespacedPolicyCmd() *cobra.Command { + doc := man.Docs.GetCommand("migrate/prune/namespaced-policy", man.WithRun(pruneNamespacedPolicy)) + doc.Args = cobra.NoArgs + doc.Flags().StringP( + doc.GetDocFlag("scope").Name, + doc.GetDocFlag("scope").Shorthand, + doc.GetDocFlag("scope").Default, + doc.GetDocFlag("scope").Description, + ) + + return &doc.Command +} + +func pruneNamespacedPolicy(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + scope := c.Flags.GetRequiredString("scope") + prompter := &namespacedpolicy.HuhPrompter{} + + commit, err := cmd.InheritedFlags().GetBool("commit") + if err != nil { + cli.ExitWithError("could not read --commit flag", err) + } + interactive, err := cmd.InheritedFlags().GetBool("interactive") + if err != nil { + cli.ExitWithError("could not read --interactive flag", err) + } + + h := otdfctl.NewHandler(c) + defer h.Close() + + planner, err := namespacedpolicy.NewPrunePlanner(&h, scope) + if err != nil { + cli.ExitWithError("could not create namespaced-policy prune planner", err) + } + + plan, err := planner.Plan(cmd.Context()) + if err != nil { + cli.ExitWithError("could not build namespaced-policy prune plan", err) + } + + if interactive { + if err := namespacedpolicy.ReviewPrunePlan(cmd.Context(), plan, prompter); err != nil { + if errors.Is(err, namespacedpolicy.ErrInteractiveReviewAborted) { + writeNamespacedPolicyPruneSummary(cmd, plan, false, namespacedpolicy.PruneSummaryResultAborted) + } + cli.ExitWithError("could not review namespaced-policy prune plan", err) + } + } + + if commit { + executeNamespacedPolicyPruneCommit(cmd, h, plan, interactive, prompter) + } + + if _, err := cmd.OutOrStdout().Write([]byte(namespacedpolicy.RenderNamespacedPolicyPruneSummary(plan, commit, namespacedpolicy.PruneSummaryResultSuccess) + "\n")); err != nil { + cli.ExitWithError("could not write namespaced-policy prune summary", err) + } +} + +func executeNamespacedPolicyPruneCommit(cmd *cobra.Command, h namespacedpolicy.ExecutorHandler, plan *namespacedpolicy.PrunePlan, interactive bool, prompter namespacedpolicy.InteractivePrompter) { + if interactive { + if err := reviewNamespacedPolicyPruneInteractiveCommit(cmd, plan, prompter); err != nil { + if namespacedPolicyPruneCommitAborted(err) { + writeNamespacedPolicyPruneSummary(cmd, plan, false, namespacedpolicy.PruneSummaryResultAborted) + } + cli.ExitWithError("could not review namespaced-policy prune commit", err) + } + } + + executor, err := namespacedpolicy.NewPruneExecutor(h) + if err != nil { + cli.ExitWithError("could not create namespaced-policy prune executor", err) + } + + if err := executor.ExecutePrune(cmd.Context(), plan); err != nil { + writeNamespacedPolicyPruneSummary(cmd, plan, true, namespacedpolicy.PruneSummaryResultFailure) + cli.ExitWithError("could not execute namespaced-policy prune commit", err) + } +} + +func reviewNamespacedPolicyPruneInteractiveCommit(cmd *cobra.Command, plan *namespacedpolicy.PrunePlan, prompter namespacedpolicy.InteractivePrompter) error { + if err := namespacedpolicy.ConfirmNamespacedPolicyPruneBackup(cmd.Context(), prompter); err != nil { + return err + } + + return namespacedpolicy.ConfirmPrunePlanDeletes(cmd.Context(), plan, prompter) +} + +func namespacedPolicyPruneCommitAborted(err error) bool { + return errors.Is(err, namespacedpolicy.ErrNamespacedPolicyBackupNotConfirmed) || + errors.Is(err, namespacedpolicy.ErrInteractiveReviewAborted) +} + +func writeNamespacedPolicyPruneSummary(cmd *cobra.Command, plan *namespacedpolicy.PrunePlan, executed bool, result namespacedpolicy.PruneSummaryResult) { + if _, err := cmd.OutOrStdout().Write([]byte(namespacedpolicy.RenderNamespacedPolicyPruneSummary(plan, executed, result) + "\n")); err != nil { + cli.ExitWithError("could not write namespaced-policy prune summary", err) + } +} diff --git a/otdfctl/cmd/migrate/prune/prune.go b/otdfctl/cmd/migrate/prune/prune.go new file mode 100644 index 0000000000..e5214f821e --- /dev/null +++ b/otdfctl/cmd/migrate/prune/prune.go @@ -0,0 +1,15 @@ +package prune + +import ( + "github.com/opentdf/platform/otdfctl/pkg/man" +) + +var ( + pruneDoc = man.Docs.GetCommand("migrate/prune") + + Cmd = &pruneDoc.Command +) + +func InitCommands() { + Cmd.AddCommand(pruneNamespacedPolicyCmd()) +} diff --git a/otdfctl/cmd/policy/actions.go b/otdfctl/cmd/policy/actions.go new file mode 100644 index 0000000000..ef4fadf8d1 --- /dev/null +++ b/otdfctl/cmd/policy/actions.go @@ -0,0 +1,274 @@ +package policy + +import ( + "fmt" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +func policyGetAction(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + name := c.Flags.GetOptionalString("name") + // TODO: switch to required namespace if id not provided once namespacing is required by policy + namespace := c.Flags.GetOptionalString("namespace") + + if id == "" && name == "" { + cli.ExitWithError("Either 'id' or 'name' must be provided", nil) + } + + action, err := h.GetAction(cmd.Context(), id, name, namespace) + if err != nil { + identifier := "id: " + id + if id == "" { + identifier = "name: " + name + } + errMsg := fmt.Sprintf("Failed to find action (%s)", identifier) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", action.GetId()}, + {"Name", action.GetName()}, + {"Namespace", action.GetNamespace().GetFqn()}, + } + if mdRows := getMetadataRows(action.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, action.GetId(), t, action) +} + +func policyListActions(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + namespace := c.Flags.GetOptionalString("namespace") + + resp, err := h.ListActions(cmd.Context(), limit, offset, namespace) + if err != nil { + cli.ExitWithError("Failed to list actions", err) + } + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("name", "Name", cli.FlexColumnWidthFour), + table.NewFlexColumn("action_type", "Action Type", cli.FlexColumnWidthFour), + table.NewFlexColumn("namespace", "Namespace", cli.FlexColumnWidthFour), + ) + rows := []table.Row{} + for _, a := range resp.GetActionsStandard() { + rows = append(rows, table.NewRow(table.RowData{ + "id": a.GetId(), + "action_type": "standard", + "name": a.GetName(), + "namespace": a.GetNamespace().GetFqn(), + })) + } + + for _, a := range resp.GetActionsCustom() { + rows = append(rows, table.NewRow(table.RowData{ + "id": a.GetId(), + "action_type": "custom", + "name": a.GetName(), + "namespace": a.GetNamespace().GetFqn(), + })) + } + + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func policyCreateAction(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + name := c.Flags.GetRequiredString("name") + namespace := c.Flags.GetOptionalString("namespace") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + action, err := h.CreateAction(cmd.Context(), name, namespace, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create action", err) + } + + rows := [][]string{ + {"Id", action.GetId()}, + {"Name", action.GetName()}, + {"Namespace", action.GetNamespace().GetFqn()}, + } + + if mdRows := getMetadataRows(action.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, action.GetId(), t, action) +} + +func policyDeleteAction(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + ctx := cmd.Context() + + action, err := h.GetAction(ctx, id, "", "") + if err != nil { + errMsg := fmt.Sprintf("Failed to find action (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDelete, "action", id, force) + + err = h.DeleteAction(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete action (%s)", id) + cli.ExitWithError(errMsg, err) + } + rows := [][]string{ + {"Id", id}, + {"Name", action.GetName()}, + {"Namespace", action.GetNamespace().GetFqn()}, + } + if mdRows := getMetadataRows(action.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, action) +} + +func policyUpdateAction(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + name := c.Flags.GetOptionalString("name") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + updated, err := h.UpdateAction( + cmd.Context(), + id, + name, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError("Failed to update action", err) + } + rows := [][]string{ + {"Id", id}, + {"Name", updated.GetName()}, + {"Namespace", updated.GetNamespace().GetFqn()}, + } + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + + common.HandleSuccess(cmd, id, t, updated) +} + +func injectNamespaceFlag(doc *man.Doc) { + doc.Flags().StringP( + doc.GetDocFlag("namespace").Name, + doc.GetDocFlag("namespace").Shorthand, + doc.GetDocFlag("namespace").Default, + doc.GetDocFlag("namespace").Description, + ) +} + +func initActionsCommands() { + getDoc := man.Docs.GetCommand("policy/actions/get", + man.WithRun(policyGetAction), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("name").Name, + getDoc.GetDocFlag("name").Shorthand, + getDoc.GetDocFlag("name").Default, + getDoc.GetDocFlag("name").Description, + ) + injectNamespaceFlag(getDoc) + + listDoc := man.Docs.GetCommand("policy/actions/list", + man.WithRun(policyListActions), + ) + injectNamespaceFlag(listDoc) + injectListPaginationFlags(listDoc) + + createDoc := man.Docs.GetCommand("policy/actions/create", + man.WithRun(policyCreateAction), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Shorthand, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + injectNamespaceFlag(createDoc) + injectLabelFlags(&createDoc.Command, false) + + updateDoc := man.Docs.GetCommand("policy/actions/update", + man.WithRun(policyUpdateAction), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("name").Name, + updateDoc.GetDocFlag("name").Shorthand, + updateDoc.GetDocFlag("name").Default, + updateDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deleteDoc := man.Docs.GetCommand("policy/actions/delete", + man.WithRun(policyDeleteAction), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + policyActionsDoc := man.Docs.GetCommand("policy/actions", + man.WithSubcommands( + getDoc, + listDoc, + createDoc, + updateDoc, + deleteDoc, + ), + ) + Cmd.AddCommand(&policyActionsDoc.Command) +} diff --git a/otdfctl/cmd/policy/attributeValues.go b/otdfctl/cmd/policy/attributeValues.go new file mode 100644 index 0000000000..b4b626476f --- /dev/null +++ b/otdfctl/cmd/policy/attributeValues.go @@ -0,0 +1,509 @@ +package policy + +import ( + "fmt" + "math" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + policycommon "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/spf13/cobra" +) + +var AttributeValuesCmd *cobra.Command + +func createAttributeValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + attrID := c.FlagHelper.GetRequiredID("attribute-id") + value := c.FlagHelper.GetRequiredString("value") + metadataLabels = c.FlagHelper.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + attr, err := h.GetAttribute(ctx, attrID) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get parent attribute (%s)", attrID), err) + } + + v, err := h.CreateAttributeValue(ctx, attr.GetId(), value, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create attribute value", err) + } + + handleValueSuccess(cmd, v) +} + +func getAttributeValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.FlagHelper.GetRequiredID("id") + + v, err := h.GetAttributeValue(cmd.Context(), id) + if err != nil { + cli.ExitWithError("Failed to find attribute value", err) + } + + handleValueSuccess(cmd, v) +} + +func filterValuesByState(values []*policy.Value, state policycommon.ActiveStateEnum) []*policy.Value { + var shouldBeActive bool + switch state { + case policycommon.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE: + shouldBeActive = true + case policycommon.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE: + shouldBeActive = false + case policycommon.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + policycommon.ActiveStateEnum_ACTIVE_STATE_ENUM_UNSPECIFIED: + return values + } + + filtered := make([]*policy.Value, 0, len(values)) + for _, v := range values { + if v.GetActive().GetValue() == shouldBeActive { + filtered = append(filtered, v) + } + } + return filtered +} + +func paginateValues(values []*policy.Value, limit, offset int32) ([]*policy.Value, *policy.PageResponse) { + total := len(values) + pagination := &policy.PageResponse{ + Total: int32(min(total, math.MaxInt32)), + CurrentOffset: offset, + } + + off := int(offset) + if off < 0 { + return nil, pagination + } + if off >= total { + return nil, pagination + } + values = values[off:] + + lim := int(limit) + if lim > 0 && lim < len(values) { + values = values[:lim] + pagination.NextOffset = offset + limit + } + + return values, pagination +} + +func listAttributeValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + attrID := c.FlagHelper.GetRequiredID("attribute-id") + state := cli.GetState(cmd) + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + + values, err := h.ListAttributeValues(cmd.Context(), attrID) + if err != nil { + cli.ExitWithError("Failed to list attribute values", err) + } + + filtered := filterValuesByState(values, state) + paged, pagination := paginateValues(filtered, limit, offset) + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("fqn", "Fqn", cli.FlexColumnWidthFour), + table.NewFlexColumn("active", "Active", cli.FlexColumnWidthThree), + table.NewFlexColumn("labels", "Labels", cli.FlexColumnWidthOne), + table.NewFlexColumn("created_at", "Created At", cli.FlexColumnWidthOne), + table.NewFlexColumn("updated_at", "Updated At", cli.FlexColumnWidthOne), + ) + rows := []table.Row{} + for _, val := range paged { + v := cli.GetSimpleAttributeValue(val) + rows = append(rows, table.NewRow(table.RowData{ + "id": v.ID, + "fqn": v.FQN, + "active": v.Active, + "labels": v.Metadata["Labels"], + "created_at": v.Metadata["Created At"], + "updated_at": v.Metadata["Updated At"], + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, pagination) + + resp := &attributes.ListAttributeValuesResponse{ + Values: paged, + Pagination: pagination, + } + common.HandleSuccess(cmd, "", t, resp) +} + +func updateAttributeValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + _, err := h.GetAttributeValue(ctx, id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get attribute value (%s)", id), err) + } + + v, err := h.UpdateAttributeValue(ctx, id, getMetadataMutable(metadataLabels), getMetadataUpdateBehavior()) + if err != nil { + cli.ExitWithError("Failed to update attribute value", err) + } + + handleValueSuccess(cmd, v) +} + +func deactivateAttributeValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + + value, err := h.GetAttributeValue(ctx, id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get attribute value (%s)", id), err) + } + + cli.ConfirmAction(cli.ActionDeactivate, "attribute value", value.GetValue(), force) + + deactivated, err := h.DeactivateAttributeValue(ctx, id) + if err != nil { + cli.ExitWithError("Failed to deactivate attribute value", err) + } + + handleValueSuccess(cmd, deactivated) +} + +func unsafeReactivateAttributeValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + + v, err := h.GetAttributeValue(ctx, id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get attribute value (%s)", id), err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionReactivate, "attribute value", cli.InputNameFQN, v.GetFqn()) + } + + if reactivated, err := h.UnsafeReactivateAttributeValue(ctx, id); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to reactivate attribute value (%s)", id), err) + } else { + rows := [][]string{ + {"Id", reactivated.GetId()}, + {"Value", reactivated.GetValue()}, + } + if mdRows := getMetadataRows(v.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, v) + } +} + +func unsafeUpdateAttributeValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + value := c.Flags.GetOptionalString("value") + + v, err := h.GetAttributeValue(ctx, id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get attribute value (%s)", id), err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionUpdateUnsafe, "attribute value", cli.InputNameFQN, v.GetFqn()) + } + + if err := h.UnsafeUpdateAttributeValue(ctx, id, value); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to update attribute value (%s)", id), err) + } else { + rows := [][]string{ + {"Id", v.GetId()}, + {"Value", value}, + } + if mdRows := getMetadataRows(v.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, v) + } +} + +func unsafeDeleteAttributeValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + + v, err := h.GetAttributeValue(ctx, id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get attribute value (%s)", id), err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionDelete, "attribute value", cli.InputNameFQN, v.GetFqn()) + } + + if err := h.UnsafeDeleteAttributeValue(ctx, id, v.GetFqn()); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to delete attribute (%s)", id), err) + } else { + rows := [][]string{ + {"Id", v.GetId()}, + {"Value", v.GetValue()}, + {"Deleted", "true"}, + } + if mdRows := getMetadataRows(v.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, v) + } +} + +func policyAssignKeyToAttrValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + value := c.Flags.GetRequiredString("value") + keyID := c.Flags.GetRequiredID("key-id") + + attrKey, err := h.AssignKeyToAttributeValue(c.Context(), value, keyID) + if err != nil { + errMsg := fmt.Sprintf("Failed to assign key: (%s) to attribute value: (%s)", keyID, value) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Value ID", attrKey.GetValueId()}, + {"Key ID", attrKey.GetKeyId()}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, value, t, attrKey) +} + +func policyRemoveKeyFromAttrValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + value := c.Flags.GetRequiredString("value") + keyID := c.Flags.GetRequiredID("key-id") + + err := h.RemoveKeyFromAttributeValue(c.Context(), value, keyID) + if err != nil { + errMsg := fmt.Sprintf("Failed to remove key (%s) from attribute value (%s)", keyID, value) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Removed", "true"}, + {"Value", value}, + {"Key ID", keyID}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, value, t, nil) +} + +func initAttributeValuesCommands() { + createCmd := man.Docs.GetCommand("policy/attributes/values/create", + man.WithRun(createAttributeValue), + ) + createCmd.Flags().StringP( + createCmd.GetDocFlag("attribute-id").Name, + createCmd.GetDocFlag("attribute-id").Shorthand, + createCmd.GetDocFlag("attribute-id").Default, + createCmd.GetDocFlag("attribute-id").Description, + ) + createCmd.Flags().StringP( + createCmd.GetDocFlag("value").Name, + createCmd.GetDocFlag("value").Shorthand, + createCmd.GetDocFlag("value").Default, + createCmd.GetDocFlag("value").Description, + ) + injectLabelFlags(&createCmd.Command, false) + + getCmd := man.Docs.GetCommand("policy/attributes/values/get", + man.WithRun(getAttributeValue), + ) + getCmd.Flags().StringP( + getCmd.GetDocFlag("id").Name, + getCmd.GetDocFlag("id").Shorthand, + getCmd.GetDocFlag("id").Default, + getCmd.GetDocFlag("id").Description, + ) + + listCmd := man.Docs.GetCommand("policy/attributes/values/list", + man.WithRun(listAttributeValue), + ) + listCmd.Flags().StringP( + listCmd.GetDocFlag("attribute-id").Name, + listCmd.GetDocFlag("attribute-id").Shorthand, + listCmd.GetDocFlag("attribute-id").Default, + listCmd.GetDocFlag("attribute-id").Description, + ) + listCmd.Flags().StringP( + listCmd.GetDocFlag("state").Name, + listCmd.GetDocFlag("state").Shorthand, + listCmd.GetDocFlag("state").Default, + listCmd.GetDocFlag("state").Description, + ) + injectListPaginationFlags(listCmd) + + updateCmd := man.Docs.GetCommand("policy/attributes/values/update", + man.WithRun(updateAttributeValue), + ) + updateCmd.Flags().StringP( + updateCmd.GetDocFlag("id").Name, + updateCmd.GetDocFlag("id").Shorthand, + updateCmd.GetDocFlag("id").Default, + updateCmd.GetDocFlag("id").Description, + ) + injectLabelFlags(&updateCmd.Command, true) + + deactivateCmd := man.Docs.GetCommand("policy/attributes/values/deactivate", + man.WithRun(deactivateAttributeValue), + ) + deactivateCmd.Flags().StringP( + deactivateCmd.GetDocFlag("id").Name, + deactivateCmd.GetDocFlag("id").Shorthand, + deactivateCmd.GetDocFlag("id").Default, + deactivateCmd.GetDocFlag("id").Description, + ) + deactivateCmd.Flags().Bool( + deactivateCmd.GetDocFlag("force").Name, + false, + deactivateCmd.GetDocFlag("force").Description, + ) + + keyCmd := man.Docs.GetCommand("policy/attributes/values/key") + + assignKasKeyCmd := man.Docs.GetCommand("policy/attributes/values/key/assign", + man.WithRun(policyAssignKeyToAttrValue), + ) + assignKasKeyCmd.Flags().StringP( + assignKasKeyCmd.GetDocFlag("value").Name, + assignKasKeyCmd.GetDocFlag("value").Shorthand, + assignKasKeyCmd.GetDocFlag("value").Default, + assignKasKeyCmd.GetDocFlag("value").Description, + ) + assignKasKeyCmd.Flags().StringP( + assignKasKeyCmd.GetDocFlag("key-id").Name, + assignKasKeyCmd.GetDocFlag("key-id").Shorthand, + assignKasKeyCmd.GetDocFlag("key-id").Default, + assignKasKeyCmd.GetDocFlag("key-id").Description, + ) + + removeKasKeyCmd := man.Docs.GetCommand("policy/attributes/values/key/remove", + man.WithRun(policyRemoveKeyFromAttrValue), + ) + removeKasKeyCmd.Flags().StringP( + removeKasKeyCmd.GetDocFlag("value").Name, + removeKasKeyCmd.GetDocFlag("value").Shorthand, + removeKasKeyCmd.GetDocFlag("value").Default, + removeKasKeyCmd.GetDocFlag("value").Description, + ) + removeKasKeyCmd.Flags().StringP( + removeKasKeyCmd.GetDocFlag("key-id").Name, + removeKasKeyCmd.GetDocFlag("key-id").Shorthand, + removeKasKeyCmd.GetDocFlag("key-id").Default, + removeKasKeyCmd.GetDocFlag("key-id").Description, + ) + + unsafeReactivateCmd := man.Docs.GetCommand("policy/attributes/values/unsafe/reactivate", + man.WithRun(unsafeReactivateAttributeValue), + ) + unsafeReactivateCmd.Flags().StringP( + unsafeReactivateCmd.GetDocFlag("id").Name, + unsafeReactivateCmd.GetDocFlag("id").Shorthand, + unsafeReactivateCmd.GetDocFlag("id").Default, + unsafeReactivateCmd.GetDocFlag("id").Description, + ) + + unsafeDeleteCmd := man.Docs.GetCommand("policy/attributes/values/unsafe/delete", + man.WithRun(unsafeDeleteAttributeValue), + ) + unsafeDeleteCmd.Flags().StringP( + unsafeDeleteCmd.GetDocFlag("id").Name, + unsafeDeleteCmd.GetDocFlag("id").Shorthand, + unsafeDeleteCmd.GetDocFlag("id").Default, + unsafeDeleteCmd.GetDocFlag("id").Description, + ) + + unsafeUpdateCmd := man.Docs.GetCommand("policy/attributes/values/unsafe/update", + man.WithRun(unsafeUpdateAttributeValue), + ) + unsafeUpdateCmd.Flags().StringP( + unsafeUpdateCmd.GetDocFlag("id").Name, + unsafeUpdateCmd.GetDocFlag("id").Shorthand, + unsafeUpdateCmd.GetDocFlag("id").Default, + unsafeUpdateCmd.GetDocFlag("id").Description, + ) + unsafeUpdateCmd.Flags().StringP( + unsafeUpdateCmd.GetDocFlag("value").Name, + unsafeUpdateCmd.GetDocFlag("value").Shorthand, + unsafeUpdateCmd.GetDocFlag("value").Default, + unsafeUpdateCmd.GetDocFlag("value").Description, + ) + + unsafeCmd := man.Docs.GetCommand("policy/attributes/values/unsafe") + unsafeCmd.PersistentFlags().BoolVar(&forceUnsafe, + unsafeCmd.GetDocFlag("force").Name, + false, + unsafeCmd.GetDocFlag("force").Description, + ) + + keyCmd.AddSubcommands(assignKasKeyCmd, removeKasKeyCmd) + unsafeCmd.AddSubcommands(unsafeReactivateCmd, unsafeDeleteCmd, unsafeUpdateCmd) + doc := man.Docs.GetCommand("policy/attributes/values", + man.WithSubcommands(createCmd, getCmd, listCmd, updateCmd, deactivateCmd, unsafeCmd, keyCmd), + ) + AttributeValuesCmd = &doc.Command + AttributesCmd.AddCommand(AttributeValuesCmd) +} + +func handleValueSuccess(cmd *cobra.Command, v *policy.Value) { + rows := [][]string{ + {"Id", v.GetId()}, + {"FQN", v.GetFqn()}, + {"Value", v.GetValue()}, + } + if mdRows := getMetadataRows(v.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, v.GetId(), t, v) +} diff --git a/otdfctl/cmd/policy/attributes.go b/otdfctl/cmd/policy/attributes.go new file mode 100644 index 0000000000..35e4000c14 --- /dev/null +++ b/otdfctl/cmd/policy/attributes.go @@ -0,0 +1,544 @@ +package policy + +import ( + "fmt" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +var ( + forceReplaceMetadataLabels bool + attributeValues []string + attributeValuesOrder []string + + AttributesCmd = man.Docs.GetCommand("policy/attributes") +) + +func createAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + name := c.Flags.GetRequiredString("name") + rule := c.Flags.GetRequiredString("rule") + attributeValues = c.Flags.GetStringSlice("value", attributeValues, cli.FlagsStringSliceOptions{}) + namespace := c.Flags.GetRequiredString("namespace") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + allowTraversal := c.Flags.GetOptionalBoolWrapper("allow-traversal") + + attr, err := h.CreateAttribute(cmd.Context(), name, rule, namespace, attributeValues, getMetadataMutable(metadataLabels), allowTraversal) + if err != nil { + cli.ExitWithError("Failed to create attribute", err) + } + + a := cli.GetSimpleAttribute(attr) + rows := [][]string{ + {"Name", a.Name}, + {"Rule", a.Rule}, + {"Values", cli.CommaSeparated(a.Values)}, + {"Namespace", a.Namespace}, + {"Allow Traversal", a.AllowTraversal}, + } + if mdRows := getMetadataRows(attr.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + + common.HandleSuccess(cmd, a.ID, t, attr) +} + +func getAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + attr, err := h.GetAttribute(cmd.Context(), id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + a := cli.GetSimpleAttribute(attr) + rows := [][]string{ + {"Id", a.ID}, + {"Name", a.Name}, + {"Rule", a.Rule}, + {"Values", cli.CommaSeparated(a.Values)}, + {"Namespace", a.Namespace}, + {"Allow Traversal", a.AllowTraversal}, + } + if mdRows := getMetadataRows(attr.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, a.ID, t, attr) +} + +func listAttributes(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + state := cli.GetState(cmd) + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) + + resp, err := h.ListAttributes(cmd.Context(), state, limit, offset, sort) + if err != nil { + cli.ExitWithError("Failed to list attributes", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("namespace", "Namespace", cli.FlexColumnWidthFour), + table.NewFlexColumn("name", "Name", cli.FlexColumnWidthThree), + table.NewFlexColumn("rule", "Rule", cli.FlexColumnWidthTwo), + table.NewFlexColumn("allow_traversal", "Allow Traversal", cli.FlexColumnWidthOne), + table.NewFlexColumn("values", "Values", cli.FlexColumnWidthTwo), + table.NewFlexColumn("active", "Active", cli.FlexColumnWidthTwo), + ) + rows := []table.Row{} + for _, attr := range resp.GetAttributes() { + a := cli.GetSimpleAttribute(attr) + rows = append(rows, table.NewRow(table.RowData{ + "id": a.ID, + "namespace": a.Namespace, + "name": a.Name, + "rule": a.Rule, + "allow_traversal": a.AllowTraversal, + "values": cli.CommaSeparated(a.Values), + "active": a.Active, + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func deactivateAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + + attr, err := h.GetAttribute(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDeactivate, "attribute", attr.GetName(), force) + + attr, err = h.DeactivateAttribute(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to deactivate attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + a := cli.GetSimpleAttribute(attr) + rows := [][]string{ + {"Name", a.Name}, + {"Rule", a.Rule}, + {"Values", cli.CommaSeparated(a.Values)}, + {"Namespace", a.Namespace}, + {"Allow Traversal", a.AllowTraversal}, + } + if mdRows := getMetadataRows(attr.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, a.ID, t, a) +} + +func updateAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + if a, err := h.UpdateAttribute(cmd.Context(), id, getMetadataMutable(metadataLabels), getMetadataUpdateBehavior()); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to update attribute (%s)", id), err) + } else { + rows := [][]string{ + {"Id", a.GetId()}, + {"Name", a.GetName()}, + } + if mdRows := getMetadataRows(a.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, a) + } +} + +func unsafeReactivateAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + + a, err := h.GetAttribute(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionReactivate, "attribute", cli.InputNameFQN, a.GetFqn()) + } + + if reactivatedAttr, err := h.UnsafeReactivateAttribute(ctx, id); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to reactivate attribute (%s)", id), err) + } else { + rows := [][]string{ + {"Id", reactivatedAttr.GetId()}, + {"Name", reactivatedAttr.GetName()}, + } + if mdRows := getMetadataRows(a.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, a) + } +} + +func unsafeUpdateAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + name := c.Flags.GetOptionalString("name") + rule := c.Flags.GetOptionalString("rule") + attributeValuesOrder = c.Flags.GetStringSlice("values-order", attributeValuesOrder, cli.FlagsStringSliceOptions{}) + allowTraversal := c.Flags.GetOptionalBoolWrapper("allow-traversal") + + a, err := h.GetAttribute(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionUpdateUnsafe, "attribute", cli.InputNameFQN, a.GetFqn()) + } + + updatedAttr, err := h.UnsafeUpdateAttribute(ctx, id, name, rule, attributeValuesOrder, allowTraversal) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to update attribute (%s)", id), err) + } else { + var ( + retrievedVals []string + valueIDs []string + ) + for _, v := range updatedAttr.GetValues() { + retrievedVals = append(retrievedVals, v.GetValue()) + valueIDs = append(valueIDs, v.GetId()) + } + if allowTraversal == nil { + allowTraversal = updatedAttr.GetAllowTraversal() + } + rows := [][]string{ + {"Id", updatedAttr.GetId()}, + {"Name", updatedAttr.GetName()}, + {"Rule", handlers.GetAttributeRuleFromAttributeType(updatedAttr.GetRule())}, + {"Values", cli.CommaSeparated(retrievedVals)}, + {"Value IDs", cli.CommaSeparated(valueIDs)}, + {"Allow Traversal", allowTraversal.String()}, + } + if mdRows := getMetadataRows(updatedAttr.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, updatedAttr) + } +} + +func unsafeDeleteAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + + a, err := h.GetAttribute(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionDelete, "attribute", cli.InputNameFQN, a.GetFqn()) + } + + if err := h.UnsafeDeleteAttribute(ctx, id, a.GetFqn()); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to delete attribute (%s)", id), err) + } else { + rows := [][]string{ + {"Deleted", "true"}, + {"Id", a.GetId()}, + {"Name", a.GetName()}, + } + if mdRows := getMetadataRows(a.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, a) + } +} + +func policyAssignKeyToAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + attribute := c.Flags.GetRequiredString("attribute") + keyID := c.Flags.GetRequiredID("key-id") + + // Get the attribute to show meaningful information in case of error + attrKey, err := h.AssignKeyToAttribute(c.Context(), attribute, keyID) + if err != nil { + errMsg := fmt.Sprintf("Failed to assign key: (%s) to attribute: (%s)", keyID, attribute) + cli.ExitWithError(errMsg, err) + } + + // Prepare and display the result + rows := [][]string{ + {"Attribute ID", attrKey.GetAttributeId()}, + {"Key ID", attrKey.GetKeyId()}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, attribute, t, attrKey) +} + +func policyRemoveKeyFromAttribute(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + attribute := c.Flags.GetRequiredString("attribute") + keyID := c.Flags.GetRequiredID("key-id") + + err := h.RemoveKeyFromAttribute(c.Context(), attribute, keyID) + if err != nil { + errMsg := fmt.Sprintf("Failed to remove key (%s) from attribute (%s)", keyID, attribute) + cli.ExitWithError(errMsg, err) + } + + // Prepare and display the result + rows := [][]string{ + {"Removed", "true"}, + {"Attribute", attribute}, + {"Key ID", keyID}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, attribute, t, nil) +} + +func initAttributesCommands() { + // Create an attribute + createDoc := man.Docs.GetCommand("policy/attributes/create", + man.WithRun(createAttribute), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Shorthand, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("rule").Name, + createDoc.GetDocFlag("rule").Shorthand, + createDoc.GetDocFlag("rule").Default, + createDoc.GetDocFlag("rule").Description, + ) + createDoc.Flags().StringSliceVarP( + &attributeValues, + createDoc.GetDocFlag("value").Name, + createDoc.GetDocFlag("value").Shorthand, + []string{}, + createDoc.GetDocFlag("value").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("namespace").Name, + createDoc.GetDocFlag("namespace").Shorthand, + createDoc.GetDocFlag("namespace").Default, + createDoc.GetDocFlag("namespace").Description, + ) + createDoc.Flags().Bool( + createDoc.GetDocFlag("allow-traversal").Name, + false, + createDoc.GetDocFlag("allow-traversal").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + // Get an attribute + getDoc := man.Docs.GetCommand("policy/attributes/get", + man.WithRun(getAttribute), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + + // List attributes + listDoc := man.Docs.GetCommand("policy/attributes/list", + man.WithRun(listAttributes), + ) + listDoc.Flags().StringP( + listDoc.GetDocFlag("state").Name, + listDoc.GetDocFlag("state").Shorthand, + listDoc.GetDocFlag("state").Default, + listDoc.GetDocFlag("state").Description, + ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) + + // Update an attribute + updateDoc := man.Docs.GetCommand("policy/attributes/update", + man.WithRun(updateAttribute), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + // Deactivate an attribute + deactivateDoc := man.Docs.GetCommand("policy/attributes/deactivate", + man.WithRun(deactivateAttribute), + ) + deactivateDoc.Flags().StringP( + deactivateDoc.GetDocFlag("id").Name, + deactivateDoc.GetDocFlag("id").Shorthand, + deactivateDoc.GetDocFlag("id").Default, + deactivateDoc.GetDocFlag("id").Description, + ) + deactivateDoc.Flags().Bool( + deactivateDoc.GetDocFlag("force").Name, + false, + deactivateDoc.GetDocFlag("force").Description, + ) + + // unsafe actions on attributes + unsafeCmd := man.Docs.GetCommand("policy/attributes/unsafe") + unsafeCmd.PersistentFlags().BoolVar(&forceUnsafe, + unsafeCmd.GetDocFlag("force").Name, + false, + unsafeCmd.GetDocFlag("force").Description, + ) + + reactivateCmd := man.Docs.GetCommand("policy/attributes/unsafe/reactivate", + man.WithRun(unsafeReactivateAttribute), + ) + reactivateCmd.Flags().StringP( + reactivateCmd.GetDocFlag("id").Name, + reactivateCmd.GetDocFlag("id").Shorthand, + reactivateCmd.GetDocFlag("id").Default, + reactivateCmd.GetDocFlag("id").Description, + ) + deleteCmd := man.Docs.GetCommand("policy/attributes/unsafe/delete", + man.WithRun(unsafeDeleteAttribute), + ) + deleteCmd.Flags().StringP( + deleteCmd.GetDocFlag("id").Name, + deleteCmd.GetDocFlag("id").Shorthand, + deleteCmd.GetDocFlag("id").Default, + deleteCmd.GetDocFlag("id").Description, + ) + unsafeUpdateCmd := man.Docs.GetCommand("policy/attributes/unsafe/update", + man.WithRun(unsafeUpdateAttribute), + ) + unsafeUpdateCmd.Flags().StringP( + unsafeUpdateCmd.GetDocFlag("id").Name, + unsafeUpdateCmd.GetDocFlag("id").Shorthand, + unsafeUpdateCmd.GetDocFlag("id").Default, + unsafeUpdateCmd.GetDocFlag("id").Description, + ) + unsafeUpdateCmd.Flags().StringP( + unsafeUpdateCmd.GetDocFlag("name").Name, + unsafeUpdateCmd.GetDocFlag("name").Shorthand, + unsafeUpdateCmd.GetDocFlag("name").Default, + unsafeUpdateCmd.GetDocFlag("name").Description, + ) + unsafeUpdateCmd.Flags().StringP( + unsafeUpdateCmd.GetDocFlag("rule").Name, + unsafeUpdateCmd.GetDocFlag("rule").Shorthand, + unsafeUpdateCmd.GetDocFlag("rule").Default, + unsafeUpdateCmd.GetDocFlag("rule").Description, + ) + unsafeUpdateCmd.Flags().StringSliceVarP( + &attributeValuesOrder, + unsafeUpdateCmd.GetDocFlag("values-order").Name, + unsafeUpdateCmd.GetDocFlag("values-order").Shorthand, + []string{}, + unsafeUpdateCmd.GetDocFlag("values-order").Description, + ) + unsafeUpdateCmd.Flags().Bool( + unsafeUpdateCmd.GetDocFlag("allow-traversal").Name, + false, + unsafeUpdateCmd.GetDocFlag("allow-traversal").Description, + ) + + keyCmd := man.Docs.GetCommand("policy/attributes/key") + + // Assign KAS key to attribute + assignKasKeyCmd := man.Docs.GetCommand("policy/attributes/key/assign", + man.WithRun(policyAssignKeyToAttribute), + ) + assignKasKeyCmd.Flags().StringP( + assignKasKeyCmd.GetDocFlag("attribute").Name, + assignKasKeyCmd.GetDocFlag("attribute").Shorthand, + assignKasKeyCmd.GetDocFlag("attribute").Default, + assignKasKeyCmd.GetDocFlag("attribute").Description, + ) + assignKasKeyCmd.Flags().StringP( + assignKasKeyCmd.GetDocFlag("key-id").Name, + assignKasKeyCmd.GetDocFlag("key-id").Shorthand, + assignKasKeyCmd.GetDocFlag("key-id").Default, + assignKasKeyCmd.GetDocFlag("key-id").Description, + ) + + removeKasKeyCmd := man.Docs.GetCommand("policy/attributes/key/remove", + man.WithRun(policyRemoveKeyFromAttribute), + ) + removeKasKeyCmd.Flags().StringP( + removeKasKeyCmd.GetDocFlag("attribute").Name, + removeKasKeyCmd.GetDocFlag("attribute").Shorthand, + removeKasKeyCmd.GetDocFlag("attribute").Default, + removeKasKeyCmd.GetDocFlag("attribute").Description, + ) + removeKasKeyCmd.Flags().StringP( + removeKasKeyCmd.GetDocFlag("key-id").Name, + removeKasKeyCmd.GetDocFlag("key-id").Shorthand, + removeKasKeyCmd.GetDocFlag("key-id").Default, + removeKasKeyCmd.GetDocFlag("key-id").Description, + ) + + keyCmd.AddSubcommands(assignKasKeyCmd, removeKasKeyCmd) + unsafeCmd.AddSubcommands(reactivateCmd, deleteCmd, unsafeUpdateCmd) + AttributesCmd.AddSubcommands(createDoc, getDoc, listDoc, updateDoc, deactivateDoc, unsafeCmd, keyCmd) + Cmd.AddCommand(&AttributesCmd.Command) +} diff --git a/otdfctl/cmd/policy/baseKeys.go b/otdfctl/cmd/policy/baseKeys.go new file mode 100644 index 0000000000..b610d6fb54 --- /dev/null +++ b/otdfctl/cmd/policy/baseKeys.go @@ -0,0 +1,172 @@ +package policy + +import ( + "fmt" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/spf13/cobra" +) + +const ( + kasURIKey = "kas_uri" + kasURIColumn = "Kas URI" + algKey = "algorithm" + algColumn = "Algorithm" + pubPemKey = "public_key_pem" + pubPemColumn = "Public Key PEM" + kasKidKey = "kas_key_id" + kasKidColumn = "Key ID" + isBaseKey = "is_base_key" + isBaseKeyColumn = "Is Base Key" +) + +// KAS Registry Base Keys Command +var policyKasRegistryBaseKeysCmd *cobra.Command + +func getKasKeyIdentifier(c *cli.Cli) (*kasregistry.KasKeyIdentifier, error) { + keyIdentifier := c.Flags.GetRequiredString("key") + kasIdentifier := c.Flags.GetRequiredString("kas") + + identifier := &kasregistry.KasKeyIdentifier{ + Kid: keyIdentifier, + } + + kasInputType := utils.ClassifyString(kasIdentifier) + switch kasInputType { //nolint:exhaustive // default catches unknown + case utils.StringTypeUUID: + identifier.Identifier = &kasregistry.KasKeyIdentifier_KasId{KasId: kasIdentifier} + case utils.StringTypeURI: + identifier.Identifier = &kasregistry.KasKeyIdentifier_Uri{Uri: kasIdentifier} + case utils.StringTypeGeneric: + identifier.Identifier = &kasregistry.KasKeyIdentifier_Name{Name: kasIdentifier} + default: // Catches StringTypeUnknown and any other unexpected types + return nil, fmt.Errorf("invalid KAS identifier: '%s'. Must be a KAS UUID, URI, or Name", kasIdentifier) + } + return identifier, nil +} + +func getBaseKeyTableRows(simpleKey *policy.SimpleKasKey, additionalInfo map[string]string) table.Row { + readableAlg, _ := cli.KeyEnumToAlg(simpleKey.GetPublicKey().GetAlgorithm()) + rowData := table.RowData{ + kasKidKey: simpleKey.GetPublicKey().GetKid(), + pubPemKey: simpleKey.GetPublicKey().GetPem(), + algKey: readableAlg, + kasURIKey: simpleKey.GetKasUri(), + } + + if len(additionalInfo) > 0 { + for key, value := range additionalInfo { + rowData[key] = value + } + } + + return table.NewRow(rowData) +} + +func getBaseKeyTable(additionalColumns []table.Column) table.Model { + columns := []table.Column{ + table.NewFlexColumn(kasURIKey, kasURIColumn, cli.FlexColumnWidthOne), + table.NewFlexColumn(kasKidKey, kasKidColumn, cli.FlexColumnWidthOne), + table.NewFlexColumn(pubPemKey, pubPemColumn, cli.FlexColumnWidthOne), + table.NewFlexColumn(algKey, algColumn, cli.FlexColumnWidthOne), + } + columns = append(columns, additionalColumns...) + + return cli.NewTable( + columns..., + ) +} + +func getBaseKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + baseKey, err := h.GetBaseKey(c.Context()) + if err != nil { + cli.ExitWithError("Failed to get base key", err) + } + + if baseKey == nil { + cli.ExitWithError("No base key found", nil) + } + + t := getBaseKeyTable(nil) + t = t.WithRows([]table.Row{getBaseKeyTableRows(baseKey, nil)}) + common.HandleSuccess(cmd, "", t, baseKey) +} + +func setBaseKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + var identifier *kasregistry.KasKeyIdentifier + var err error + + id := c.Flags.GetOptionalString("key") + if utils.ClassifyString(id) != utils.StringTypeUUID { + identifier, err = getKasKeyIdentifier(c) + if err != nil { + c.ExitWithError("Invalid key identifier", err) + } + } + baseKey, err := h.SetBaseKey(c.Context(), id, identifier) + if err != nil { + cli.ExitWithError("Failed to set base key", err) + } + + t := getBaseKeyTable([]table.Column{ + table.NewFlexColumn(isBaseKey, isBaseKeyColumn, cli.FlexColumnWidthOne), + }) + + rows := []table.Row{ + getBaseKeyTableRows(baseKey.GetNewBaseKey(), map[string]string{ + isBaseKey: "true", + }), + } + if baseKey.GetPreviousBaseKey() != nil { + rows = append(rows, getBaseKeyTableRows(baseKey.GetPreviousBaseKey(), map[string]string{ + isBaseKey: "false", + })) + } + + t = t.WithRows(rows) + common.HandleSuccess(cmd, "", t, baseKey) +} + +// initBaseKeysCommands sets up the base-keys command and its subcommands. +func initBaseKeysCommands() { + getDoc := man.Docs.GetCommand("policy/kas-registry/key/base/get", + man.WithRun(getBaseKey), + ) + + setDoc := man.Docs.GetCommand("policy/kas-registry/key/base/set", + man.WithRun(setBaseKey), + ) + setDoc.Flags().StringP( + setDoc.GetDocFlag("key").Name, + setDoc.GetDocFlag("key").Shorthand, + setDoc.GetDocFlag("key").Default, + setDoc.GetDocFlag("key").Description, + ) + setDoc.Flags().StringP( + setDoc.GetDocFlag("kas").Name, + setDoc.GetDocFlag("kas").Shorthand, + setDoc.GetDocFlag("kas").Default, + setDoc.GetDocFlag("kas").Description, + ) + + doc := man.Docs.GetCommand("policy/kas-registry/key/base", + man.WithSubcommands(getDoc, setDoc)) + policyKasRegistryBaseKeysCmd = &doc.Command + policyKasRegistryKeysCmd.AddCommand( + policyKasRegistryBaseKeysCmd, + ) +} diff --git a/otdfctl/cmd/policy/kasGrants.go b/otdfctl/cmd/policy/kasGrants.go new file mode 100644 index 0000000000..e25abb159b --- /dev/null +++ b/otdfctl/cmd/policy/kasGrants.go @@ -0,0 +1,289 @@ +package policy + +import ( + "errors" + "fmt" + + "github.com/evertras/bubble-table/table" + "github.com/google/uuid" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +var forceFlagValue = false + +func assignKasGrant(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + cmd.Println(cli.WarningMessage(`Grants are now Key Mappings. To assign a key to attribute definition, value or namespace use the following commands. + + policy attributes namespace key assign + + policy attributes key assign + + policy attributes value key assign + `)) +} + +func unassignKasGrant(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + cmd.Println(cli.WarningMessage(`Grants are now Key Mappings. The unassign grant command will be removed in the next release. + + policy attributes namespace key remove + + policy attributes key remove + + policy attributes value key remove + `)) + + ctx := cmd.Context() + nsID := c.Flags.GetOptionalID("namespace-id") + attrID := c.Flags.GetOptionalID("attribute-id") + valID := c.Flags.GetOptionalID("value-id") + kasID := c.Flags.GetRequiredID("kas-id") + force := c.Flags.GetOptionalBool("force") + + count := 0 + for _, v := range []string{nsID, attrID, valID} { + if v != "" { + count++ + } + } + if count != 1 { + cli.ExitWithError("Must specify exactly one Attribute Namespace ID, Definition ID, or Value ID to unassign", errors.New("invalid flag values")) + } + var ( + res interface{} + err error + confirm string + rowID []string + rowFQN []string + ) + + kas, err := h.GetKasRegistryEntry(ctx, handlers.KasIdentifier{ + ID: kasID, + }) + if err != nil || kas == nil { + cli.ExitWithError("Failed to get registered KAS", err) + } + kasURI := kas.GetUri() + + //nolint:gocritic,nestif // this is more readable than a switch statement + if nsID != "" { + ns, err := h.GetNamespace(ctx, nsID) + if err != nil || ns == nil { + cli.ExitWithError("Failed to get namespace definition", err) + } + confirm = fmt.Sprintf("the grant to namespace FQN (%s) of KAS URI", ns.GetFqn()) + cli.ConfirmAction(cli.ActionDelete, confirm, kasURI, force) + res, err = h.DeleteKasGrantFromNamespace(ctx, nsID, kasID) + if err != nil { + cli.ExitWithError("Failed to update KAS grant for namespace", err) + } + + rowID = []string{"Namespace ID", nsID} + rowFQN = []string{"Namespace FQN", ns.GetFqn()} + } else if attrID != "" { + attr, err := h.GetAttribute(ctx, attrID) + if err != nil || attr == nil { + cli.ExitWithError("Failed to get attribute definition", err) + } + confirm = fmt.Sprintf("the grant to attribute FQN (%s) of KAS URI", attr.GetFqn()) + cli.ConfirmAction(cli.ActionDelete, confirm, kasURI, force) + res, err = h.DeleteKasGrantFromAttribute(ctx, attrID, kasID) + if err != nil { + cli.ExitWithError("Failed to update KAS grant for attribute", err) + } + + rowID = []string{"Attribute ID", attrID} + rowFQN = []string{"Attribute FQN", attr.GetFqn()} + } else { + val, err := h.GetAttributeValue(ctx, valID) + if err != nil || val == nil { + cli.ExitWithError("Failed to get attribute value", err) + } + confirm = fmt.Sprintf("the grant to attribute value FQN (%s) of KAS URI", val.GetFqn()) + cli.ConfirmAction(cli.ActionDelete, confirm, kasURI, force) + _, err = h.DeleteKasGrantFromValue(ctx, valID, kasID) + if err != nil { + cli.ExitWithError("Failed to update KAS grant for attribute value", err) + } + rowID = []string{"Value ID", valID} + rowFQN = []string{"Value FQN", val.GetFqn()} + } + + t := cli.NewTabular(rowID, rowFQN, + []string{"KAS ID", kasID}, + []string{"Unassigned Granted KAS URI", kasURI}, + ) + common.HandleSuccess(cmd, "", t, res) +} + +func listKasGrants(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + cmd.Println(cli.WarningMessage(`Grants are now Key Mappings. The ability to list grants will be removed in the next release.`)) + + kasF := c.Flags.GetOptionalString("kas") + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + var ( + kasID string + kasURI string + ) + + // if not a UUID, infer flag value passed was a URI + if kasF != "" { + _, err := uuid.Parse(kasF) + if err != nil { + kasURI = kasF + } else { + kasID = kasF + } + } + + grants, page, err := h.ListKasGrants(cmd.Context(), kasID, kasURI, limit, offset) + if err != nil { + cli.ExitWithError("Failed to list assigned KAS Grants", err) + } + + rows := []table.Row{} + t := cli.NewTable( + // columns should be kas id, kas uri, type, id, fqn + table.NewFlexColumn("kas_id", "KAS ID", cli.FlexColumnWidthThree), + table.NewFlexColumn("kas_uri", "KAS URI", cli.FlexColumnWidthThree), + table.NewFlexColumn("grant_type", "Assigned To", cli.FlexColumnWidthOne), + table.NewFlexColumn("id", "Granted Object ID", cli.FlexColumnWidthThree), + table.NewFlexColumn("fqn", "Granted Object FQN", cli.FlexColumnWidthThree), + ) + + for _, g := range grants { + grantedKasID := g.GetKeyAccessServer().GetId() + grantedKasURI := g.GetKeyAccessServer().GetUri() + for _, ag := range g.GetAttributeGrants() { + rows = append(rows, table.NewRow(table.RowData{ + "kas_id": grantedKasID, + "kas_uri": grantedKasURI, + "grant_type": "Definition", + "id": ag.GetId(), + "fqn": ag.GetFqn(), + })) + } + for _, vg := range g.GetValueGrants() { + rows = append(rows, table.NewRow(table.RowData{ + "kas_id": grantedKasID, + "kas_uri": grantedKasURI, + "grant_type": "Value", + "id": vg.GetId(), + "fqn": vg.GetFqn(), + })) + } + for _, ng := range g.GetNamespaceGrants() { + rows = append(rows, table.NewRow(table.RowData{ + "kas_id": grantedKasID, + "kas_uri": grantedKasURI, + "grant_type": "Namespace", + "id": ng.GetId(), + "fqn": ng.GetFqn(), + })) + } + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, page) + + // Do not supporting printing the 'get --id=...' helper message as grants are atypical + // with no individual ID. + cmd.Use = "" + common.HandleSuccess(cmd, "", t, grants) +} + +func initKASGrantsCommands() { + assignCmd := man.Docs.GetCommand("policy/kas-grants/assign", + man.WithRun(assignKasGrant), + ) + assignCmd.Flags().StringP( + assignCmd.GetDocFlag("namespace-id").Name, + assignCmd.GetDocFlag("namespace-id").Shorthand, + assignCmd.GetDocFlag("namespace-id").Default, + assignCmd.GetDocFlag("namespace-id").Description, + ) + assignCmd.Flags().StringP( + assignCmd.GetDocFlag("attribute-id").Name, + assignCmd.GetDocFlag("attribute-id").Shorthand, + assignCmd.GetDocFlag("attribute-id").Default, + assignCmd.GetDocFlag("attribute-id").Description, + ) + assignCmd.Flags().StringP( + assignCmd.GetDocFlag("value-id").Name, + assignCmd.GetDocFlag("value-id").Shorthand, + assignCmd.GetDocFlag("value-id").Default, + assignCmd.GetDocFlag("value-id").Description, + ) + assignCmd.Flags().StringP( + assignCmd.GetDocFlag("kas-id").Name, + assignCmd.GetDocFlag("kas-id").Shorthand, + assignCmd.GetDocFlag("kas-id").Default, + assignCmd.GetDocFlag("kas-id").Description, + ) + injectLabelFlags(&assignCmd.Command, true) + + unassignCmd := man.Docs.GetCommand("policy/kas-grants/unassign", + man.WithRun(unassignKasGrant), + ) + unassignCmd.Flags().StringP( + unassignCmd.GetDocFlag("namespace-id").Name, + unassignCmd.GetDocFlag("namespace-id").Shorthand, + unassignCmd.GetDocFlag("namespace-id").Default, + unassignCmd.GetDocFlag("namespace-id").Description, + ) + unassignCmd.Flags().StringP( + unassignCmd.GetDocFlag("attribute-id").Name, + unassignCmd.GetDocFlag("attribute-id").Shorthand, + unassignCmd.GetDocFlag("attribute-id").Default, + unassignCmd.GetDocFlag("attribute-id").Description, + ) + unassignCmd.Flags().StringP( + unassignCmd.GetDocFlag("value-id").Name, + unassignCmd.GetDocFlag("value-id").Shorthand, + unassignCmd.GetDocFlag("value-id").Default, + unassignCmd.GetDocFlag("value-id").Description, + ) + unassignCmd.Flags().StringP( + unassignCmd.GetDocFlag("kas-id").Name, + unassignCmd.GetDocFlag("kas-id").Shorthand, + unassignCmd.GetDocFlag("kas-id").Default, + unassignCmd.GetDocFlag("kas-id").Description, + ) + unassignCmd.Flags().BoolVar( + &forceFlagValue, + unassignCmd.GetDocFlag("force").Name, + false, + unassignCmd.GetDocFlag("force").Description, + ) + + listCmd := man.Docs.GetCommand("policy/kas-grants/list", + man.WithRun(listKasGrants), + ) + listCmd.Flags().StringP( + listCmd.GetDocFlag("kas").Name, + listCmd.GetDocFlag("kas").Shorthand, + listCmd.GetDocFlag("kas").Default, + listCmd.GetDocFlag("kas").Description, + ) + injectListPaginationFlags(listCmd) + + cmd := man.Docs.GetCommand("policy/kas-grants", + man.WithSubcommands(assignCmd, unassignCmd, listCmd), + ) + Cmd.AddCommand(&cmd.Command) +} diff --git a/otdfctl/cmd/policy/kasKeys.go b/otdfctl/cmd/policy/kasKeys.go new file mode 100644 index 0000000000..14f70c55ee --- /dev/null +++ b/otdfctl/cmd/policy/kasKeys.go @@ -0,0 +1,1123 @@ +package policy + +import ( + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/spf13/cobra" +) + +const ( + rsa2048Len = 2048 + rsa4096Len = 4096 + ecSecp256Len = 256 + ecSecp384Len = 384 + ecSecp521Len = 521 + keyStatusActive = "active" + keyStatusRotated = "rotated" + keyModeLocal = "local" + keyModeProvider = "provider" + keyModeRemote = "remote" + keyModePublicKeyOnly = "public_key" +) + +var policyKasRegistryKeysCmd = man.Docs.GetCommand("policy/kas-registry/key") + +func wrapKey(key string, wrappingKey []byte) ([]byte, error) { + aesKey, err := ocrypto.NewAESGcm(wrappingKey) + if err != nil { + return nil, errors.Join(errors.New("failed to create AES key"), err) + } + + wrappedKek, err := aesKey.Encrypt([]byte(key)) + if err != nil { + return nil, errors.Join(errors.New("failed to wrap key"), err) + } + + return wrappedKek, nil +} + +func generateKeys(alg policy.Algorithm) (string, string, error) { + kek, err := generateKeyPair(alg) + if err != nil { + return "", "", errors.Join(errors.New("failed to generate key pair"), err) + } + + kekPrivPem, err := kek.PrivateKeyInPemFormat() + if err != nil { + return "", "", errors.Join(errors.New("failed to get private key in pem format"), err) + } + + kekPubPem, err := kek.PublicKeyInPemFormat() + if err != nil { + return "", "", errors.Join(errors.New("failed to get public key in pem format"), err) + } + + return kekPrivPem, kekPubPem, nil +} + +func generateKeyPair(alg policy.Algorithm) (ocrypto.KeyPair, error) { + var key ocrypto.KeyPair + var err error + switch alg { + case policy.Algorithm_ALGORITHM_RSA_2048: + key, err = generateRSAKey(rsa2048Len) + case policy.Algorithm_ALGORITHM_RSA_4096: + key, err = generateRSAKey(rsa4096Len) + case policy.Algorithm_ALGORITHM_EC_P256: + key, err = generateECCKey(ecSecp256Len) + case policy.Algorithm_ALGORITHM_EC_P384: + key, err = generateECCKey(ecSecp384Len) + case policy.Algorithm_ALGORITHM_EC_P521: + key, err = generateECCKey(ecSecp521Len) + case policy.Algorithm_ALGORITHM_HPQT_XWING: + key, err = ocrypto.NewKeyPair(ocrypto.HybridXWingKey) + case policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768: + key, err = ocrypto.NewKeyPair(ocrypto.HybridSecp256r1MLKEM768Key) + case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: + key, err = ocrypto.NewKeyPair(ocrypto.HybridSecp384r1MLKEM1024Key) + case policy.Algorithm_ALGORITHM_UNSPECIFIED: + fallthrough + default: + return nil, errors.New("unsupported algorithm") + } + + return key, err +} + +func generateRSAKey(size int) (ocrypto.RsaKeyPair, error) { + return ocrypto.NewRSAKeyPair(size) +} + +func generateECCKey(size int) (ocrypto.ECKeyPair, error) { + mode, err := ocrypto.ECSizeToMode(size) + if err != nil { + return ocrypto.ECKeyPair{}, err + } + + return ocrypto.NewECKeyPair(mode) +} + +func enumToStatus(enum policy.KeyStatus) (string, error) { + switch enum { //nolint:exhaustive // UNSPECIFIED is not needed here + case policy.KeyStatus_KEY_STATUS_ACTIVE: + return keyStatusActive, nil + case policy.KeyStatus_KEY_STATUS_ROTATED: + return keyStatusRotated, nil + default: + return "", errors.New("invalid enum status") + } +} + +func enumToMode(enum policy.KeyMode) (string, error) { + switch enum { //nolint:exhaustive // UNSPECIFIED is not needed here + case policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY: + return keyModeLocal, nil + case policy.KeyMode_KEY_MODE_PROVIDER_ROOT_KEY: + return keyModeProvider, nil + case policy.KeyMode_KEY_MODE_REMOTE: + return keyModeRemote, nil + case policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY: + return keyModePublicKeyOnly, nil + default: + return "", errors.New("invalid enum mode") + } +} + +func modeToEnum(mode string) (policy.KeyMode, error) { + switch strings.ToLower(mode) { + case keyModeLocal: + return policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, nil + case keyModeProvider: + return policy.KeyMode_KEY_MODE_PROVIDER_ROOT_KEY, nil + case keyModeRemote: + return policy.KeyMode_KEY_MODE_REMOTE, nil + case keyModePublicKeyOnly: + return policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, nil + default: + return policy.KeyMode_KEY_MODE_UNSPECIFIED, errors.New("invalid mode") + } +} + +func getTableRows(kasKey *policy.KasKey) [][]string { + var err error + asymkey := kasKey.GetKey() + + statusStr, err := enumToStatus(asymkey.GetKeyStatus()) + if err != nil { + cli.ExitWithError("Failed to convert status", err) + } + modeStr, err := enumToMode(asymkey.GetKeyMode()) + if err != nil { + cli.ExitWithError("Failed to convert mode", err) + } + algStr, err := cli.KeyEnumToAlg(asymkey.GetKeyAlgorithm()) + if err != nil { + cli.ExitWithError("Failed to convert algorithm", err) + } + + rows := [][]string{ + {"ID", asymkey.GetId()}, + {"KAS URI", kasKey.GetKasUri()}, + {"Key ID", asymkey.GetKeyId()}, + {"Algorithm", algStr}, + {"Status", statusStr}, + {"Mode", modeStr}, + {"Legacy", strconv.FormatBool(asymkey.GetLegacy())}, + } + return rows +} + +// TODO: Handle wrapping the generated key with provider config. +func policyCreateKasKey(cmd *cobra.Command, args []string) { + var wrappingKeyID string + + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + keyIdentifier := c.Flags.GetRequiredString("key-id") + kasIdentifier := c.Flags.GetRequiredString("kas") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + // Use the helper function to get and validate key parameters + alg, mode, wrappingKeyID, err := prepareKeyParams(c) + if err != nil { + cli.ExitWithError("Invalid key parameters", err) + } + + // Use the helper function to prepare key contexts + publicKeyCtx, privateKeyCtx, providerConfigID, err := prepareKeyContexts(c, mode, alg, wrappingKeyID) + if err != nil { + cli.ExitWithError("Failed to prepare key contexts", err) + } + + kasLookup, err := resolveKasIdentifier(kasIdentifier) + if err != nil { + cli.ExitWithError("Invalid kas identifier", err) + } + + var resolvedKasID string + if kasLookup.ID != "" { + resolvedKasID = kasLookup.ID + } else { + // If not a UUID, resolve it to get the UUID + kasEntry, err := h.GetKasRegistryEntry(c.Context(), kasLookup) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to resolve KAS identifier '%s'", kasIdentifier), err) + } + resolvedKasID = kasEntry.GetId() + } + + kasKey, err := h.CreateKasKey( + c.Context(), + resolvedKasID, + keyIdentifier, + alg, + mode, + publicKeyCtx, + privateKeyCtx, + providerConfigID, + getMetadataMutable(metadataLabels), + false, + ) + if err != nil { + cli.ExitWithError("Failed to create kas key", err) + } + + rows := getTableRows(kasKey) + if mdRows := getMetadataRows(kasKey.GetKey().GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, kasKey.GetKey().GetId(), t, kasKey) +} + +func policyImportKasKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + privateKeyPem := c.Flags.GetRequiredString("private-key-pem") + wrappingKey := c.Flags.GetRequiredString("wrapping-key") + wrappingKeyID := c.Flags.GetRequiredString("wrapping-key-id") + publicKeyPem := c.Flags.GetRequiredString("public-key-pem") + keyIdentifier := c.Flags.GetRequiredString("key-id") + algorithm := c.Flags.GetRequiredString("algorithm") + kasIdentifier := c.Flags.GetRequiredString("kas") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + legacy, err := getLegacyFlag(c) + if err != nil { + cli.ExitWithError("Invalid legacy flag", err) + } + if legacy == nil { + legacy = new(bool) + *legacy = false + } + + // Parse algorithm early for validation + alg, err := cli.KeyAlgToEnum(algorithm) + if err != nil { + cli.ExitWithError("Invalid algorithm", err) + } + + decodedPub, err := base64.StdEncoding.DecodeString(publicKeyPem) + if err != nil { + cli.ExitWithError("public-key-pem must be base64 encoded", err) + } + if err := validatePublicKey(decodedPub, alg); err != nil { + cli.ExitWithError("Invalid public key pem", err) + } + nonBase64PrivateKey, err := base64.StdEncoding.DecodeString(privateKeyPem) + if err != nil { + cli.ExitWithError("private-key-pem must be base64 encoded", err) + } + + wrappingKeyBytes, err := hex.DecodeString(wrappingKey) + if err != nil { + cli.ExitWithError("wrapping-key must be hex encoded", err) + } + wrappedPrivateKey, err := wrapKey(string(nonBase64PrivateKey), wrappingKeyBytes) + if err != nil { + cli.ExitWithError("failed to wrap key", err) + } + + kasLookup, err := resolveKasIdentifier(kasIdentifier) + if err != nil { + cli.ExitWithError("Invalid kas identifier", err) + } + var resolvedKasID string + if kasLookup.ID != "" { + resolvedKasID = kasLookup.ID + } else { + // If not a UUID, resolve it to get the UUID + kasEntry, err := h.GetKasRegistryEntry(c.Context(), kasLookup) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to resolve KAS identifier '%s'", kasIdentifier), err) + } + resolvedKasID = kasEntry.GetId() + } + + importedKey, err := h.CreateKasKey(c.Context(), + resolvedKasID, + keyIdentifier, + alg, + policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + &policy.PublicKeyCtx{Pem: publicKeyPem}, + &policy.PrivateKeyCtx{ + KeyId: wrappingKeyID, + WrappedKey: base64.StdEncoding.EncodeToString(wrappedPrivateKey), + }, + "", + getMetadataMutable(metadataLabels), + *legacy, + ) + if err != nil { + cli.ExitWithError("Failed to import kas key", err) + } + + rows := getTableRows(importedKey) + if mdRows := getMetadataRows(importedKey.GetKey().GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, importedKey.GetKey().GetId(), t, importedKey) +} + +func policyGetKasKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalString("key") + + var identifier *kasregistry.KasKeyIdentifier + var err error + + if utils.ClassifyString(id) != utils.StringTypeUUID { + identifier, err = getKasKeyIdentifier(c) + if err != nil { + cli.ExitWithError("Invalid key identifier", err) + } + } + kasKey, err := h.GetKasKey(c.Context(), id, identifier) + if err != nil { + cli.ExitWithError("Failed to get kas key", err) + } + + rows := getTableRows(kasKey) + if mdRows := getMetadataRows(kasKey.GetKey().GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, kasKey.GetKey().GetId(), t, kasKey) +} + +func policyUpdateKasKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + resp, err := h.UpdateKasKey( + c.Context(), + id, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior()) + if err != nil { + cli.ExitWithError("Failed to update kas key", err) + } + + // Get KAS Key. + kasKey, err := h.GetKasKey(c.Context(), resp.GetKey().GetId(), nil) + if err != nil { + cli.ExitWithError("Failed to get kas key", err) + } + + rows := getTableRows(kasKey) + if mdRows := getMetadataRows(kasKey.GetKey().GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, kasKey.GetKey().GetId(), t, kasKey) +} + +func policyListKasKeys(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + algArg := c.Flags.GetOptionalString("algorithm") + var alg policy.Algorithm + if algArg != "" { + var err error + alg, err = cli.KeyAlgToEnum(algArg) + if err != nil { + cli.ExitWithError("Invalid algorithm", err) + } + } + kasIdentifier := c.Flags.GetOptionalString("kas") + legacy, err := getLegacyFlag(c) + if err != nil { + cli.ExitWithError("Invalid legacy flag", err) + } + sort := getSortOption(c) + + kasLookup, err := resolveKasIdentifier(kasIdentifier) + if err != nil { + cli.ExitWithError("Invalid kas identifier", err) + } + + // Get the list of keys. + resp, err := h.ListKasKeys(c.Context(), limit, offset, alg, kasLookup, legacy, sort) + if err != nil { + cli.ExitWithError("Failed to list kas keys", err) + } + + t := cli.NewTable( + // columns should be id, name, config, labels, created_at, updated_at + cli.NewUUIDColumn(), + table.NewFlexColumn("kasUri", "KAS URI", cli.FlexColumnWidthThree), + table.NewFlexColumn("keyId", "Key ID", cli.FlexColumnWidthOne), + table.NewFlexColumn("keyAlgorithm", "Key Algorithm", cli.FlexColumnWidthOne), + table.NewFlexColumn("keyStatus", "Key Status", cli.FlexColumnWidthOne), + table.NewFlexColumn("keyMode", "Key Mode", cli.FlexColumnWidthOne), + table.NewFlexColumn("legacy", "Legacy", cli.FlexColumnWidthOne), + ) + rows := []table.Row{} + for _, kasKey := range resp.GetKasKeys() { + key := kasKey.GetKey() + statusStr, err := enumToStatus(key.GetKeyStatus()) + if err != nil { + cli.ExitWithError("Failed to convert status", err) + } + modeStr, err := enumToMode(key.GetKeyMode()) + if err != nil { + cli.ExitWithError("Failed to convert mode", err) + } + algStr, err := cli.KeyEnumToAlg(key.GetKeyAlgorithm()) + if err != nil { + cli.ExitWithError("Failed to convert algorithm", err) + } + + rows = append(rows, table.NewRow(table.RowData{ + "id": key.GetId(), + "kasUri": kasKey.GetKasUri(), + "keyId": key.GetKeyId(), + "keyAlgorithm": algStr, + "keyStatus": statusStr, + "keyMode": modeStr, + "legacy": strconv.FormatBool(key.GetLegacy()), + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func policyListKeyMappings(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + id := c.Flags.GetOptionalID("id") + keyID := c.Flags.GetOptionalString("key-id") + kasIdentifier := c.Flags.GetOptionalString("kas") + + var keyIdentifier *kasregistry.KasKeyIdentifier + // Since keyID and kasIdentifier are required together, if one is provided, the other must be provided as well. + if id == "" && keyID != "" { + kasLookup, err := resolveKasIdentifier(kasIdentifier) + if err != nil { + cli.ExitWithError("Could not resolve KAS identifier", err) + } + keyIdentifier = &kasregistry.KasKeyIdentifier{ + Kid: keyID, + } + switch { + case kasLookup.ID != "": + keyIdentifier.Identifier = &kasregistry.KasKeyIdentifier_KasId{ + KasId: kasLookup.ID, + } + case kasLookup.URI != "": + keyIdentifier.Identifier = &kasregistry.KasKeyIdentifier_Uri{ + Uri: kasLookup.URI, + } + case kasLookup.Name != "": + keyIdentifier.Identifier = &kasregistry.KasKeyIdentifier_Name{ + Name: kasLookup.Name, + } + } + } + + resp, err := h.ListKeyMappings(c.Context(), limit, offset, id, keyIdentifier) + if err != nil { + cli.ExitWithError("Could not list key mappings", err) + } + + rows := getKeyMappingsTableRows(resp.GetKeyMappings()) + t := cli.NewTable( + table.NewFlexColumn("kas_uri", "KAS URI", cli.FlexColumnWidthOne), + table.NewFlexColumn("key_id", "Key ID", cli.FlexColumnWidthOne), + table.NewFlexColumn("namespace_mappings", "Namespaces", cli.FlexColumnWidthThree), + table.NewFlexColumn("attribute_mappings", "Attributes", cli.FlexColumnWidthThree), + table.NewFlexColumn("value_mappings", "Values", cli.FlexColumnWidthThree), + ).WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + + common.HandleSuccess(cmd, "", t, resp) +} + +func getKeyMappingsTableRows(mappings []*kasregistry.KeyMapping) []table.Row { + rows := make([]table.Row, len(mappings)) + for i, m := range mappings { + rows[i] = table.NewRow(table.RowData{ + "kas_uri": m.GetKasUri(), + "key_id": m.GetKid(), + "namespace_mappings": formatMappedPolicyObject(m.GetNamespaceMappings()), + "attribute_mappings": formatMappedPolicyObject(m.GetAttributeMappings()), + "value_mappings": formatMappedPolicyObject(m.GetValueMappings()), + }) + } + return rows +} + +func formatMappedPolicyObject(m []*kasregistry.MappedPolicyObject) string { + if len(m) == 0 { + return "No mappings found" + } + fqns := make([]string, len(m)) + for i, obj := range m { + fqns[i] = obj.GetFqn() + } + return strings.Join(fqns, ", ") +} + +func policyRotateKasKey(cmd *cobra.Command, args []string) { + var wrappingKeyID string + + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + // Get parameters for the old key + oldKey := c.Flags.GetRequiredString("key") + + // Get parameters for creating the new key + newKeyID := c.Flags.GetRequiredString("key-id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + // Use the helper function to get and validate key parameters + alg, mode, wrappingKeyID, err := prepareKeyParams(c) + if err != nil { + cli.ExitWithError("Invalid key parameters", err) + } + + // Use the helper function to prepare key contexts + publicKeyCtx, privateKeyCtx, providerConfigID, err := prepareKeyContexts(c, mode, alg, wrappingKeyID) + if err != nil { + cli.ExitWithError("Failed to prepare key contexts", err) + } + + // Create the new key request with the contexts created by the helper + newKey := &kasregistry.RotateKeyRequest_NewKey{ + KeyId: newKeyID, + Algorithm: alg, + KeyMode: mode, + PublicKeyCtx: publicKeyCtx, + PrivateKeyCtx: privateKeyCtx, + ProviderConfigId: providerConfigID, + Metadata: getMetadataMutable(metadataLabels), + } + + var identifier *kasregistry.KasKeyIdentifier + if utils.ClassifyString(oldKey) != utils.StringTypeUUID { + identifier, err = getKasKeyIdentifier(c) + if err != nil { + cli.ExitWithError("Invalid key identifier", err) + } + } + + // Call the rotate key function + rotateKeyResult, err := h.RotateKasKey( + c.Context(), + oldKey, + identifier, + newKey, + ) + if err != nil { + cli.ExitWithError("Failed to rotate key", err) + } + + rows := getTableRows(rotateKeyResult.KasKey) + if mdRows := getMetadataRows(rotateKeyResult.KasKey.GetKey().GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, rotateKeyResult.KasKey.GetKey().GetId(), t, rotateKeyResult) +} + +func resolveKasIdentifier(ident string) (handlers.KasIdentifier, error) { + // If the identifier is empty, it means no KAS filter is applied. + // Return an empty KasIdentifier and no error. + if ident == "" { + return handlers.KasIdentifier{}, nil + } + + // Use the ClassifyString helper to determine how to look up the KAS + kasLookup := handlers.KasIdentifier{} + kasInputType := utils.ClassifyString(ident) + + switch kasInputType { //nolint:exhaustive // default catches unknown + case utils.StringTypeUUID: + kasLookup.ID = ident + case utils.StringTypeURI: + kasLookup.URI = ident + case utils.StringTypeGeneric: + kasLookup.Name = ident + default: + return kasLookup, errors.New("invalid kas identifier") + } + + return kasLookup, nil +} + +// prepareKeyParams parses and validates the common key parameters used by both create and rotate operations. +// It returns the algorithm, mode, wrapping key ID, and any error that occurred. +func prepareKeyParams(c *cli.Cli) (policy.Algorithm, policy.KeyMode, string, error) { + // Parse algorithm + alg, err := cli.KeyAlgToEnum(c.Flags.GetRequiredString("algorithm")) + if err != nil { + return alg, 0, "", err + } + + // Parse mode + mode, err := modeToEnum(c.Flags.GetRequiredString("mode")) + if err != nil { + return alg, mode, "", err + } + + // Get wrapping key ID and validate based on mode + wrappingKeyID := c.Flags.GetOptionalString("wrapping-key-id") + if mode != policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY && wrappingKeyID == "" { + formattedMode, _ := enumToMode(mode) + return alg, mode, "", fmt.Errorf("wrapping-key-id is required for mode %s", formattedMode) + } + + return alg, mode, wrappingKeyID, nil +} + +// prepareKeyContexts prepares the key contexts based on the specified mode and parameters. +// This function encapsulates the common logic between key creation and key rotation. +func prepareKeyContexts( + c *cli.Cli, + mode policy.KeyMode, + alg policy.Algorithm, + wrappingKeyID string, +) (*policy.PublicKeyCtx, *policy.PrivateKeyCtx, string, error) { + var publicKeyCtx *policy.PublicKeyCtx + var privateKeyCtx *policy.PrivateKeyCtx + var providerConfigID string + + switch mode { + case policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY: + // Local mode: generate keys locally and wrap with provided wrapping key + wrappingKey := c.Flags.GetRequiredString("wrapping-key") + wrappedKeyBytes, err := hex.DecodeString(wrappingKey) + if err != nil { + return nil, nil, "", errors.Join(errors.New("wrapping-key must be hex encoded"), err) + } + + privateKeyPem, publicKeyPem, err := generateKeys(alg) + if err != nil { + return nil, nil, "", errors.Join(errors.New("failed to generate keys"), err) + } + + privateKey, err := wrapKey(privateKeyPem, wrappedKeyBytes) + if err != nil { + return nil, nil, "", errors.Join(errors.New("failed to wrap key"), err) + } + + pubPemBase64 := base64.StdEncoding.EncodeToString([]byte(publicKeyPem)) + privPemBase64 := base64.StdEncoding.EncodeToString(privateKey) + publicKeyCtx = &policy.PublicKeyCtx{ + Pem: pubPemBase64, + } + privateKeyCtx = &policy.PrivateKeyCtx{ + KeyId: wrappingKeyID, + WrappedKey: privPemBase64, + } + case policy.KeyMode_KEY_MODE_PROVIDER_ROOT_KEY: + providerConfigID = c.Flags.GetRequiredString("provider-config-id") + publicPem := c.Flags.GetRequiredString("public-key-pem") + privatePem := c.Flags.GetRequiredString("private-key-pem") + decodedPub, err := base64.StdEncoding.DecodeString(publicPem) + if err != nil { + return nil, nil, "", errors.Join(errors.New("public key pem must be base64 encoded"), err) + } + if err := validatePublicKey(decodedPub, alg); err != nil { + return nil, nil, "", err + } + _, err = base64.StdEncoding.DecodeString(privatePem) + if err != nil { + return nil, nil, "", errors.Join(errors.New("private key pem must be base64 encoded"), err) + } + publicKeyCtx = &policy.PublicKeyCtx{ + Pem: publicPem, + } + privateKeyCtx = &policy.PrivateKeyCtx{ + KeyId: wrappingKeyID, + WrappedKey: privatePem, + } + case policy.KeyMode_KEY_MODE_REMOTE: + pem := c.Flags.GetRequiredString("public-key-pem") + providerConfigID = c.Flags.GetRequiredString("provider-config-id") + + decoded, err := base64.StdEncoding.DecodeString(pem) + if err != nil { + return nil, nil, "", errors.Join(errors.New("pem must be base64 encoded"), err) + } + if err := validatePublicKey(decoded, alg); err != nil { + return nil, nil, "", err + } + + publicKeyCtx = &policy.PublicKeyCtx{ + Pem: pem, + } + privateKeyCtx = &policy.PrivateKeyCtx{ + KeyId: wrappingKeyID, + } + case policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY: + pem := c.Flags.GetRequiredString("public-key-pem") + decoded, err := base64.StdEncoding.DecodeString(pem) + if err != nil { + return nil, nil, "", errors.Join(errors.New("pem must be base64 encoded"), err) + } + if err := validatePublicKey(decoded, alg); err != nil { + return nil, nil, "", err + } + publicKeyCtx = &policy.PublicKeyCtx{ + Pem: pem, + } + case policy.KeyMode_KEY_MODE_UNSPECIFIED: + fallthrough + default: + return nil, nil, "", errors.New("invalid mode") + } + + return publicKeyCtx, privateKeyCtx, providerConfigID, nil +} + +// validatePublicKey validates the PEM public key against the expected algorithm. +func validatePublicKey(pemDecoded []byte, alg policy.Algorithm) error { + err := utils.ValidatePublicKeyPEM(pemDecoded, alg) + if err != nil { + return fmt.Errorf("invalid public key pem: %w", err) + } + return nil +} + +func policyUnsafeDeleteKasKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + kid := c.Flags.GetRequiredString("key-id") + kasURI := c.Flags.GetRequiredString("kas-uri") + force := c.Flags.GetOptionalBool("force") + + cli.ConfirmAction(cli.ActionDelete, "key with kas uri: "+kasURI+", and key identifier: "+kid, "Id: "+id, force) + + key, err := h.UnsafeDeleteKasKey(ctx, id, kid, kasURI) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to delete key (%s)", id), err) + } + + rows := [][]string{ + {"Deleted", "true"}, + {"Id", key.GetKey().GetId()}, + {"KasURI", key.GetKasUri()}, + {"Key Identifier", key.GetKey().GetKeyId()}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, key) +} + +func getLegacyFlag(c *cli.Cli) (*bool, error) { + var ( + legacy *bool + err error + ) + + legacyStr := c.Flags.GetOptionalString("legacy") + if legacyStr == "" { + return legacy, nil + } + + legacy = new(bool) + *legacy, err = strconv.ParseBool(legacyStr) + if err != nil { + return nil, errors.New("invalid value for legacy flag. Must be true or false") + } + return legacy, nil +} + +func initKASKeysCommands() { + // Create Kas Key + createDoc := man.Docs.GetCommand("policy/kas-registry/key/create", + man.WithRun(policyCreateKasKey), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("key-id").Name, + createDoc.GetDocFlag("key-id").Shorthand, + createDoc.GetDocFlag("key-id").Default, + createDoc.GetDocFlag("key-id").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("algorithm").Name, + createDoc.GetDocFlag("algorithm").Shorthand, + createDoc.GetDocFlag("algorithm").Default, + createDoc.GetDocFlag("algorithm").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("mode").Name, + createDoc.GetDocFlag("mode").Shorthand, + createDoc.GetDocFlag("mode").Default, + createDoc.GetDocFlag("mode").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("kas").Name, + createDoc.GetDocFlag("kas").Shorthand, + createDoc.GetDocFlag("kas").Default, + createDoc.GetDocFlag("kas").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("wrapping-key-id").Name, + createDoc.GetDocFlag("wrapping-key-id").Shorthand, + createDoc.GetDocFlag("wrapping-key-id").Default, + createDoc.GetDocFlag("wrapping-key-id").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("wrapping-key").Name, + createDoc.GetDocFlag("wrapping-key").Shorthand, + createDoc.GetDocFlag("wrapping-key").Default, + createDoc.GetDocFlag("wrapping-key").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("provider-config-id").Name, + createDoc.GetDocFlag("provider-config-id").Shorthand, + createDoc.GetDocFlag("provider-config-id").Default, + createDoc.GetDocFlag("provider-config-id").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("public-key-pem").Name, + createDoc.GetDocFlag("public-key-pem").Shorthand, + createDoc.GetDocFlag("public-key-pem").Default, + createDoc.GetDocFlag("public-key-pem").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("private-key-pem").Name, + createDoc.GetDocFlag("private-key-pem").Shorthand, + createDoc.GetDocFlag("private-key-pem").Default, + createDoc.GetDocFlag("private-key-pem").Description, + ) + injectLabelFlags(&createDoc.Command, false) + createDoc.MarkSensitiveFlags() + + // Get Kas Key + getDoc := man.Docs.GetCommand("policy/kas-registry/key/get", + man.WithRun(policyGetKasKey), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("key").Name, + getDoc.GetDocFlag("key").Shorthand, + getDoc.GetDocFlag("key").Default, + getDoc.GetDocFlag("key").Description, + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("kas").Name, + getDoc.GetDocFlag("kas").Shorthand, + getDoc.GetDocFlag("kas").Default, + getDoc.GetDocFlag("kas").Description, + ) + // Update Kas Key + updateDoc := man.Docs.GetCommand("policy/kas-registry/key/update", + man.WithRun(policyUpdateKasKey), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + // List Kas Keys + listDoc := man.Docs.GetCommand("policy/kas-registry/key/list", + man.WithRun(policyListKasKeys), + ) + listDoc.Flags().StringP( + listDoc.GetDocFlag("algorithm").Name, + listDoc.GetDocFlag("algorithm").Shorthand, + listDoc.GetDocFlag("algorithm").Default, + listDoc.GetDocFlag("algorithm").Description, + ) + listDoc.Flags().StringP( + listDoc.GetDocFlag("kas").Name, + listDoc.GetDocFlag("kas").Shorthand, + listDoc.GetDocFlag("kas").Default, + listDoc.GetDocFlag("kas").Description, + ) + listDoc.Flags().StringP( + listDoc.GetDocFlag("legacy").Name, + listDoc.GetDocFlag("legacy").Shorthand, + listDoc.GetDocFlag("legacy").Default, + listDoc.GetDocFlag("legacy").Description, + ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) + + // Rotate Kas Key + rotateDoc := man.Docs.GetCommand("policy/kas-registry/key/rotate", + man.WithRun(policyRotateKasKey), + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("key").Name, + rotateDoc.GetDocFlag("key").Shorthand, + rotateDoc.GetDocFlag("key").Default, + rotateDoc.GetDocFlag("key").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("kas").Name, + rotateDoc.GetDocFlag("kas").Shorthand, + rotateDoc.GetDocFlag("kas").Default, + rotateDoc.GetDocFlag("kas").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("key-id").Name, + rotateDoc.GetDocFlag("key-id").Shorthand, + rotateDoc.GetDocFlag("key-id").Default, + rotateDoc.GetDocFlag("key-id").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("algorithm").Name, + rotateDoc.GetDocFlag("algorithm").Shorthand, + rotateDoc.GetDocFlag("algorithm").Default, + rotateDoc.GetDocFlag("algorithm").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("mode").Name, + rotateDoc.GetDocFlag("mode").Shorthand, + rotateDoc.GetDocFlag("mode").Default, + rotateDoc.GetDocFlag("mode").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("wrapping-key-id").Name, + rotateDoc.GetDocFlag("wrapping-key-id").Shorthand, + rotateDoc.GetDocFlag("wrapping-key-id").Default, + rotateDoc.GetDocFlag("wrapping-key-id").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("wrapping-key").Name, + rotateDoc.GetDocFlag("wrapping-key").Shorthand, + rotateDoc.GetDocFlag("wrapping-key").Default, + rotateDoc.GetDocFlag("wrapping-key").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("provider-config-id").Name, + rotateDoc.GetDocFlag("provider-config-id").Shorthand, + rotateDoc.GetDocFlag("provider-config-id").Default, + rotateDoc.GetDocFlag("provider-config-id").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("public-key-pem").Name, + rotateDoc.GetDocFlag("public-key-pem").Shorthand, + rotateDoc.GetDocFlag("public-key-pem").Default, + rotateDoc.GetDocFlag("public-key-pem").Description, + ) + rotateDoc.Flags().StringP( + rotateDoc.GetDocFlag("private-key-pem").Name, + rotateDoc.GetDocFlag("private-key-pem").Shorthand, + rotateDoc.GetDocFlag("private-key-pem").Default, + rotateDoc.GetDocFlag("private-key-pem").Description, + ) + injectLabelFlags(&rotateDoc.Command, true) + rotateDoc.MarkSensitiveFlags() + + // Import Kas Key + importDoc := man.Docs.GetCommand("policy/kas-registry/key/import", + man.WithRun(policyImportKasKey), + ) + importDoc.Flags().StringP( + importDoc.GetDocFlag("key-id").Name, + importDoc.GetDocFlag("key-id").Shorthand, + importDoc.GetDocFlag("key-id").Default, + importDoc.GetDocFlag("key-id").Description, + ) + importDoc.Flags().StringP( + importDoc.GetDocFlag("algorithm").Name, + importDoc.GetDocFlag("algorithm").Shorthand, + importDoc.GetDocFlag("algorithm").Default, + importDoc.GetDocFlag("algorithm").Description, + ) + importDoc.Flags().StringP( + importDoc.GetDocFlag("kas").Name, + importDoc.GetDocFlag("kas").Shorthand, + importDoc.GetDocFlag("kas").Default, + importDoc.GetDocFlag("kas").Description, + ) + importDoc.Flags().StringP( + importDoc.GetDocFlag("wrapping-key-id").Name, + importDoc.GetDocFlag("wrapping-key-id").Shorthand, + importDoc.GetDocFlag("wrapping-key-id").Default, + importDoc.GetDocFlag("wrapping-key-id").Description, + ) + importDoc.Flags().StringP( + importDoc.GetDocFlag("wrapping-key").Name, + importDoc.GetDocFlag("wrapping-key").Shorthand, + importDoc.GetDocFlag("wrapping-key").Default, + importDoc.GetDocFlag("wrapping-key").Description, + ) + importDoc.Flags().StringP( + importDoc.GetDocFlag("public-key-pem").Name, + importDoc.GetDocFlag("public-key-pem").Shorthand, + importDoc.GetDocFlag("public-key-pem").Default, + importDoc.GetDocFlag("public-key-pem").Description, + ) + importDoc.Flags().StringP( + importDoc.GetDocFlag("private-key-pem").Name, + importDoc.GetDocFlag("private-key-pem").Shorthand, + importDoc.GetDocFlag("private-key-pem").Default, + importDoc.GetDocFlag("private-key-pem").Description, + ) + importDoc.Flags().StringP( + importDoc.GetDocFlag("legacy").Name, + importDoc.GetDocFlag("legacy").Shorthand, + importDoc.GetDocFlag("legacy").Default, + importDoc.GetDocFlag("legacy").Description, + ) + injectLabelFlags(&importDoc.Command, false) + importDoc.MarkSensitiveFlags() + + mappingsDoc := man.Docs.GetCommand("policy/kas-registry/key/list-mappings", + man.WithRun(policyListKeyMappings), + ) + mappingsDoc.Flags().StringP( + mappingsDoc.GetDocFlag("id").Name, + mappingsDoc.GetDocFlag("id").Shorthand, + mappingsDoc.GetDocFlag("id").Default, + mappingsDoc.GetDocFlag("id").Description, + ) + mappingsDoc.Flags().StringP( + mappingsDoc.GetDocFlag("key-id").Name, + mappingsDoc.GetDocFlag("key-id").Shorthand, + mappingsDoc.GetDocFlag("key-id").Default, + mappingsDoc.GetDocFlag("key-id").Description, + ) + mappingsDoc.Flags().StringP( + mappingsDoc.GetDocFlag("kas").Name, + mappingsDoc.GetDocFlag("kas").Shorthand, + mappingsDoc.GetDocFlag("kas").Default, + mappingsDoc.GetDocFlag("kas").Description, + ) + mappingsDoc.MarkFlagsMutuallyExclusive("key-id", "id") + mappingsDoc.MarkFlagsMutuallyExclusive("kas", "id") + mappingsDoc.MarkFlagsRequiredTogether("key-id", "kas") + injectListPaginationFlags(mappingsDoc) + + // Unsafe Delete Kas Key + unsafeCmd := man.Docs.GetCommand("policy/kas-registry/key/unsafe") + unsafeCmd.PersistentFlags().Bool( + unsafeCmd.GetDocFlag("force").Name, + false, + unsafeCmd.GetDocFlag("force").Description, + ) + + unsafeDeleteDoc := man.Docs.GetCommand("policy/kas-registry/key/unsafe/delete", + man.WithRun(policyUnsafeDeleteKasKey), + ) + unsafeDeleteDoc.Flags().StringP( + unsafeDeleteDoc.GetDocFlag("id").Name, + unsafeDeleteDoc.GetDocFlag("id").Shorthand, + unsafeDeleteDoc.GetDocFlag("id").Default, + unsafeDeleteDoc.GetDocFlag("id").Description, + ) + unsafeDeleteDoc.Flags().StringP( + unsafeDeleteDoc.GetDocFlag("key-id").Name, + unsafeDeleteDoc.GetDocFlag("key-id").Shorthand, + unsafeDeleteDoc.GetDocFlag("key-id").Default, + unsafeDeleteDoc.GetDocFlag("key-id").Description, + ) + unsafeDeleteDoc.Flags().StringP( + unsafeDeleteDoc.GetDocFlag("kas-uri").Name, + unsafeDeleteDoc.GetDocFlag("kas-uri").Shorthand, + unsafeDeleteDoc.GetDocFlag("kas-uri").Default, + unsafeDeleteDoc.GetDocFlag("kas-uri").Description, + ) + + unsafeCmd.AddSubcommands(unsafeDeleteDoc) + policyKasRegistryKeysCmd.AddSubcommands(createDoc, getDoc, updateDoc, listDoc, rotateDoc, importDoc, mappingsDoc, unsafeCmd) + KasRegistryCmd.AddCommand(&policyKasRegistryKeysCmd.Command) +} diff --git a/otdfctl/cmd/policy/kasKeys_test.go b/otdfctl/cmd/policy/kasKeys_test.go new file mode 100644 index 0000000000..f1fc882d3a --- /dev/null +++ b/otdfctl/cmd/policy/kasKeys_test.go @@ -0,0 +1,43 @@ +package policy + +import ( + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/require" +) + +func TestGenerateKeyPair_Hybrid(t *testing.T) { + tests := []struct { + name string + alg policy.Algorithm + keyType ocrypto.KeyType + }{ + {"X-Wing", policy.Algorithm_ALGORITHM_HPQT_XWING, ocrypto.HybridXWingKey}, + {"P256-MLKEM768", policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, ocrypto.HybridSecp256r1MLKEM768Key}, + {"P384-MLKEM1024", policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, ocrypto.HybridSecp384r1MLKEM1024Key}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kp, err := generateKeyPair(tt.alg) + require.NoError(t, err) + require.Equal(t, tt.keyType, kp.GetKeyType()) + + pubPem, err := kp.PublicKeyInPemFormat() + require.NoError(t, err) + require.NotEmpty(t, pubPem) + + privPem, err := kp.PrivateKeyInPemFormat() + require.NoError(t, err) + require.NotEmpty(t, privPem) + }) + } +} + +func TestGenerateKeyPair_Unsupported(t *testing.T) { + _, err := generateKeyPair(policy.Algorithm_ALGORITHM_UNSPECIFIED) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported algorithm") +} diff --git a/otdfctl/cmd/policy/kasRegistry.go b/otdfctl/cmd/policy/kasRegistry.go new file mode 100644 index 0000000000..4b5884cfa5 --- /dev/null +++ b/otdfctl/cmd/policy/kasRegistry.go @@ -0,0 +1,313 @@ +package policy + +import ( + "fmt" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/spf13/cobra" +) + +var KasRegistryCmd = man.Docs.GetCommand("policy/kas-registry") + +func getKeyAccessRegistry(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.FlagHelper.GetRequiredID("id") + + kas, err := h.GetKasRegistryEntry(cmd.Context(), handlers.KasIdentifier{ + ID: id, + }) + if err != nil { + errMsg := fmt.Sprintf("Failed to get Registered KAS entry (%s)", id) + cli.ExitWithError(errMsg, err) + } + + // TODO: Remove in next release + key := &policy.PublicKey{} + key.PublicKey = &policy.PublicKey_Cached{Cached: kas.GetPublicKey().GetCached()} + if kas.GetPublicKey().GetRemote() != "" { + key.PublicKey = &policy.PublicKey_Remote{Remote: kas.GetPublicKey().GetRemote()} + } + + rows := [][]string{ + {"Id", kas.GetId()}, + {"URI", kas.GetUri()}, + {"PublicKey", kas.GetPublicKey().String()}, + } + name := kas.GetName() + if name != "" { + rows = append(rows, []string{"Name", name}) + } + + if mdRows := getMetadataRows(kas.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + + common.HandleSuccess(cmd, kas.GetId(), t, kas) +} + +func listKeyAccessRegistries(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) + + resp, err := h.ListKasRegistryEntries(cmd.Context(), limit, offset, sort) + if err != nil { + cli.ExitWithError("Failed to list Registered KAS entries", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("uri", "URI", cli.FlexColumnWidthFour), + table.NewFlexColumn("name", "Name", cli.FlexColumnWidthThree), + table.NewFlexColumn("pk", "PublicKey", cli.FlexColumnWidthFour), + ) + rows := []table.Row{} + for _, kas := range resp.GetKeyAccessServers() { + // TODO: Remove in next release + key := policy.PublicKey{} + key.PublicKey = &policy.PublicKey_Cached{Cached: kas.GetPublicKey().GetCached()} + if kas.GetPublicKey().GetRemote() != "" { + key.PublicKey = &policy.PublicKey_Remote{Remote: kas.GetPublicKey().GetRemote()} + } + rows = append(rows, table.NewRow(table.RowData{ + "id": kas.GetId(), + "uri": kas.GetUri(), + "name": kas.GetName(), + "pk": kas.GetPublicKey().String(), + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func createKeyAccessRegistry(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + uri := c.Flags.GetRequiredString("uri") + cachedJSON := c.Flags.GetOptionalString("public-keys") // Deprecated + remote := c.Flags.GetOptionalString("public-key-remote") // Deprecated + name := c.Flags.GetOptionalString("name") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + if cachedJSON != "" || remote != "" { + message := "\nDEPRECATION WARNING: --public-keys and --public-key-remote are deprecated and will be removed in an upcoming release.\n" + + "Please use the 'policy kas-registry key' command instead.\n" + cmd.Println(cli.WarningMessage(message)) + } + + created, err := h.CreateKasRegistryEntry( + cmd.Context(), + uri, + name, + getMetadataMutable(metadataLabels), + ) + if err != nil { + cli.ExitWithError("Failed to create Registered KAS entry", err) + } + + rows := [][]string{ + {"Id", created.GetId()}, + {"URI", created.GetUri()}, + } + if name != "" { + rows = append(rows, []string{"Name", name}) + } + if mdRows := getMetadataRows(created.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + + common.HandleSuccess(cmd, created.GetId(), t, created) +} + +func updateKeyAccessRegistry(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + uri := c.Flags.GetOptionalString("uri") + name := c.Flags.GetOptionalString("name") + cachedJSON := c.Flags.GetOptionalString("public-keys") // Deprecated + remote := c.Flags.GetOptionalString("public-key-remote") // Deprecated + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + if cachedJSON != "" || remote != "" { + message := "\nDEPRECATION WARNING: --public-keys and --public-key-remote are deprecated and will be removed in an upcoming release.\n" + + "Please use the 'policy kas-registry key' command instead.\n" + cmd.Println(cli.WarningMessage(message)) + } + + updated, err := h.UpdateKasRegistryEntry( + cmd.Context(), + id, + uri, + name, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to update Registered KAS entry (%s)", id), err) + } + rows := [][]string{ + {"Id", id}, + {"URI", updated.GetUri()}, + } + if updated.GetName() != "" { + rows = append(rows, []string{"Name", updated.GetName()}) + } + + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, updated) +} + +func deleteKeyAccessRegistry(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + + kas, err := h.GetKasRegistryEntry(ctx, handlers.KasIdentifier{ + ID: id, + }) + if err != nil { + errMsg := fmt.Sprintf("Failed to get Registered KAS entry (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDelete, "Registered KAS", id, force) + + if _, err := h.DeleteKasRegistryEntry(ctx, id); err != nil { + errMsg := fmt.Sprintf("Failed to delete Registered KAS entry (%s)", id) + cli.ExitWithError(errMsg, err) + } + + t := cli.NewTabular( + []string{"Id", kas.GetId()}, + []string{"URI", kas.GetUri()}, + ) + + common.HandleSuccess(cmd, kas.GetId(), t, kas) +} + +func initKASRegistryCommands() { + getDoc := man.Docs.GetCommand("policy/kas-registry/get", + man.WithRun(getKeyAccessRegistry), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + + listDoc := man.Docs.GetCommand("policy/kas-registry/list", + man.WithRun(listKeyAccessRegistries), + ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) + + createDoc := man.Docs.GetCommand("policy/kas-registry/create", + man.WithRun(createKeyAccessRegistry), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("uri").Name, + createDoc.GetDocFlag("uri").Shorthand, + createDoc.GetDocFlag("uri").Default, + createDoc.GetDocFlag("uri").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("public-keys").Name, + createDoc.GetDocFlag("public-keys").Shorthand, + createDoc.GetDocFlag("public-keys").Default, + createDoc.GetDocFlag("public-keys").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("public-key-remote").Name, + createDoc.GetDocFlag("public-key-remote").Shorthand, + createDoc.GetDocFlag("public-key-remote").Default, + createDoc.GetDocFlag("public-key-remote").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Shorthand, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + updateDoc := man.Docs.GetCommand("policy/kas-registry/update", + man.WithRun(updateKeyAccessRegistry), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("uri").Name, + updateDoc.GetDocFlag("uri").Shorthand, + updateDoc.GetDocFlag("uri").Default, + updateDoc.GetDocFlag("uri").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("public-keys").Name, + updateDoc.GetDocFlag("public-keys").Shorthand, + updateDoc.GetDocFlag("public-keys").Default, + updateDoc.GetDocFlag("public-keys").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("public-key-remote").Name, + updateDoc.GetDocFlag("public-key-remote").Shorthand, + updateDoc.GetDocFlag("public-key-remote").Default, + updateDoc.GetDocFlag("public-key-remote").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("name").Name, + updateDoc.GetDocFlag("name").Shorthand, + updateDoc.GetDocFlag("name").Default, + updateDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deleteDoc := man.Docs.GetCommand("policy/kas-registry/delete", + man.WithRun(deleteKeyAccessRegistry), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + KasRegistryCmd.AddSubcommands(createDoc, getDoc, listDoc, updateDoc, deleteDoc) + Cmd.AddCommand(&KasRegistryCmd.Command) +} diff --git a/otdfctl/cmd/policy/keyManagement.go b/otdfctl/cmd/policy/keyManagement.go new file mode 100644 index 0000000000..69216b205b --- /dev/null +++ b/otdfctl/cmd/policy/keyManagement.go @@ -0,0 +1,13 @@ +package policy + +import ( + "github.com/opentdf/platform/otdfctl/pkg/man" +) + +// KeyManagementCmd is the command for managing keys +var KeyManagementCmd = man.Docs.GetCommand("policy/key-management") + +// initKeyManagementCommands sets up the key-management command. +func initKeyManagementCommands() { + Cmd.AddCommand(&KeyManagementCmd.Command) +} diff --git a/otdfctl/cmd/policy/keyManagementProvider.go b/otdfctl/cmd/policy/keyManagementProvider.go new file mode 100644 index 0000000000..55b31f8d94 --- /dev/null +++ b/otdfctl/cmd/policy/keyManagementProvider.go @@ -0,0 +1,281 @@ +package policy + +import ( + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +func createProviderConfig(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + name := c.Flags.GetRequiredString("name") + manager := c.Flags.GetRequiredString("manager") + config := c.Flags.GetRequiredString("config") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + // Do not need to get provider config after, since this endpoint returns the created config. + pc, err := h.CreateProviderConfig(c.Context(), name, manager, []byte(config), getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create provider config", err) + } + + rows := [][]string{ + {"ID", pc.GetId()}, + {"Name", pc.GetName()}, + {"Config", string(pc.GetConfigJson())}, + {"Manager", pc.GetManager()}, + } + + if mdRows := getMetadataRows(pc.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + + common.HandleSuccess(cmd, pc.GetId(), t, pc) +} + +func getProviderConfig(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + name := c.Flags.GetOptionalString("name") + + pc, err := h.GetProviderConfig(c.Context(), id, name) + if err != nil { + cli.ExitWithError("Failed to get provider config", err) + } + + rows := [][]string{ + {"ID", pc.GetId()}, + {"Name", pc.GetName()}, + {"Config", string(pc.GetConfigJson())}, + {"Manager", pc.GetManager()}, + } + + if mdRows := getMetadataRows(pc.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + + common.HandleSuccess(cmd, pc.GetId(), t, pc) +} + +func updateProviderConfig(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + name := c.Flags.GetOptionalString("name") + manager := c.Flags.GetOptionalString("manager") + config := c.Flags.GetOptionalString("config") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + if name == "" && manager == "" && config == "" && len(metadataLabels) == 0 { + cli.ExitWithError("At least one field (name, manager, config, or metadata labels) must be updated", nil) + } + + pc, err := h.UpdateProviderConfig(c.Context(), id, name, manager, []byte(config), getMetadataMutable(metadataLabels), getMetadataUpdateBehavior()) + if err != nil { + cli.ExitWithError("Failed to update provider config", err) + } + + rows := [][]string{ + {"ID", pc.GetId()}, + {"Name", pc.GetName()}, + {"Config", string(pc.GetConfigJson())}, + {"Manager", pc.GetManager()}, + } + + if mdRows := getMetadataRows(pc.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + + common.HandleSuccess(cmd, pc.GetId(), t, pc) +} + +func listProviderConfig(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + + // Get all provider configs + resp, err := h.ListProviderConfigs(c.Context(), limit, offset) + if err != nil { + cli.ExitWithError("Failed to list provider configs", err) + } + + t := cli.NewTable( + // columns should be id, name, config, labels, created_at, updated_at + table.NewFlexColumn("id", "Provider Config ID", cli.FlexColumnWidthThree), + table.NewFlexColumn("name", "Provider Config Name", cli.FlexColumnWidthTwo), + table.NewFlexColumn("manager", "Manager", cli.FlexColumnWidthTwo), + table.NewFlexColumn("config", "Provider Config", cli.FlexColumnWidthOne), + table.NewFlexColumn("labels", "Labels", cli.FlexColumnWidthOne), + table.NewFlexColumn("created_at", "Created At", cli.FlexColumnWidthOne), + table.NewFlexColumn("updated_at", "Updated At", cli.FlexColumnWidthOne), + ) + rows := []table.Row{} + for _, pc := range resp.GetProviderConfigs() { + metadata := cli.ConstructMetadata(pc.GetMetadata()) + rows = append(rows, table.NewRow(table.RowData{ + "id": pc.GetId(), + "name": pc.GetName(), + "config": string(pc.GetConfigJson()), + "labels": metadata["Labels"], + "created_at": metadata["Created At"], + "updated_at": metadata["Updated At"], + "manager": pc.GetManager(), + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func deleteProviderConfig(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + + // Get provider config. + pc, err := h.GetProviderConfig(c.Context(), id, "") + if err != nil { + cli.ExitWithError("Failed to get provider config", err) + } + + cli.ConfirmAction(cli.ActionDelete, "key provider config with id: "+id, "Provider Name: "+pc.GetName(), force) + + err = h.DeleteProviderConfig(c.Context(), id) + if err != nil { + cli.ExitWithError("Failed to delete provider config", err) + } + + rows := [][]string{ + {"Deleted", "true"}, + {"Id", id}, + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, nil) +} + +func initKeyManagementProviderCommands() { + // Create Provider Config + createDoc := man.Docs.GetCommand("policy/key-management/provider/create", + man.WithRun(createProviderConfig), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Shorthand, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("manager").Name, + createDoc.GetDocFlag("manager").Shorthand, + createDoc.GetDocFlag("manager").Default, + createDoc.GetDocFlag("manager").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("config").Name, + createDoc.GetDocFlag("config").Shorthand, + createDoc.GetDocFlag("config").Default, + createDoc.GetDocFlag("config").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + // Get Provider Config + getDoc := man.Docs.GetCommand("policy/key-management/provider/get", + man.WithRun(getProviderConfig), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("name").Name, + getDoc.GetDocFlag("name").Shorthand, + getDoc.GetDocFlag("name").Default, + getDoc.GetDocFlag("name").Description, + ) + getDoc.MarkFlagsOneRequired("id", "name") + getDoc.MarkFlagsMutuallyExclusive("id", "name") + + // Update Provider Config + updateDoc := man.Docs.GetCommand("policy/key-management/provider/update", + man.WithRun(updateProviderConfig), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("name").Name, + updateDoc.GetDocFlag("name").Shorthand, + updateDoc.GetDocFlag("name").Default, + updateDoc.GetDocFlag("name").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("manager").Name, + updateDoc.GetDocFlag("manager").Shorthand, + updateDoc.GetDocFlag("manager").Default, + updateDoc.GetDocFlag("manager").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("config").Name, + updateDoc.GetDocFlag("config").Shorthand, + updateDoc.GetDocFlag("config").Default, + updateDoc.GetDocFlag("config").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + // List Provider Configs + listDoc := man.Docs.GetCommand("policy/key-management/provider/list", + man.WithRun(listProviderConfig), + ) + injectListPaginationFlags(listDoc) + + // Add Delete Provider Config + deleteDoc := man.Docs.GetCommand("policy/key-management/provider/delete", + man.WithRun(deleteProviderConfig), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().BoolP( + deleteDoc.GetDocFlag("force").Name, + deleteDoc.GetDocFlag("force").Shorthand, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + doc := man.Docs.GetCommand("policy/key-management/provider", + man.WithSubcommands(createDoc, getDoc, updateDoc, listDoc, deleteDoc)) + + KeyManagementCmd.AddCommand(&doc.Command) +} diff --git a/otdfctl/cmd/policy/namespaces.go b/otdfctl/cmd/policy/namespaces.go new file mode 100644 index 0000000000..b4f741cdf8 --- /dev/null +++ b/otdfctl/cmd/policy/namespaces.go @@ -0,0 +1,467 @@ +package policy + +import ( + "fmt" + "strconv" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +var forceUnsafe bool + +func getAttributeNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + ns, err := h.GetNamespace(cmd.Context(), id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", ns.GetId()}, + {"Name", ns.GetName()}, + } + if mdRows := getMetadataRows(ns.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, ns.GetId(), t, ns) +} + +func listAttributeNamespaces(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + state := cli.GetState(cmd) + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) + + resp, err := h.ListNamespaces(cmd.Context(), state, limit, offset, sort) + if err != nil { + cli.ExitWithError("Failed to list namespaces", err) + } + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("name", "Name", cli.FlexColumnWidthFour), + table.NewFlexColumn("active", "Active", cli.FlexColumnWidthThree), + table.NewFlexColumn("labels", "Labels", cli.FlexColumnWidthOne), + table.NewFlexColumn("created_at", "Created At", cli.FlexColumnWidthOne), + table.NewFlexColumn("updated_at", "Updated At", cli.FlexColumnWidthOne), + ) + rows := []table.Row{} + for _, ns := range resp.GetNamespaces() { + metadata := cli.ConstructMetadata(ns.GetMetadata()) + rows = append(rows, + table.NewRow(table.RowData{ + "id": ns.GetId(), + "name": ns.GetName(), + "active": strconv.FormatBool(ns.GetActive().GetValue()), + "labels": metadata["Labels"], + "created_at": metadata["Created At"], + "updated_at": metadata["Updated At"], + }), + ) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func createAttributeNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + name := c.Flags.GetRequiredString("name") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + created, err := h.CreateNamespace(cmd.Context(), name, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create namespace", err) + } + rows := [][]string{ + {"Name", name}, + {"Id", created.GetId()}, + } + if mdRows := getMetadataRows(created.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, created.GetId(), t, created) +} + +func deactivateAttributeNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + force := c.Flags.GetOptionalBool("force") + id := c.Flags.GetRequiredID("id") + + ns, err := h.GetNamespace(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to find namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDeactivate, "namespace", ns.GetName(), force) + + d, err := h.DeactivateNamespace(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to deactivate namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + rows := [][]string{ + {"Id", ns.GetId()}, + {"Name", ns.GetName()}, + } + if mdRows := getMetadataRows(d.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, ns.GetId(), t, d) +} + +func updateAttributeNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + ns, err := h.UpdateNamespace( + cmd.Context(), + id, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to update namespace (%s)", id), err) + } + rows := [][]string{ + {"Id", ns.GetId()}, + {"Name", ns.GetName()}, + } + if mdRows := getMetadataRows(ns.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, ns) +} + +func unsafeDeleteAttributeNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + + ns, err := h.GetNamespace(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to find namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionDelete, "namespace", cli.InputNameFQN, ns.GetFqn()) + } + + if err := h.UnsafeDeleteNamespace(ctx, id, ns.GetFqn()); err != nil { + errMsg := fmt.Sprintf("Failed to delete namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", ns.GetId()}, + {"Name", ns.GetName()}, + } + if mdRows := getMetadataRows(ns.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, ns.GetId(), t, ns) +} + +func unsafeReactivateAttributeNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + + ns, err := h.GetNamespace(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to find namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionReactivate, "namespace", cli.InputNameFQN, ns.GetFqn()) + } + + ns, err = h.UnsafeReactivateNamespace(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to reactivate namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", ns.GetId()}, + {"Name", ns.GetName()}, + } + if mdRows := getMetadataRows(ns.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, ns.GetId(), t, ns) +} + +func unsafeUpdateAttributeNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + name := c.Flags.GetRequiredString("name") + + ns, err := h.GetNamespace(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to find namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionUpdateUnsafe, "namespace", cli.InputNameFQNUpdated, ns.GetFqn()) + } + + ns, err = h.UnsafeUpdateNamespace(ctx, id, name) + if err != nil { + errMsg := fmt.Sprintf("Failed to reactivate namespace (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", ns.GetId()}, + {"Name", ns.GetName()}, + } + if mdRows := getMetadataRows(ns.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, ns.GetId(), t, ns) +} + +func policyAssignKeyToNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + namespace := c.Flags.GetRequiredString("namespace") + keyID := c.Flags.GetRequiredID("key-id") + + // Get the attribute namespace to show meaningful information in case of error + attrKey, err := h.AssignKeyToAttributeNamespace(c.Context(), namespace, keyID) + if err != nil { + errMsg := fmt.Sprintf("Failed to assign key: (%s) to attribute namespace: (%s)", keyID, namespace) + cli.ExitWithError(errMsg, err) + } + + // Prepare and display the result + rows := [][]string{ + {"Namespace ID", attrKey.GetNamespaceId()}, + {"Key ID", attrKey.GetKeyId()}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, namespace, t, attrKey) +} + +func policyRemoveKeyFromNamespace(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + namespace := c.Flags.GetRequiredString("namespace") + keyID := c.Flags.GetRequiredID("key-id") + + err := h.RemoveKeyFromAttributeNamespace(c.Context(), namespace, keyID) + if err != nil { + errMsg := fmt.Sprintf("Failed to remove key (%s) from attribute namespace (%s)", keyID, namespace) + cli.ExitWithError(errMsg, err) + } + + // Prepare and display the result + rows := [][]string{ + {"Removed", "true"}, + {"Namespace", namespace}, + {"Key ID", keyID}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, namespace, t, nil) +} + +func initNamespacesCommands() { + nsDoc := man.Docs.GetCommand("policy/namespaces") + + getDoc := man.Docs.GetCommand("policy/namespaces/get", + man.WithRun(getAttributeNamespace), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + + listDoc := man.Docs.GetCommand("policy/namespaces/list", + man.WithRun(listAttributeNamespaces), + ) + listDoc.Flags().StringP( + listDoc.GetDocFlag("state").Name, + listDoc.GetDocFlag("state").Shorthand, + listDoc.GetDocFlag("state").Default, + listDoc.GetDocFlag("state").Description, + ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) + + createDoc := man.Docs.GetCommand("policy/namespaces/create", + man.WithRun(createAttributeNamespace), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Shorthand, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + updateDoc := man.Docs.GetCommand("policy/namespaces/update", + man.WithRun(updateAttributeNamespace), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deactivateDoc := man.Docs.GetCommand("policy/namespaces/deactivate", + man.WithRun(deactivateAttributeNamespace), + ) + deactivateDoc.Flags().StringP( + deactivateDoc.GetDocFlag("id").Name, + deactivateDoc.GetDocFlag("id").Shorthand, + deactivateDoc.GetDocFlag("id").Default, + deactivateDoc.GetDocFlag("id").Description, + ) + deactivateDoc.Flags().Bool( + deactivateDoc.GetDocFlag("force").Name, + false, + deactivateDoc.GetDocFlag("force").Description, + ) + + // unsafe + unsafeDoc := man.Docs.GetCommand("policy/namespaces/unsafe") + unsafeDoc.PersistentFlags().BoolVar( + &forceUnsafe, + unsafeDoc.GetDocFlag("force").Name, + false, + unsafeDoc.GetDocFlag("force").Description, + ) + + deleteDoc := man.Docs.GetCommand("policy/namespaces/unsafe/delete", + man.WithRun(unsafeDeleteAttributeNamespace), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + + reactivateDoc := man.Docs.GetCommand("policy/namespaces/unsafe/reactivate", + man.WithRun(unsafeReactivateAttributeNamespace), + ) + reactivateDoc.Flags().StringP( + reactivateDoc.GetDocFlag("id").Name, + reactivateDoc.GetDocFlag("id").Shorthand, + reactivateDoc.GetDocFlag("id").Default, + reactivateDoc.GetDocFlag("id").Description, + ) + + unsafeUpdateDoc := man.Docs.GetCommand("policy/namespaces/unsafe/update", + man.WithRun(unsafeUpdateAttributeNamespace), + ) + unsafeUpdateDoc.Flags().StringP( + unsafeUpdateDoc.GetDocFlag("id").Name, + unsafeUpdateDoc.GetDocFlag("id").Shorthand, + unsafeUpdateDoc.GetDocFlag("id").Default, + unsafeUpdateDoc.GetDocFlag("id").Description, + ) + unsafeUpdateDoc.Flags().StringP( + unsafeUpdateDoc.GetDocFlag("name").Name, + unsafeUpdateDoc.GetDocFlag("name").Shorthand, + unsafeUpdateDoc.GetDocFlag("name").Default, + unsafeUpdateDoc.GetDocFlag("name").Description, + ) + + // key + keyDoc := man.Docs.GetCommand("policy/namespaces/key") + + assignDoc := man.Docs.GetCommand("policy/namespaces/key/assign", + man.WithRun(policyAssignKeyToNamespace), + ) + assignDoc.Flags().StringP( + assignDoc.GetDocFlag("namespace").Name, + assignDoc.GetDocFlag("namespace").Shorthand, + assignDoc.GetDocFlag("namespace").Default, + assignDoc.GetDocFlag("namespace").Description, + ) + assignDoc.Flags().StringP( + assignDoc.GetDocFlag("key-id").Name, + assignDoc.GetDocFlag("key-id").Shorthand, + assignDoc.GetDocFlag("key-id").Default, + assignDoc.GetDocFlag("key-id").Description, + ) + + removeDoc := man.Docs.GetCommand("policy/namespaces/key/remove", + man.WithRun(policyRemoveKeyFromNamespace), + ) + removeDoc.Flags().StringP( + removeDoc.GetDocFlag("namespace").Name, + removeDoc.GetDocFlag("namespace").Shorthand, + removeDoc.GetDocFlag("namespace").Default, + removeDoc.GetDocFlag("namespace").Description, + ) + removeDoc.Flags().StringP( + removeDoc.GetDocFlag("key-id").Name, + removeDoc.GetDocFlag("key-id").Shorthand, + removeDoc.GetDocFlag("key-id").Default, + removeDoc.GetDocFlag("key-id").Description, + ) + + keyDoc.AddSubcommands(assignDoc, removeDoc) + unsafeDoc.AddSubcommands(deleteDoc, reactivateDoc, unsafeUpdateDoc) + nsDoc.AddSubcommands(getDoc, listDoc, createDoc, updateDoc, deactivateDoc, unsafeDoc, keyDoc) + AttributesCmd.AddCommand(&nsDoc.Command) + Cmd.AddCommand(&nsDoc.Command) +} diff --git a/otdfctl/cmd/policy/obligations.go b/otdfctl/cmd/policy/obligations.go new file mode 100644 index 0000000000..d8252be874 --- /dev/null +++ b/otdfctl/cmd/policy/obligations.go @@ -0,0 +1,771 @@ +package policy + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/obligations" + "github.com/spf13/cobra" +) + +// +// Obligations +// + +var obligationValues []string + +// TriggerRequest represents the JSON structure for a trigger +type TriggerRequest struct { + Action string `json:"action"` + AttributeValue string `json:"attribute_value"` + Context *policy.RequestContext `json:"context,omitempty"` +} + +func policyCreateObligation(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + name := c.Flags.GetRequiredString("name") + obligationValues = c.Flags.GetStringSlice("value", obligationValues, cli.FlagsStringSliceOptions{}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + namespace := c.Flags.GetRequiredString("namespace") + obl, err := h.CreateObligation(cmd.Context(), namespace, name, obligationValues, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create obligation", err) + } + + simpleObligationValues := cli.GetSimpleObligationValues(obl.GetValues()) + + rows := [][]string{ + {"Id", obl.GetId()}, + {"Name", obl.GetName()}, + {"Values", cli.CommaSeparated(simpleObligationValues)}, + } + + if mdRows := getMetadataRows(obl.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, obl.GetId(), t, obl) +} + +func policyGetObligation(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + fqn := c.Flags.GetOptionalString("fqn") + + obl, err := h.GetObligation(cmd.Context(), id, fqn) + if err != nil { + identifier := "id: " + id + if id == "" { + identifier = "fqn: " + fqn + } + errMsg := "Failed to find obligation (" + identifier + ")" + cli.ExitWithError(errMsg, err) + } + + simpleObligationValues := cli.GetSimpleObligationValues(obl.GetValues()) + + rows := [][]string{ + {"Id", obl.GetId()}, + {"Name", obl.GetName()}, + {"Values", cli.CommaSeparated(simpleObligationValues)}, + } + if mdRows := getMetadataRows(obl.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, obl.GetId(), t, obl) +} + +func policyListObligations(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + namespace := c.Flags.GetOptionalString("namespace") + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) + + resp, err := h.ListObligations(cmd.Context(), limit, offset, namespace, sort) + if err != nil { + cli.ExitWithError("Failed to list obligations", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("name", "Name", cli.FlexColumnWidthFour), + table.NewFlexColumn("values", "Values", cli.FlexColumnWidthTwo), + ) + rows := []table.Row{} + for _, r := range resp.GetObligations() { + simpleObligationValues := cli.GetSimpleObligationValues(r.GetValues()) + rows = append(rows, table.NewRow(table.RowData{ + "id": r.GetId(), + "name": r.GetName(), + "values": cli.CommaSeparated(simpleObligationValues), + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func policyUpdateObligation(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + name := c.Flags.GetOptionalString("name") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + updated, err := h.UpdateObligation( + cmd.Context(), + id, + name, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError("Failed to update obligation", err) + } + + rows := [][]string{ + {"Id", id}, + {"Name", updated.GetName()}, + } + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, updated) +} + +func policyDeleteObligation(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + fqn := c.Flags.GetOptionalString("fqn") + + force := c.Flags.GetRequiredBool("force") + ctx := cmd.Context() + + obl, err := h.GetObligation(ctx, id, fqn) + identifier := id + if id == "" { + identifier = fqn + } + if err != nil { + errMsg := fmt.Sprintf("Failed to find obligation (%s)", identifier) + cli.ExitWithError(errMsg, err) + } + id = obl.GetId() + cli.ConfirmAction(cli.ActionDelete, "obligation", identifier, force) + + err = h.DeleteObligation(ctx, id, fqn) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete obligation (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", id}, + {"Name", obl.GetName()}, + } + if mdRows := getMetadataRows(obl.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, obl) +} + +// +// Obligation Values +// + +func policyCreateObligationValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + obligation := c.Flags.GetRequiredString("obligation") + value := c.Flags.GetRequiredString("value") + triggerJSON := c.Flags.GetOptionalString("triggers") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + // Parse triggers if provided + triggers, err := parseTriggers(triggerJSON) + if err != nil { + cli.ExitWithError("Invalid trigger configuration", err) + } + + oblVal, err := h.CreateObligationValue(ctx, obligation, value, triggers, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create obligation value", err) + } + + rows := [][]string{ + {"Id", oblVal.GetId()}, + {"Name", oblVal.GetObligation().GetName()}, + {"Value", oblVal.GetValue()}, + {"Number of Triggers", strconv.Itoa(len(oblVal.GetTriggers()))}, + } + if mdRows := getMetadataRows(oblVal.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, oblVal.GetId(), t, oblVal) +} + +func policyGetObligationValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + fqn := c.Flags.GetOptionalString("fqn") + + value, err := h.GetObligationValue(cmd.Context(), id, fqn) + if err != nil { + identifier := "id: " + id + if id == "" { + identifier = "fqn: " + fqn + } + errMsg := "Failed to find obligation value (" + identifier + ")" + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", value.GetId()}, + {"Name", value.GetObligation().GetName()}, + {"Value", value.GetValue()}, + {"Number of Triggers", strconv.Itoa(len(value.GetTriggers()))}, + } + if mdRows := getMetadataRows(value.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, value.GetId(), t, value) +} + +func policyUpdateObligationValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + value := c.Flags.GetOptionalString("value") + triggerJSON := c.Flags.GetOptionalString("triggers") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + // Parse triggers if provided + triggers, err := parseTriggers(triggerJSON) + if err != nil { + cli.ExitWithError("Invalid trigger configuration", err) + } + + updated, err := h.UpdateObligationValue( + cmd.Context(), + id, + value, + triggers, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError("Failed to update obligation value", err) + } + + rows := [][]string{ + {"Id", id}, + {"Name", updated.GetObligation().GetName()}, + {"Value", updated.GetValue()}, + {"Number of Triggers", strconv.Itoa(len(updated.GetTriggers()))}, + } + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, updated) +} + +func policyDeleteObligationValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + fqn := c.Flags.GetOptionalString("fqn") + + force := c.Flags.GetOptionalBool("force") + ctx := cmd.Context() + + val, err := h.GetObligationValue(ctx, id, fqn) + identifier := id + if id == "" { + identifier = fqn + } + if err != nil { + errMsg := fmt.Sprintf("Failed to find obligation value (%s)", identifier) + cli.ExitWithError(errMsg, err) + } + id = val.GetId() + cli.ConfirmAction(cli.ActionDelete, "obligation value", identifier, force) + + err = h.DeleteObligationValue(ctx, id, fqn) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete obligation value (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", id}, + {"Value", val.GetValue()}, + } + if mdRows := getMetadataRows(val.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, val) +} + +// **** +// Obligation Triggers +// **** +func policyCreateObligationTrigger(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + attributeValue := c.Flags.GetRequiredString("attribute-value") + action := c.Flags.GetRequiredString("action") + obligationValue := c.Flags.GetRequiredString("obligation-value") + clientID := c.Flags.GetOptionalString("client-id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + trigger, err := h.CreateObligationTrigger(ctx, attributeValue, action, obligationValue, clientID, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create obligation trigger", err) + } + + rows := getObligationTriggerRows(trigger) + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, trigger.GetId(), t, trigger) +} + +func policyDeleteObligationTrigger(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredString("id") + force := c.Flags.GetOptionalBool("force") + ctx := cmd.Context() + + cli.ConfirmAction(cli.ActionDelete, "obligation trigger", id, force) + + resp, err := h.DeleteObligationTrigger(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete obligation trigger (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", id}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, resp) +} + +func policyListObligationTriggers(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + namespace := c.Flags.GetOptionalString("namespace") + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + + resp, err := h.ListObligationTriggers(cmd.Context(), namespace, limit, offset) + if err != nil { + cli.ExitWithError("Failed to list obligation triggers", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("attribute", "Attribute Value FQN", cli.FlexColumnWidthThree), + table.NewFlexColumn("action", "Action", cli.FlexColumnWidthOne), + table.NewFlexColumn("obligation", "Obligation Value FQN", cli.FlexColumnWidthThree), + table.NewFlexColumn("client_ids", "Client IDs", cli.FlexColumnWidthOne), + ) + rows := []table.Row{} + for _, r := range resp.GetTriggers() { + rows = append(rows, table.NewRow(table.RowData{ + "id": r.GetId(), + "attribute": r.GetAttributeValue().GetFqn(), + "action": r.GetAction().GetName(), + "obligation": r.GetObligationValue().GetFqn(), + "client_ids": cli.CommaSeparated(cli.AggregateClientIDs(r.GetContext())), + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func getObligationTriggerRows(trigger *policy.ObligationTrigger) [][]string { + rows := [][]string{ + {"Id", trigger.GetId()}, + {"Attribute Value FQN", trigger.GetAttributeValue().GetFqn()}, + {"Action", trigger.GetAction().GetName()}, + {"Obligation Value FQN", trigger.GetObligationValue().GetFqn()}, + {"Client IDs", cli.CommaSeparated(cli.AggregateClientIDs(trigger.GetContext()))}, + } + if mdRows := getMetadataRows(trigger.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + return rows +} + +// parseTriggers unmarshals the trigger JSON string or reads from file and validates required fields +func parseTriggers(triggerInput string) ([]*obligations.ValueTriggerRequest, error) { + if triggerInput == "" { + return nil, nil + } + + // Determine if input is a file path or JSON string + triggerJSON, err := cli.GetJSONInput(triggerInput) + if err != nil { + return nil, fmt.Errorf("failed to get JSON input: %w", err) + } + + var triggerRequests []TriggerRequest + if err := json.Unmarshal([]byte(triggerJSON), &triggerRequests); err != nil { + return nil, fmt.Errorf("failed to parse trigger JSON: %w", err) + } + + var valueTriggerRequests []*obligations.ValueTriggerRequest + for i, tr := range triggerRequests { + // Validate required fields + if strings.TrimSpace(tr.Action) == "" { + return nil, fmt.Errorf("trigger at index %d: action is required", i) + } + if strings.TrimSpace(tr.AttributeValue) == "" { + return nil, fmt.Errorf("trigger at index %d: attribute_value is required", i) + } + + // Create the ValueTriggerRequest + valueTrigger := &obligations.ValueTriggerRequest{ + Action: handlers.ParseToIDNameIdentifier(tr.Action), + AttributeValue: handlers.ParseToIDFqnIdentifier(tr.AttributeValue), + } + + // Add context if client_id is provided + if tr.Context != nil { + valueTrigger.Context = tr.Context + } + + valueTriggerRequests = append(valueTriggerRequests, valueTrigger) + } + + return valueTriggerRequests, nil +} + +func initObligationsCommands() { + // Obligations commands + getDoc := man.Docs.GetCommand("policy/obligations/get", + man.WithRun(policyGetObligation), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("fqn").Name, + getDoc.GetDocFlag("fqn").Shorthand, + getDoc.GetDocFlag("fqn").Default, + getDoc.GetDocFlag("fqn").Description, + ) + getDoc.MarkFlagsMutuallyExclusive("id", "fqn") + getDoc.MarkFlagsOneRequired("id", "fqn") + + listDoc := man.Docs.GetCommand("policy/obligations/list", + man.WithRun(policyListObligations), + ) + listDoc.Flags().StringP( + listDoc.GetDocFlag("namespace").Name, + listDoc.GetDocFlag("namespace").Shorthand, + listDoc.GetDocFlag("namespace").Default, + listDoc.GetDocFlag("namespace").Description, + ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) + + createDoc := man.Docs.GetCommand("policy/obligations/create", + man.WithRun(policyCreateObligation), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Shorthand, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("namespace").Name, + createDoc.GetDocFlag("namespace").Shorthand, + createDoc.GetDocFlag("namespace").Default, + createDoc.GetDocFlag("namespace").Description, + ) + createDoc.Flags().StringSliceVarP( + &obligationValues, + createDoc.GetDocFlag("value").Name, + createDoc.GetDocFlag("value").Shorthand, + []string{}, + createDoc.GetDocFlag("value").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + updateDoc := man.Docs.GetCommand("policy/obligations/update", + man.WithRun(policyUpdateObligation), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("name").Name, + updateDoc.GetDocFlag("name").Shorthand, + updateDoc.GetDocFlag("name").Default, + updateDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deleteDoc := man.Docs.GetCommand("policy/obligations/delete", + man.WithRun(policyDeleteObligation), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("fqn").Name, + deleteDoc.GetDocFlag("fqn").Shorthand, + deleteDoc.GetDocFlag("fqn").Default, + deleteDoc.GetDocFlag("fqn").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + deleteDoc.MarkFlagsMutuallyExclusive("id", "fqn") + deleteDoc.MarkFlagsOneRequired("id", "fqn") + + // Obligation Values commands + + getValueDoc := man.Docs.GetCommand("policy/obligations/values/get", + man.WithRun(policyGetObligationValue), + ) + getValueDoc.Flags().StringP( + getValueDoc.GetDocFlag("id").Name, + getValueDoc.GetDocFlag("id").Shorthand, + getValueDoc.GetDocFlag("id").Default, + getValueDoc.GetDocFlag("id").Description, + ) + getValueDoc.Flags().StringP( + getValueDoc.GetDocFlag("fqn").Name, + getValueDoc.GetDocFlag("fqn").Shorthand, + getValueDoc.GetDocFlag("fqn").Default, + getValueDoc.GetDocFlag("fqn").Description, + ) + getValueDoc.MarkFlagsMutuallyExclusive("id", "fqn") + getValueDoc.MarkFlagsOneRequired("id", "fqn") + + createValueDoc := man.Docs.GetCommand("policy/obligations/values/create", + man.WithRun(policyCreateObligationValue), + ) + createValueDoc.Flags().StringP( + createValueDoc.GetDocFlag("obligation").Name, + createValueDoc.GetDocFlag("obligation").Shorthand, + createValueDoc.GetDocFlag("obligation").Default, + createValueDoc.GetDocFlag("obligation").Description, + ) + createValueDoc.Flags().StringP( + createValueDoc.GetDocFlag("value").Name, + createValueDoc.GetDocFlag("value").Shorthand, + createValueDoc.GetDocFlag("value").Default, + createValueDoc.GetDocFlag("value").Description, + ) + createValueDoc.Flags().StringP( + createValueDoc.GetDocFlag("triggers").Name, + createValueDoc.GetDocFlag("triggers").Shorthand, + createValueDoc.GetDocFlag("triggers").Default, + createValueDoc.GetDocFlag("triggers").Description, + ) + injectLabelFlags(&createValueDoc.Command, false) + + updateValueDoc := man.Docs.GetCommand("policy/obligations/values/update", + man.WithRun(policyUpdateObligationValue), + ) + updateValueDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateValueDoc.Flags().StringP( + updateValueDoc.GetDocFlag("value").Name, + updateValueDoc.GetDocFlag("value").Shorthand, + updateValueDoc.GetDocFlag("value").Default, + updateValueDoc.GetDocFlag("value").Description, + ) + updateValueDoc.Flags().StringP( + updateValueDoc.GetDocFlag("triggers").Name, + updateValueDoc.GetDocFlag("triggers").Shorthand, + updateValueDoc.GetDocFlag("triggers").Default, + updateValueDoc.GetDocFlag("triggers").Description, + ) + injectLabelFlags(&updateValueDoc.Command, true) + deleteValueDoc := man.Docs.GetCommand("policy/obligations/values/delete", + man.WithRun(policyDeleteObligationValue), + ) + deleteValueDoc.Flags().StringP( + deleteValueDoc.GetDocFlag("id").Name, + deleteValueDoc.GetDocFlag("id").Shorthand, + deleteValueDoc.GetDocFlag("id").Default, + deleteValueDoc.GetDocFlag("id").Description, + ) + deleteValueDoc.Flags().StringP( + deleteValueDoc.GetDocFlag("fqn").Name, + deleteValueDoc.GetDocFlag("fqn").Shorthand, + deleteValueDoc.GetDocFlag("fqn").Default, + deleteValueDoc.GetDocFlag("fqn").Description, + ) + deleteValueDoc.Flags().Bool( + deleteValueDoc.GetDocFlag("force").Name, + false, + deleteValueDoc.GetDocFlag("force").Description, + ) + deleteValueDoc.MarkFlagsMutuallyExclusive("id", "fqn") + deleteValueDoc.MarkFlagsOneRequired("id", "fqn") + + // Obligation Triggers commands + createTriggerDoc := man.Docs.GetCommand("policy/obligations/triggers/create", + man.WithRun(policyCreateObligationTrigger), + ) + createTriggerDoc.Flags().StringP( + createTriggerDoc.GetDocFlag("attribute-value").Name, + createTriggerDoc.GetDocFlag("attribute-value").Shorthand, + createTriggerDoc.GetDocFlag("attribute-value").Default, + createTriggerDoc.GetDocFlag("attribute-value").Description, + ) + createTriggerDoc.Flags().StringP( + createTriggerDoc.GetDocFlag("action").Name, + createTriggerDoc.GetDocFlag("action").Shorthand, + createTriggerDoc.GetDocFlag("action").Default, + createTriggerDoc.GetDocFlag("action").Description, + ) + createTriggerDoc.Flags().StringP( + createTriggerDoc.GetDocFlag("obligation-value").Name, + createTriggerDoc.GetDocFlag("obligation-value").Shorthand, + createTriggerDoc.GetDocFlag("obligation-value").Default, + createTriggerDoc.GetDocFlag("obligation-value").Description, + ) + createTriggerDoc.Flags().StringP( + createTriggerDoc.GetDocFlag("client-id").Name, + createTriggerDoc.GetDocFlag("client-id").Shorthand, + createTriggerDoc.GetDocFlag("client-id").Default, + createTriggerDoc.GetDocFlag("client-id").Description, + ) + injectLabelFlags(&createTriggerDoc.Command, false) + + deleteTriggerDoc := man.Docs.GetCommand("policy/obligations/triggers/delete", + man.WithRun(policyDeleteObligationTrigger), + ) + deleteTriggerDoc.Flags().StringP( + deleteTriggerDoc.GetDocFlag("id").Name, + deleteTriggerDoc.GetDocFlag("id").Shorthand, + deleteTriggerDoc.GetDocFlag("id").Default, + deleteTriggerDoc.GetDocFlag("id").Description, + ) + deleteTriggerDoc.Flags().Bool( + deleteTriggerDoc.GetDocFlag("force").Name, + false, + deleteTriggerDoc.GetDocFlag("force").Description, + ) + + listTriggerDoc := man.Docs.GetCommand("policy/obligations/triggers/list", + man.WithRun(policyListObligationTriggers), + ) + listTriggerDoc.Flags().StringP( + listTriggerDoc.GetDocFlag("namespace").Name, + listTriggerDoc.GetDocFlag("namespace").Shorthand, + listTriggerDoc.GetDocFlag("namespace").Default, + listTriggerDoc.GetDocFlag("namespace").Description, + ) + injectListPaginationFlags(listTriggerDoc) + + // Add commands to the policy command + + policyObligationsDoc := man.Docs.GetCommand("policy/obligations", + man.WithSubcommands( + getDoc, + listDoc, + createDoc, + updateDoc, + deleteDoc, + ), + ) + + policyObligationValuesDoc := man.Docs.GetCommand("policy/obligations/values", + man.WithSubcommands( + getValueDoc, + createValueDoc, + updateValueDoc, + deleteValueDoc, + ), + ) + + policyObligationTriggersDoc := man.Docs.GetCommand("policy/obligations/triggers", + man.WithSubcommands( + createTriggerDoc, + deleteTriggerDoc, + listTriggerDoc, + ), + ) + + policyObligationsDoc.AddCommand(&policyObligationValuesDoc.Command) + policyObligationsDoc.AddCommand(&policyObligationTriggersDoc.Command) + Cmd.AddCommand(&policyObligationsDoc.Command) +} diff --git a/otdfctl/cmd/policy/policy.go b/otdfctl/cmd/policy/policy.go new file mode 100644 index 0000000000..49e3d40195 --- /dev/null +++ b/otdfctl/cmd/policy/policy.go @@ -0,0 +1,122 @@ +package policy + +import ( + "strings" + + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/protocol/go/common" + "github.com/spf13/cobra" +) + +var ( + metadataLabels []string + defaultListFlagLimit int32 = 300 + defaultListFlagOffset int32 + + Cmd = &cobra.Command{ + Use: man.Docs.GetDoc("policy").Use, + Short: man.Docs.GetDoc("policy").Short, + Long: man.Docs.GetDoc("policy").Long, + } +) + +func getMetadataRows(m *common.Metadata) [][]string { + if m != nil { + metadata := cli.ConstructMetadata(m) + metadataRows := [][]string{ + {"Created At", metadata["Created At"]}, + {"Updated At", metadata["Updated At"]}, + } + if m.Labels != nil { + metadataRows = append(metadataRows, []string{"Labels", metadata["Labels"]}) + } + return metadataRows + } + return nil +} + +const keyValLength = 2 + +func getMetadataMutable(labels []string) *common.MetadataMutable { + metadata := common.MetadataMutable{} + if len(labels) > 0 { + metadata.Labels = map[string]string{} + for _, label := range labels { + kv := strings.Split(label, "=") + if len(kv) != keyValLength { + cli.ExitWithError("Invalid label format", nil) + } + metadata.Labels[kv[0]] = kv[1] + } + return &metadata + } + return nil +} + +func getMetadataUpdateBehavior() common.MetadataUpdateEnum { + if forceReplaceMetadataLabels { + return common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE + } + return common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_EXTEND +} + +// Adds reusable create/update label flags to a Policy command and the optional force-replace-labels flag for updates only +func injectLabelFlags(cmd *cobra.Command, isUpdate bool) { + cmd.Flags().StringSliceVarP(&metadataLabels, "label", "l", []string{}, "Optional metadata 'labels' in the format: key=value") + if isUpdate { + cmd.Flags().BoolVar(&forceReplaceMetadataLabels, "force-replace-labels", false, "Destructively replace entire set of existing metadata 'labels' with any provided to this command") + } +} + +// Adds reusable limit/offset flags to a Policy LIST command +func injectListPaginationFlags(listDoc *man.Doc) { + listDoc.Flags().Int32P( + listDoc.GetDocFlag("limit").Name, + listDoc.GetDocFlag("limit").Shorthand, + defaultListFlagLimit, + listDoc.GetDocFlag("limit").Description, + ) + listDoc.Flags().Int32P( + listDoc.GetDocFlag("offset").Name, + listDoc.GetDocFlag("offset").Shorthand, + defaultListFlagOffset, + listDoc.GetDocFlag("offset").Description, + ) +} + +func injectListSortFlags(listDoc *man.Doc) { + sortFlag := listDoc.GetDocFlag("sort") + listDoc.Flags().String(sortFlag.Name, sortFlag.Default, sortFlag.Description) + + orderFlag := listDoc.GetDocFlag("order") + listDoc.Flags().String(orderFlag.Name, orderFlag.Default, orderFlag.Description) +} + +func getSortOption(c *cli.Cli) handlers.SortOption { + sort, err := handlers.NewSortOption(c.Flags.GetOptionalString("sort"), c.Flags.GetOptionalString("order")) + if err != nil { + cli.ExitWithError("Invalid sort order", err) + } + return sort +} + +func InitCommands() { + initActionsCommands() + initAttributesCommands() + initAttributeValuesCommands() + initNamespacesCommands() + initSubjectConditionSetsCommands() + initSubjectMappingsCommands() + initObligationsCommands() + initResourceMappingsCommands() + initResourceMappingGroupsCommands() + initRegisteredResourcesCommands() + initKeyManagementCommands() + initKeyManagementProviderCommands() + initKASRegistryCommands() + initKASKeysCommands() + initKASGrantsCommands() + initBaseKeysCommands() +} diff --git a/otdfctl/cmd/policy/registeredResources.go b/otdfctl/cmd/policy/registeredResources.go new file mode 100644 index 0000000000..c4a026f836 --- /dev/null +++ b/otdfctl/cmd/policy/registeredResources.go @@ -0,0 +1,687 @@ +package policy + +import ( + "fmt" + "strings" + + "github.com/evertras/bubble-table/table" + "github.com/google/uuid" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/spf13/cobra" +) + +var ( + registeredResourceValues []string + actionAttributeValues []string +) + +const actionAttributeValueArgSplitCount = 2 + +// +// Registered Resources +// + +func policyCreateRegisteredResource(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + name := c.Flags.GetRequiredString("name") + namespace := c.Flags.GetOptionalString("namespace") + registeredResourceValues = c.Flags.GetStringSlice("value", registeredResourceValues, cli.FlagsStringSliceOptions{}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + resource, err := h.CreateRegisteredResource(cmd.Context(), namespace, name, registeredResourceValues, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create registered resource", err) + } + + simpleRegResValues := cli.GetSimpleRegisteredResourceValues(resource.GetValues()) + + rows := [][]string{ + {"Id", resource.GetId()}, + {"Name", resource.GetName()}, + {"Namespace", resource.GetNamespace().GetFqn()}, + {"Values", cli.CommaSeparated(simpleRegResValues)}, + } + + if mdRows := getMetadataRows(resource.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resource.GetId(), t, resource) +} + +func policyGetRegisteredResource(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + name := c.Flags.GetOptionalString("name") + namespace := c.Flags.GetOptionalString("namespace") + + if id == "" && name == "" { + cli.ExitWithError("Either 'id' or 'name' must be provided", nil) + } + + resource, err := h.GetRegisteredResource(cmd.Context(), id, name, namespace) + if err != nil { + identifier := "id: " + id + if id == "" { + identifier = "name: " + name + } + errMsg := "Failed to find registered resource (" + identifier + ")" + cli.ExitWithError(errMsg, err) + } + + simpleRegResValues := cli.GetSimpleRegisteredResourceValues(resource.GetValues()) + + rows := [][]string{ + {"Id", resource.GetId()}, + {"Name", resource.GetName()}, + {"Namespace", resource.GetNamespace().GetFqn()}, + {"Values", cli.CommaSeparated(simpleRegResValues)}, + } + if mdRows := getMetadataRows(resource.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resource.GetId(), t, resource) +} + +func policyListRegisteredResources(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + namespace := c.Flags.GetOptionalString("namespace") + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) + + resp, err := h.ListRegisteredResources(cmd.Context(), limit, offset, namespace, sort) + if err != nil { + cli.ExitWithError("Failed to list registered resources", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("name", "Name", cli.FlexColumnWidthFour), + table.NewFlexColumn("namespace", "Namespace", cli.FlexColumnWidthFour), + table.NewFlexColumn("values", "Values", cli.FlexColumnWidthTwo), + ) + rows := []table.Row{} + for _, r := range resp.GetResources() { + simpleRegResValues := cli.GetSimpleRegisteredResourceValues(r.GetValues()) + rows = append(rows, table.NewRow(table.RowData{ + "id": r.GetId(), + "name": r.GetName(), + "namespace": r.GetNamespace().GetFqn(), + "values": cli.CommaSeparated(simpleRegResValues), + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func policyUpdateRegisteredResource(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + name := c.Flags.GetOptionalString("name") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + updated, err := h.UpdateRegisteredResource( + cmd.Context(), + id, + name, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError("Failed to update registered resource", err) + } + + rows := [][]string{ + {"Id", id}, + {"Name", updated.GetName()}, + {"Namespace", updated.GetNamespace().GetFqn()}, + } + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, updated) +} + +func policyDeleteRegisteredResource(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetRequiredBool("force") + ctx := cmd.Context() + + resource, err := h.GetRegisteredResource(ctx, id, "", "") + if err != nil { + errMsg := fmt.Sprintf("Failed to find registered resource (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDelete, "registered resource", id, force) + + err = h.DeleteRegisteredResource(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete registered resource (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", id}, + {"Name", resource.GetName()}, + {"Namespace", resource.GetNamespace().GetFqn()}, + } + if mdRows := getMetadataRows(resource.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, resource) +} + +// +// Registered Resource Values +// + +func policyCreateRegisteredResourceValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + resource := c.Flags.GetRequiredString("resource") + value := c.Flags.GetRequiredString("value") + actionAttributeValues = c.Flags.GetStringSlice("action-attribute-value", actionAttributeValues, cli.FlagsStringSliceOptions{Min: 0}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + namespace := c.Flags.GetOptionalString("namespace") + + var resourceID string + if uuid.Validate(resource) == nil { + resourceID = resource + } else { + resourceByName, err := h.GetRegisteredResource(ctx, "", resource, namespace) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to find registered resource (name: %s)", resource), err) + } + resourceID = resourceByName.GetId() + } + + parsedActionAttributeValues := parseActionAttributeValueArgs(actionAttributeValues) + + resourceValue, err := h.CreateRegisteredResourceValue(ctx, resourceID, value, parsedActionAttributeValues, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create registered resource value", err) + } + + simpleActionAttributeValues := cli.GetSimpleRegisteredResourceActionAttributeValues(resourceValue.GetActionAttributeValues()) + + rows := [][]string{ + {"Id", resourceValue.GetId()}, + {"Value", resourceValue.GetValue()}, + {"Action Attribute Values", cli.CommaSeparated(simpleActionAttributeValues)}, + } + if mdRows := getMetadataRows(resourceValue.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceValue.GetId(), t, resourceValue) +} + +func policyGetRegisteredResourceValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + fqn := c.Flags.GetOptionalString("fqn") + + if id == "" && fqn == "" { + cli.ExitWithError("Either 'id' or 'fqn' must be provided", nil) + } + + value, err := h.GetRegisteredResourceValue(cmd.Context(), id, fqn) + if err != nil { + identifier := "id: " + id + if id == "" { + identifier = "fqn: " + fqn + } + errMsg := "Failed to find registered resource value (" + identifier + ")" + cli.ExitWithError(errMsg, err) + } + + simpleActionAttributeValues := cli.GetSimpleRegisteredResourceActionAttributeValues(value.GetActionAttributeValues()) + + rows := [][]string{ + {"Id", value.GetId()}, + {"Value", value.GetValue()}, + {"Action Attribute Values", cli.CommaSeparated(simpleActionAttributeValues)}, + } + if mdRows := getMetadataRows(value.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, value.GetId(), t, value) +} + +func policyListRegisteredResourceValues(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + resource := c.Flags.GetRequiredString("resource") + namespace := c.Flags.GetOptionalString("namespace") + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + + var resourceID string + if uuid.Validate(resource) == nil { + resourceID = resource + } else { + resourceByName, err := h.GetRegisteredResource(ctx, "", resource, namespace) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to find registered resource (name: %s)", resource), err) + } + resourceID = resourceByName.GetId() + } + + resp, err := h.ListRegisteredResourceValues(ctx, resourceID, limit, offset) + if err != nil { + cli.ExitWithError("Failed to list registered resource values", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("value", "Value", cli.FlexColumnWidthFour), + table.NewFlexColumn("action-attribute-values", "Action Attribute Values", cli.FlexColumnWidthFour), + ) + rows := []table.Row{} + for _, v := range resp.GetValues() { + simpleActionAttributeValues := cli.GetSimpleRegisteredResourceActionAttributeValues(v.GetActionAttributeValues()) + + rows = append(rows, table.NewRow(table.RowData{ + "id": v.GetId(), + "value": v.GetValue(), + "action-attribute-values": cli.CommaSeparated(simpleActionAttributeValues), + })) + } + + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func policyUpdateRegisteredResourceValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + value := c.Flags.GetOptionalString("value") + actionAttributeValues = c.Flags.GetStringSlice("action-attribute-value", actionAttributeValues, cli.FlagsStringSliceOptions{Min: 0}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + force := c.Flags.GetOptionalBool("force") + + parsedActionAttributeValues := parseActionAttributeValueArgs(actionAttributeValues) + + // only confirm if new action attribute values provided + if len(parsedActionAttributeValues) > 0 { + cli.ConfirmActionSubtext(cli.ActionUpdate, "registered resource value", id, + "All existing action attribute values will be replaced with the new ones provided.", + force) + } + + updated, err := h.UpdateRegisteredResourceValue( + cmd.Context(), + id, + value, + parsedActionAttributeValues, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError("Failed to update registered resource value", err) + } + + simpleActionAttributeValues := cli.GetSimpleRegisteredResourceActionAttributeValues(updated.GetActionAttributeValues()) + + rows := [][]string{ + {"Id", id}, + {"Value", updated.GetValue()}, + {"Action Attribute Values", cli.CommaSeparated(simpleActionAttributeValues)}, + } + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, updated) +} + +func policyDeleteRegisteredResourceValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + ctx := cmd.Context() + + resource, err := h.GetRegisteredResourceValue(ctx, id, "") + if err != nil { + errMsg := fmt.Sprintf("Failed to find registered resource value (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDelete, "registered resource value", id, force) + + err = h.DeleteRegisteredResourceValue(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete registered resource value (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", id}, + {"Value", resource.GetValue()}, + } + if mdRows := getMetadataRows(resource.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, resource) +} + +func parseActionAttributeValueArgs(args []string) []*registeredresources.ActionAttributeValue { + parsed := make([]*registeredresources.ActionAttributeValue, len(args)) + + for i, a := range args { + // split on semicolon + split := strings.Split(a, ";") + if len(split) != actionAttributeValueArgSplitCount { + cli.ExitWithError("Invalid action attribute value arg format", nil) + } + + actionIdentifier := split[0] + attrValIdentifier := split[1] + + newActionAttrVal := ®isteredresources.ActionAttributeValue{} + + if uuid.Validate(actionIdentifier) == nil { + newActionAttrVal.ActionIdentifier = ®isteredresources.ActionAttributeValue_ActionId{ + ActionId: actionIdentifier, + } + } else { + newActionAttrVal.ActionIdentifier = ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: actionIdentifier, + } + } + + if uuid.Validate(attrValIdentifier) == nil { + newActionAttrVal.AttributeValueIdentifier = ®isteredresources.ActionAttributeValue_AttributeValueId{ + AttributeValueId: attrValIdentifier, + } + } else { + newActionAttrVal.AttributeValueIdentifier = ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: attrValIdentifier, + } + } + + parsed[i] = newActionAttrVal + } + + return parsed +} + +func initRegisteredResourcesCommands() { + // Registered Resources commands + + getDoc := man.Docs.GetCommand("policy/registered-resources/get", + man.WithRun(policyGetRegisteredResource), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("name").Name, + getDoc.GetDocFlag("name").Shorthand, + getDoc.GetDocFlag("name").Default, + getDoc.GetDocFlag("name").Description, + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("namespace").Name, + getDoc.GetDocFlag("namespace").Shorthand, + getDoc.GetDocFlag("namespace").Default, + getDoc.GetDocFlag("namespace").Description, + ) + + listDoc := man.Docs.GetCommand("policy/registered-resources/list", + man.WithRun(policyListRegisteredResources), + ) + listDoc.Flags().StringP( + listDoc.GetDocFlag("namespace").Name, + listDoc.GetDocFlag("namespace").Shorthand, + listDoc.GetDocFlag("namespace").Default, + listDoc.GetDocFlag("namespace").Description, + ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) + + createDoc := man.Docs.GetCommand("policy/registered-resources/create", + man.WithRun(policyCreateRegisteredResource), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Shorthand, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("namespace").Name, + createDoc.GetDocFlag("namespace").Shorthand, + createDoc.GetDocFlag("namespace").Default, + createDoc.GetDocFlag("namespace").Description, + ) + createDoc.Flags().StringSliceVarP( + ®isteredResourceValues, + createDoc.GetDocFlag("value").Name, + createDoc.GetDocFlag("value").Shorthand, + []string{}, + createDoc.GetDocFlag("value").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + updateDoc := man.Docs.GetCommand("policy/registered-resources/update", + man.WithRun(policyUpdateRegisteredResource), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("name").Name, + updateDoc.GetDocFlag("name").Shorthand, + updateDoc.GetDocFlag("name").Default, + updateDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deleteDoc := man.Docs.GetCommand("policy/registered-resources/delete", + man.WithRun(policyDeleteRegisteredResource), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + // Registered Resource Values commands + + getValueDoc := man.Docs.GetCommand("policy/registered-resources/values/get", + man.WithRun(policyGetRegisteredResourceValue), + ) + getValueDoc.Flags().StringP( + getValueDoc.GetDocFlag("id").Name, + getValueDoc.GetDocFlag("id").Shorthand, + getValueDoc.GetDocFlag("id").Default, + getValueDoc.GetDocFlag("id").Description, + ) + getValueDoc.Flags().StringP( + getValueDoc.GetDocFlag("fqn").Name, + getValueDoc.GetDocFlag("fqn").Shorthand, + getValueDoc.GetDocFlag("fqn").Default, + getValueDoc.GetDocFlag("fqn").Description, + ) + + listValuesDoc := man.Docs.GetCommand("policy/registered-resources/values/list", + man.WithRun(policyListRegisteredResourceValues), + ) + listValuesDoc.Flags().StringP( + listValuesDoc.GetDocFlag("resource").Name, + listValuesDoc.GetDocFlag("resource").Shorthand, + listValuesDoc.GetDocFlag("resource").Default, + listValuesDoc.GetDocFlag("resource").Description, + ) + listValuesDoc.Flags().StringP( + listValuesDoc.GetDocFlag("namespace").Name, + listValuesDoc.GetDocFlag("namespace").Shorthand, + listValuesDoc.GetDocFlag("namespace").Default, + listValuesDoc.GetDocFlag("namespace").Description, + ) + injectListPaginationFlags(listValuesDoc) + + createValueDoc := man.Docs.GetCommand("policy/registered-resources/values/create", + man.WithRun(policyCreateRegisteredResourceValue), + ) + createValueDoc.Flags().StringP( + createValueDoc.GetDocFlag("resource").Name, + createValueDoc.GetDocFlag("resource").Shorthand, + createValueDoc.GetDocFlag("resource").Default, + createValueDoc.GetDocFlag("resource").Description, + ) + createValueDoc.Flags().StringP( + createValueDoc.GetDocFlag("value").Name, + createValueDoc.GetDocFlag("value").Shorthand, + createValueDoc.GetDocFlag("value").Default, + createValueDoc.GetDocFlag("value").Description, + ) + createValueDoc.Flags().StringP( + createValueDoc.GetDocFlag("namespace").Name, + createValueDoc.GetDocFlag("namespace").Shorthand, + createValueDoc.GetDocFlag("namespace").Default, + createValueDoc.GetDocFlag("namespace").Description, + ) + createValueDoc.Flags().StringSliceVarP( + &actionAttributeValues, + createValueDoc.GetDocFlag("action-attribute-value").Name, + createValueDoc.GetDocFlag("action-attribute-value").Shorthand, + []string{}, + createValueDoc.GetDocFlag("action-attribute-value").Description, + ) + injectLabelFlags(&createValueDoc.Command, false) + + updateValueDoc := man.Docs.GetCommand("policy/registered-resources/values/update", + man.WithRun(policyUpdateRegisteredResourceValue), + ) + updateValueDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateValueDoc.Flags().StringP( + updateValueDoc.GetDocFlag("value").Name, + updateValueDoc.GetDocFlag("value").Shorthand, + updateValueDoc.GetDocFlag("value").Default, + updateValueDoc.GetDocFlag("value").Description, + ) + updateValueDoc.Flags().StringSliceVarP( + &actionAttributeValues, + updateValueDoc.GetDocFlag("action-attribute-value").Name, + updateValueDoc.GetDocFlag("action-attribute-value").Shorthand, + []string{}, + updateValueDoc.GetDocFlag("action-attribute-value").Description, + ) + injectLabelFlags(&updateValueDoc.Command, true) + updateValueDoc.Flags().Bool( + updateValueDoc.GetDocFlag("force").Name, + false, + updateValueDoc.GetDocFlag("force").Description, + ) + + deleteValueDoc := man.Docs.GetCommand("policy/registered-resources/values/delete", + man.WithRun(policyDeleteRegisteredResourceValue), + ) + deleteValueDoc.Flags().StringP( + deleteValueDoc.GetDocFlag("id").Name, + deleteValueDoc.GetDocFlag("id").Shorthand, + deleteValueDoc.GetDocFlag("id").Default, + deleteValueDoc.GetDocFlag("id").Description, + ) + deleteValueDoc.Flags().Bool( + deleteValueDoc.GetDocFlag("force").Name, + false, + deleteValueDoc.GetDocFlag("force").Description, + ) + + // Add commands to the policy command + + policyRegisteredResourcesDoc := man.Docs.GetCommand("policy/registered-resources", + man.WithSubcommands( + getDoc, + listDoc, + createDoc, + updateDoc, + deleteDoc, + ), + ) + + policyRegisteredResourceValuesDoc := man.Docs.GetCommand("policy/registered-resources/values", + man.WithSubcommands( + getValueDoc, + listValuesDoc, + createValueDoc, + updateValueDoc, + deleteValueDoc, + ), + ) + + policyRegisteredResourcesDoc.AddCommand(&policyRegisteredResourceValuesDoc.Command) + Cmd.AddCommand(&policyRegisteredResourcesDoc.Command) +} diff --git a/otdfctl/cmd/policy/resourceMappingGroups.go b/otdfctl/cmd/policy/resourceMappingGroups.go new file mode 100644 index 0000000000..1df8815da2 --- /dev/null +++ b/otdfctl/cmd/policy/resourceMappingGroups.go @@ -0,0 +1,224 @@ +package policy + +import ( + "fmt" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +var policyResourceMappingGroupsCmd *cobra.Command + +func policyCreateResourceMappingGroup(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + nsID := c.Flags.GetRequiredID("namespace-id") + name := c.Flags.GetRequiredString("name") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + resourceMappingGroup, err := h.CreateResourceMappingGroup(cmd.Context(), nsID, name, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create resource mapping group", err) + } + rows := [][]string{ + {"Id", resourceMappingGroup.GetId()}, + {"Namespace Id", resourceMappingGroup.GetNamespaceId()}, + {"Group Name", resourceMappingGroup.GetName()}, + } + if mdRows := getMetadataRows(resourceMappingGroup.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceMappingGroup.GetId(), t, resourceMappingGroup) +} + +func policyGetResourceMappingGroup(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + resourceMappingGroup, err := h.GetResourceMappingGroup(cmd.Context(), id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get resource mapping group (%s)", id), err) + } + rows := [][]string{ + {"Id", resourceMappingGroup.GetId()}, + {"Namespace Id", resourceMappingGroup.GetNamespaceId()}, + {"Group Name", resourceMappingGroup.GetName()}, + } + if mdRows := getMetadataRows(resourceMappingGroup.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceMappingGroup.GetId(), t, resourceMappingGroup) +} + +func policyListResourceMappingGroups(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + + resp, err := h.ListResourceMappingGroups(cmd.Context(), limit, offset) + if err != nil { + cli.ExitWithError("Failed to list resource mapping groups", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("ns_id", "Namespace ID", cli.FlexColumnWidthFour), + table.NewFlexColumn("name", "Name", cli.FlexColumnWidthFour), + table.NewFlexColumn("labels", "Labels", cli.FlexColumnWidthOne), + table.NewFlexColumn("created_at", "Created At", cli.FlexColumnWidthOne), + table.NewFlexColumn("updated_at", "Updated At", cli.FlexColumnWidthOne), + ) + rows := []table.Row{} + for _, rmg := range resp.GetResourceMappingGroups() { + metadata := cli.ConstructMetadata(rmg.GetMetadata()) + rows = append(rows, table.NewRow(table.RowData{ + "id": rmg.GetId(), + "ns_id": rmg.GetNamespaceId(), + "name": rmg.GetName(), + "labels": metadata["Labels"], + "created_at": metadata["Created At"], + "updated_at": metadata["Updated At"], + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func policyUpdateResourceMappingGroup(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + nsID := c.Flags.GetOptionalID("namespace-id") + name := c.Flags.GetOptionalString("name") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + resourceMappingGroup, err := h.UpdateResourceMappingGroup(cmd.Context(), id, nsID, name, getMetadataMutable(metadataLabels), getMetadataUpdateBehavior()) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to update resource mapping group (%s)", id), err) + } + rows := [][]string{ + {"Id", resourceMappingGroup.GetId()}, + {"Namespace Id", resourceMappingGroup.GetNamespaceId()}, + {"Group Name", resourceMappingGroup.GetName()}, + } + if mdRows := getMetadataRows(resourceMappingGroup.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceMappingGroup.GetId(), t, resourceMappingGroup) +} + +func policyDeleteResourceMappingGroup(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + + cli.ConfirmAction(cli.ActionDelete, "resource-mapping-group", id, force) + + resourceMappingGroup, err := h.GetResourceMappingGroup(cmd.Context(), id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get resource mapping group for delete (%s)", id), err) + } + + _, err = h.DeleteResourceMappingGroup(cmd.Context(), id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to delete resource mapping group (%s)", id), err) + } + rows := [][]string{ + {"Id", resourceMappingGroup.GetId()}, + {"Namespace Id", resourceMappingGroup.GetNamespaceId()}, + {"Group Name", resourceMappingGroup.GetName()}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceMappingGroup.GetId(), t, resourceMappingGroup) +} + +func initResourceMappingGroupsCommands() { + createDoc := man.Docs.GetCommand("policy/resource-mapping-groups/create", + man.WithRun(policyCreateResourceMappingGroup), + ) + createDoc.Flags().String( + createDoc.GetDocFlag("namespace-id").Name, + createDoc.GetDocFlag("namespace-id").Default, + createDoc.GetDocFlag("namespace-id").Description, + ) + createDoc.Flags().String( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + getDoc := man.Docs.GetCommand("policy/resource-mapping-groups/get", + man.WithRun(policyGetResourceMappingGroup), + ) + getDoc.Flags().String( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + + listDoc := man.Docs.GetCommand("policy/resource-mapping-groups/list", + man.WithRun(policyListResourceMappingGroups), + ) + injectListPaginationFlags(listDoc) + + updateDoc := man.Docs.GetCommand("policy/resource-mapping-groups/update", + man.WithRun(policyUpdateResourceMappingGroup), + ) + updateDoc.Flags().String( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateDoc.Flags().String( + updateDoc.GetDocFlag("namespace-id").Name, + updateDoc.GetDocFlag("namespace-id").Default, + updateDoc.GetDocFlag("namespace-id").Description, + ) + updateDoc.Flags().String( + updateDoc.GetDocFlag("name").Name, + updateDoc.GetDocFlag("name").Default, + updateDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deleteDoc := man.Docs.GetCommand("policy/resource-mapping-groups/delete", + man.WithRun(policyDeleteResourceMappingGroup), + ) + deleteDoc.Flags().String( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + doc := man.Docs.GetCommand("policy/resource-mapping-groups", + man.WithSubcommands(createDoc, getDoc, listDoc, updateDoc, deleteDoc), + ) + policyResourceMappingGroupsCmd = &doc.Command + Cmd.AddCommand(policyResourceMappingGroupsCmd) +} diff --git a/otdfctl/cmd/policy/resourceMappings.go b/otdfctl/cmd/policy/resourceMappings.go new file mode 100644 index 0000000000..57a0ba2fde --- /dev/null +++ b/otdfctl/cmd/policy/resourceMappings.go @@ -0,0 +1,263 @@ +package policy + +import ( + _ "embed" // required for go:embed directives + "fmt" + "strings" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/spf13/cobra" +) + +var ( + terms []string + resourceMappingsCmd *cobra.Command +) + +func createResourceMapping(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + attrID := c.Flags.GetRequiredID("attribute-value-id") + grpID := c.Flags.GetOptionalID("group-id") + terms = c.Flags.GetStringSlice("terms", terms, cli.FlagsStringSliceOptions{ + Min: 1, + }) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + resourceMapping, err := h.CreateResourceMapping(attrID, terms, grpID, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create resource mapping", err) + } + rows := [][]string{ + {"Id", resourceMapping.GetId()}, + {"Attribute Value Id", resourceMapping.GetAttributeValue().GetId()}, + {"Attribute Value", resourceMapping.GetAttributeValue().GetValue()}, + {"Terms", strings.Join(resourceMapping.GetTerms(), ", ")}, + {"Group Id", resourceMapping.GetGroup().GetId()}, + {"Group Name", resourceMapping.GetGroup().GetName()}, + } + if mdRows := getMetadataRows(resourceMapping.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceMapping.GetId(), t, resourceMapping) +} + +func getResourceMapping(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + resourceMapping, err := h.GetResourceMapping(id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get resource mapping (%s)", id), err) + } + rows := [][]string{ + {"Id", resourceMapping.GetId()}, + {"Attribute Value Id", resourceMapping.GetAttributeValue().GetId()}, + {"Attribute Value", resourceMapping.GetAttributeValue().GetValue()}, + {"Terms", strings.Join(resourceMapping.GetTerms(), ", ")}, + {"Group Id", resourceMapping.GetGroup().GetId()}, + {"Group Name", resourceMapping.GetGroup().GetName()}, + } + if mdRows := getMetadataRows(resourceMapping.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceMapping.GetId(), t, resourceMapping) +} + +func listResourceMappings(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + + resp, err := h.ListResourceMappings(cmd.Context(), limit, offset) + if err != nil { + cli.ExitWithError("Failed to list resource mappings", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("attr_value_id", "Attribute Value Id", cli.FlexColumnWidthFive), + table.NewFlexColumn("attr_value", "Attribute Value", cli.FlexColumnWidthTwo), + table.NewFlexColumn("terms", "Terms", cli.FlexColumnWidthFour), + table.NewFlexColumn("group_id", "Group Id", cli.FlexColumnWidthFive), + table.NewFlexColumn("group_name", "Group Name", cli.FlexColumnWidthTwo), + table.NewFlexColumn("labels", "Labels", cli.FlexColumnWidthOne), + table.NewFlexColumn("created_at", "Created At", cli.FlexColumnWidthOne), + table.NewFlexColumn("updated_at", "Updated At", cli.FlexColumnWidthOne), + ) + rows := []table.Row{} + for _, resourceMapping := range resp.GetResourceMappings() { + metadata := cli.ConstructMetadata(resourceMapping.GetMetadata()) + rows = append(rows, table.NewRow(table.RowData{ + "id": resourceMapping.GetId(), + "attr_value_id": resourceMapping.GetAttributeValue().GetId(), + "attr_value": resourceMapping.GetAttributeValue().GetValue(), + "group_id": resourceMapping.GetGroup().GetId(), + "group_name": resourceMapping.GetGroup().GetName(), + "terms": strings.Join(resourceMapping.GetTerms(), ", "), + "labels": metadata["Labels"], + "created_at": metadata["Created At"], + "updated_at": metadata["Updated At"], + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func updateResourceMapping(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + attrValueID := c.Flags.GetOptionalID("attribute-value-id") + grpID := c.Flags.GetOptionalID("group-id") + terms = c.Flags.GetStringSlice("terms", terms, cli.FlagsStringSliceOptions{}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + resourceMapping, err := h.UpdateResourceMapping(id, attrValueID, grpID, terms, getMetadataMutable(metadataLabels), getMetadataUpdateBehavior()) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to update resource mapping (%s)", id), err) + } + rows := [][]string{ + {"Id", resourceMapping.GetId()}, + {"Attribute Value Id", resourceMapping.GetAttributeValue().GetId()}, + {"Attribute Value", resourceMapping.GetAttributeValue().GetValue()}, + {"Terms", strings.Join(resourceMapping.GetTerms(), ", ")}, + {"Group Id", resourceMapping.GetGroup().GetId()}, + {"Group Name", resourceMapping.GetGroup().GetName()}, + } + if mdRows := getMetadataRows(resourceMapping.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceMapping.GetId(), t, resourceMapping) +} + +func deleteResourceMapping(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + + cli.ConfirmAction(cli.ActionDelete, "resource-mapping", id, force) + + resourceMapping, err := h.GetResourceMapping(id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get resource mapping for delete (%s)", id), err) + } + + _, err = h.DeleteResourceMapping(id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to delete resource mapping (%s)", id), err) + } + rows := [][]string{ + {"Id", resourceMapping.GetId()}, + {"Attribute Value Id", resourceMapping.GetAttributeValue().GetId()}, + {"Attribute Value", resourceMapping.GetAttributeValue().GetValue()}, + {"Terms", strings.Join(resourceMapping.GetTerms(), ", ")}, + {"Group Id", resourceMapping.GetGroup().GetId()}, + {"Group Name", resourceMapping.GetGroup().GetName()}, + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, resourceMapping.GetId(), t, resourceMapping) +} + +func initResourceMappingsCommands() { + createDoc := man.Docs.GetCommand("policy/resource-mappings/create", + man.WithRun(createResourceMapping), + ) + createDoc.Flags().String( + createDoc.GetDocFlag("attribute-value-id").Name, + createDoc.GetDocFlag("attribute-value-id").Default, + createDoc.GetDocFlag("attribute-value-id").Description, + ) + createDoc.Flags().StringSliceVar( + &terms, + createDoc.GetDocFlag("terms").Name, + []string{}, + createDoc.GetDocFlag("terms").Description, + ) + createDoc.Flags().String( + createDoc.GetDocFlag("group-id").Name, + createDoc.GetDocFlag("group-id").Default, + createDoc.GetDocFlag("group-id").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + getDoc := man.Docs.GetCommand("policy/resource-mappings/get", + man.WithRun(getResourceMapping), + ) + getDoc.Flags().String( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + + listDoc := man.Docs.GetCommand("policy/resource-mappings/list", + man.WithRun(listResourceMappings), + ) + injectListPaginationFlags(listDoc) + + updateDoc := man.Docs.GetCommand("policy/resource-mappings/update", + man.WithRun(updateResourceMapping), + ) + updateDoc.Flags().String( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateDoc.Flags().String( + updateDoc.GetDocFlag("attribute-value-id").Name, + updateDoc.GetDocFlag("attribute-value-id").Default, + updateDoc.GetDocFlag("attribute-value-id").Description, + ) + updateDoc.Flags().StringSliceVar( + &terms, + updateDoc.GetDocFlag("terms").Name, + []string{}, + updateDoc.GetDocFlag("terms").Description, + ) + updateDoc.Flags().String( + updateDoc.GetDocFlag("group-id").Name, + updateDoc.GetDocFlag("group-id").Default, + updateDoc.GetDocFlag("group-id").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deleteDoc := man.Docs.GetCommand("policy/resource-mappings/delete", + man.WithRun(deleteResourceMapping), + ) + deleteDoc.Flags().String( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + doc := man.Docs.GetCommand("policy/resource-mappings", + man.WithSubcommands(createDoc, getDoc, listDoc, updateDoc, deleteDoc), + ) + resourceMappingsCmd = &doc.Command + Cmd.AddCommand(resourceMappingsCmd) +} diff --git a/otdfctl/cmd/policy/subjectConditionSets.go b/otdfctl/cmd/policy/subjectConditionSets.go new file mode 100644 index 0000000000..8344e9d6ab --- /dev/null +++ b/otdfctl/cmd/policy/subjectConditionSets.go @@ -0,0 +1,433 @@ +package policy + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/spf13/cobra" + "google.golang.org/protobuf/encoding/protojson" +) + +// Helper to unmarshal SubjectSets from JSON (stored as JSONB in the database column) +func unmarshalSubjectSetsProto(conditionJSON []byte) ([]*policy.SubjectSet, error) { + var ( + raw []json.RawMessage + ss []*policy.SubjectSet + ) + if err := json.Unmarshal(conditionJSON, &raw); err != nil { + return nil, err + } + + for _, r := range raw { + s := policy.SubjectSet{} + if err := protojson.Unmarshal(r, &s); err != nil { + return nil, err + } + ss = append(ss, &s) + } + + return ss, nil +} + +// Helper to marshal SubjectSets into JSON (stored as JSONB in the database column) +func marshalSubjectSetsProto(subjectSet []*policy.SubjectSet) ([]byte, error) { + var raw []json.RawMessage + for _, ss := range subjectSet { + b, err := protojson.Marshal(ss) + if err != nil { + return nil, err + } + raw = append(raw, b) + } + return json.Marshal(raw) +} + +func createSubjectConditionSet(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + var ssBytes []byte + + ssFlagJSON := c.Flags.GetOptionalString("subject-sets") + ssFileJSON := c.Flags.GetOptionalString("subject-sets-file-json") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + namespace := c.Flags.GetOptionalString("namespace") + + // validate no flag conflicts + if ssFileJSON == "" && ssFlagJSON == "" { + cli.ExitWithError("At least one subject set must be provided ('--subject-sets', '--subject-sets-file-json')", nil) + } else if ssFileJSON != "" && ssFlagJSON != "" { + cli.ExitWithError("Only one of '--subject-sets' or '--subject-sets-file-json' can be provided", nil) + } + + // read subject sets into bytes from either the flagged json file or json string + if ssFileJSON != "" { + jsonFile, err := os.Open(ssFileJSON) + if err != nil { + cli.ExitWithError("Failed to open file at path: "+ssFileJSON, err) + } + defer jsonFile.Close() + + bytes, err := io.ReadAll(jsonFile) + if err != nil { + cli.ExitWithError("Failed to read bytes from file at path: "+ssFileJSON, err) + } + ssBytes = bytes + } else { + ssBytes = []byte(ssFlagJSON) + } + + ss, err := unmarshalSubjectSetsProto(ssBytes) + if err != nil { + cli.ExitWithError("Error unmarshalling subject sets", err) + } + + scs, err := h.CreateSubjectConditionSet(cmd.Context(), ss, getMetadataMutable(metadataLabels), namespace) + if err != nil { + cli.ExitWithError("Error creating subject condition set", err) + } + + subjectSetsJSON, err := marshalSubjectSetsProto(scs.GetSubjectSets()) + if err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + + rows := [][]string{ + {"Id", scs.GetId()}, + {"Namespace", scs.GetNamespace().GetFqn()}, + {"SubjectSets", string(subjectSetsJSON)}, + } + + if mdRows := getMetadataRows(scs.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, scs.GetId(), t, scs) +} + +func getSubjectConditionSet(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + scs, err := h.GetSubjectConditionSet(cmd.Context(), id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err) + } + subjectSetsJSON, err := marshalSubjectSetsProto(scs.GetSubjectSets()) + if err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + + rows := [][]string{ + {"Id", scs.GetId()}, + {"Namespace", scs.GetNamespace().GetFqn()}, + {"SubjectSets", string(subjectSetsJSON)}, + } + if mdRows := getMetadataRows(scs.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, scs.GetId(), t, scs) +} + +func listSubjectConditionSets(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + namespace := c.Flags.GetOptionalString("namespace") + sort := getSortOption(c) + + resp, err := h.ListSubjectConditionSets(cmd.Context(), limit, offset, namespace, sort) + if err != nil { + cli.ExitWithError("Error listing subject condition sets", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("namespace", "Namespace", cli.FlexColumnWidthFour), + table.NewFlexColumn("subject_sets", "SubjectSets", cli.FlexColumnWidthFour), + table.NewFlexColumn("labels", "Labels", cli.FlexColumnWidthOne), + table.NewFlexColumn("created_at", "Created At", cli.FlexColumnWidthOne), + table.NewFlexColumn("updated_at", "Updated At", cli.FlexColumnWidthOne), + ) + rows := []table.Row{} + for _, scs := range resp.GetSubjectConditionSets() { + subjectSetsJSON, err := marshalSubjectSetsProto(scs.GetSubjectSets()) + if err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + metadata := cli.ConstructMetadata(scs.GetMetadata()) + rows = append(rows, table.NewRow(table.RowData{ + "id": scs.GetId(), + "namespace": scs.GetNamespace().GetFqn(), + "subject_sets": string(subjectSetsJSON), + "labels": metadata["Labels"], + "created_at": metadata["Created At"], + "updated_at": metadata["Updated At"], + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func updateSubjectConditionSet(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + ssFlagJSON := c.Flags.GetOptionalString("subject-sets") + ssFileJSON := c.Flags.GetOptionalString("subject-sets-file-json") + + var ssBytes []byte + // validate no flag conflicts + if ssFileJSON == "" && ssFlagJSON == "" { + cli.ExitWithError("At least one subject set must be provided ('--subject-sets', '--subject-sets-file-json')", nil) + } else if ssFileJSON != "" && ssFlagJSON != "" { + cli.ExitWithError("Only one of '--subject-sets' or '--subject-sets-file-json' can be provided", nil) + } + + // read subject sets into bytes from either the flagged json file or json string + if ssFileJSON != "" { + jsonFile, err := os.Open(ssFileJSON) + if err != nil { + cli.ExitWithError("Failed to open file at path: "+ssFileJSON, err) + } + defer jsonFile.Close() + + bytes, err := io.ReadAll(jsonFile) + if err != nil { + cli.ExitWithError("Failed to read bytes from file at path: "+ssFileJSON, err) + } + ssBytes = bytes + } else { + ssBytes = []byte(ssFlagJSON) + } + + ss, err := unmarshalSubjectSetsProto(ssBytes) + if err != nil { + cli.ExitWithError("Error unmarshalling subject sets", err) + } + + _, err = h.UpdateSubjectConditionSet(ctx, id, ss, getMetadataMutable(metadataLabels), getMetadataUpdateBehavior()) + if err != nil { + cli.ExitWithError("Error updating subject condition set", err) + } + + scs, err := h.GetSubjectConditionSet(ctx, id) + if err != nil { + cli.ExitWithError("Error getting subject condition set", err) + } + + subjectSetsJSON, err := marshalSubjectSetsProto(scs.GetSubjectSets()) + if err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + + rows := [][]string{ + {"Id", scs.GetId()}, + {"SubjectSets", string(subjectSetsJSON)}, + } + + if mdRows := getMetadataRows(scs.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, scs.GetId(), t, scs) +} + +func deleteSubjectConditionSet(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + + scs, err := h.GetSubjectConditionSet(ctx, id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err) + } + + cli.ConfirmAction(cli.ActionDelete, "Subject Condition Sets", "all unmapped", force) + + if err := h.DeleteSubjectConditionSet(ctx, id); err != nil { + cli.ExitWithError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err) + } + + subjectSetsJSON, err := marshalSubjectSetsProto(scs.GetSubjectSets()) + if err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + + rows := [][]string{ + {"Id", scs.GetId()}, + {"SubjectSets", string(subjectSetsJSON)}, + } + + if mdRows := getMetadataRows(scs.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, scs.GetId(), t, scs) +} + +func pruneSubjectConditionSet(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + force := c.Flags.GetOptionalBool("force") + + cli.ConfirmAction(cli.ActionDelete, "all unmapped Subject Condition Sets", "", force) + + pruned, err := h.PruneSubjectConditionSets(cmd.Context()) + if err != nil { + cli.ExitWithError("Failed to prune unmapped Subject Condition Sets", err) + } + + rows := []table.Row{} + for _, scs := range pruned { + rows = append(rows, table.NewRow(table.RowData{ + "id": scs.GetId(), + })) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + ) + t = t.WithRows(rows) + common.HandleSuccess(cmd, "", t, pruned) +} + +var subjectConditionSetsCmd *cobra.Command + +func initSubjectConditionSetsCommands() { + createDoc := man.Docs.GetCommand("policy/subject-condition-sets/create", + man.WithRun(createSubjectConditionSet), + ) + injectLabelFlags(&createDoc.Command, false) + createDoc.Flags().StringP( + createDoc.GetDocFlag("subject-sets").Name, + createDoc.GetDocFlag("subject-sets").Shorthand, + createDoc.GetDocFlag("subject-sets").Default, + createDoc.GetDocFlag("subject-sets").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("subject-sets-file-json").Name, + createDoc.GetDocFlag("subject-sets-file-json").Shorthand, + createDoc.GetDocFlag("subject-sets-file-json").Default, + createDoc.GetDocFlag("subject-sets-file-json").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("namespace").Name, + createDoc.GetDocFlag("namespace").Shorthand, + createDoc.GetDocFlag("namespace").Default, + createDoc.GetDocFlag("namespace").Description, + ) + + getDoc := man.Docs.GetCommand("policy/subject-condition-sets/get", + man.WithRun(getSubjectConditionSet), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + + listDoc := man.Docs.GetCommand("policy/subject-condition-sets/list", + man.WithRun(listSubjectConditionSets), + ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) + listDoc.Flags().StringP( + listDoc.GetDocFlag("namespace").Name, + listDoc.GetDocFlag("namespace").Shorthand, + listDoc.GetDocFlag("namespace").Default, + listDoc.GetDocFlag("namespace").Description, + ) + + updateDoc := man.Docs.GetCommand("policy/subject-condition-sets/update", + man.WithRun(updateSubjectConditionSet), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("subject-sets").Name, + updateDoc.GetDocFlag("subject-sets").Shorthand, + updateDoc.GetDocFlag("subject-sets").Default, + updateDoc.GetDocFlag("subject-sets").Description, + ) + updateDoc.Flags().StringP( + createDoc.GetDocFlag("subject-sets-file-json").Name, + createDoc.GetDocFlag("subject-sets-file-json").Shorthand, + createDoc.GetDocFlag("subject-sets-file-json").Default, + createDoc.GetDocFlag("subject-sets-file-json").Description, + ) + + deleteDoc := man.Docs.GetCommand( + "policy/subject-condition-sets/delete", + man.WithRun(deleteSubjectConditionSet), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + pruneDoc := man.Docs.GetCommand( + "policy/subject-condition-sets/prune", + man.WithRun(pruneSubjectConditionSet), + ) + pruneDoc.Flags().Bool( + pruneDoc.GetDocFlag("force").Name, + false, + pruneDoc.GetDocFlag("force").Description, + ) + + doc := man.Docs.GetCommand("policy/subject-condition-sets", + man.WithSubcommands( + createDoc, + getDoc, + listDoc, + updateDoc, + deleteDoc, + pruneDoc, + ), + ) + subjectConditionSetsCmd = &doc.Command + Cmd.AddCommand(subjectConditionSetsCmd) +} diff --git a/otdfctl/cmd/policy/subjectMappings.go b/otdfctl/cmd/policy/subjectMappings.go new file mode 100644 index 0000000000..0317148b39 --- /dev/null +++ b/otdfctl/cmd/policy/subjectMappings.go @@ -0,0 +1,484 @@ +package policy + +import ( + "encoding/json" + "fmt" + + "github.com/evertras/bubble-table/table" + "github.com/google/uuid" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" + "github.com/spf13/cobra" +) + +var ( + actionFlagValues []string + selectors []string +) + +func policyGetSubjectMapping(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + mapping, err := h.GetSubjectMapping(cmd.Context(), id) + if err != nil { + errMsg := fmt.Sprintf("Failed to find subject mapping (%s)", id) + cli.ExitWithError(errMsg, err) + } + var actionsJSON []byte + if actionsJSON, err = json.Marshal(mapping.GetActions()); err != nil { + cli.ExitWithError("Error marshalling subject mapping actions", err) + } + + var subjectSetsJSON []byte + if subjectSetsJSON, err = json.Marshal(mapping.GetSubjectConditionSet().GetSubjectSets()); err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + + rows := [][]string{ + {"Id", mapping.GetId()}, + {"Namespace", mapping.GetNamespace().GetFqn()}, + {"Attribute Value: Id", mapping.GetAttributeValue().GetId()}, + {"Attribute Value: Value", mapping.GetAttributeValue().GetValue()}, + {"Actions", string(actionsJSON)}, + {"Subject Condition Set: Id", mapping.GetSubjectConditionSet().GetId()}, + {"Subject Condition Set", string(subjectSetsJSON)}, + } + if mdRows := getMetadataRows(mapping.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, mapping.GetId(), t, mapping) +} + +func policyListSubjectMappings(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + namespace := c.Flags.GetOptionalString("namespace") + sort := getSortOption(c) + + resp, err := h.ListSubjectMappings(cmd.Context(), limit, offset, namespace, sort) + if err != nil { + cli.ExitWithError("Failed to get subject mappings", err) + } + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("namespace", "Namespace", cli.FlexColumnWidthFour), + table.NewFlexColumn("value_id", "Attribute Value Id", cli.FlexColumnWidthFour), + table.NewFlexColumn("value_fqn", "Attibribute Value FQN", cli.FlexColumnWidthFour), + table.NewFlexColumn("actions", "Actions", cli.FlexColumnWidthTwo), + table.NewFlexColumn("subject_condition_set_id", "Subject Condition Set: Id", cli.FlexColumnWidthFour), + table.NewFlexColumn("subject_condition_set", "Subject Condition Set", cli.FlexColumnWidthThree), + ) + rows := []table.Row{} + for _, sm := range resp.GetSubjectMappings() { + var actionsJSON []byte + if actionsJSON, err = json.Marshal(sm.GetActions()); err != nil { + cli.ExitWithError("Error marshalling subject mapping actions", err) + } + + var subjectSetsJSON []byte + if subjectSetsJSON, err = json.Marshal(sm.GetSubjectConditionSet().GetSubjectSets()); err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + + rows = append(rows, table.NewRow(table.RowData{ + "id": sm.GetId(), + "namespace": sm.GetNamespace().GetFqn(), + "value_id": sm.GetAttributeValue().GetId(), + "value_fqn": sm.GetAttributeValue().GetFqn(), + "actions": string(actionsJSON), + "subject_condition_set_id": sm.GetSubjectConditionSet().GetId(), + "subject_condition_set": string(subjectSetsJSON), + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, resp.GetPagination()) + common.HandleSuccess(cmd, "", t, resp) +} + +func policyCreateSubjectMapping(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + attrValueID := c.Flags.GetRequiredID("attribute-value-id") + actionFlagValues = c.Flags.GetStringSlice("action", actionFlagValues, cli.FlagsStringSliceOptions{Min: 0}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + existingSCSId := c.Flags.GetOptionalID("subject-condition-set-id") + // NOTE: labels within a new Subject Condition Set created on a SM creation are not supported + newScsJSON := c.Flags.GetOptionalString("subject-condition-set-new") + namespace := c.Flags.GetOptionalString("namespace") + + // validations + if len(actionFlagValues) == 0 { + cli.ExitWithError("At least one Action [--action] is required", nil) + } + if existingSCSId == "" && newScsJSON == "" { + cli.ExitWithError("At least one Subject Condition Set flag [--subject-condition-set-id, --subject-condition-set-new] must be provided", nil) + } + + actions := make([]*policy.Action, len(actionFlagValues)) + for i, a := range actionFlagValues { + action := &policy.Action{} + _, err := uuid.Parse(a) + if err != nil { + action.Name = a + } else { + action.Id = a + } + actions[i] = action + } + + var scs *subjectmapping.SubjectConditionSetCreate + if newScsJSON != "" { + ss, err := unmarshalSubjectSetsProto([]byte(newScsJSON)) + if err != nil { + cli.ExitWithError("Error unmarshalling subject sets", err) + } + scs = &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: ss, + } + } + + mapping, err := h.CreateNewSubjectMapping(cmd.Context(), attrValueID, actions, existingSCSId, scs, getMetadataMutable(metadataLabels), namespace) + if err != nil { + cli.ExitWithError("Failed to create subject mapping", err) + } + + var actionsJSON []byte + if actionsJSON, err = json.Marshal(mapping.GetActions()); err != nil { + cli.ExitWithError("Error marshalling subject mapping actions", err) + } + + var subjectSetsJSON []byte + if mapping.GetSubjectConditionSet() != nil { + if subjectSetsJSON, err = json.Marshal(mapping.GetSubjectConditionSet().GetSubjectSets()); err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + } + + rows := [][]string{ + {"Id", mapping.GetId()}, + {"Namespace", mapping.GetNamespace().GetFqn()}, + {"Attribute Value Id", mapping.GetAttributeValue().GetId()}, + {"Actions", string(actionsJSON)}, + {"Subject Condition Set: Id", mapping.GetSubjectConditionSet().GetId()}, + {"Subject Condition Set", string(subjectSetsJSON)}, + } + + if mdRows := getMetadataRows(mapping.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, mapping.GetId(), t, mapping) +} + +func policyDeleteSubjectMapping(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + + sm, err := h.GetSubjectMapping(cmd.Context(), id) + if err != nil { + errMsg := fmt.Sprintf("Failed to find subject mapping (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDelete, "subject mapping", sm.GetId(), force) + + deleted, err := h.DeleteSubjectMapping(cmd.Context(), id) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete subject mapping (%s)", id) + cli.ExitWithError(errMsg, err) + } + rows := [][]string{{"Id", sm.GetId()}} + if mdRows := getMetadataRows(deleted.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + common.HandleSuccess(cmd, id, t, deleted) +} + +func policyUpdateSubjectMapping(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + actionFlagValues = c.Flags.GetStringSlice("action", actionFlagValues, cli.FlagsStringSliceOptions{Min: 0}) + scsID := c.Flags.GetOptionalID("subject-condition-set-id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + var actions []*policy.Action + if len(actionFlagValues) > 0 { + for _, a := range actionFlagValues { + action := &policy.Action{} + _, err := uuid.Parse(a) + if err != nil { + action.Name = a + } else { + action.Id = a + } + actions = append(actions, action) + } + } + + updated, err := h.UpdateSubjectMapping( + cmd.Context(), + id, + scsID, + actions, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError("Failed to update subject mapping", err) + } + rows := [][]string{ + {"Id", id}, + } + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + + common.HandleSuccess(cmd, id, t, updated) +} + +func policyMatchSubjectMappings(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := common.NewHandler(c) + defer h.Close() + + subject := c.Flags.GetOptionalString("subject") + selectors = c.Flags.GetStringSlice("selector", selectors, cli.FlagsStringSliceOptions{Min: 0}) + + if len(selectors) > 0 && subject != "" { + cli.ExitWithError("Must provide either '--subject' or '--selector' flag values, not both", nil) + } + + if subject != "" { + flattened, err := handlers.FlattenSubjectContext(subject) + if err != nil { + cli.ExitWithError("Could not process '--subject' value", err) + } + for _, item := range flattened { + selectors = append(selectors, item.Key) + } + } + + matched, err := h.MatchSubjectMappings(cmd.Context(), selectors) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to match subject mappings with selectors %v", selectors), err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("subject_attrval_id", "Subject AttrVal: Id", cli.FlexColumnWidthFour), + table.NewFlexColumn("subject_attrval_value", "Subject AttrVal: Value", cli.FlexColumnWidthThree), + table.NewFlexColumn("actions", "Actions", cli.FlexColumnWidthTwo), + table.NewFlexColumn("subject_condition_set_id", "Subject Condition Set: Id", cli.FlexColumnWidthFour), + table.NewFlexColumn("subject_condition_set", "Subject Condition Set", cli.FlexColumnWidthThree), + ) + rows := []table.Row{} + for _, sm := range matched { + var actionsJSON []byte + if actionsJSON, err = json.Marshal(sm.GetActions()); err != nil { + cli.ExitWithError("Error marshalling subject mapping actions", err) + } + + var subjectSetsJSON []byte + if subjectSetsJSON, err = json.Marshal(sm.GetSubjectConditionSet().GetSubjectSets()); err != nil { + cli.ExitWithError("Error marshalling subject condition set", err) + } + metadata := cli.ConstructMetadata(sm.GetMetadata()) + + rows = append(rows, table.NewRow(table.RowData{ + "id": sm.GetId(), + "subject_attrval_id": sm.GetAttributeValue().GetId(), + "subject_attrval_value": sm.GetAttributeValue().GetValue(), + "actions": string(actionsJSON), + "subject_condition_set_id": sm.GetSubjectConditionSet().GetId(), + "subject_condition_set": string(subjectSetsJSON), + "labels": metadata["Labels"], + "created_at": metadata["Created At"], + "updated_at": metadata["Updated At"], + })) + } + t = t.WithRows(rows) + common.HandleSuccess(cmd, "", t, matched) +} + +func initSubjectMappingsCommands() { + getDoc := man.Docs.GetCommand("policy/subject-mappings/get", + man.WithRun(policyGetSubjectMapping), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + + listDoc := man.Docs.GetCommand("policy/subject-mappings/list", + man.WithRun(policyListSubjectMappings), + ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) + listDoc.Flags().StringP( + listDoc.GetDocFlag("namespace").Name, + listDoc.GetDocFlag("namespace").Shorthand, + listDoc.GetDocFlag("namespace").Default, + listDoc.GetDocFlag("namespace").Description, + ) + + createDoc := man.Docs.GetCommand("policy/subject-mappings/create", + man.WithRun(policyCreateSubjectMapping), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("attribute-value-id").Name, + createDoc.GetDocFlag("attribute-value-id").Shorthand, + createDoc.GetDocFlag("attribute-value-id").Default, + createDoc.GetDocFlag("attribute-value-id").Description, + ) + // deprecated + createDoc.Flags().StringSliceVarP( + &[]string{}, + createDoc.GetDocFlag("action-standard").Name, + createDoc.GetDocFlag("action-standard").Shorthand, + []string{}, + createDoc.GetDocFlag("action-standard").Description, + ) + // deprecated + createDoc.Flags().StringSliceVarP( + &[]string{}, + createDoc.GetDocFlag("action-custom").Name, + createDoc.GetDocFlag("action-custom").Shorthand, + []string{}, + createDoc.GetDocFlag("action-custom").Description, + ) + createDoc.Flags().StringSliceVarP( + &actionFlagValues, + createDoc.GetDocFlag("action").Name, + createDoc.GetDocFlag("action").Shorthand, + []string{}, + createDoc.GetDocFlag("action").Description, + ) + createDoc.Flags().String( + createDoc.GetDocFlag("subject-condition-set-id").Name, + createDoc.GetDocFlag("subject-condition-set-id").Default, + createDoc.GetDocFlag("subject-condition-set-id").Description, + ) + createDoc.Flags().String( + createDoc.GetDocFlag("subject-condition-set-new").Name, + createDoc.GetDocFlag("subject-condition-set-new").Default, + createDoc.GetDocFlag("subject-condition-set-new").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("namespace").Name, + createDoc.GetDocFlag("namespace").Shorthand, + createDoc.GetDocFlag("namespace").Default, + createDoc.GetDocFlag("namespace").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + updateDoc := man.Docs.GetCommand("policy/subject-mappings/update", + man.WithRun(policyUpdateSubjectMapping), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + // deprecated + updateDoc.Flags().StringSliceVarP( + &[]string{}, + updateDoc.GetDocFlag("action-standard").Name, + updateDoc.GetDocFlag("action-standard").Shorthand, + []string{}, + updateDoc.GetDocFlag("action-standard").Description, + ) + updateDoc.Flags().StringSliceVarP( + &[]string{}, + updateDoc.GetDocFlag("action-custom").Name, + updateDoc.GetDocFlag("action-custom").Shorthand, + []string{}, + updateDoc.GetDocFlag("action-custom").Description, + ) + updateDoc.Flags().StringSliceVarP( + &actionFlagValues, + updateDoc.GetDocFlag("action").Name, + updateDoc.GetDocFlag("action").Shorthand, + []string{}, + updateDoc.GetDocFlag("action").Description, + ) + updateDoc.Flags().String( + updateDoc.GetDocFlag("subject-condition-set-id").Name, + updateDoc.GetDocFlag("subject-condition-set-id").Default, + updateDoc.GetDocFlag("subject-condition-set-id").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deleteDoc := man.Docs.GetCommand("policy/subject-mappings/delete", + man.WithRun(policyDeleteSubjectMapping), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + matchDoc := man.Docs.GetCommand("policy/subject-mappings/match", + man.WithRun(policyMatchSubjectMappings), + ) + matchDoc.Flags().StringP( + matchDoc.GetDocFlag("subject").Name, + matchDoc.GetDocFlag("subject").Shorthand, + matchDoc.GetDocFlag("subject").Default, + matchDoc.GetDocFlag("subject").Description, + ) + matchDoc.Flags().StringSliceVarP( + &selectors, + matchDoc.GetDocFlag("selector").Name, + matchDoc.GetDocFlag("selector").Shorthand, + []string{}, + matchDoc.GetDocFlag("selector").Description, + ) + + doc := man.Docs.GetCommand("policy/subject-mappings", + man.WithSubcommands( + createDoc, + getDoc, + listDoc, + updateDoc, + deleteDoc, + matchDoc, + ), + ) + subjectMappingCmd := &doc.Command + Cmd.AddCommand(subjectMappingCmd) +} diff --git a/otdfctl/cmd/profile.go b/otdfctl/cmd/profile.go new file mode 100644 index 0000000000..883bfabcc4 --- /dev/null +++ b/otdfctl/cmd/profile.go @@ -0,0 +1,381 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "runtime" + "strconv" + "strings" + + osprofiles "github.com/jrschumacher/go-osprofiles" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/config" + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "github.com/spf13/cobra" +) + +var ( + runningInLinux = runtime.GOOS == "linux" + runningInTestMode = config.TestMode == "true" +) + +const ( + profileMigrationLongDesc = "Migrate all profiles from keyring to filesystem. " + + "If you get stuck during your migration due to name collisions across the filesystem/keyring, please" + + " delete the specific profile from either the filesystem or keyring and run the migration again." + + " If that still doesn't work, you can remove all profiles from the filesystem via the `delete-all` command." +) + +type profileListOutput struct { + Store string `json:"store"` + Profiles []profileSummary `json:"profiles"` +} + +type profileSummary struct { + Name string `json:"name"` + IsDefault bool `json:"is_default"` +} + +type profileGetOutput struct { + Profile string `json:"profile"` + Endpoint string `json:"endpoint"` + IsDefault bool `json:"is_default"` + OutputFormat string `json:"output_format"` + AuthType string `json:"auth_type,omitempty"` + ClientID string `json:"client_id,omitempty"` +} + +func newProfilerFromCLI(c *cli.Cli) *osprofiles.Profiler { + driverType := getDriverTypeFromUser(c) + profiler, err := profiles.NewProfiler(string(driverType)) + if err != nil { + cli.ExitWithError("Error creating profiler", err) + } + + return profiler +} + +func getDriverTypeFromUser(c *cli.Cli) profiles.ProfileDriver { + driverTypeStr := string(profiles.ProfileDriverDefault) + store := c.FlagHelper.GetOptionalString("store") + if len(store) > 0 { + driverTypeStr = store + } + + driverType, err := profiles.ToProfileDriver(driverTypeStr) + if err != nil { + cli.ExitWithError("Error converting store type", err) + } + + return driverType +} + +var profileCmd = &cobra.Command{ + Use: "profile", + Aliases: []string{"profiles", "prof"}, + Short: "Manage profiles (experimental)", + Hidden: runningInLinux && !runningInTestMode, +} + +var profileCreateCmd = &cobra.Command{ + Use: "create ", + Aliases: []string{"add"}, + Short: "Create a new profile", + //nolint:mnd // two args + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + profileName := args[0] + endpoint := args[1] + + setDefault := c.FlagHelper.GetOptionalBool("set-default") + tlsNoVerify := c.FlagHelper.GetOptionalBool("tls-no-verify") + outputFormat := c.FlagHelper.GetOptionalString("output-format") + if !profiles.IsValidOutputFormat(outputFormat) { + c.ExitWithError("Output format must be either 'styled' or 'json'", nil) + } + + profileConfig := profiles.ProfileConfig{ + Name: profileName, + Endpoint: endpoint, + TLSNoVerify: tlsNoVerify, + OutputFormat: profiles.NormalizeOutputFormat(outputFormat), + } + _, err := profiles.NewOtdfctlProfileStore(profiles.ProfileDriverFileSystem, &profileConfig, setDefault) + if err != nil { + c.ExitWithError("Failed to create profile", err) + } + c.ExitWithSuccess(fmt.Sprintf("Profile %s created", profileName)) + }, +} + +var profileListCmd = &cobra.Command{ + Use: "list", + Short: "List profiles", + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + driverType := getDriverTypeFromUser(c) + profiler := newProfilerFromCLI(c) + + globalCfg := osprofiles.GetGlobalConfig(profiler) + defaultProfile := globalCfg.GetDefaultProfile() + + var sb strings.Builder + fmt.Fprintf(&sb, "Listing profiles from %s\n", driverType) + + out := profileListOutput{ + Store: string(driverType), + Profiles: []profileSummary{}, + } + for _, p := range osprofiles.ListProfiles(profiler) { + isDefault := p == defaultProfile + out.Profiles = append(out.Profiles, profileSummary{ + Name: p, + IsDefault: isDefault, + }) + if isDefault { + fmt.Fprintf(&sb, "* %s\n", p) + continue + } + fmt.Fprintf(&sb, " %s\n", p) + } + + c.ExitWith(sb.String(), out, cli.ExitCodeSuccess, os.Stdout) + }, +} + +var profileGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get a profile value", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + profileName := args[0] + + driverType := getDriverTypeFromUser(c) + profileStore, err := profiles.LoadOtdfctlProfileStore(driverType, profileName) + if err != nil { + cli.ExitWithError("Error loading profile store for profile "+profileName, err) + } + + isDefault := profileStore.IsDefault() + + var auth string + authType := "" + clientID := "" + ac := profileStore.GetAuthCredentials() + if ac.AuthType == profiles.AuthTypeClientCredentials { + maskedSecret := "********" + auth = "client-credentials (" + ac.ClientID + ", " + maskedSecret + ")" + authType = ac.AuthType + clientID = ac.ClientID + } + + t := cli.NewTabular( + []string{"Profile", profileStore.Name()}, + []string{"Endpoint", profileStore.GetEndpoint()}, + []string{"Is default", strconv.FormatBool(isDefault)}, + []string{"Output format", profileStore.GetOutputFormat()}, + []string{"Auth type", auth}, + ) + + c.ExitWith(t.View(), profileGetOutput{ + Profile: profileStore.Name(), + Endpoint: profileStore.GetEndpoint(), + IsDefault: isDefault, + OutputFormat: profileStore.GetOutputFormat(), + AuthType: authType, + ClientID: clientID, + }, cli.ExitCodeSuccess, os.Stdout) + }, +} + +var profileDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a profile", + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + profileName := args[0] + + // TODO: suggest delete-all command to delete all profiles including default + driverType := getDriverTypeFromUser(c) + profiler := newProfilerFromCLI(c) + + if err := osprofiles.DeleteProfile[*profiles.ProfileConfig](profiler, profileName); err != nil { + if errors.Is(err, osprofiles.ErrCannotDeleteDefaultProfile) { + c.ExitWithWarning("Profile is set as default. Please set another profile as default before deleting.") + } + c.ExitWithError("Failed to delete profile", err) + } + c.ExitWithMessage(fmt.Sprintf("Deleted profile %s from %s", profileName, driverType), cli.ExitCodeSuccess) + }, +} + +var profileDeleteAllCmd = &cobra.Command{ + Use: "delete-all", + Short: "Delete all profiles", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + + force := c.Flags.GetOptionalBool("force") + driverType := getDriverTypeFromUser(c) + profiler := newProfilerFromCLI(c) + + profilesList := osprofiles.ListProfiles(profiler) + if len(profilesList) == 0 { + c.ExitWithMessage("No profiles found to delete", cli.ExitCodeSuccess) + return + } + + cli.ConfirmAction(cli.ActionDelete, fmt.Sprintf("all profiles from %s", driverType), config.AppName, force) + + if err := profiler.DeleteAllProfiles(); err != nil { + c.ExitWithError("Failed to delete all profiles", err) + } + c.ExitWithMessage(fmt.Sprintf("Deleted %d profiles from %s", len(profilesList), driverType), cli.ExitCodeSuccess) + }, +} + +var profileSetDefaultCmd = &cobra.Command{ + Use: "set-default ", + Short: "Set a profile as default", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + profileName := args[0] + profiler := newProfilerFromCLI(c) + + if err := osprofiles.SetDefaultProfile(profiler, profileName); err != nil { + c.ExitWithError("Failed to set default profile", err) + } + c.ExitWithMessage(fmt.Sprintf("Set profile %s as default", profileName), cli.ExitCodeSuccess) + }, +} + +var profileSetEndpointCmd = &cobra.Command{ + Use: "set-endpoint ", + Short: "Set a profile value", + //nolint:mnd // two args + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + profileName := args[0] + endpoint := args[1] + profiler := newProfilerFromCLI(c) + + store, err := osprofiles.GetProfile[*profiles.ProfileConfig](profiler, profileName) + if err != nil { + cli.ExitWithError("Failed to load profile", err) + } + + p, ok := store.Profile.(*profiles.ProfileConfig) + if !ok || p == nil { + cli.ExitWithError("Failed to load profile", errors.New("invalid profile configuration")) + } + + u, err := utils.NormalizeEndpoint(endpoint) + if err != nil { + c.ExitWithError("Failed to set endpoint", err) + } + + p.Endpoint = u.String() + if err := store.Save(); err != nil { + c.ExitWithError("Failed to set endpoint", err) + } + c.ExitWithMessage(fmt.Sprintf("Set endpoint %s for profile %s ", endpoint, profileName), cli.ExitCodeSuccess) + }, +} + +var profileSetOutputFormatCmd = &cobra.Command{ + Use: "set-output-format ", + Short: "Set the preferred output format for a profile", + Args: cobra.ExactArgs(2), //nolint:mnd // ignore argument as magic number, self-explanatory + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + profileName := args[0] + format := args[1] + + if !profiles.IsValidOutputFormat(format) { + c.ExitWithError("Output format must be either 'styled' or 'json'", nil) + } + + store, err := profiles.LoadOtdfctlProfileStore(profiles.ProfileDriverFileSystem, profileName) + if err != nil { + cli.ExitWithError("Failed to load profile", err) + } + + if err := store.SetOutputFormat(format); err != nil { + c.ExitWithError("Failed to set output format", err) + } + c.ExitWithSuccess(fmt.Sprintf("Set output format to %s for profile %s", format, profileName)) + }, +} + +var profileMigrateCmd = &cobra.Command{ + Use: "migrate", + Short: "Migrate all profiles from keyring to filesystem.", + Long: profileMigrationLongDesc, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + err := profiles.Migrate(profiles.ProfileDriverFileSystem, profiles.ProfileDriverKeyring) + if err != nil { + c.ExitWithError("Failed to migrate", err) + } + c.ExitWithMessage("Migration complete.", cli.ExitCodeSuccess) + }, +} + +var profileKeyringCleanupCmd = &cobra.Command{ + Use: "cleanup", + Short: "Remove all profiles and configuration from the keyring store. Use when migration fails.", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + + force := c.Flags.GetOptionalBool("force") + cli.ConfirmAction(cli.ActionDelete, "all profiles and configuration stored in the keyring", config.AppName, force) + + keyringProfiler, err := osprofiles.New(config.AppName, osprofiles.WithKeyringStore()) + if err != nil { + c.ExitWithError("Failed to initialize keyring profile store", err) + } + + if err := keyringProfiler.Cleanup(force); err != nil { + cli.ExitWithError(profiles.ErrCleaningUpProfiles.Error(), err) + } + c.ExitWithMessage("Keyring profile store cleanup complete", cli.ExitCodeSuccess) + }, +} + +func InitProfileCommands() { + profileCreateCmd.Flags().Bool("set-default", false, "Set the profile as default") + profileCreateCmd.Flags().Bool("tls-no-verify", false, "Disable TLS verification") + profileCreateCmd.Flags().String("output-format", profiles.OutputStyled, "Preferred output format: styled or json") + + profileListCmd.Flags().String("store", "filesystem", "Profile store to use: filesystem or keyring") + profileGetCmd.Flags().String("store", "filesystem", "Profile store to use: filesystem or keyring") + profileDeleteCmd.Flags().String("store", "filesystem", "Profile store to use: filesystem or keyring") + profileDeleteAllCmd.Flags().String("store", "filesystem", "Profile store to use: filesystem or keyring") + profileDeleteAllCmd.Flags().Bool("force", false, "Skip confirmation prompt") + + profileSetEndpointCmd.Flags().Bool("tls-no-verify", false, "Disable TLS verification") + + RootCmd.AddCommand(profileCmd) + + profileCmd.AddCommand(profileCreateCmd) + profileCmd.AddCommand(profileListCmd) + profileCmd.AddCommand(profileGetCmd) + profileCmd.AddCommand(profileDeleteCmd) + profileCmd.AddCommand(profileDeleteAllCmd) + profileCmd.AddCommand(profileSetDefaultCmd) + profileCmd.AddCommand(profileSetEndpointCmd) + profileCmd.AddCommand(profileSetOutputFormatCmd) + profileCmd.AddCommand(profileMigrateCmd) + profileCmd.AddCommand(profileKeyringCleanupCmd) + + profileKeyringCleanupCmd.Flags().Bool("force", false, "Skip confirmation prompt") +} diff --git a/otdfctl/cmd/root.go b/otdfctl/cmd/root.go new file mode 100644 index 0000000000..89ab6f9fcb --- /dev/null +++ b/otdfctl/cmd/root.go @@ -0,0 +1,182 @@ +package cmd + +import ( + "fmt" + "log/slog" + "os" + + "github.com/opentdf/platform/otdfctl/cmd/auth" + cfg "github.com/opentdf/platform/otdfctl/cmd/config" + "github.com/opentdf/platform/otdfctl/cmd/dev" + "github.com/opentdf/platform/otdfctl/cmd/migrate" + "github.com/opentdf/platform/otdfctl/cmd/policy" + "github.com/opentdf/platform/otdfctl/cmd/tdf" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/config" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/sdk" + "github.com/spf13/cobra" +) + +var ( + clientCredsFile string + clientCredsJSON string + + RootCmd = &man.Docs.GetDoc("").Command +) + +type version struct { + AppName string `json:"app_name"` + Version string `json:"version"` + CommitSha string `json:"commit_sha"` + BuildTime string `json:"build_time"` + SDKVersion string `json:"sdk_version"` + SchemaVersion string `json:"schema_version"` +} + +func init() { + rootCmd := man.Docs.GetCommand("", man.WithRun(func(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + + if c.Flags.GetOptionalBool("version") { + v := version{ + AppName: config.AppName, + Version: config.Version, + CommitSha: config.CommitSha, + BuildTime: config.BuildTime, + SDKVersion: sdk.Version, + SchemaVersion: sdk.TDFSpecVersion, + } + + version := fmt.Sprintf("%s version %s (%s) %s", config.AppName, config.Version, config.BuildTime, config.CommitSha) + slog.Debug("otdfctl version", + slog.String("app", config.AppName), + slog.String("version", config.Version), + slog.String("build_time", config.BuildTime), + slog.String("commit_sha", config.CommitSha), + ) + c.ExitWith(version, v, cli.ExitCodeSuccess, os.Stdout) + return + } + + //nolint:errcheck // error does not need to be checked + cmd.Help() + })) + + RootCmd = &rootCmd.Command + + // Run logger setup for all commands + RootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + c := cli.New(cmd, args) + isDebug := c.Flags.GetOptionalBool("debug") + logLevel := c.Flags.GetOptionalString("log-level") + if isDebug { + logLevel = "DEBUG" + } + + // log-level from flag will take precedence over env var + if logLevel != "" { + l := new(slog.LevelVar) + if err := l.UnmarshalText([]byte(logLevel)); err != nil { + return fmt.Errorf("invalid log level: %s", logLevel) + } + logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + Level: l, + })) + + slog.SetDefault(logger) + } + return nil + } + + RootCmd.AddCommand( + // config + cfg.Cmd, + // tdf + tdf.EncryptCmd, + tdf.DecryptCmd, + tdf.InspectCmd, + // auth + auth.Cmd, + // policy + policy.Cmd, + // migrate + migrate.Cmd, + // dev + dev.Cmd, + ) + + RootCmd.Flags().Bool( + rootCmd.GetDocFlag("version").Name, + rootCmd.GetDocFlag("version").DefaultAsBool(), + rootCmd.GetDocFlag("version").Description, + ) + + RootCmd.PersistentFlags().Bool( + rootCmd.GetDocFlag("json").Name, + rootCmd.GetDocFlag("json").DefaultAsBool(), + rootCmd.GetDocFlag("json").Description, + ) + + RootCmd.PersistentFlags().String( + rootCmd.GetDocFlag("profile").Name, + rootCmd.GetDocFlag("profile").Default, + rootCmd.GetDocFlag("profile").Description, + ) + + RootCmd.PersistentFlags().String( + rootCmd.GetDocFlag("host").Name, + rootCmd.GetDocFlag("host").Default, + rootCmd.GetDocFlag("host").Description, + ) + RootCmd.PersistentFlags().Bool( + rootCmd.GetDocFlag("tls-no-verify").Name, + rootCmd.GetDocFlag("tls-no-verify").DefaultAsBool(), + rootCmd.GetDocFlag("tls-no-verify").Description, + ) + RootCmd.PersistentFlags().String( + rootCmd.GetDocFlag("log-level").Name, + rootCmd.GetDocFlag("log-level").Default, + rootCmd.GetDocFlag("log-level").Description, + ) + RootCmd.PersistentFlags().Bool( + rootCmd.GetDocFlag("debug").Name, + rootCmd.GetDocFlag("debug").DefaultAsBool(), + rootCmd.GetDocFlag("debug").Description, + ) + if err := RootCmd.PersistentFlags().MarkDeprecated(rootCmd.GetDocFlag("debug").Name, "use --log-level"); err != nil { + panic(fmt.Sprintf("failed to mark debug flag deprecated: %v", err)) + } + RootCmd.PersistentFlags().StringVar( + &clientCredsFile, + rootCmd.GetDocFlag("with-client-creds-file").Name, + rootCmd.GetDocFlag("with-client-creds-file").Default, + rootCmd.GetDocFlag("with-client-creds-file").Description, + ) + RootCmd.PersistentFlags().StringVar( + &clientCredsJSON, + rootCmd.GetDocFlag("with-client-creds").Name, + rootCmd.GetDocFlag("with-client-creds").Default, + rootCmd.GetDocFlag("with-client-creds").Description, + ) + RootCmd.PersistentFlags().String( + rootCmd.GetDocFlag("with-access-token").Name, + rootCmd.GetDocFlag("with-access-token").Default, + rootCmd.GetDocFlag("with-access-token").Description, + ) + RootCmd.AddGroup(&cobra.Group{ID: tdf.GroupID}) + + // Initialize all subcommands that have been refactored to use explicit initialization + cfg.InitCommands() + auth.InitCommands() + migrate.InitCommands() + policy.InitCommands() + dev.InitCommands() + tdf.InitEncryptCommand() + tdf.InitDecryptCommand() + tdf.InitInspectCommand() + InitProfileCommands() + + // Add interactive command + RootCmd.AddCommand(newInteractiveCmd()) +} diff --git a/otdfctl/cmd/tdf/decrypt.go b/otdfctl/cmd/tdf/decrypt.go new file mode 100644 index 0000000000..f6cd7beded --- /dev/null +++ b/otdfctl/cmd/tdf/decrypt.go @@ -0,0 +1,138 @@ +package tdf + +import ( + "errors" + "fmt" + "os" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "github.com/spf13/cobra" +) + +var ( + assertionVerification string + kasAllowList []string + + decryptDoc = man.Docs.GetCommand("decrypt", man.WithRun(decryptRun)) + DecryptCmd = &decryptDoc.Command +) + +func decryptRun(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args, cli.WithPrintJSON()) + h := common.NewHandler(c) + defer h.Close() + + output := c.Flags.GetOptionalString("out") + disableAssertionVerification := c.Flags.GetOptionalBool("no-verify-assertions") + sessionKeyAlgStr := c.Flags.GetOptionalString("session-key-algorithm") + var sessionKeyAlgorithm ocrypto.KeyType + switch sessionKeyAlgStr { + case string(ocrypto.RSA2048Key): + sessionKeyAlgorithm = ocrypto.RSA2048Key + case string(ocrypto.EC256Key): + sessionKeyAlgorithm = ocrypto.EC256Key + case string(ocrypto.EC384Key): + sessionKeyAlgorithm = ocrypto.EC384Key + case string(ocrypto.EC521Key): + sessionKeyAlgorithm = ocrypto.EC521Key + default: + sessionKeyAlgorithm = ocrypto.RSA2048Key + } + + // check for piped input + piped := readPipedStdin() + + // Prefer file argument over piped input over default filename + bytesToDecrypt := piped + var tdfFile string + var err error + if len(args) > 0 { + tdfFile = args[0] + bytesToDecrypt, err = utils.ReadBytesFromFile(tdfFile, MaxFileSize) + if err != nil { + cli.ExitWithError("Failed to read file:", err) + } + } + + if len(bytesToDecrypt) == 0 { + cli.ExitWithError("Must provide ONE of the following to decrypt: [file argument, stdin input]", errors.New("no input provided")) + } + + ignoreAllowlist := len(kasAllowList) == 1 && kasAllowList[0] == "*" + + decrypted, err := h.DecryptBytes( + c.Context(), + bytesToDecrypt, + assertionVerification, + disableAssertionVerification, + sessionKeyAlgorithm, + kasAllowList, + ignoreAllowlist, + nil, + ) + if err != nil { + cli.ExitWithError("Failed to decrypt file", err) + } + + if output == "" { + //nolint:forbidigo // printing decrypted content to stdout + fmt.Print(decrypted.String()) + return + } + // Here 'output' is the filename given with -o + f, err := os.Create(output) + if err != nil { + cli.ExitWithError("Failed to write decrypted data to file", err) + } + defer f.Close() + _, err = f.Write(decrypted.Bytes()) + if err != nil { + cli.ExitWithError("Failed to write decrypted data to file", err) + } +} + +func InitDecryptCommand() { + decryptDoc.Flags().StringP( + decryptDoc.GetDocFlag("out").Name, + decryptDoc.GetDocFlag("out").Shorthand, + decryptDoc.GetDocFlag("out").Default, + decryptDoc.GetDocFlag("out").Description, + ) + // deprecated flag + decryptDoc.Flags().StringP( + decryptDoc.GetDocFlag("tdf-type").Name, + decryptDoc.GetDocFlag("tdf-type").Shorthand, + decryptDoc.GetDocFlag("tdf-type").Default, + decryptDoc.GetDocFlag("tdf-type").Description, + ) + decryptDoc.Flags().StringVarP( + &assertionVerification, + decryptDoc.GetDocFlag("with-assertion-verification-keys").Name, + decryptDoc.GetDocFlag("with-assertion-verification-keys").Shorthand, + "", + decryptDoc.GetDocFlag("with-assertion-verification-keys").Description, + ) + decryptDoc.Flags().String( + decryptDoc.GetDocFlag("session-key-algorithm").Name, + decryptDoc.GetDocFlag("session-key-algorithm").Default, + decryptDoc.GetDocFlag("session-key-algorithm").Description, + ) + decryptDoc.Flags().Bool( + decryptDoc.GetDocFlag("no-verify-assertions").Name, + decryptDoc.GetDocFlag("no-verify-assertions").DefaultAsBool(), + decryptDoc.GetDocFlag("no-verify-assertions").Description, + ) + decryptDoc.Flags().StringSliceVarP( + &kasAllowList, + decryptDoc.GetDocFlag("kas-allowlist").Name, + decryptDoc.GetDocFlag("kas-allowlist").Shorthand, + nil, + decryptDoc.GetDocFlag("kas-allowlist").Description, + ) + + decryptDoc.GroupID = TDF +} diff --git a/otdfctl/cmd/tdf/encrypt.go b/otdfctl/cmd/tdf/encrypt.go new file mode 100644 index 0000000000..3935825f28 --- /dev/null +++ b/otdfctl/cmd/tdf/encrypt.go @@ -0,0 +1,195 @@ +package tdf + +import ( + "io" + "log/slog" + "os" + "path/filepath" + "strings" + + "github.com/gabriel-vasile/mimetype" + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "github.com/spf13/cobra" +) + +var ( + attrValues []string + assertions string + + encryptDoc = man.Docs.GetCommand("encrypt", man.WithRun(encryptRun)) + EncryptCmd = &encryptDoc.Command +) + +func encryptRun(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args, cli.WithPrintJSON()) + h := common.NewHandler(c) + defer h.Close() + + var filePath string + var fileExt string + if len(args) > 0 { + filePath = args[0] + fileExt = strings.ToLower(strings.TrimPrefix(filepath.Ext(filePath), ".")) + } + + out := c.Flags.GetOptionalString("out") + fileMimeType := c.Flags.GetOptionalString("mime-type") + attrValues = c.Flags.GetStringSlice("attr", attrValues, cli.FlagsStringSliceOptions{Min: 0}) + tdfType := c.Flags.GetOptionalString("tdf-type") + kasURLPath := c.Flags.GetOptionalString("kas-url-path") + wrappingKeyAlgStr := c.Flags.GetOptionalString("wrapping-key-algorithm") + targetMode := c.Flags.GetOptionalString("target-mode") + var wrappingKeyAlgorithm ocrypto.KeyType + switch wrappingKeyAlgStr { + case string(ocrypto.RSA2048Key): + wrappingKeyAlgorithm = ocrypto.RSA2048Key + case string(ocrypto.EC256Key): + wrappingKeyAlgorithm = ocrypto.EC256Key + case string(ocrypto.EC384Key): + wrappingKeyAlgorithm = ocrypto.EC384Key + case string(ocrypto.EC521Key): + wrappingKeyAlgorithm = ocrypto.EC521Key + default: + wrappingKeyAlgorithm = ocrypto.RSA2048Key + } + + piped := readPipedStdin() + + inputCount := 0 + if filePath != "" { + inputCount++ + } + if len(piped) > 0 { + inputCount++ + } + + cliExit := func(s string) { + cli.ExitWithError("Must provide "+s+" of the following to encrypt: [file argument, stdin input]", nil) + } + if inputCount == 0 { + cliExit("ONE") + } else if inputCount > 1 { + cliExit("ONLY ONE") + } + + // prefer filepath argument over stdin input + bytesSlice := piped + var err error + if filePath != "" { + bytesSlice, err = utils.ReadBytesFromFile(filePath, MaxFileSize) + if err != nil { + cli.ExitWithError("Failed to read file:", err) + } + } + + // auto-detect mime type if not provided + if fileMimeType == "" { + slog.Debug("detecting mime type of file") + // get the mime type of the file + mimetype.SetLimit(Size1MB) // limit to 1MB + m := mimetype.Detect(bytesSlice) + // default to application/octet-stream if no mime type is detected + fileMimeType = m.String() + + if fileMimeType == "application/octet-stream" { + if fileExt != "" { + fileMimeType = mimetype.Lookup(fileExt).String() + } + } + } + slog.Debug("encrypting file", + slog.Int("file_len", len(bytesSlice)), + slog.String("mime_type", fileMimeType), + ) + + // Do the encryption + encrypted, err := h.EncryptBytes( + tdfType, + bytesSlice, + attrValues, + fileMimeType, + kasURLPath, + assertions, + wrappingKeyAlgorithm, + targetMode, + ) + if err != nil { + cli.ExitWithError("Failed to encrypt", err) + } + + // Find the destination as the output flag filename or stdout + var dest *os.File + if out != "" { + // make sure output ends in .tdf extension + if !strings.HasSuffix(out, ".tdf") { + out += ".tdf" + } + tdfFile, err := os.Create(out) + if err != nil { + cli.ExitWithError("Failed to write encrypted file "+out, err) + } + defer tdfFile.Close() + dest = tdfFile + } else { + dest = os.Stdout + } + + _, e := io.Copy(dest, encrypted) + if e != nil { + cli.ExitWithError("Failed to write encrypted data to stdout", e) + } +} + +func InitEncryptCommand() { + encryptDoc.Flags().StringP( + encryptDoc.GetDocFlag("out").Name, + encryptDoc.GetDocFlag("out").Shorthand, + encryptDoc.GetDocFlag("out").Default, + encryptDoc.GetDocFlag("out").Description, + ) + encryptDoc.Flags().StringSliceVarP( + &attrValues, + encryptDoc.GetDocFlag("attr").Name, + encryptDoc.GetDocFlag("attr").Shorthand, + []string{}, + encryptDoc.GetDocFlag("attr").Description, + ) + encryptDoc.Flags().StringVarP( + &assertions, + encryptDoc.GetDocFlag("with-assertions").Name, + encryptDoc.GetDocFlag("with-assertions").Shorthand, + "", + encryptDoc.GetDocFlag("with-assertions").Description, + ) + encryptDoc.Flags().String( + encryptDoc.GetDocFlag("mime-type").Name, + encryptDoc.GetDocFlag("mime-type").Default, + encryptDoc.GetDocFlag("mime-type").Description, + ) + encryptDoc.Flags().String( + encryptDoc.GetDocFlag("tdf-type").Name, + encryptDoc.GetDocFlag("tdf-type").Default, + encryptDoc.GetDocFlag("tdf-type").Description, + ) + encryptDoc.Flags().StringP( + encryptDoc.GetDocFlag("wrapping-key-algorithm").Name, + encryptDoc.GetDocFlag("wrapping-key-algorithm").Shorthand, + encryptDoc.GetDocFlag("wrapping-key-algorithm").Default, + encryptDoc.GetDocFlag("wrapping-key-algorithm").Description, + ) + encryptDoc.Flags().String( + encryptDoc.GetDocFlag("kas-url-path").Name, + encryptDoc.GetDocFlag("kas-url-path").Default, + encryptDoc.GetDocFlag("kas-url-path").Description, + ) + encryptDoc.Flags().String( + encryptDoc.GetDocFlag("target-mode").Name, + encryptDoc.GetDocFlag("target-mode").Default, + encryptDoc.GetDocFlag("target-mode").Description, + ) + encryptDoc.GroupID = TDF +} diff --git a/otdfctl/cmd/tdf/inspect.go b/otdfctl/cmd/tdf/inspect.go new file mode 100644 index 0000000000..a35a3e0d04 --- /dev/null +++ b/otdfctl/cmd/tdf/inspect.go @@ -0,0 +1,91 @@ +package tdf + +import ( + "errors" + + "github.com/opentdf/platform/otdfctl/cmd/common" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/pkg/man" + "github.com/opentdf/platform/sdk" + "github.com/spf13/cobra" +) + +type tdfInspectManifest struct { + Algorithm string `json:"algorithm"` + KeyAccessType string `json:"keyAccessType"` + MimeType string `json:"mimeType"` + Policy string `json:"policy"` + Protocol string `json:"protocol"` + SegmentHashAlgorithm string `json:"segmentHashAlgorithm"` + Signature string `json:"signature"` + Type string `json:"type"` + Method sdk.Method `json:"method"` + IntegrityInformation sdk.IntegrityInformation `json:"integrityInformation"` + EncryptionInformation sdk.EncryptionInformation `json:"encryptionInformation"` + Assertions []sdk.Assertion `json:"assertions,omitempty"` + SchemaVersion string `json:"schemaVersion,omitempty"` +} + +type tdfInspectResult struct { + Manifest tdfInspectManifest `json:"manifest"` + Attributes []string `json:"attributes"` +} + +var ( + inspectDoc = man.Docs.GetCommand("inspect", man.WithRun(inspectRun)) + InspectCmd = &inspectDoc.Command +) + +func inspectRun(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args, cli.WithPrintJSON()) + h := common.NewHandler(c) + defer h.Close() + + data := cli.ReadFromArgsOrPipe(args, nil) + if len(data) == 0 { + c.ExitWithError("must provide ONE of the following: [file argument, stdin input]", errors.New("no input provided")) + } + + result, errs := h.InspectTDF(data) + for _, err := range errs { + if errors.Is(err, handlers.ErrTDFInspectFailNotValidTDF) { + c.ExitWithError("not a valid TDF", err) + } else if errors.Is(err, handlers.ErrTDFInspectFailNotInspectable) { + c.ExitWithError("failed to inspect TDF", err) + } + } + + if result.ZTDFManifest != nil { + m := tdfInspectResult{ + Manifest: tdfInspectManifest{ + Algorithm: result.ZTDFManifest.Algorithm, + KeyAccessType: result.ZTDFManifest.KeyAccessType, + MimeType: result.ZTDFManifest.MimeType, + Policy: result.ZTDFManifest.Policy, + Protocol: result.ZTDFManifest.Protocol, + SegmentHashAlgorithm: result.ZTDFManifest.SegmentHashAlgorithm, + Signature: result.ZTDFManifest.Signature, + Type: result.ZTDFManifest.Type, + Method: result.ZTDFManifest.Method, + IntegrityInformation: result.ZTDFManifest.IntegrityInformation, + EncryptionInformation: result.ZTDFManifest.EncryptionInformation, + Assertions: result.ZTDFManifest.Assertions, + SchemaVersion: result.ZTDFManifest.TDFVersion, + }, + Attributes: result.Attributes, + } + + c.ExitWithJSON(m, cli.ExitCodeSuccess) + } + c.ExitWithError("failed to inspect TDF", nil) +} + +func InitInspectCommand() { + inspectDoc.GroupID = TDF + + inspectDoc.PreRun = func(cmd *cobra.Command, args []string) { + // Set the json flag to true since we only support json output + cmd.SetArgs(append(args, "--json")) + } +} diff --git a/otdfctl/cmd/tdf/tdf.go b/otdfctl/cmd/tdf/tdf.go new file mode 100644 index 0000000000..7f727af92b --- /dev/null +++ b/otdfctl/cmd/tdf/tdf.go @@ -0,0 +1,31 @@ +package tdf + +import ( + "io" + "os" + + "github.com/opentdf/platform/otdfctl/pkg/cli" +) + +const ( + Size1MB = 1024 * 1024 + MaxFileSize = int64(10 * 1024 * 1024 * 1024) // 10 GB + TDF = "TDF" + // GroupID is the group ID for TDF commands + GroupID = TDF +) + +func readPipedStdin() []byte { + stat, err := os.Stdin.Stat() + if err != nil { + cli.ExitWithError("Failed to read stat from stdin", err) + } + if (stat.Mode() & os.ModeCharDevice) == 0 { + buf, err := io.ReadAll(os.Stdin) + if err != nil { + cli.ExitWithError("failed to scan bytes from stdin", err) + } + return buf + } + return nil +} diff --git a/otdfctl/docs/README.md b/otdfctl/docs/README.md new file mode 100644 index 0000000000..c19cef1218 --- /dev/null +++ b/otdfctl/docs/README.md @@ -0,0 +1,6 @@ +# otdfctl - OpenTDF Control Tool Documentation + +This directory contains the manual pages for the OpenTDF Control Tool (otdfctl). These docs are used +to drive the help system for the tool and provide a way to support internationalization. + +The docs are published to the [OpenTDF docs website](https://opentdf.github.io/docs/category/cli). diff --git a/otdfctl/docs/main.go b/otdfctl/docs/main.go new file mode 100644 index 0000000000..3e6f48258e --- /dev/null +++ b/otdfctl/docs/main.go @@ -0,0 +1,6 @@ +package docs + +import "embed" + +//go:embed all:man/* +var ManFiles embed.FS diff --git a/otdfctl/docs/man/_index.md b/otdfctl/docs/man/_index.md new file mode 100644 index 0000000000..bd72aa8c29 --- /dev/null +++ b/otdfctl/docs/man/_index.md @@ -0,0 +1,46 @@ +--- +title: otdfctl - OpenTDF Control Tool + +command: + name: otdfctl + flags: + - name: version + description: show version + default: false + - name: profile + description: profile to use for interacting with the platform + default: + - name: host + description: Hostname of the platform (i.e. https://localhost) + default: + - name: tls-no-verify + description: disable verification of the server's TLS certificate + default: false + - name: log-level + description: log level, default level is INFO + enum: + - debug + - info + - warn + - error + - name: with-access-token + description: access token for authentication via bearer token + - name: with-client-creds-file + description: path to a JSON file containing a 'clientId', 'clientSecret', and optional 'scopes' for auth via client-credentials flow + - name: with-client-creds + description: JSON string containing a 'clientId', 'clientSecret', and optional 'scopes' for auth via client-credentials flow + default: "" + - name: json + description: output in JSON format + default: false + - name: debug + description: DEPRECATED Use log-level. Setting this will enable debug logs + default: false +--- + +**Note**: Starting with version 1.67 of go-grpc, ALPN (Application-Layer Protocol Negotiation) is now enforced. + +To work around this, you can either: + +- Disable ALPN enforcement by setting the following environment variable: `export GRPC_ENFORCE_ALPN_ENABLED=false` +- Enable HTTP/2 on your load balancer. diff --git a/otdfctl/docs/man/auth/_index.md b/otdfctl/docs/man/auth/_index.md new file mode 100644 index 0000000000..909ba22f31 --- /dev/null +++ b/otdfctl/docs/man/auth/_index.md @@ -0,0 +1,12 @@ +--- +title: Manage local authentication session + +command: + name: auth +--- + +> [!NOTE] +> Requires experimental profiles feature. (Linux not yet supported. Windows is brittle.) + +The auth commands facilitate the process of authenticating the user with the system using profiles to store the +credentials. diff --git a/otdfctl/docs/man/auth/clear-client-credentials.md b/otdfctl/docs/man/auth/clear-client-credentials.md new file mode 100644 index 0000000000..d24b0ae420 --- /dev/null +++ b/otdfctl/docs/man/auth/clear-client-credentials.md @@ -0,0 +1,13 @@ +--- +title: Clear the cached client credentials + +command: + name: clear-client-credentials + flags: + - name: all + description: Deprecated -- see the `profile` subcommand + default: false +--- + +> [!WARNING] +> Deprecated. Use the `profile` subcommand to manage profiles and credentials. diff --git a/otdfctl/docs/man/auth/client-credentials.md b/otdfctl/docs/man/auth/client-credentials.md new file mode 100644 index 0000000000..a2a9527066 --- /dev/null +++ b/otdfctl/docs/man/auth/client-credentials.md @@ -0,0 +1,51 @@ +--- +title: Authenticate to the platform with the client-credentials flow + +command: + name: client-credentials + args: + - client-id + arbitrary_args: + - client-secret + flags: + - name: scopes + description: OIDC scopes to request (space-separated). +--- + +> [!NOTE] +> Requires experimental profiles feature. +> +> | OS | Keychain | State | +> | --- | --- | --- | +> | MacOS | Keychain | Stable | +> | Windows | Credential Manager | Alpha | +> | Linux | Secret Service | Not yet supported | + +Allows the user to login in via Client Credentials flow. The client credentials will be stored safely +in the OS keyring for future use. + +## Examples + +Authenticate with client credentials (id and secret provided interactively) + +```shell +otdfctl auth client-credentials +``` + +Authenticate with client credentials (secret provided interactively) + +```shell +otdfctl auth client-credentials +``` + +Authenticate with client credentials (secret provided as argument) + +```shell +otdfctl auth client-credentials +``` + +Authenticate with client credentials and explicit scopes + +```shell +otdfctl auth client-credentials --scopes "api:access:read api:access:write" +``` diff --git a/otdfctl/docs/man/auth/login.md b/otdfctl/docs/man/auth/login.md new file mode 100644 index 0000000000..36b47d0430 --- /dev/null +++ b/otdfctl/docs/man/auth/login.md @@ -0,0 +1,31 @@ +--- +title: Open a browser and login + +command: + name: login + flags: + - name: client-id + description: A clientId for a public (no-secret) IdP client supporting the auth code flow from any localhost port (e.g. cli-client) + shorthand: i + required: true + - name: port + description: A preferred port number to faciliate the auth flow process. + shorthand: p + required: false +--- + +> [!NOTE] +> Requires experimental profiles feature. +> +> | OS | Keychain | State | +> | --- | --- | --- | +> | MacOS | Keychain | Stable | +> | Windows | Credential Manager | Alpha | +> | Linux | Secret Service | Not yet supported | + +Authenticate for use of the OpenTDF Platform through a browser (required). + +Provide a specific public 'client-id' known to support the Auth Code PKCE flow and recognized +by the OpenTDF Platform (e.g. `cli-client`). + +The OIDC Access Token will be stored in the OS-specific keychain by default (Linux not yet supported). diff --git a/otdfctl/docs/man/auth/logout.md b/otdfctl/docs/man/auth/logout.md new file mode 100644 index 0000000000..f92e2a6809 --- /dev/null +++ b/otdfctl/docs/man/auth/logout.md @@ -0,0 +1,19 @@ +--- +title: Clear credentials from profile + +command: + name: logout +--- + + +> [!NOTE] +> Requires experimental profiles feature. +> +> | OS | Keychain | State | +> | --- | --- | --- | +> | MacOS | Keychain | Stable | +> | Windows | Credential Manager | Alpha | +> | Linux | Secret Service | Not yet supported | + +Removes any auth credentials (Client Credentials or an Access Token from a login) +from the current profile. diff --git a/otdfctl/docs/man/auth/print-access-token.md b/otdfctl/docs/man/auth/print-access-token.md new file mode 100644 index 0000000000..50774446da --- /dev/null +++ b/otdfctl/docs/man/auth/print-access-token.md @@ -0,0 +1,21 @@ +--- +title: Print the cached OIDC access token (if found) + +command: + name: print-access-token + flags: + - name: json + description: Print the full token in JSON format + default: false +--- + +> [!NOTE] +> Requires experimental profiles feature. +> +> | OS | Keychain | State | +> | --- | --- | --- | +> | MacOS | Keychain | Stable | +> | Windows | Credential Manager | Alpha | +> | Linux | Secret Service | Not yet supported | + +Retrieves a new OIDC Access Token using the client credentials and prints to stdout if found. diff --git a/otdfctl/docs/man/config/_index.md b/otdfctl/docs/man/config/_index.md new file mode 100644 index 0000000000..247aa36b55 --- /dev/null +++ b/otdfctl/docs/man/config/_index.md @@ -0,0 +1,10 @@ +--- +title: Manage Configuration + +command: + name: config +--- + +## DEPRECATED + +**Please use `profile set-output-format` instead** diff --git a/otdfctl/docs/man/config/output.md b/otdfctl/docs/man/config/output.md new file mode 100644 index 0000000000..7ed18831c4 --- /dev/null +++ b/otdfctl/docs/man/config/output.md @@ -0,0 +1,15 @@ +--- +title: Define the configured output format + +command: + name: output + flags: + - name: format + description: "'json' or 'styled' as the configured output format" + default: "styled" + required: false +--- + +## DEPRECATED + +**Please use `profile set-output-format` instead** diff --git a/otdfctl/docs/man/decrypt/_index.md b/otdfctl/docs/man/decrypt/_index.md new file mode 100644 index 0000000000..2b7c7b6f22 --- /dev/null +++ b/otdfctl/docs/man/decrypt/_index.md @@ -0,0 +1,90 @@ +--- +title: Decrypt a TDF file +command: + name: decrypt [file] + flags: + - name: out + shorthand: o + description: 'The file destination for decrypted content to be written instead of stdout.' + default: '' + - name: tdf-type + shorthand: t + description: Deprecated. TDF type is now auto-detected. + - name: no-verify-assertions + description: disable verification of assertions + default: false + - name: session-key-algorithm + description: > + EXPERIMENTAL: The type of session key algorithm to use for decryption + enum: + - rsa:2048 + - ec:secp256r1 + - ec:secp384r1 + - ec:secp521r1 + default: rsa:2048 + - name: with-assertion-verification-keys + description: > + EXPERIMENTAL: path to JSON file of keys to verify signed assertions. See examples for more information. + - name: kas-allowlist + description: A custom allowlist of comma-separated KAS Urls, e.g. `https://example.com/kas,http://localhost:8080`. If none specified, the platform will use the list of KASes in the KAS registry. To ignore the allowlist, use a quoted wildcard e.g. `--kas-allowlist '*'` **WARNING:** Bypassing the allowlist may expose you to potential security risks, as untrusted KAS URLs could be used. +--- + +Decrypt a Trusted Data Format (TDF) file and output the contents to stdout or a file in the current working directory. + +The first argument is the TDF file with path from the current working directory being decrypted. + +## Examples + +Various ways to decrypt a TDF file + +```shell +# decrypt file and write to standard output +otdfctl decrypt hello.txt.tdf + +# decrypt file and write to hello.txt file +otdfctl decrypt hello.txt.tdf -o hello.txt + +# decrypt piped TDF content and write to hello.txt file +cat hello.txt.tdf | otdfctl decrypt -o hello.txt +``` + +Advanced piping is supported + +```shell +$ echo "hello world" | otdfctl encrypt | otdfctl decrypt | cat +hello world +``` + +## Session Key Algorithm -- EXPERIMENTAL + +The session-key-algorithm specifies the algorithm to use for the session key. The available options are (default: rsa:2048): + +- rsa:2048 +- ec:secp256r1 +- ec:secp384r1 +- ec:secp521r1 + +Example + +```shell +# Decrypt a file using the ec:secp256r1 algorithm for the session key +# EXPERIMENTAL +otdfctl decrypt hello.txt --session-key-algorithm ec:secp256r1 +``` + +### ZTDF Assertion Verification (experimental) + +To verify the signed assertions (metadata bound to the TDF), you can provide verification keys. The supported assertion signing algorithms are HS256 and RS256 so the keys provided should either be an HS256 key or a public RS256 key. + +```shell +# decrypt file and write to standard output +otdfctl decrypt hello.txt.tdf --with-assertion-verification-keys my_assertion_verification_keys.json +``` + +Where my_assertion_verification_keys.json looks like: + +```json +{"keys":{"assertion1":{ "alg":"HS256","key":"k0cn4xBcY+49z5gs4OHUs/kbQ3/T8p+uUW9pIQ/9aqE="},"assertion2":{ "alg":"RS256","key":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCsgKCAQEAmr0wRsdXN0O9NiltxoGy\nC6ZYwHbdiPVzvOnm9ven5g7Fpm3HOmygdi021WX1OlSua+OSrXGPjM2xbY3LTrFH\nQXQEITjraXQRp5vlKDbBnOrtjYDaKazBXgTYVdelE4AIAuQaGoTudMasHBGiLPEW\niTL4ySec0NzHn2s72Q4hn5/KJpIJOGqj0SlNViufdNylkjrJ3apoYFv1Mhwi3EF/\niFZQ5encDDJmcG/UYF3msbuHRzArJJQ733BNRvicWF/nqixKxprvm8Ts8a54tr8N\nZ7cEu1u5G6AY/pZFGk4ml8q3v5o1ja7xw2dgpJlS8Tl88tUzs+7GG8Ib8n7mHqeP\nTQIDAQAB\n-----END PUBLIC KEY-----\n"}}} +``` + +If no verification keys are provided, the SDK will default to verifying using the payload key. If the assertions were not signed with the payload key, the decrypt call will fail. diff --git a/otdfctl/docs/man/dev/_index.md b/otdfctl/docs/man/dev/_index.md new file mode 100644 index 0000000000..50771c912f --- /dev/null +++ b/otdfctl/docs/man/dev/_index.md @@ -0,0 +1,9 @@ +--- +title: Development Tools +command: + name: dev + hidden: true +--- + +Development mode is a primarily used to aid in the development of this CLI. There are other uses for +this command and exploration is encouraged. diff --git a/otdfctl/docs/man/dev/design-system.md b/otdfctl/docs/man/dev/design-system.md new file mode 100644 index 0000000000..498f7f76b5 --- /dev/null +++ b/otdfctl/docs/man/dev/design-system.md @@ -0,0 +1,7 @@ +--- +title: Design System +command: + name: design-system +--- + +The design system is a collection of design tokens, components, and guidelines that are used to create a consistent user experience across all of the CLI's interfaces. The design system is a living document and is subject to change as the CLI evolves. diff --git a/otdfctl/docs/man/dev/selectors/_index.md b/otdfctl/docs/man/dev/selectors/_index.md new file mode 100644 index 0000000000..7d08a52848 --- /dev/null +++ b/otdfctl/docs/man/dev/selectors/_index.md @@ -0,0 +1,10 @@ +--- +title: Selectors +command: + name: selectors + aliases: + - sel +--- + +Commands to generate and test selectors on Subject Entity Representations. For more information, see the help manual for each subcommand +or additional context within Subject Condition Sets. diff --git a/otdfctl/docs/man/dev/selectors/generate.md b/otdfctl/docs/man/dev/selectors/generate.md new file mode 100644 index 0000000000..30eff2c69e --- /dev/null +++ b/otdfctl/docs/man/dev/selectors/generate.md @@ -0,0 +1,45 @@ +--- +title: Generate a set of selector expressions for keys and values of a Subject Context +command: + name: generate + aliases: + - gen + flags: + - name: subject + shorthand: s + description: A Subject Context string (JSON or JWT, default JSON) + default: '' +--- + +Take in an Entity Representation as a JWT or JSON object, such as that provided by +an Identity Provider (idP), LDAP, or OIDC Access Token JWT, and generate +sample selectors employing [flattening syntax](#flattening-syntax) to utilize within +within Subject Condition Sets that resolve an external Subject Context into mapped Attribute +Values. + +# Flattening-syntax + +The platform maintains a very simple flattening library such that the below structure flattens into the key/value pairs beneath. + +Subject input (`--subject`): + +```json +{ + "key": "abc", + "something": { + "nested": "nested_value", + "list": ["item_1", "item_2"] + } +} +``` + +Generated Selectors: + +| Selector | Value | Significance | +| -------------------- | -------------- | ------------------------- | +| ".key" | "abc" | specified field | +| ".something.nested" | "nested_value" | nested field | +| ".something.list[0]" | "item_1" | first index specifically | +| ".something.list[]" | "item_1" | any index in the list | +| ".something.list[1]" | "item_2" | second index specifically | +| ".something.list[]" | "item_2" | any index in the list | diff --git a/otdfctl/docs/man/dev/selectors/test.md b/otdfctl/docs/man/dev/selectors/test.md new file mode 100644 index 0000000000..c30328e01a --- /dev/null +++ b/otdfctl/docs/man/dev/selectors/test.md @@ -0,0 +1,48 @@ +--- +title: Test resolution of a set of selector expressions for keys and values of a Subject Context. +command: + name: test + flags: + - name: subject + shorthand: s + description: A Subject Context string (JSON or JWT, auto-detected) + default: '' + - name: selector + shorthand: x + description: "Individual selectors to test against the Subject Context (i.e. '.key,.realm_access.roles[]')" +--- + +Test a subject Entity Representation as a JWT or JSON object, such as that provided by +an Identity Provider (idP), LDAP, or OIDC Access Token JWT, against provided selectors employing [flattening syntax](#flattening-syntax) to +validate their resolution to field values on the subject's entity representation. + +# Flattening-syntax + +The platform maintains a very simple flattening library such that the below structure flattens into the key/value pairs beneath. + +Original: + +```json +{ + "key": "abc", + "something": { + "nested": "nested_value", + "list": ["item_1", "item_2"] + } +} +``` + +Flattened: + +| Selector | Value | Significance | +| -------------------- | -------------- | ------------------------- | +| ".key" | "abc" | specified field | +| ".something.nested" | "nested_value" | nested field | +| ".something.list[0]" | "item_1" | first index specifically | +| ".something.list[]" | "item_1" | any index in the list | +| ".something.list[1]" | "item_2" | second index specifically | +| ".something.list[]" | "item_2" | any index in the list | + +Testing the example above with `--selector '.key'` would find the value `abc` on the `key` field and return it in the command output. + +Testing the example above with `--selector .values[]` would not find a list at a field named `values` because it is missing entirely from the input object. diff --git a/otdfctl/docs/man/encrypt/_index.md b/otdfctl/docs/man/encrypt/_index.md new file mode 100644 index 0000000000..36fa33647e --- /dev/null +++ b/otdfctl/docs/man/encrypt/_index.md @@ -0,0 +1,135 @@ +--- +title: Encrypt file or stdin as a TDF +command: + name: encrypt [file] + flags: + - name: out + shorthand: o + description: The output file TDF in the current working directory instead of stdout ('-o file.txt' and '-o file.txt.tdf' both write the TDF as file.txt.tdf). + default: '' + - name: attr + shorthand: a + description: Attribute value Fully Qualified Names (FQNs, i.e. 'https://example.com/attr/attr1/value/value1') to apply to the encrypted data. + - name: wrapping-key-algorithm + description: > + EXPERIMENTAL: The algorithm to use for the wrapping key + enum: + - rsa:2048 + - ec:secp256r1 + - ec:secp384r1 + - ec:secp521r1 + default: rsa:2048 + - name: mime-type + description: The MIME type of the input data. If not provided, the MIME type is inferred from the input data. + - name: tdf-type + shorthand: t + description: The type of TDF to encrypt as (tdf3 is an alias for ztdf). + enum: + - ztdf + - tdf3 + default: ztdf + - name: kas-url-path + description: URL path to the KAS service at the platform endpoint domain. Leading slash is required if needed. + default: /kas + - name: target-mode + description: The target TDF spec version (e.g., "4.3.0"); intended for legacy compatibility and subject to removal. + default: "" + - name: with-assertions + description: > + EXPERIMENTAL: JSON string or path to a JSON file of assertions to bind metadata to the TDF. See examples for more information. WARNING: Providing keys in a JSON string is strongly discouraged. If including sensitive keys, instead provide a path to a JSON file containing that information. +--- + +Build a Trusted Data Format (TDF) with encrypted content from a specified file or input from stdin utilizing OpenTDF platform. + +## Examples + +Various ways to encrypt a file + +```shell +# output to stdout +otdfctl encrypt hello.txt + +# output to hello.txt.tdf +otdfctl encrypt hello.txt --out hello.txt.tdf + +# encrypt piped content and write to hello.txt.tdf +cat hello.txt | otdfctl encrypt --out hello.txt.tdf +``` + +Automatically append .tdf to the output file name + +```shell +$ cat hello.txt | otdfctl encrypt --out hello.txt; ls +hello.txt hello.txt.tdf + +$ cat hello.txt | otdfctl encrypt --out hello.txt.tdf; ls +hello.txt hello.txt.tdf +``` + +Advanced piping is supported + +```shell +$ echo "hello world" | otdfctl encrypt | otdfctl decrypt | cat +hello world +``` + +## Wrapping Key Algorithm - EXPERIMENTAL + +The wrapping-key-algorithm specifies the algorithm to use for the wrapping key. The available options are (default: rsa:2048): +- rsa:2048 +- ec:secp256r1 +- ec:secp384r1 +- ec:secp521r1 + +Example +```shell +# Encrypt a file using the ec:secp256r1 algorithm for the wrapping key +# EXPERIMENTAL +otdfctl encrypt hello.txt --wrapping-key-algorithm ec:secp256r1 --out hello.txt.tdf +``` + +## Attributes + +Attributes can be added to the encrypted data. The attribute value is a Fully Qualified Name (FQN) that is used to +restrict access to the data based on entity entitlements. + +```shell +# output to hello.txt.tdf with attribute +otdfctl encrypt hello.txt --out hello.txt.tdf --attr https://example.com/attr/attr1/value/value1 +``` + +## ZTDF Assertions (experimental) + +Assertions are a way to bind metadata to the TDF data object in a cryptographically secure way. The data is signed with the provided signing key, or if none is provided, the payload key. The signing key algorithms supported are HS256 and RS256. + +### STANAG 5636 + +The following example demonstrates how to bind a STANAG 5636 metadata assertion, to the TDF data object. + +```shell +otdfctl encrypt hello.txt --out hello.txt.tdf --with-assertions '[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"}]' +``` + +We also support providing an assertions json file. +You can optionally provide your own signing key. In this example, we provide an RS256 private key. +```json +[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"},"signingKey":{"alg":"RS256","key":"-----BEGIN PRIVATE KEY-----\nMIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCavTBGx1c3Q702\nKW3GgbILpljAdt2I9XO86eb296fmDsWmbcc6bKB2LTbVZfU6VK5r45KtcY+MzbFt\njctOsUdBdAQhOOtpdBGnm+UoNsGc6u2NgNoprMFeBNhV16UTgAgC5BoahO50xqwc\nEaIs8RaJMvjJJ5zQ3MefazvZDiGfn8omkgk4aqPRKU1WK5903KWSOsndqmhgW/Uy\nHCLcQX+IVlDl6dwMMmZwb9RgXeaxu4dHMCsklDvfcE1G+JxYX+eqLErGmu+bxOzx\nrni2vw1ntwS7W7kboBj+lkUaTiaXyre/mjWNrvHDZ2CkmVLxOXzy1TOz7sYbwhvy\nfuYep49NAgMBAAECgf8N2RrYrTRyIZmlzMJZgpc4gCujIqSPjJfEn3D5XC5+w9XA\nu/lfONZbn/9Y6/CeTgRcpYRNKO9QI0pb3RQzgiLBO+/Z1UJjtORxR0gXdJ0XXVTz\ntLWsD4dCycpkyT8snLkMQFdzXXRAefNyYdavOVz0kvCNgGgw606rZhkYbtHUCM3X\nb1LZFcIAYrpftKUXxn+xOcSjIKdqKoUlBW6Yk7iTjJuy/Su63gTJ5PbgKpNvK7Xu\nyzu4L7t2pswE5pWxb7uMMpTujqLNYiaXDlzpy/fPN8EjL1mhKzia365+EJ3uKH8c\nQ9dz/1g36lSQnD/lus0cES9xXzQ6+1izc17dTsECgYEA1XGM4PVxCt4TaApDoT7X\npeLDG9pQW55DQQiix4A/0EmQgxf6WN0uZ4b8lds02JhNBGVUIe2nyTNknV+9styu\nJsKJhq+KjrcHmE8uy18++G2cZuOM2S49p8y0HPA8YBcRBC4fAoKFFG3cmrIJW5Vu\nMzzaN+W3/1h/xdkUTpI1lYkCgYEAuZdHWrMNt96WMUuaSwu2tg3BHaYhSeyIcbwi\nm2mIOeLQ6gGtGqyALC6N/K8Ie8KwkisTI9GqcX8O9FrkZx4RvkQrONUaS4aXEJ28\nEZzwJenybkSuWunypVLMmp/pN7+mZZ7GUaDbXTF6pg4GOrlp6MIUk4plJYGXXumg\nqaXvPqUCgYA0pmvf2etmiN00nsOL9Npw+vyx1CpaTzG7ywuMNqCHGn5hN/rzDKwz\nsWKA/K+OdhMZcH1OWTc4NEsvXryGcFUtDnOqG4cMKS3gbjfWxsnbsf4QizTlJbjj\nuWT8dm4OLeJuq4nOrq9xGKCAMEaKptOmI+6YNzwp6oSqIyAVOY+qMQKBgDM7IlRU\nNwY5qIYlE4uByUcKFvQDRw8r/yI+R+NUx2kLRpZCLjG9yofntgQ5oQLg5HME9vyd\nRQqdg1hKuuAIOeem07OVh/OvTIYmtKK8CsK8iNKNnP+1suiWKarJV8yu19UXdjFU\nURmxreSm3GtbgXPiF2H/AxrOYiWuIk6SYq+NAoGAZy96GLP3HfA41UWFZH6b8ZdP\nM6CXKDDvHOk06S/hwmhvq3UO5lQULZ+pd+aURv/TDF9DXhZIyl1CXqyOYB5IqJjk\nAFI8A9n/naq7GyIZZRjzJu2blhSjW3ukkS/5CO4zJ6HfauSUjQA4u+5RStjeK3zd\nF267fElUPN4+pSOAhPI=\n-----END PRIVATE KEY-----\n"}}] +``` +```shell +otdfctl encrypt hello.txt --out hello.txt.tdf --with-assertions my_assertions_signed_rs256.json +``` +Signing with HS256 is also available. +```json +[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"},"signingKey":{"alg":"HS256","key":"k0cn4xBcY+49z5gs4OHUs/kbQ3/T8p+uUW9pIQ/9aqE="}}] +``` +```shell +otdfctl encrypt hello.txt --out hello.txt.tdf --with-assertions my_assertions_signed_hs256.json +``` + +## Target Mode + +To encrypt with a target tdf spec version, use the `--target-mode` flag. A version < 4.3.0 will include hex encoded signature hashes and will not include a schema version in the manifest. + +```shell +otdfctl encrypt hello.txt --out hello.txt.tdf --target-mode 4.3.0 +``` diff --git a/otdfctl/docs/man/example.xmd b/otdfctl/docs/man/example.xmd new file mode 100644 index 0000000000..8bb962625a --- /dev/null +++ b/otdfctl/docs/man/example.xmd @@ -0,0 +1,26 @@ +--- +title: Example command + +command: + name: example + # short: use the title + # long: uses body of the markdown and prepends the title to it + # example: is not supported since developer can implement in the long description + aliases: + - ex + flags: + - name: flag + short: f + description: A flag that does something + required: false + default: "default value" + # - name: another-flag + # short: a + # description: Another flag that does something else + # type: []string + # required: false + # default: "another default value" +--- + +Long description of the command goes here. This is where you can describe what the command does, how +it works, and what the user can expect when they run it. diff --git a/otdfctl/docs/man/inspect/_index.md b/otdfctl/docs/man/inspect/_index.md new file mode 100644 index 0000000000..46a1ec73f4 --- /dev/null +++ b/otdfctl/docs/man/inspect/_index.md @@ -0,0 +1,18 @@ +--- +title: Inspect a TDF file +command: + name: inspect [file] + flags: +--- + +# Inspect a TDF file + +Prints the `manifest.json` of the specified TDF for inspection. + +This is useful for development and administration. + +## Example + +```shell +$ otdfctl inspect example.tdf +``` diff --git a/otdfctl/docs/man/interactive.md b/otdfctl/docs/man/interactive.md new file mode 100644 index 0000000000..656738d4f6 --- /dev/null +++ b/otdfctl/docs/man/interactive.md @@ -0,0 +1,8 @@ +--- +title: Interactive Mode (experimental) + +command: + name: interactive + aliases: + - i +--- diff --git a/otdfctl/docs/man/migrate/_index.md b/otdfctl/docs/man/migrate/_index.md new file mode 100644 index 0000000000..273ddb080c --- /dev/null +++ b/otdfctl/docs/man/migrate/_index.md @@ -0,0 +1,27 @@ +--- +title: Migrate resources + +command: + name: migrate + aliases: + - migration + description: Migrate policy resources + flags: + - name: commit + shorthand: c + description: Writes changes to policy storage + default: false + - name: interactive + shorthand: i + description: Interactive walk through of migrations + default: false +--- + +`migrate` groups migration and migration-related cleanup workflows. + +Use this command family when you want to inspect a migration plan, review changes interactively, or apply migration-related updates. + +The parent `migrate` command owns flags shared by its subcommands: + +- `--commit`, `-c`: apply the planned changes instead of only rendering the plan +- `--interactive`, `-i`: walk through the plan interactively before continuing diff --git a/otdfctl/docs/man/migrate/namespaced-policy.md b/otdfctl/docs/man/migrate/namespaced-policy.md new file mode 100644 index 0000000000..8c0cd5ef6a --- /dev/null +++ b/otdfctl/docs/man/migrate/namespaced-policy.md @@ -0,0 +1,65 @@ +--- +title: Migrate Namespaced Policy + +command: + name: namespaced-policy + flags: + - name: scope + shorthand: s + description: "Comma-separated scopes: actions, subject-condition-sets, subject-mappings, registered-resources, obligation-triggers" + default: '' +--- + +## General Information + +`namespaced-policy` is the migration entrypoint for moving legacy policy objects into namespaced policy. + +The command prints a human-readable migration summary to stdout. Dry runs show the plan summary; `--commit` shows the committed summary with created target IDs. + +Commit mode can partially apply changes before an error occurs. When that happens, the command prints a committed summary with `Result: failure` and per-scope `Created`, `Will Create`, and `Failed` sections so you can see what was applied and what still remains. + +`--scope` is required and selects any subset of `actions`, `subject-condition-sets`, `subject-mappings`, `registered-resources`, and `obligation-triggers`. + +The parent `migrate` command provides the shared `--commit` and `--interactive` flags. + +`namespaced-policy` is intended to be non-destructive. Commit should create namespaced copies and record migration metadata, but it should not delete legacy objects. Cleanup belongs to `migrate prune namespaced-policy`. + +All target namespaces must already exist before the command runs. Planning should fail before any writes if a required namespace is missing. + +## Pre-requisites + +1. Run at least `v0.14.0` of the OpenTDF platform before using this migration. + +2. Standard actions are seeded per namespace in that platform version. The migration expects those namespaced standard actions to exist when migrating references to `create`, `read`, `update`, and `delete`. + +## Best practices + +1. Before running any migration commands you should take a backup of your database to avoid any potential issues. + +2. Turn on the `namespaced_policy` feature flag within your deployed service yaml to avoid creating any accidental non-namespaced policy objects. + +3. While we allow you to perform a full migration of policy at once with the multiple scopes, we recommend that you migrate one-by-one in the following order: + - Actions + - Subject-Condition-Sets + - Subject-Mappings + - Obligation-Triggers + - Registered-Resources + +4. Use `--interactive` mode when migrating policy, this will give you the best chance of success for handling any issues that might occur. In addition, we ask for confirmation for each +object before creating it. + +## Examples + +```shell +otdfctl migrate namespaced-policy --scope=registered-resources +otdfctl migrate namespaced-policy --scope=actions,subject-mappings,registered-resources --commit +otdfctl migrate namespaced-policy --scope=actions,subject-mappings --interactive --commit +``` + +## Other information + +1. For subject-condition-sets and actions, if they are not used by any other policy object they will not be migrated or considered for migration. +2. If you provide a parent scope, the dependent scopes will also be added. For example, given scope `subject-mappings` will also add scope: `actions`, `subject-condition-sets` +since those are required by `subject-mappings` to be migrated to a new namespace. +3. In `--interactive --commit` mode, declining the backup confirmation or aborting review prints an `aborted` summary and exits without applying the remaining changes. +4. In `--interactive` mode only create operations are reviewed. Existing standard objects and already migrated objects are not prompted. diff --git a/otdfctl/docs/man/migrate/prune/_index.md b/otdfctl/docs/man/migrate/prune/_index.md new file mode 100644 index 0000000000..493f8370c8 --- /dev/null +++ b/otdfctl/docs/man/migrate/prune/_index.md @@ -0,0 +1,14 @@ +--- +title: Prune Migrated Policy Objects + +command: + name: prune +--- + +`prune` groups commands used to remove policy resources that are no longer needed after migration or cleanup workflows. + +Available subcommands currently include `namespaced-policy` for policy cleanup workflows. + +The parent `migrate` command provides the shared `--commit` and `--interactive` flags. `--interactive` lets you review prune plans before execution, and when paired with `--commit` it also adds confirmation before deletions are applied. + +`migrate prune` is not the same as `otdfctl policy subject-condition-sets prune`. The existing subject-condition-set prune command deletes unmapped subject condition sets. `migrate prune` is only for cleaning up legacy objects after a migration run. diff --git a/otdfctl/docs/man/migrate/prune/namespaced-policy.md b/otdfctl/docs/man/migrate/prune/namespaced-policy.md new file mode 100644 index 0000000000..bf4aeff53b --- /dev/null +++ b/otdfctl/docs/man/migrate/prune/namespaced-policy.md @@ -0,0 +1,79 @@ +--- +title: Prune Namespaced Policy + +command: + name: namespaced-policy + flags: + - name: scope + shorthand: s + description: "One scope to prune: actions, subject-condition-sets, subject-mappings, registered-resources, obligation-triggers" + default: '' +--- + +## General Information + +`namespaced-policy` is the cleanup entrypoint for namespaced policy migration. + +The command prints a human-readable prune summary to stdout. Dry runs show the planned deletions and blocked items; `--commit` shows the committed summary with the objects that were deleted. + +`--scope` is required and must be exactly one of `actions`, `subject-condition-sets`, `subject-mappings`, `registered-resources`, or `obligation-triggers`. + +`namespaced-policy` rebuilds the live dependency graph, inspects migration labels, and deletes only legacy objects it can prove are safe to remove for the selected scope. + +The parent `migrate` command provides the shared `--commit` and `--interactive` flags. `--interactive` lets you review the prune plan before execution, and when paired with `--commit` it also asks for backup confirmation and per-delete confirmation before any deletion is applied. + +## Pre-requisites + +1. Run at least `v0.14.0` of the OpenTDF platform before using this prune flow. + +2. Run `otdfctl migrate namespaced-policy` successfully before pruning. Prune only deletes legacy objects after it can match them to the expected migrated targets and their `migrated_from` labels. + +## Delete safety + +An object is safe to delete only when prune can tie the legacy source object to the expected migrated target and prove the source is no longer needed. + +- `delete`: the source has the expected migrated target and prune found no remaining legacy dependency that still requires the source object. +- `blocked`: prune will not delete the source. Common reasons are that the source is still referenced by legacy policy, that the source object has not actually been migrated yet, or that an unmigrated registered resource spans multiple target namespaces and must be deleted manually. +- `unresolved`: prune found something close to a migrated target, but it cannot prove the source and target match safely. Common reasons are missing or mismatched `migrated_from` labels or no matching labeled target. + +In practice, prune relies on current legacy references plus `migrated_from` metadata on the namespaced targets. If that evidence is incomplete or inconsistent, the object is left in place instead of being deleted. + +## Best practices + +1. Before running any prune commands you should take a backup of your database to avoid any potential issues. + +2. Turn on the `namespaced_policy` feature flag within your deployed service yaml to avoid creating any accidental non-namespaced policy objects. + +3. Prune one scope at a time. + +4. We recommend pruning in the reverse order of migration so dependents are removed before their dependencies: + - Registered-Resources + - Obligation-Triggers + - Subject-Mappings + - Subject-Condition-Sets + - Actions + +## Examples + +```shell +otdfctl migrate prune namespaced-policy --scope=registered-resources +otdfctl migrate prune namespaced-policy --scope=obligation-triggers --commit +otdfctl migrate prune namespaced-policy --scope=obligation-triggers --interactive --commit +``` + +## Other Information + +Action / Subject-Condition-Set pruning + +- Actions and subject-condition-sets are pruned a little differently from the other scopes. Instead of reusing the resolved migration view, prune classifies them directly from the current legacy objects, their current legacy references, and the canonical migrated targets it can find. +- They are expected to be pruned last. By the time you reach those scopes, their legacy dependents such as subject mappings, registered resources, and obligation triggers should already be gone, so the safest decision comes from checking the live legacy dependency graph at prune time. +- Some actions or subject-condition-sets that were never used by any other legacy policy object can still end up `blocked`. +- If prune cannot find a canonical migrated target for the source object, it leaves the source in place as `blocked` instead of assuming it is safe to delete. +- Example: if a custom action `decrypt` is no longer referenced by any legacy subject mapping, registered resource, or obligation trigger and prune finds a namespaced `decrypt` target with `metadata.labels.migrated_from=`, the source action is safe to delete. If no canonical namespaced `decrypt` target exists, the source action is reported as `blocked` because prune cannot prove that the object was actually migrated. + +Registered-Resource manual deletion + +- A legacy registered resource can only be auto-pruned when migration resolved it to one target namespace and prune can match it to the expected migrated target. +- If a registered resource spans multiple target namespaces and was never migrated, prune reports it as `blocked` with the `MultiNamespaceManualDelete` reason. +- In that case prune does not guess which namespace copy should have existed, and it does not delete the legacy source automatically. +- Clean this up manually after you verify the intended namespaced registered resources already exist and the legacy object is no longer needed. diff --git a/otdfctl/docs/man/policy/_index.md b/otdfctl/docs/man/policy/_index.md new file mode 100644 index 0000000000..a8c76f7756 --- /dev/null +++ b/otdfctl/docs/man/policy/_index.md @@ -0,0 +1,18 @@ +--- +title: Manage policy + +command: + name: policy + aliases: + - pol + - policies + flags: + - name: json + description: output single command in JSON (overrides configured output format) + default: 'false' +--- + +Policy is a set of rules that are enforced by the platform. Specific to the the data-centric +security, policy revolves around data attributes (referred to as attributes). Within the context +of attributes are namespaces, values, subject-mappings, resource-mappings, registered-resources, key-access-server grants, +and other key elements. diff --git a/otdfctl/docs/man/policy/actions/_index.md b/otdfctl/docs/man/policy/actions/_index.md new file mode 100644 index 0000000000..9c2f172575 --- /dev/null +++ b/otdfctl/docs/man/policy/actions/_index.md @@ -0,0 +1,26 @@ +--- +title: Manage Actions +command: + name: actions + aliases: + - action +--- + +Actions are a set of `standard` and `custom` verbs at the core of an Access Decision or an +Obligation. In the context of an entitlement decision, adding Actions to Subject Mappings answers +"what can an Entity _do_ to a Resource?" + +Standard Actions in Policy are comprised of the below, and only their metadata labels are mutable: +- create +- read (considered within all TDF `decrypt` flows) +- update +- delete + +Custom Actions known to Policy are admin-defined, unique within a namespace, and will be lower +cased when stored. They may contain underscores (`_`) or hyphens (`-`) if preceded or followed +by an alphanumeric character. For example: +- download +- queue-to-print +- send_email + +For more information about entitlement and Subject Mappings, see the `subject-mappings` command. diff --git a/otdfctl/docs/man/policy/actions/create.md b/otdfctl/docs/man/policy/actions/create.md new file mode 100644 index 0000000000..b20c6650b5 --- /dev/null +++ b/otdfctl/docs/man/policy/actions/create.md @@ -0,0 +1,36 @@ +--- +title: Create a Custom Action +command: + name: create + aliases: + - c + - add + - new + flags: + - name: name + shorthand: n + description: Name of the custom action (must be unique within a namespace) + required: true + - name: namespace + shorthand: s + description: Namespace ID or FQN + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Add a custom `action` to the platform Policy. + +An Action `name` is normalized to lower case and may contain underscores (`_`) or hyphens (`-`) +between other alphanumeric characters. Each name must be unique within a namespace. + +For more information, see the `actions` subcommand. + +## Examples + +Create a custom action named 'install_package': + +```shell +otdfctl policy actions create --name install_package --namespace https://example.com +``` diff --git a/otdfctl/docs/man/policy/actions/delete.md b/otdfctl/docs/man/policy/actions/delete.md new file mode 100644 index 0000000000..0a67c7061e --- /dev/null +++ b/otdfctl/docs/man/policy/actions/delete.md @@ -0,0 +1,28 @@ +--- +title: Delete a Custom Action +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the custom action + required: true + - name: force + description: Force deletion without interactive confirmation +--- + +Removes a Custom Action from platform Policy. Standard Actions ('create', 'read', 'update', +'delete'), cannot be deleted. + +Action deletion cascades to any associated entitlement Subject Mappings, Obligations, +and Registered Resource entitlement requirements. + +Make sure you know what you are doing. + +For more information about Actions, see the manual for the `actions` subcommand. + +## Example + +```shell +otdfctl policy actions delete --id 217b300a-47f9-4bee-be8c-d38c880053f7 +``` diff --git a/otdfctl/docs/man/policy/actions/get.md b/otdfctl/docs/man/policy/actions/get.md new file mode 100644 index 0000000000..4ea0a10798 --- /dev/null +++ b/otdfctl/docs/man/policy/actions/get.md @@ -0,0 +1,37 @@ +--- +title: Get a Standard or Custom Action +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the action + - name: name + shorthand: n + description: Name of the action + - name: namespace + shorthand: s + description: Namespace ID or FQN +--- + +If both `id` and `name` flag values are provided, `id` is preferred. + +When using `--name`, `--namespace` is required. + +For more information about Actions, see the manual for the `actions` subcommand. + +## Example + +Get by ID: + +```shell +otdfctl policy actions get --id e1402c63-eeaa-45e2-85d2-b939d135941f +``` + +Get by Name: + +```shell +otdfctl policy actions get --name read --namespace https://example.com +``` diff --git a/otdfctl/docs/man/policy/actions/list.md b/otdfctl/docs/man/policy/actions/list.md new file mode 100644 index 0000000000..76e5e987ba --- /dev/null +++ b/otdfctl/docs/man/policy/actions/list.md @@ -0,0 +1,25 @@ +--- +title: List Actions +command: + name: list + aliases: + - l + flags: + - name: namespace + shorthand: s + description: Namespace ID or FQN + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +For more information about Actions, see the manual for the `actions` subcommand. + +## Example + +```shell +otdfctl policy actions list --namespace https://example.com +``` diff --git a/otdfctl/docs/man/policy/actions/update.md b/otdfctl/docs/man/policy/actions/update.md new file mode 100644 index 0000000000..5deb085635 --- /dev/null +++ b/otdfctl/docs/man/policy/actions/update.md @@ -0,0 +1,36 @@ +--- +title: Update a Custom Action +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the action to update + required: true + - name: name + shorthand: n + description: Optional updated name of the custom action (must be unique within a namespace) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Update the `name` and/or metadata labels for a Custom Action. + +If PEPs rely on this action name, a name update could break access. + +Make sure you know what you are doing. + +For more information about Actions, see the manual for the `actions` subcommand. + +## Example + +```shell +otdfctl policy actions update --id 34c62145-5d99-45cb-a732-13cb16270e63 --name new_action_name +``` diff --git a/otdfctl/docs/man/policy/attributes/_index.md b/otdfctl/docs/man/policy/attributes/_index.md new file mode 100644 index 0000000000..600ee798f9 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/_index.md @@ -0,0 +1,13 @@ +--- +title: Manage attributes +command: + name: attributes + aliases: + - attr + - attribute +--- + +Commands to manage attributes within the platform. + +Attributes are used to to define the properties of a piece of data. These attributes will then be +used to define the access controls based on subject encodings and entity entitlements. diff --git a/otdfctl/docs/man/policy/attributes/create.md b/otdfctl/docs/man/policy/attributes/create.md new file mode 100644 index 0000000000..3e3a147ac7 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/create.md @@ -0,0 +1,73 @@ +--- +title: Create an attribute definition +command: + name: create + aliases: + - new + - add + - c + flags: + - name: name + shorthand: n + description: Name of the attribute + required: true + - name: rule + shorthand: r + description: Rule of the attribute + enum: + - ANY_OF + - ALL_OF + - HIERARCHY + required: true + - name: value + shorthand: v + description: Value of the attribute (i.e. 'value1') + required: true + - name: namespace + shorthand: s + description: Namespace ID of the attribute + required: true + - name: allow-traversal + description: Allow for platform to use the attribute definition when the value is missing during encryption + default: false + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Under a namespace, create an attribute with a rule. An attribute definition `name` is normalized to lower case +and may contain hyphens and underscores between other alphanumeric characters. + +### Rules + +#### ANY_OF + +If an Attribute is defined with logical rule `ANY_OF`, an Entity who is mapped to `any` of the associated Values of the Attribute +on TDF'd Resource Data will be Entitled to take the actions in the mapping. + +#### ALL_OF + +If an Attribute is defined with logical rule `ALL_OF`, an Entity must be mapped to `all` of the associated Values of the Attribute +on TDF'd Resource Data to be Entitled to take the actions in the mapping. + +### HIERARCHY + +If an Attribute is defined with logical rule `HIERARCHY`, an Entity must be mapped to the same level Value or a level above in hierarchy +compared to a given Value on TDF'd Resource Data. Hierarchical values are considered highest at index 0 and lowest at the last index. Actions +propagate down through the hierarchy, so a mapping of a `read` action on the highest level Value on the Attribute will entitle the action +to each hierarchically lower value, and so on. + +For more general information about attributes, see the `attributes` subcommand. + +### Allow Traversal + +Setting the `allow_traversal` flag on an attribute definition allows a TDF to be created with a missing attribute value. +During encryption while `autoconfigure` is true, if the attribute value is missing and the definition has `allow_traversal` +set our system will encrypt using the attribute definitions key, if a key has been mapped to the definition. + +## Example + +```shell +otdfctl policy attributes create --namespace 3d25d33e-2469-4990-a9ed-fdd13ce74436 --name myattribute --rule ANY_OF +``` diff --git a/otdfctl/docs/man/policy/attributes/deactivate.md b/otdfctl/docs/man/policy/attributes/deactivate.md new file mode 100644 index 0000000000..3a35fb9fee --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/deactivate.md @@ -0,0 +1,27 @@ +--- +title: Deactivate an attribute definition +command: + name: deactivate + flags: + - name: id + shorthand: i + description: ID of the attribute + required: true + - name: force + description: Force deactivation without interactive confirmation (dangerous) +--- + +Deactivation preserves uniqueness of the attribute and values underneath within policy and all existing relations, +essentially reserving them. + +However, a deactivation of an attribute means its associated values cannot be entitled in an access decision. + +For information about reactivation, see the `unsafe reactivate` subcommand. + +For more general information about attributes, see the `attributes` subcommand. + +## Example + +```shell +otdfctl policy attributes deactivate --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` diff --git a/otdfctl/docs/man/policy/attributes/get.md b/otdfctl/docs/man/policy/attributes/get.md new file mode 100644 index 0000000000..ef463c6c52 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/get.md @@ -0,0 +1,21 @@ +--- +title: Get an attribute definition +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the attribute +--- + +Retrieve an attribute along with its metadata, rule, and values. + +For more general information about attributes, see the `attributes` subcommand. + +## Example + +```shell +otdfctl policy attributes get --id=3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` diff --git a/otdfctl/docs/man/policy/attributes/key/_index.md b/otdfctl/docs/man/policy/attributes/key/_index.md new file mode 100644 index 0000000000..5d3f79da21 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/key/_index.md @@ -0,0 +1,7 @@ +--- +title: Key Management changes to attribute definition +command: + name: key +--- + +Manages KAS key associations for attribute definitions. diff --git a/otdfctl/docs/man/policy/attributes/key/assign.md b/otdfctl/docs/man/policy/attributes/key/assign.md new file mode 100644 index 0000000000..036df643ca --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/key/assign.md @@ -0,0 +1,26 @@ +--- +title: Assign a KAS key to an attribute definition +command: + name: assign + flags: + - name: attribute + shorthand: a + description: URI or ID of the attribute definition + required: true + - name: key-id + shorthand: k + description: ID of the KAS key to assign + required: true +--- + +Assigns a KAS key to a policy attribute. This enables the attribute to be used with the specified KAS key for encryption and decryption operations. + +## Example + +```shell +otdfctl policy attributes key assign --attribute 3d25d33e-2469-4990-a9ed-fdd13ce74436 --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` + +```shell +otdfctl policy attributes key assign --attribute "https://example.com/attr/example" --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` diff --git a/otdfctl/docs/man/policy/attributes/key/remove.md b/otdfctl/docs/man/policy/attributes/key/remove.md new file mode 100644 index 0000000000..c79da0d472 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/key/remove.md @@ -0,0 +1,26 @@ +--- +title: Remove a KAS key from an attribute definition +command: + name: remove + flags: + - name: attribute + shorthand: a + description: URI or ID of attribute definition + required: true + - name: key-id + shorthand: k + description: ID of the KAS key to remove + required: true +--- + +Removes a KAS key association from a policy attribute. This will prevent the attribute from being used with the specified KAS key for encryption and decryption operations. + +## Example + +```shell +otdfctl policy attributes key remove --attribute 3d25d33e-2469-4990-a9ed-fdd13ce74436 --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` + +```shell +otdfctl policy attributes key remove --attribute "https://example.com/attr/example" --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` diff --git a/otdfctl/docs/man/policy/attributes/list.md b/otdfctl/docs/man/policy/attributes/list.md new file mode 100644 index 0000000000..c3972d6ca0 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/list.md @@ -0,0 +1,69 @@ +--- +title: List attribute definitions +command: + name: list + aliases: + - l + flags: + - name: state + shorthand: s + description: Filter by state + enum: + - active + - inactive + - any + default: active + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc +--- + +By default, the list will only provide `active` attributes if unspecified, but the filter can be controlled with the `--state` flag. + +For more general information about attributes, see the `attributes` subcommand. + +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | Attribute name | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy attributes list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy attributes list --order asc +``` + +## Example + +```shell +otdfctl policy attributes list +``` + +Sort attributes by name ascending: + +```shell +otdfctl policy attributes list --sort name --order asc +``` diff --git a/otdfctl/docs/man/policy/attributes/unsafe/_index.md b/otdfctl/docs/man/policy/attributes/unsafe/_index.md new file mode 100644 index 0000000000..0616808c8e --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/unsafe/_index.md @@ -0,0 +1,19 @@ +--- +title: Unsafe changes to attribute definitions +command: + name: unsafe + flags: + - name: force + description: Force unsafe change without confirmation + required: false +--- + +Unsafe changes are dangerous mutations to Policy that can significantly change access behavior around existing attributes +and entitlement. + +Depending on the unsafe change introduced and already existing TDFs, TDFs might become inaccessible that were previously +accessible or vice versa. + +Make sure you know what you are doing. + +For more general information about attributes, see the `attributes` subcommand. diff --git a/otdfctl/docs/man/policy/attributes/unsafe/delete.md b/otdfctl/docs/man/policy/attributes/unsafe/delete.md new file mode 100644 index 0000000000..4b6d0864ea --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/unsafe/delete.md @@ -0,0 +1,26 @@ +--- +title: Delete an attribute definition +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the attribute definition + required: true +--- + +# Unsafe Delete Warning + +Deleting an Attribute Definition cascades deletion of any Attribute Values and any associated mappings underneath. + +Any existing TDFs containing the deleted attribute of this name will be rendered inaccessible until it has been recreated. + +Make sure you know what you are doing. + +For more general information about attributes, see the `attributes` subcommand. + +## Example + +```shell +otdfctl policy attributes unsafe delete --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` diff --git a/otdfctl/docs/man/policy/attributes/unsafe/reactivate.md b/otdfctl/docs/man/policy/attributes/unsafe/reactivate.md new file mode 100644 index 0000000000..dad61f816b --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/unsafe/reactivate.md @@ -0,0 +1,26 @@ +--- +title: Reactivate an attribute definition +command: + name: reactivate + flags: + - name: id + shorthand: i + description: ID of the attribute definition + required: true +--- + +# Unsafe Reactivate Warning + +Reactivating an Attribute Definition can potentially open up an access path to any existing TDFs referencing values under that definition. + +The Active/Inactive state of any Attribute Values under this Definition will NOT be changed. + +Make sure you know what you are doing. + +For more general information about attributes, see the `attributes` subcommand. + +## Example + +```shell +otdfctl policy attributes unsafe reactivate --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` diff --git a/otdfctl/docs/man/policy/attributes/unsafe/update.md b/otdfctl/docs/man/policy/attributes/unsafe/update.md new file mode 100644 index 0000000000..3004839fed --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/unsafe/update.md @@ -0,0 +1,58 @@ +--- +title: Update an attribute definition +command: + name: update + flags: + - name: id + shorthand: i + description: ID of the attribute definition + required: true + - name: name + shorthand: n + description: Name of the attribute definition + - name: rule + shorthand: r + description: Rule of the attribute definition + enum: + - ANY_OF + - ALL_OF + - HIERARCHY + - name: values-order + shorthand: o + description: Order of the attribute values (IDs) + - name: allow-traversal + description: Allow for platform to use the attribute definition when the value is missing during encryption +--- + +# Unsafe Update Warning + +## Name Update + +Renaming an Attribute Definition means any Values and any associated mappings underneath will now be tied to the new name. + +Any existing TDFs containing attributes under the old definition name will be rendered inaccessible, and any TDFs tied to the new name +and already created may now become accessible. + +## Rule Update + +Altering a rule of an Attribute Definition changes the evaluation of entitlement to data. Existing TDFs of the same definition name +and values will now be accessible based on the updated rule. An `anyOf` rule becoming `hierarchy` or vice versa, for example, have +entirely different meanings and access evaluations. + +## Values-Order Update + +In the case of a `hierarchy` Attribute Definition Rule, the order of Values on the attribute has significant impact on data access. +Changing this order (complete, destructive replacement of the existing order) will impact access to data. + +To remove Values from an Attribute Definition, delete them separately via the `values unsafe` commands. To add, utilize safe +`values create` commands. + +Make sure you know what you are doing. + +For more general information about attributes, see the `attributes` subcommand. + +## Example + +```shell +otdfctl policy attributes unsafe update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --name mynewname +``` diff --git a/otdfctl/docs/man/policy/attributes/update.md b/otdfctl/docs/man/policy/attributes/update.md new file mode 100644 index 0000000000..f523cb7d18 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/update.md @@ -0,0 +1,31 @@ +--- +title: Update an attribute definition +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the attribute + required: true + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: "" + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Attribute Definition changes can be dangerous, so this command is for updates considered "safe" (currently just mutations to metadata `labels`). + +For unsafe updates, see the dedicated `unsafe update` command. For more general information, see the `attributes` subcommand. + +For more general information about attributes, see the `attributes` subcommand. + +## Example + +```shell +otdfctl policy attributes update --id=3c51a593-cbf8-419d-b7dc-b656d0bedfbb --label hello=world +``` diff --git a/otdfctl/docs/man/policy/attributes/values/_index.md b/otdfctl/docs/man/policy/attributes/values/_index.md new file mode 100644 index 0000000000..32badd5eb9 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/_index.md @@ -0,0 +1,30 @@ +--- +title: Manage attribute values +command: + name: values + aliases: + - val + - value +--- + +Attribute values are the individual units tagged on TDFs containing Resource Data. + +They are mapped to entitle person and non-person entities through Subject Mappings, to varied terms for tagging providers +through Resource Mappings, to individual keys and Key Access Servers through KAS Grants, and more. + +They are fully-qualified through the FQN structure `https:///attr//value/`, and the presence +of one or more values on a piece of Resource Data (a TDF) determines an entity's access to the data through a combination +of entitlements and the attribute definition rule evaluation. + +In other words, Attribute Values are the atomic units that drive access control relation of Data -> Entities and vice versa. + +Values are contextualized by Attribute Definitions within Namespaces, and only have logical meaning as part of a Definition. + +Giving data multiple Attribute Values across the same or multiple Definitions/Namespaces will require all of the definition rules to be satisfied +by an Entity's mapped Entitlements to result in key release, decryption, and resulting access to TDF'd data. + +For more information on: + +- values, see the `attributes values` subcommand +- attribute definitions, see the `attributes` subcommand +- namespaces, see the `attributes namespaces` subcommand diff --git a/otdfctl/docs/man/policy/attributes/values/create.md b/otdfctl/docs/man/policy/attributes/values/create.md new file mode 100644 index 0000000000..99aa890c76 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/create.md @@ -0,0 +1,35 @@ +--- +title: Create an attribute value +command: + name: create + aliases: + - new + - add + - c + flags: + - name: attribute-id + shorthand: a + description: The ID of the attribute to create a value for + - name: value + shorthand: v + description: The value to create + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Add a single new value underneath an existing attribute. + +An attribute `value` is normalized to lower case and may contain hyphens and underscores +between other alphanumeric characters. + +For a hierarchical attribute, a new value is added in lowest hierarchy (last). + +For more information on attribute values, see the `values` subcommand. + +## Example + +```shell +otdfctl policy attributes values create --attribute-id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value myvalue1 +``` diff --git a/otdfctl/docs/man/policy/attributes/values/deactivate.md b/otdfctl/docs/man/policy/attributes/values/deactivate.md new file mode 100644 index 0000000000..b2211c5678 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/deactivate.md @@ -0,0 +1,25 @@ +--- +title: Deactivate an attribute value +command: + name: deactivate + flags: + - name: id + shorthand: i + description: The ID of the attribute value to deactivate + - name: force + description: Force deactivation without interactive confirmation (dangerous) +--- + +Deactivation preserves uniqueness of the attribute value within policy and all existing relations, essentially reserving it. + +However, a deactivation of an attribute value means it cannot be entitled in an access decision. + +For information about reactivation, see the `unsafe reactivate` subcommand. + +For more information on attribute values, see the `values` subcommand. + +## Example + +```shell +otdfctl policy attributes values deactivate --id 355743c1-c0ef-4e8d-9790-d49d883dbc7d +``` diff --git a/otdfctl/docs/man/policy/attributes/values/get.md b/otdfctl/docs/man/policy/attributes/values/get.md new file mode 100644 index 0000000000..51f41c37aa --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/get.md @@ -0,0 +1,21 @@ +--- +title: Get an attribute value +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: The ID of the attribute value to get +--- + +Retrieve an attribute value along with its metadata. + +For more general information about attribute values, see the `values` subcommand. + +## Example + +```shell +otdfctl policy attributes values get --id 355743c1-c0ef-4e8d-9790-d49d883dbc7d +``` diff --git a/otdfctl/docs/man/policy/attributes/values/key/_index.md b/otdfctl/docs/man/policy/attributes/values/key/_index.md new file mode 100644 index 0000000000..0c0eeb2035 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/key/_index.md @@ -0,0 +1,7 @@ +--- +title: Key Management changes to attribute value +command: + name: key +--- + +Manages KAS key associations for attribute values. diff --git a/otdfctl/docs/man/policy/attributes/values/key/assign.md b/otdfctl/docs/man/policy/attributes/values/key/assign.md new file mode 100644 index 0000000000..4932f2eaca --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/key/assign.md @@ -0,0 +1,26 @@ +--- +title: Assign a KAS key to an attribute value +command: + name: assign + flags: + - name: value + shorthand: v + description: URI or ID of attribute value + required: true + - name: key-id + shorthand: k + description: ID of the KAS key to assign + required: true +--- + +Assigns a KAS key to a policy attribute value. This enables the attribute value to be used with the specified KAS key for encryption and decryption operations. + +## Example + +```shell +otdfctl policy attributes values assign --value 3d25d33e-2469-4990-a9ed-fdd13ce74436 --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` + +```shell +otdfctl policy attributes values assign --value "https://demo.com/attr/example/value/1" --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` diff --git a/otdfctl/docs/man/policy/attributes/values/key/remove.md b/otdfctl/docs/man/policy/attributes/values/key/remove.md new file mode 100644 index 0000000000..995a70ab4d --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/key/remove.md @@ -0,0 +1,26 @@ +--- +title: Remove a KAS key from an attribute value +command: + name: remove + flags: + - name: value + shorthand: v + description: URI or ID of attribute value + required: true + - name: key-id + shorthand: k + description: ID of the KAS key to remove + required: true +--- + +Removes a KAS key from a policy attribute value. After removing the key, the attribute value can no longer be used with the specified KAS key for encryption and decryption operations. + +## Example + +```shell +otdfctl policy attributes values remove --value 3d25d33e-2469-4990-a9ed-fdd13ce74436 --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` + +```shell +otdfctl policy attributes values remove --value "https://example.com/attr/example/value/1" --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` diff --git a/otdfctl/docs/man/policy/attributes/values/list.md b/otdfctl/docs/man/policy/attributes/values/list.md new file mode 100644 index 0000000000..79415aa498 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/list.md @@ -0,0 +1,36 @@ +--- +title: List attribute values +command: + name: list + aliases: + - ls + - l + flags: + - name: attribute-id + shorthand: a + description: The ID of the attribute to list values for + - name: state + shorthand: s + description: Filter by state + enum: + - active + - inactive + - any + default: active + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +By default, the list will only provide `active` values if unspecified, but the filter can be controlled with the `--state` flag. + +For more general information about attribute values, see the `values` subcommand. + +## Example + +```shell +otdfctl policy attributes values list --attribute-id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` diff --git a/otdfctl/docs/man/policy/attributes/values/unsafe/_index.md b/otdfctl/docs/man/policy/attributes/values/unsafe/_index.md new file mode 100644 index 0000000000..56857f77da --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/unsafe/_index.md @@ -0,0 +1,19 @@ +--- +title: Unsafe changes to attribute values +command: + name: unsafe + flags: + - name: force + description: Force unsafe change without confirmation + required: false +--- + +Unsafe changes are dangerous mutations to Policy that can significantly change access behavior around existing attributes +and entitlement. + +Depending on the unsafe change introduced and already existing TDFs, TDFs might become inaccessible that were previously +accessible or vice versa. + +Make sure you know what you are doing. + +For more information on attribute values, see the `values` subcommand. diff --git a/otdfctl/docs/man/policy/attributes/values/unsafe/delete.md b/otdfctl/docs/man/policy/attributes/values/unsafe/delete.md new file mode 100644 index 0000000000..cc0a48418a --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/unsafe/delete.md @@ -0,0 +1,26 @@ +--- +title: Delete an attribute value +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the attribute value + required: true +--- + +# Unsafe Delete Warning + +Deleting an Attribute Value cascades deletion of any associated mappings underneath. + +Any existing TDFs containing the deleted attribute of this value will be rendered inaccessible until it has been recreated. + +Make sure you know what you are doing. + +For more information on attribute values, see the `values` subcommand. + +## Example + +```shell +otdfctl policy attributes values unsafe delete --id b20458b0-1855-4608-8869-3f6199bc2878 +``` diff --git a/otdfctl/docs/man/policy/attributes/values/unsafe/reactivate.md b/otdfctl/docs/man/policy/attributes/values/unsafe/reactivate.md new file mode 100644 index 0000000000..288aee877e --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/unsafe/reactivate.md @@ -0,0 +1,26 @@ +--- +title: Reactivate an attribute value +command: + name: reactivate + flags: + - name: id + shorthand: i + description: ID of the attribute value + required: true +--- + +# Unsafe Reactivate Warning + +Reactivating an Attribute Value can potentially open up an access path to any existing TDFs referencing values under that definition. + +The Active/Inactive state of the Attribute Definition and Namespace above this Value will NOT be changed. + +Make sure you know what you are doing. + +For more information on attribute values, see the `values` subcommand. + +## Example + +```shell +otdfctl policy attributes values unsafe reactivate --id 355743c1-c0ef-4e8d-9790-d49d883dbc7d +``` diff --git a/otdfctl/docs/man/policy/attributes/values/unsafe/update.md b/otdfctl/docs/man/policy/attributes/values/unsafe/update.md new file mode 100644 index 0000000000..c7eba02b65 --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/unsafe/update.md @@ -0,0 +1,32 @@ +--- +title: Update an attribute value +command: + name: update + flags: + - name: id + shorthand: i + description: ID of the attribute value + required: true + - name: value + shorthand: v + description: The new value replacing the current value +--- + +# Unsafe Update Warning + +## Value Update + +Changing an Attribute Value means any associated mappings underneath will now be tied to the new value. + +Any existing TDFs containing attributes under the old value will be rendered inaccessible, and any TDFs tied to the new value +and already created may now become accessible. + +Make sure you know what you are doing. + +For more information on attribute values, see the `values` subcommand. + +## Example + +```shell +otdfctl policy attributes values unsafe update --id 355743c1-c0ef-4e8d-9790-d49d883dbc7d --name mynewvalue1 +``` diff --git a/otdfctl/docs/man/policy/attributes/values/update.md b/otdfctl/docs/man/policy/attributes/values/update.md new file mode 100644 index 0000000000..cc31ef71aa --- /dev/null +++ b/otdfctl/docs/man/policy/attributes/values/update.md @@ -0,0 +1,31 @@ +--- +title: Update attribute value + +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: The ID of the attribute value to update + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Attribute Value changes can be dangerous, so this command is for updates considered "safe" (currently just mutations to metadata `labels`). + +For unsafe updates, see the dedicated `unsafe update` command. For more general information, see the `values` subcommand. + +For more general information about attributes, see the `attributes` subcommand. + +## Example + +```shell +otdfctl policy attributes values update --id 355743c1-c0ef-4e8d-9790-d49d883dbc7d --label hello=world +``` diff --git a/otdfctl/docs/man/policy/kas-grants/_index.md b/otdfctl/docs/man/policy/kas-grants/_index.md new file mode 100644 index 0000000000..ba208ed926 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-grants/_index.md @@ -0,0 +1,121 @@ +--- +title: (Deprecated) Manage Key Access Server grants + +command: + name: kas-grants + aliases: + - kasg + - kas-grant +--- +# Deprecated + +Once Key Access Servers (KASs) have been registered within a platform's policy, +they can be assigned grants to various attribute objects (namespaces, definitions, values). + +> See `kas-registry` command within `policy` to manage the KASs known to the platform. + +Key Access Grants are associations between a registered KAS (see KAS Registry docs) and an Attribute. + +An attribute can be assigned a KAS Grant on its namespace, its definition, or any one of its values. + +Grants enable key split behaviors on TDFs with attributes, which can be useful for various collaboration scenarios around shared policy. + +> [!WARNING] +> KAS Grants are considered experimental, as grants to namespaces are not fully utilized within encrypt/decrypt flows at present. + +## Utilization + +The steps below are driven by the SDK on encrypt, and they are the same steps followed +on decrypt by a KAS making a decision request on a key release (once the decision +is found to be permissible): + +1. look up the attributes on the TDF within the platform +2. find any associated grants for those attributes' values, definitions, namespaces +3. retrieve the public key of each KAS granted to those attribute objects +4. determine based on the specificity matrix below which keys to utilize in splits + +## Specificity + +When KAS grants are considered, they follow a most-to-least specificity matrix. Grants to +Attribute Values supersede any grants to Definitions which also supersede any grants to a Namespace. + +Grants to Attribute Objects: + +| Namespace Grant | Attr Definition Grant | Attr Value Grant | Data Encryption Key Utilized | +| --------------- | --------------------- | ---------------- | ---------------------------- | +| yes | no | no | namespace | +| yes | yes | no | attr definition | +| no | yes | no | attr definition | +| yes | yes | yes | value | +| no | yes | yes | value | +| no | no | yes | value | +| no | no | no | default KAS/platform key | + +> [!NOTE] +> A namespace grant may soon be required with deprecation of a default KAS/platform key. + +## Split Scenarios + +### AnyOf Split + +`Bob` and `Alice` want to share data equally, but maintain their ability to decrypt the data without sharing each other’s private keys. + +With KAS Grants, they can define a key split where the shared data is wrapped with both of their public keys and AnyOf logic, meaning that each partner could decrypt the data with just one of those keys. + +If `Bob` assigns a grant between Bob's running/registered KAS to a known attribute value, and `Alice` defines a grant of Alice's running/registered KAS to the same attribute value, +any data encrypted in a TDF will be decryptable with a key released by _either_ of their Key Access Servers. + +Attribute A: `https://conglomerate.com/attr/organization/value/acmeco` + +Attribute B: `https://conglomerate.com/attr/organization/value/example_inc` + +| Attribute | Namespace | Definition | Value | +| --------- | ---------------- | ------------ | ----------- | +| A | conglomerate.com | organization | acmeco | +| B | conglomerate.com | organization | example_inc | + +**Attribute KAS Grant Scenarios** + +1. Bob & Alice represent individual KAS Grants to attributes on TDF'd data +2. Note that the attributes A and B are of _the same definition and namespace_ + +| Definition: organization | Value: acmeco | Value: example_inc | Split | +| ------------------------ | ------------- | ------------------ | ----- | +| Bob, Alice | - | - | OR | +| - | Bob, Alice | - | OR | +| - | - | Bob, Alice | OR | +| - | Bob | Alice | OR | + +### AllOf Split + +Unlike the `AnyOf` split above, this time `Bob` and `Alice` want to make sure _both_ of their keys must be granted for data in a TDF +to be decrypted. With KAS Grants, they can define a key split where the shared data is wrapped with both of their public keys and +AllOf logic, meaning that neither partner can decrypt the data with just one of those keys. + +To accomplish this, they each define KAS Grants between their KASes and policy attributes, and TDF data with at least two attributes - +one assigned a KAS Grant to Bob's KAS and another assigned a KAS Grant to Alice's KAS. + +Both KASes will need to permit access and release payload keys for the data TDF'd with multiple attributes assigned KAS Grants to be accessible and decrypted. + +Attribute A: `https://conglomerate.com/attr/organization/value/acmeco` + +Attribute B: `https://conglomerate.com/attr/department/value/marketing` + +| Attribute | Namespace | Definition | Value | +| --------- | ---------------- | ------------ | --------- | +| A | conglomerate.com | organization | acmeco | +| A | conglomerate.com | department | marketing | + +**Attribute KAS Grant Scenarios** + +1. Bob & Alice represent individual KAS Grants to attributes on TDF'd data +2. Note that the attributes A and B are of _the same namespace but different definitions_ + +| Definition: A | Value: A | Definition: B | Value: B | Split | +| ------------- | -------- | ------------- | -------- | ----- | +| Bob | - | Alice | - | AND | +| Bob | - | - | Alice | AND | +| - | Bob | - | Alice | AND | + +> [!NOTE] +> Any KAS Grants to attributes across different definitions or namespaces will be `AND` splits. diff --git a/otdfctl/docs/man/policy/kas-grants/assign.md b/otdfctl/docs/man/policy/kas-grants/assign.md new file mode 100644 index 0000000000..dcff8387b1 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-grants/assign.md @@ -0,0 +1,60 @@ +--- +title: (Deprecated) Assign a grant + +command: + name: assign + aliases: + - u + - update + - create + - add + - new + - upsert + description: Assign a grant of a KAS to an Attribute Definition or Value + flags: + - name: namespace-id + shorthand: n + description: The ID of the Namespace being assigned a KAS Grant + - name: attribute-id + shorthand: a + description: The ID of the Attribute Definition being assigned a KAS Grant + required: true + - name: value-id + shorthand: v + description: The ID of the Value being assigned a KAS Grant + required: true + - name: kas-id + shorthand: k + description: The ID of the Key Access Server being assigned to the grant + required: true + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +# Deprecated\n\nThis command is deprecated. Use `policy attributes namespace key assign`, `policy attributes key assign`, or `policy attributes value key assign` instead. + +Assign a registered Key Access Server (KAS) to an attribute namespace, definition, or value. + +For more information, see `kas-registry` and `kas-grants` manuals. + +## Example + +Namespace grant: +```shell +otdfctl policy kas-grants assign --namespace-id 3d25d33e-2469-4990-a9ed-fdd13ce74436 --kas-id 62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +Attribute grant: +```shell +otdfctl policy kas-grants assign --attribute-id a21eb299-3a7d-4035-8a39-c8662c03cb15 --kas-id 62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +Attribute value grant: +```shell +otdfctl policy kas-grants assign --value-id 0a40b27c-6cc9-49e8-a6ae-663cac2c324b --kas-id 62857b55-560c-4b67-96e3-33e4670ecb3b +``` diff --git a/otdfctl/docs/man/policy/kas-grants/list.md b/otdfctl/docs/man/policy/kas-grants/list.md new file mode 100644 index 0000000000..1d8e669b52 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-grants/list.md @@ -0,0 +1,34 @@ +--- +title: (Deprecated) List KAS Grants + +command: + name: list + aliases: + - l + description: List the Grants of KASes to Attribute Namespaces, Definitions, and Values + flags: + - name: kas + shorthand: k + description: The optional ID or URI of a KAS to filter the list + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- +# Deprecated\n\nThis command is deprecated and will be removed in a future release. + +List the Grants of Registered Key Access Servers (KASes) to attribute namespaces, definitions, +or values. + +Omitting `kas` lists all grants known to platform policy, otherwise results are filtered to +the KAS URI or ID specified by the flag value. + +For more information, see `kas-registry` and `kas-grants` manuals. + +## Example + +```shell +otdfctl policy kas-grants list +``` diff --git a/otdfctl/docs/man/policy/kas-grants/unassign.md b/otdfctl/docs/man/policy/kas-grants/unassign.md new file mode 100644 index 0000000000..fce822f585 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-grants/unassign.md @@ -0,0 +1,51 @@ +--- +title: (Deprecated) Unassign a grant + +command: + name: unassign + aliases: + - delete + - remove + description: Remove a grant assignment of a KAS to an Attribute Definition or Value + flags: + - name: namespace-id + shorthand: n + description: The ID of the Namespace being unassigned a KAS Grant + - name: attribute-id + shorthand: a + description: The ID of the Attribute Definition being unassigned the KAS grant + required: true + - name: value-id + shorthand: v + description: The ID of the Value being unassigned the KAS Grant + required: true + - name: kas-id + shorthand: k + description: The Key Access Server (KAS) ID being unassigned a grant + required: true + - name: force + description: Force the unassignment with no confirmation +--- + +# Deprecated\n\nThis command is deprecated and will be removed in a future release. Use `policy attributes namespace key remove`, `policy attributes key remove`, or `policy attributes value key remove` instead. + +Unassign a registered Key Access Server (KAS) to an attribute namespace, definition, or value. + +For more information, see `kas-registry` and `kas-grants` manuals. + +## Example + +Namespace grant: +```shell +otdfctl policy kas-grants unassign --namespace-id 3d25d33e-2469-4990-a9ed-fdd13ce74436 --kas-id 62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +Attribute grant: +```shell +otdfctl policy kas-grants unassign --attribute-id a21eb299-3a7d-4035-8a39-c8662c03cb15 --kas-id 62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +Attribute value grant: +```shell +otdfctl policy kas-grants unassign --value-id 0a40b27c-6cc9-49e8-a6ae-663cac2c324b --kas-id 62857b55-560c-4b67-96e3-33e4670ecb3b +``` diff --git a/otdfctl/docs/man/policy/kas-registry/_index.md b/otdfctl/docs/man/policy/kas-registry/_index.md new file mode 100644 index 0000000000..8cbe14638e --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/_index.md @@ -0,0 +1,18 @@ +--- +title: Manage KAS registrations +command: + name: kas-registry + aliases: + - kasr + - kas-registries +--- + +The Key Access Server (KAS) registry is a record of KASes safeguarding access and maintaining public keys. + +The registry contains critical information like each server's uri, its public key (which can be +either cached or at a remote uri), and any metadata about the server. + +Registered Key Access Servers may grant keys for specified Namespaces, Attributes, and their Values via KAS Grants. + +For more information about grants and how KASs are utilized once registered, see the manual for the +`kas-grants` command. diff --git a/otdfctl/docs/man/policy/kas-registry/create.md b/otdfctl/docs/man/policy/kas-registry/create.md new file mode 100644 index 0000000000..4a40452d21 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/create.md @@ -0,0 +1,34 @@ +--- +title: Create a Key Access Server registration +command: + name: create + aliases: + - c + - add + - new + flags: + - name: uri + shorthand: u + description: URI of the Key Access Server + required: true + - name: public-keys + shorthand: c + description: "(Deprecated: Use otdfctl policy kas-registry keys) One or more public keys saved for the KAS" + - name: public-key-remote + shorthand: r + description: "(Deprecated: Use otdfctl policy kas-registry keys) Remote URI where the public key can be retrieved for the KAS" + - name: label + - name: name + shorthand: n + description: Optional name of the registered KAS (must be unique within Policy) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +## Examples + +```shell +otdfctl policy kas-registry create --uri http://example.com/kas --name example-kas +``` diff --git a/otdfctl/docs/man/policy/kas-registry/delete.md b/otdfctl/docs/man/policy/kas-registry/delete.md new file mode 100644 index 0000000000..d30439c270 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/delete.md @@ -0,0 +1,28 @@ +--- +title: Delete a Key Access Server registration +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the Key Access Server registration + required: true + - name: force + description: Force deletion without interactive confirmation (dangerous) +--- + +Removes knowledge of a KAS (registration) from a platform's policy. + +If resource data has been TDFd utilizing key splits from the registered KAS, deletion from +the registry (and therefore any associated grants) may prevent decryption depending on the +type of grants and relevant key splits. + +Make sure you know what you are doing. + +For more information about registration of Key Access Servers, see the manual for `kas-registry`. + +## Example + +```shell +otdfctl policy kas-registry delete --id 3c39618a-cd8c-48cf-a60c-e8a2f4be4dd5 +``` diff --git a/otdfctl/docs/man/policy/kas-registry/get.md b/otdfctl/docs/man/policy/kas-registry/get.md new file mode 100644 index 0000000000..19968e843d --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/get.md @@ -0,0 +1,20 @@ +--- +title: Get a registered Key Access Server +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the Key Access Server registration + required: true +--- + +For more information about registration of Key Access Servers, see the manual for `kas-registry`. + +## Example + +```shell +otdfctl policy kas-registry get --id=62857b55-560c-4b67-96e3-33e4670ecb3b +``` diff --git a/otdfctl/docs/man/policy/kas-registry/key/_index.md b/otdfctl/docs/man/policy/kas-registry/key/_index.md new file mode 100644 index 0000000000..9ec4a2011b --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/_index.md @@ -0,0 +1,13 @@ +--- +title: Key management for KAS Registry + +command: + name: key + aliases: + - k + - keys +--- + +Provides a set of subcommands for managing cryptographic keys within the Key Access Server (KAS) registry. +These keys are essential for encryption and decryption operations within the OpenTDF platform. +Operations include creating, retrieving, listing, updating, and managing the platform's base key. diff --git a/otdfctl/docs/man/policy/kas-registry/key/base/_index.md b/otdfctl/docs/man/policy/kas-registry/key/base/_index.md new file mode 100644 index 0000000000..b7971eaec5 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/base/_index.md @@ -0,0 +1,14 @@ +--- +title: Platform Base Key Management + +command: + name: base +--- + +Provides subcommands for managing the platform's base cryptographic key. +This base key is a fallback used for encryption operations in specific scenarios: + +- No attributes present when encrypting a file +- No keys associated with an attribute + +Available operations include `get` to retrieve the current base key and `set` to designate a new base key. diff --git a/otdfctl/docs/man/policy/kas-registry/key/base/get.md b/otdfctl/docs/man/policy/kas-registry/key/base/get.md new file mode 100644 index 0000000000..545584010f --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/base/get.md @@ -0,0 +1,18 @@ +--- +title: Get Base Key +command: + name: get + aliases: + - g +--- + +Command for retrieving information about the currently configured platform base key. This key is used for encryption operations when no attributes are present or when attributes lack associated keys. + +The command will display details such as the key's identifier (KeyID or UUID) and the Key Access Server (KAS) it is registered with. + +## Examples + +Retrieve the platform base key information in the default (human-readable) format: +``` +otdfctl policy kas-registry key base get +``` diff --git a/otdfctl/docs/man/policy/kas-registry/key/base/set.md b/otdfctl/docs/man/policy/kas-registry/key/base/set.md new file mode 100644 index 0000000000..d672b245ad --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/base/set.md @@ -0,0 +1,25 @@ +--- +title: Set Base Key +command: + name: set + aliases: + - s + flags: + - name: key + shorthand: k + description: The KeyID (human-readable identifier) or the internal UUID of an existing key within the specified KAS. This key will be designated as the platform base key. The system will attempt to resolve the provided value as either a UUID or a KeyID. + required: true + - name: kas + description: Specify the Key Access Server (KAS) where the key (identified by `--key`) is registered. The KAS can be identified by its ID, URI, or Name. +--- + +Command for setting a base key to be used for encryption operations on data where no attributes are present or where no keys are present on found attributes. The key to be set as the base key must be identified using its KeyID or UUID via the `--key` flag, and the KAS it belongs to must be specified with the `--kas` flag. + +## Examples + +Set the platform base key using the internal UUID of a key from a KAS specified by its URI: +``` +otdfctl policy kas-registry key base set --key 8af2059f-5d0b-46c2-84f0-bed8a6101d90 --kas https://kas.example.com/kas + +otdfctl policy kas-registry key base set --key my-platform-base-key-v1 --kas primary-key-access-server +``` diff --git a/otdfctl/docs/man/policy/kas-registry/key/create.md b/otdfctl/docs/man/policy/kas-registry/key/create.md new file mode 100644 index 0000000000..76f5a85af9 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/create.md @@ -0,0 +1,89 @@ +--- +title: Create Key +command: + name: create + aliases: + - c + flags: + - name: key-id + description: A unique, often human-readable, identifier for the new key to be created. + required: true + - name: algorithm + shorthand: a + description: Algorithm for the new key (see table below for options). + required: true + - name: mode + shorthand: m + description: Describes how the private key is managed (see table below for options). + required: true + - name: kas + description: Specify the Key Access Server (KAS) where the new key will be created. The KAS can be identified by its ID, URI, or Name. + required: true + - name: wrapping-key-id + description: Identifier related to the wrapping key. Its meaning depends on the `mode`. For `local` mode, it's a descriptive ID for the `wrappingKey` you provide. For `provider` or `remote` mode, it's the ID of the key within the external provider/system used for wrapping. + - name: wrapping-key + shorthand: w + sensitive: true + description: The symmetric key material (AES cipher, hex encoded) used to wrap the generated private key. Primarily used when `mode` is `local`. + - name: private-key-pem + sensitive: true + description: The private key PEM (encrypted by an AES 32-byte key, then base64 encoded). Used when importing an existing key pair, typically with `provider` mode. + - name: provider-config-id + shorthand: p + description: Configuration ID for the key provider. Often required when `mode` is `provider` or `remote` and an external key provider is used. + - name: public-key-pem + shorthand: e + description: The base64 encoded public key PEM. Required for `remote` and `public_key` modes, and can be used with `provider` mode if importing an existing key pair. + - name: label + shorthand: l + description: Comma-separated key=value pairs for metadata labels to associate with the new key (e.g., "owner=team-a,env=production"). +--- + +Creates a new cryptographic key within a specified Key Access Server (KAS). +This key is primarily used for encrypting and decrypting data keys in the TDF (Trusted Data Format) ecosystem, forming a crucial part of data protection policies. + +## Examples + +### Create a key in `local` mode + +The KAS generates the key pair, and the private key is wrapped by the provided `wrappingKey`. The KAS is identified by its ID. + +```shell +otdfctl policy kas-registry key create --key-id "aws-key" --algorithm "rsa:2048" --mode "local" --kas 891cfe85-b381-4f85-9699-5f7dbfe2a9ab --wrapping-key-id "virtru-stored-key" --wrapping-key "a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1" + +otdfctl policy kas-registry key create --key-id "aws-key" --algorithm "rsa:2048" --mode "local" --kas "https://test-kas.com" --wrapping-key-id "virtru-stored-key" --wrapping-key "a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1" +``` + +```shell +otdfctl policy kas-registry key create --key-id "aws-key" --algorithm "rsa:2048" --mode "provider" --kas "https://test-kas.com" --public-key-pem "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlDL1RDQ0FlV2dBd0lCQWdJVVNIVEoyYnpBaDdkUW1tRjAzcTZJcS9uMGw5MHdEUVlKS29aSWh2Y05BUUVMXG5CUUF3RGpFTU1Bb0dBMVVFQXd3RGEyRnpNQjRYRFRJME1EWXdOakUzTkRZMU5Gb1hEVEkxTURZd05qRTNORFkxXG5ORm93RGpFTU1Bb0dBMVVFQXd3RGEyRnpNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDXG5BUUVBeE4zQVBpaFRpb2pjYUg2b1dqMXRNdFpNYWFaK0lBMXF0cUZtcHk1Rmc4RDViRXNQNzM2R3h6VU1Gc01WXG5zaHJLRVh6OGRZOUtwMjN1SXd5ZUMwUlBXTGU1eElmVGtKVWJ5THBxR2RsRWdxajEwUlE4a1NWcTI3MFhQRVMyXG5HWlVpajJEdUpWZndwVHBMemN0aTJQc2dFT29PS0M2Tm5uQUkwTlMxbWFvLzJEeFF4cy9EOWhBSmpHZHB6eW1iXG54aTJUeEdudllidm9mQ1BkOFJkRlRDUHZnd0tMUzcrTXFCY21pYzlWZFg5MVFOT1BtclAzcklvS3RqamQrNVBZXG5sL3o3M1BBeFIzSzNTSXpJWkx2SXRxMmFob2JPT01pU3h3OHNvT2xPZEhOVUpUcEVDY2R1aFJicXVxbUs2ZlR3XG5WT2ZyY1JRaGhVNFRrRHU5MkxJN1NnbE9XUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVZGd4eDdVNUFRZ2ZpXG5pUVd1M2toaTl5bmVFVm93SHdZRFZSMGpCQmd3Rm9BVWRneHg3VTVBUWdmaWlRV3Uza2hpOXluZUVWb3dEd1lEXG5WUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQVRjTFliSG9tSmdMUS9INmlEdmNBXG5JcElTRi9SY3hnaDdObklxUmtCK1RtNHhObE5ISXhsNFN6K0trRVpFUGgwV0tJdEdWRGozMjkzckFyUk9FT1hJXG50Vm1uMk9CdjlNLzVEUWtIajc2UnU0UFEyVGNMMENBQ2wxSktmcVhMc01jNkhIVHA4WlRQOGxNZHBXNGt6RWMzXG5mVnRndnRwSmM0V0hkVUlFekF0VGx6WVJxSWJ5eUJNV2VUalh3YTU0YU12M1JaUWRKK0MwZWh3V1REUURwaDduXG5LWTMrN0cwZW5ORVZ0eVc0ZHR4dlFRYmlkTWFueTBKRXByNlFwUG14QzhlMFoyM2RNRGRrUjFJb1Q5OVBoZFcvXG5RQzh4TWp1TENpUkVWN2E2ZTJNeENHajNmeHJuTVh3T0lxTzNBek5zd2UyYW1jb3oya3R1b3FnRFRZbG8rRmtLXG41dz09XG4tLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tXG4=" --private-key-pem "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlDL1RDQ0FlV2dBd0lCQWdJVVNIVEoyYnpBaDdkUW1tRjAzcTZJcS9uMGw5MHdEUVlKS29aSWh2Y05BUUVMXG5CUUF3RGpFTU1Bb0dBMVVFQXd3RGEyRnpNQjRYRFRJME1EWXdOakUzTkRZMU5Gb1hEVEkxTURZd05qRTNORFkxXG5ORm93RGpFTU1Bb0dBMVVFQXd3RGEyRnpNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDXG5BUUVBeE4zQVBpaFRpb2pjYUg2b1dqMXRNdFpNYWFaK0lBMXF0cUZtcHk1Rmc4RDViRXNQNzM2R3h6VU1Gc01WXG5zaHJLRVh6OGRZOUtwMjN1SXd5ZUMwUlBXTGU1eElmVGtKVWJ5THBxR2RsRWdxajEwUlE4a1NWcTI3MFhQRVMyXG5HWlVpajJEdUpWZndwVHBMemN0aTJQc2dFT29PS0M2Tm5uQUkwTlMxbWFvLzJEeFF4cy9EOWhBSmpHZHB6eW1iXG54aTJUeEdudllidm9mQ1BkOFJkRlRDUHZnd0tMUzcrTXFCY21pYzlWZFg5MVFOT1BtclAzcklvS3RqamQrNVBZXG5sL3o3M1BBeFIzSzNTSXpJWkx2SXRxMmFob2JPT01pU3h3OHNvT2xPZEhOVUpUcEVDY2R1aFJicXVxbUs2ZlR3XG5WT2ZyY1JRaGhVNFRrRHU5MkxJN1NnbE9XUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVZGd4eDdVNUFRZ2ZpXG5pUVd1M2toaTl5bmVFVm93SHdZRFZSMGpCQmd3Rm9BVWRneHg3VTVBUWdmaWlRV3Uza2hpOXluZUVWb3dEd1lEXG5WUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQVRjTFliSG9tSmdMUS9INmlEdmNBXG5JcElTRi9SY3hnaDdObklxUmtCK1RtNHhObE5ISXhsNFN6K0trRVpFUGgwV0tJdEdWRGozMjkzckFyUk9FT1hJXG50Vm1uMk9CdjlNLzVEUWtIajc2UnU0UFEyVGNMMENBQ2wxSktmcVhMc01jNkhIVHA4WlRQOGxNZHBXNGt6RWMzXG5mVnRndnRwSmM0V0hkVUlFekF0VGx6WVJxSWJ5eUJNV2VUalh3YTU0YU12M1JaUWRKK0MwZWh3V1REUURwaDduXG5LWTMrN0cwZW5ORVZ0eVc0ZHR4dlFRYmlkTWFueTBKRXByNlFwUG14QzhlMFoyM2RNRGRrUjFJb1Q5OVBoZFcvXG5RQzh4TWp1TENpUkVWN2E2ZTJNeENHajNmeHJuTVh3T0lxTzNBek5zd2UyYW1jb3oya3R1b3FnRFRZbG8rRmtLXG41dz09XG4tLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tXG4=" --wrapping-key-id "openbao-key" --provider-config-id "f86b166a-98a5-407a-939f-ef84916ce1e5" +``` + +```shell +otdfctl policy kas-registry key create --key-id "aws-key" --algorithm "rsa:2048" --mode "remote" --kas "https://test-kas.com" --wrapping-key-id "openbao-key" --provider-config-id "f86b166a-98a5-407a-939f-ef84916ce1e5" --public-key-pem "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlDL1RDQ0FlV2dBd0lCQWdJVVNIVEoyYnpBaDdkUW1tRjAzcTZJcS9uMGw5MHdEUVlKS29aSWh2Y05BUUVMXG5CUUF3RGpFTU1Bb0dBMVVFQXd3RGEyRnpNQjRYRFRJME1EWXdOakUzTkRZMU5Gb1hEVEkxTURZd05qRTNORFkxXG5ORm93RGpFTU1Bb0dBMVVFQXd3RGEyRnpNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDXG5BUUVBeE4zQVBpaFRpb2pjYUg2b1dqMXRNdFpNYWFaK0lBMXF0cUZtcHk1Rmc4RDViRXNQNzM2R3h6VU1Gc01WXG5zaHJLRVh6OGRZOUtwMjN1SXd5ZUMwUlBXTGU1eElmVGtKVWJ5THBxR2RsRWdxajEwUlE4a1NWcTI3MFhQRVMyXG5HWlVpajJEdUpWZndwVHBMemN0aTJQc2dFT29PS0M2Tm5uQUkwTlMxbWFvLzJEeFF4cy9EOWhBSmpHZHB6eW1iXG54aTJUeEdudllidm9mQ1BkOFJkRlRDUHZnd0tMUzcrTXFCY21pYzlWZFg5MVFOT1BtclAzcklvS3RqamQrNVBZXG5sL3o3M1BBeFIzSzNTSXpJWkx2SXRxMmFob2JPT01pU3h3OHNvT2xPZEhOVUpUcEVDY2R1aFJicXVxbUs2ZlR3XG5WT2ZyY1JRaGhVNFRrRHU5MkxJN1NnbE9XUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVZGd4eDdVNUFRZ2ZpXG5pUVd1M2toaTl5bmVFVm93SHdZRFZSMGpCQmd3Rm9BVWRneHg3VTVBUWdmaWlRV3Uza2hpOXluZUVWb3dEd1lEXG5WUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQVRjTFliSG9tSmdMUS9INmlEdmNBXG5JcElTRi9SY3hnaDdObklxUmtCK1RtNHhObE5ISXhsNFN6K0trRVpFUGgwV0tJdEdWRGozMjkzckFyUk9FT1hJXG50Vm1uMk9CdjlNLzVEUWtIajc2UnU0UFEyVGNMMENBQ2wxSktmcVhMc01jNkhIVHA4WlRQOGxNZHBXNGt6RWMzXG5mVnRndnRwSmM0V0hkVUlFekF0VGx6WVJxSWJ5eUJNV2VUalh3YTU0YU12M1JaUWRKK0MwZWh3V1REUURwaDduXG5LWTMrN0cwZW5ORVZ0eVc0ZHR4dlFRYmlkTWFueTBKRXByNlFwUG14QzhlMFoyM2RNRGRrUjFJb1Q5OVBoZFcvXG5RQzh4TWp1TENpUkVWN2E2ZTJNeENHajNmeHJuTVh3T0lxTzNBek5zd2UyYW1jb3oya3R1b3FnRFRZbG8rRmtLXG41dz09XG4tLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tXG4=" +``` + +```shell +otdfctl policy kas-registry key create --key-id "aws-key" --algorithm "rsa:2048" --mode "public_key" --kas "https://test-kas.com" --public-key-pem "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlDL1RDQ0FlV2dBd0lCQWdJVVNIVEoyYnpBaDdkUW1tRjAzcTZJcS9uMGw5MHdEUVlKS29aSWh2Y05BUUVMXG5CUUF3RGpFTU1Bb0dBMVVFQXd3RGEyRnpNQjRYRFRJME1EWXdOakUzTkRZMU5Gb1hEVEkxTURZd05qRTNORFkxXG5ORm93RGpFTU1Bb0dBMVVFQXd3RGEyRnpNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDXG5BUUVBeE4zQVBpaFRpb2pjYUg2b1dqMXRNdFpNYWFaK0lBMXF0cUZtcHk1Rmc4RDViRXNQNzM2R3h6VU1Gc01WXG5zaHJLRVh6OGRZOUtwMjN1SXd5ZUMwUlBXTGU1eElmVGtKVWJ5THBxR2RsRWdxajEwUlE4a1NWcTI3MFhQRVMyXG5HWlVpajJEdUpWZndwVHBMemN0aTJQc2dFT29PS0M2Tm5uQUkwTlMxbWFvLzJEeFF4cy9EOWhBSmpHZHB6eW1iXG54aTJUeEdudllidm9mQ1BkOFJkRlRDUHZnd0tMUzcrTXFCY21pYzlWZFg5MVFOT1BtclAzcklvS3RqamQrNVBZXG5sL3o3M1BBeFIzSzNTSXpJWkx2SXRxMmFob2JPT01pU3h3OHNvT2xPZEhOVUpUcEVDY2R1aFJicXVxbUs2ZlR3XG5WT2ZyY1JRaGhVNFRrRHU5MkxJN1NnbE9XUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVZGd4eDdVNUFRZ2ZpXG5pUVd1M2toaTl5bmVFVm93SHdZRFZSMGpCQmd3Rm9BVWRneHg3VTVBUWdmaWlRV3Uza2hpOXluZUVWb3dEd1lEXG5WUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQVRjTFliSG9tSmdMUS9INmlEdmNBXG5JcElTRi9SY3hnaDdObklxUmtCK1RtNHhObE5ISXhsNFN6K0trRVpFUGgwV0tJdEdWRGozMjkzckFyUk9FT1hJXG50Vm1uMk9CdjlNLzVEUWtIajc2UnU0UFEyVGNMMENBQ2wxSktmcVhMc01jNkhIVHA4WlRQOGxNZHBXNGt6RWMzXG5mVnRndnRwSmM0V0hkVUlFekF0VGx6WVJxSWJ5eUJNV2VUalh3YTU0YU12M1JaUWRKK0MwZWh3V1REUURwaDduXG5LWTMrN0cwZW5ORVZ0eVc0ZHR4dlFRYmlkTWFueTBKRXByNlFwUG14QzhlMFoyM2RNRGRrUjFJb1Q5OVBoZFcvXG5RQzh4TWp1TENpUkVWN2E2ZTJNeENHajNmeHJuTVh3T0lxTzNBek5zd2UyYW1jb3oya3R1b3FnRFRZbG8rRmtLXG41dz09XG4tLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tXG4=" +``` + +1. The `"algorithm"` specifies the key algorithm: + + | Key Algorithm | + | -------------- | + | `rsa:2048` | + | `rsa:4096` | + | `ec:secp256r1` | + | `ec:secp384r1` | + | `ec:secp521r1` | + | `hpqt:xwing` | + | `hpqt:secp256r1-mlkem768` | + | `hpqt:secp384r1-mlkem1024` | + +2. The `"mode"` specifies where the key that is encrypting TDFs is stored. All keys will be encrypted when stored in Virtru's DB, for modes `"local"` and `"provider"` + + | Mode | Description | + | ------------ | ------------------------------------------------------------------------------------------------------- | + | `local` | Root Key is stored within Virtru's database and the symmetric wrapping key is stored in KAS | + | `provider` | Root Key is stored within Virtru's database and the symmetric wrapping key is stored externally | + | `remote` | Root Key and wrapping key are stored remotely | + | `public_key` | Root Key and wrapping key are stored remotely. Use this when importing another org's policy information | diff --git a/otdfctl/docs/man/policy/kas-registry/key/get.md b/otdfctl/docs/man/policy/kas-registry/key/get.md new file mode 100644 index 0000000000..fc25d06b2b --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/get.md @@ -0,0 +1,30 @@ +--- +title: Get Key +command: + name: get + aliases: + - g + flags: + - name: key + shorthand: k + description: The KeyID (human-readable identifier) or the internal UUID of the key to retrieve from the specified KAS. The system will attempt to resolve the provided value as either a UUID or a KeyID. + required: true + - name: kas + description: Specify the Key Access Server (KAS) where the key (identified by `--key`) is registered. The KAS can be identified by its ID, URI, or Name. + required: true + +--- + +This command retrieves detailed information about a specific key registered within a Key Access Server (KAS). You must specify the key using its KeyID or UUID and the KAS it belongs to. + +## Examples + +Retrieve details for a key identified by its UUID from a KAS specified by its URI: +``` +otdfctl policy kas-registry key get --key "123e4567-e89b-12d3-a456-426614174000" --kas "https://kas.example.com/kas" +``` + +Retrieve details for a key identified by its human-readable KeyID from a KAS specified by its name, and output in JSON format: +``` +otdfctl policy kas-registry key get --key "my-specific-key-v2" --kas "Secondary KAS" --json +``` diff --git a/otdfctl/docs/man/policy/kas-registry/key/import.md b/otdfctl/docs/man/policy/kas-registry/key/import.md new file mode 100644 index 0000000000..b08a2ea843 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/import.md @@ -0,0 +1,84 @@ +--- +title: Import Key +command: + name: import + aliases: + - i + flags: + - name: key-id + description: A unique, often human-readable, identifier for the key being imported. + required: true + - name: algorithm + shorthand: a + description: Algorithm for the key being imported (see table below for options). + required: true + - name: kas + description: Specify the Key Access Server (KAS) where the key will be imported. The KAS can be identified by its ID, URI, or Name. + required: true + - name: wrapping-key-id + description: Identifier related to the wrapping key. + required: true + - name: wrapping-key + shorthand: w + sensitive: true + description: The symmetric key material (AES cipher, hex encoded) used to wrap the imported private key. + required: true + - name: private-key-pem + sensitive: true + description: The base64 encoded private key PEM to import + required: true + - name: public-key-pem + shorthand: e + description: The base64 encoded public key PEM to import + required: true + - name: legacy + description: Mark the imported key as a legacy key. + default: false + - name: label + shorthand: l + description: Comma-separated key=value pairs for metadata labels to associate with the imported key (e.g., "owner=team-a,env=production"). +--- + +Imports an existing cryptographic key into a specified Key Access Server (KAS). + +>[!IMPORTANT] +>Use this command when migrating keys from KAS over to the platform. +>All keys created with import will be of key_mode=**KEY_MODE_CONFIG_ROOT_KEY** + +## Examples + +### Import a key + +```shell +otdfctl policy kas-registry key import --key-id "imported-key" --algorithm "rsa:2048" \ + --kas 891cfe85-b381-4f85-9699-5f7dbfe2a9ab \ + --wrapping-key-id "my-wrapping-key" \ + --wrapping-key "a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1" \ + --public-key-pem \ + --private-key-pem \ +``` + +### Import a legacy key + +```shell +otdfctl policy kas-registry key import --key-id "imported-key" --algorithm "rsa:2048" \ + --kas 891cfe85-b381-4f85-9699-5f7dbfe2a9ab \ + --wrapping-key-id "my-wrapping-key" \ + --wrapping-key "a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1" \ + --public-key-pem \ + --private-key-pem \ + --legacy true +``` + +1. The `algorithm` specifies the key algorithm: + + | Key Algorithm | + | -------------- | + | `rsa:2048` | + | `rsa:4096` | + | `ec:secp256r1` | + | `ec:secp384r1` | + | `ec:secp521r1` | + | `hpqt:xwing` | + | `hpqt:secp256r1-mlkem768` | + | `hpqt:secp384r1-mlkem1024` | diff --git a/otdfctl/docs/man/policy/kas-registry/key/list-mappings.md b/otdfctl/docs/man/policy/kas-registry/key/list-mappings.md new file mode 100644 index 0000000000..4db131e1ab --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/list-mappings.md @@ -0,0 +1,49 @@ +--- +title: List Key Mappings +command: + name: list-mappings + aliases: + - m + flags: + - name: limit + shorthand: l + description: Maximum number of key mappings to return + required: true + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + required: true + - name: id + shorthand: i + description: The system ID of the key for which to list mappings. + - name: key-id + description: The user-defined ID of the key for which to list mappings. Must be used with --kas. + - name: kas + description: Specify the Key Access Server (KAS) where the key (identified by `--key-id`) is registered. The KAS can be identified by its ID, URI, or Name. +--- + +This command lists key mappings. You can list all key mappings, or filter by a specific key. + +To filter by a key, you can either provide the system ID of the key, or the user-defined key ID along with the KAS identifier. + +The list is paginated, so you must provide `limit` and `offset` flags. + +## Examples + +List the first 10 key mappings: + +```bash +otdfctl policy kas-registry key list-mappings --limit 10 --offset 0 +``` + +List key mappings for a key with a specific system ID: + +```bash +otdfctl policy kas-registry key list-mappings --id "cc8bf36a-8c76-4c8c-9723-3c0d1ce897b8" --limit 10 --offset 0 +``` + +List key mappings for a key with a user-defined ID within a KAS specified by its URI: + +```bash +otdfctl policy kas-registry key list-mappings --key-id "my-key" --kas "https://kas.example.com/kas" --limit 10 --offset 0 +``` diff --git a/otdfctl/docs/man/policy/kas-registry/key/list.md b/otdfctl/docs/man/policy/kas-registry/key/list.md new file mode 100644 index 0000000000..99beaf84ef --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/list.md @@ -0,0 +1,97 @@ +--- +title: List Keys +command: + name: list + aliases: + - l + flags: + - name: limit + shorthand: l + description: Maximum number of keys to return + required: true + - name: offset + shorthand: o + description: Number of keys to skip before starting to return results + required: true + - name: algorithm + shorthand: a + description: Key Algorithm to filter for + - name: kas + description: Specify the Key Access Server (KAS) where the key (identified by `--key`) is registered. The KAS can be identified by its ID, URI, or Name. + - name: legacy + description: Filter keys by legacy status. + required: false + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc +--- + +This command lists keys registered within a specified Key Access Server (KAS). You must specify the KAS using its ID, URI, or Name. + +The list can be filtered by key algorithm. Pagination is supported using `limit` and `offset` flags to manage the number of results returned. + +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `key_id` | Key ID | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy kas-registry key list --kas "https://kas.example.com/kas" --sort key_id +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy kas-registry key list --kas "https://kas.example.com/kas" --order asc +``` + +## Examples + +List the first 10 keys from a KAS specified by its URI: + +```shell +otdfctl policy kas-registry key list --kas "https://kas.example.com/kas" --limit 10 --offset 0 +``` + +List keys from a KAS named "Primary KAS", filtering for keys using the "RSA:2048" algorithm, and output in JSON format: + +```shell +otdfctl policy kas-registry key list --kas "Primary KAS" --alg "RSA:2048" --limit 20 --offset 0 --json +``` + +List the next 5 keys (skipping the first 5) from a KAS identified by its ID: + +```shell +otdfctl policy kas-registry key list --kas "kas-id-12345" --limit 5 --offset 5 +``` + +List only legacy keys + +```shell +otdfctl policy kas-registry key list --legacy true +``` + +Exclude legacy keys + +```shell +otdfctl policy kas-registry key list --legacy false +``` + +Sort keys by key ID descending: + +```shell +otdfctl policy kas-registry key list --kas "https://kas.example.com/kas" --sort key_id --order desc +``` diff --git a/otdfctl/docs/man/policy/kas-registry/key/rotate.md b/otdfctl/docs/man/policy/kas-registry/key/rotate.md new file mode 100644 index 0000000000..726d85f74b --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/rotate.md @@ -0,0 +1,102 @@ +--- +title: Rotate Key +command: + name: rotate + aliases: + - r + flags: + # Flags for identifying the old key (from get.md) + - name: key + shorthand: k + description: The KeyID (human-readable identifier) or the internal UUID of the existing key to rotate from the specified KAS. The system will attempt to resolve the provided value as either a UUID or a KeyID. + required: true + - name: kas + description: Specify the Key Access Server (KAS) where the key is registered. The KAS can be identified by its ID, URI, or Name. + required: true + + # Flags for the new key creation (from create.md) + - name: key-id + description: A unique, often human-readable, identifier for the new key to be created. + required: true + - name: algorithm + shorthand: a + description: Algorithm for the new key (see table below for options). + required: true + - name: mode + shorthand: m + description: Describes how the private key is managed (see table below for options). + required: true + - name: wrapping-key-id + description: Identifier related to the wrapping key. Its meaning depends on the `mode`. For `local` mode, it's a descriptive ID for the `wrappingKey` you provide. For `provider` or `remote` mode, it's the ID of the key within the external provider/system used for wrapping. + - name: wrapping-key + shorthand: w + sensitive: true + description: The symmetric key material (AES cipher, hex encoded) used to wrap the generated private key. Primarily used when `mode` is `local`. + - name: private-key-pem + sensitive: true + description: The private key PEM (encrypted by an AES 32-byte key, then base64 encoded). Used when importing an existing key pair, typically with `provider` mode. + - name: provider-config-id + shorthand: p + description: Configuration ID for the key provider. Often required when `mode` is `provider` or `remote` and an external key provider is used. + - name: public-key-pem + shorthand: e + description: The base64 encoded public key PEM. Required for `remote` and `public_key` modes, and can be used with `provider` mode if importing an existing key pair. + - name: label + shorthand: l + description: Comma-separated key=value pairs for metadata labels to associate with the new key (e.g., "owner=team-a,env=production"). +--- + +Rotates a cryptographic key within a specified Key Access Server (KAS). +This command replaces an existing key with a new one while maintaining references to the old key to ensure data encrypted with the old key can still be decrypted. + +## Examples + +### Rotate a key in `local` mode + +Rotate an existing key to a new key in local mode, where the KAS generates the key pair and the private key is wrapped by the provided `wrappingKey`: + +```shell +otdfctl policy kas-registry key rotate --key "old-key-id" --kas "https://kas.example.com/kas" --key-id "new-key-v2" --algorithm "rsa:2048" --mode "local" --wrapping-key-id "virtru-stored-key" --wrapping-key "a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1" +``` + +### Rotate a key in `provider` mode + +```shell +otdfctl policy kas-registry key rotate --key "123e4567-e89b-12d3-a456-426614174000" --kas "https://kas.example.com/kas" --key-id "provider-key-v2" --algorithm "rsa:2048" --mode "provider" --public-key-pem "LS0tLS1CRUdJTi..." --private-key-pem "LS0tLS1CRUdJTi..." --wrapping-key-id "openbao-key" --provider-config-id "f86b166a-98a5-407a-939f-ef84916ce1e5" +``` + +### Rotate a key in `remote` mode + +```shell +otdfctl policy kas-registry key rotate --key "my-remote-key" --kas "Secondary KAS" --key-id "remote-key-v2" --algorithm "rsa:2048" --mode "remote" --wrapping-key-id "openbao-key" --provider-config-id "f86b166a-98a5-407a-939f-ef84916ce1e5" --public-key-pem "LS0tLS1CRUdJTi..." +``` + +### Rotate a key in `public_key` mode + +```shell +otdfctl policy kas-registry key rotate --key "public-key-old" --kas "Secondary KAS" --key-id "public-key-v2" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "LS0tLS1CRUdJTi..." +``` + +## Key Algorithms and Modes + +1. The `"algorithm"` specifies the key algorithm: + + | Key Algorithm | + | -------------- | + | `rsa:2048` | + | `rsa:4096` | + | `ec:secp256r1` | + | `ec:secp384r1` | + | `ec:secp521r1` | + | `hpqt:xwing` | + | `hpqt:secp256r1-mlkem768` | + | `hpqt:secp384r1-mlkem1024` | + +2. The `"mode"` specifies where the key that is encrypting TDFs is stored. All keys will be encrypted when stored in Virtru's DB, for modes `"local"` and `"provider"` + + | Mode | Description | + | ------------ | ------------------------------------------------------------------------------------------------------- | + | `local` | Root Key is stored within Virtru's database and the symmetric wrapping key is stored in KAS | + | `provider` | Root Key is stored within Virtru's database and the symmetric wrapping key is stored externally | + | `remote` | Root Key and wrapping key are stored remotely | + | `public_key` | Root Key and wrapping key are stored remotely. Use this when importing another org's policy information | diff --git a/otdfctl/docs/man/policy/kas-registry/key/unsafe/_index.md b/otdfctl/docs/man/policy/kas-registry/key/unsafe/_index.md new file mode 100644 index 0000000000..26155272ba --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/unsafe/_index.md @@ -0,0 +1,17 @@ +--- +title: Unsafe changes to keys +command: + name: unsafe + flags: + - name: force + description: Force unsafe change without confirmation + required: false +--- + +Unsafe changes are dangerous mutations to KAS that can significantly change access behavior around existing keys +and entitlement. + +Depending on the unsafe change introduced and already existing TDFs, TDFs might become inaccessible that were previously +accessible or vice versa. + +Make sure you know what you are doing. \ No newline at end of file diff --git a/otdfctl/docs/man/policy/kas-registry/key/unsafe/delete.md b/otdfctl/docs/man/policy/kas-registry/key/unsafe/delete.md new file mode 100644 index 0000000000..0aa8828a0c --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/unsafe/delete.md @@ -0,0 +1,28 @@ +--- +title: Delete a key +command: + name: delete + flags: + - name: id + shorthand: i + description: Sytem given ID of the key + required: true + - name: kas-uri + description: The URI of the KAS instance + required: true + - name: key-id + description: The ID of the key assigned by the admin + required: true +--- + +# Unsafe Delete Warning + +Deleting a key is a destructive operation. Any existing TDFs encrypted with this key will be rendered inaccessible. + +Make sure you know what you are doing. + +## Example + +```shell +otdfctl policy kas-keys unsafe delete --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --kas-uri https://kas.example.com --key-id "key-1" +``` diff --git a/otdfctl/docs/man/policy/kas-registry/key/update.md b/otdfctl/docs/man/policy/kas-registry/key/update.md new file mode 100644 index 0000000000..02946f9d01 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/key/update.md @@ -0,0 +1,26 @@ +--- +title: Update Key Access Server Key +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: The internal UUID of the key to be updated. + required: true + - name: label + shorthand: l + description: Comma-separated key=value pairs for metadata labels (e.g., "owner=team-a,env=production"). Providing new labels will replace any existing labels on the key. +--- + +This command updates the key for an existing key registered in a Key Access Server (KAS). +You must identify the key using its UUID via the `--id` flag. +Currently, this command primarily supports updating the metadata labels associated with the key. + +## Examples + +Update key identified by its UUID: +``` +otdfctl policy kas-registry key update --id "123e4567-e89b-12d3-a456-426614174000" --label "status=active,project=phoenix" +``` diff --git a/otdfctl/docs/man/policy/kas-registry/list.md b/otdfctl/docs/man/policy/kas-registry/list.md new file mode 100644 index 0000000000..ab51a17bf0 --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/list.md @@ -0,0 +1,60 @@ +--- +title: List Key Access Server registrations +command: + name: list + aliases: + - l + flags: + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc +--- + +For more information about registration of Key Access Servers, see the manual for `kas-registry`. + +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | KAS registration name | No | +| `uri` | KAS URI | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy kas-registry list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy kas-registry list --order asc +``` + +## Example + +```shell +otdfctl policy kas-registry list +``` + +Sort KAS registrations by URI descending: + +```shell +otdfctl policy kas-registry list --sort uri --order desc +``` diff --git a/otdfctl/docs/man/policy/kas-registry/update.md b/otdfctl/docs/man/policy/kas-registry/update.md new file mode 100644 index 0000000000..a95ad29c1c --- /dev/null +++ b/otdfctl/docs/man/policy/kas-registry/update.md @@ -0,0 +1,47 @@ +--- +title: Update a Key Access Server registration +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the Key Access Server registration + required: true + - name: uri + shorthand: u + description: URI of the Key Access Server + - name: public-keys + shorthand: c + description: One or more 'cached' public keys saved for the KAS + - name: public-key-remote + shorthand: r + description: URI of the 'remote' public key of the Key Access Server + - name: name + shorthand: n + description: Optional name of the registered KAS (must be unique within Policy) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Update the `uri`, `metadata`, or key material (remote/cached) for a KAS registered to the platform. + +If resource data has been TDFd utilizing key splits from the registered KAS, deletion from +the registry (and therefore any associated grants) may prevent decryption depending on the +type of grants and relevant key splits. + +Make sure you know what you are doing. + +For more information about registration of Key Access Servers, see the manual for `kas-registry`. + +## Example + +```shell +otdfctl policy kas-registry update --id 3c39618a-cd8c-48cf-a60c-e8a2f4be4dd5 --name example-kas2-newname --public-key-remote "https://example.com/kas2/new_public_key" +``` diff --git a/otdfctl/docs/man/policy/key-management/_index.md b/otdfctl/docs/man/policy/key-management/_index.md new file mode 100644 index 0000000000..53f35eb456 --- /dev/null +++ b/otdfctl/docs/man/policy/key-management/_index.md @@ -0,0 +1,14 @@ +--- +title: Key management + +command: + name: keymanagement + aliases: + - k + flags: + - name: json + description: output single command in JSON (overrides configured output format) + default: 'false' +--- + +Set of commands for managing key configuration, currently supports managing key provider configuration via the `provider` command. diff --git a/otdfctl/docs/man/policy/key-management/provider/_index.md b/otdfctl/docs/man/policy/key-management/provider/_index.md new file mode 100644 index 0000000000..25e11c02cf --- /dev/null +++ b/otdfctl/docs/man/policy/key-management/provider/_index.md @@ -0,0 +1,18 @@ +--- +title: Provider configuration for Key Management + +command: + name: provider + aliases: + - p +--- + +Commands used for managing a key providers configuration. You should register key providers when creating keys where the key is either: + +1. Wrapped by a key stored outside of your KAS server. For example. if you created a key that is of `mode``provider` +2. The actual wrapped key is not stored within the platform database, but a reference to the key is. For example, if you created a key that is of `mode` `remote`. + +**You should not** create provider configurations for keys of mode: + +- `local` +- `public_key` diff --git a/otdfctl/docs/man/policy/key-management/provider/create.md b/otdfctl/docs/man/policy/key-management/provider/create.md new file mode 100644 index 0000000000..076e73969d --- /dev/null +++ b/otdfctl/docs/man/policy/key-management/provider/create.md @@ -0,0 +1,35 @@ +--- +title: Create a Provider Config +command: + name: create + aliases: + - c + flags: + - name: name + shorthand: n + description: Name of the provider config to create + required: true + - name: manager + shorthand: m + description: Key Manager for the provider config + required: true + - name: config + shorthand: c + description: JSON configuration for the provider + required: true + - name: label + shorthand: l + description: Metadata labels for the provider config +--- + +Creates a new provider config with the specified name and configuration. + +## Examples + +```shell +otdfctl keymanagement provider create --name --config +``` + +```shell +otdfctl keymanagement provider create --name aws --config `{"region": "us-west-2"}` +``` diff --git a/otdfctl/docs/man/policy/key-management/provider/delete.md b/otdfctl/docs/man/policy/key-management/provider/delete.md new file mode 100644 index 0000000000..a9491e9726 --- /dev/null +++ b/otdfctl/docs/man/policy/key-management/provider/delete.md @@ -0,0 +1,32 @@ +--- +title: Delete a Provider Config +command: + name: delete + aliases: + - d + - remove + flags: + - name: force + shorthand: f + description: Force the deletion of a provider configuration without confirmation + - name: id + shorthand: i + description: ID of the provider config to delete + required: true +--- + +Deletes a provider config by its unique ID. + +## Examples + +```shell +otdfctl keymanagement provider delete --id +``` + +```shell +otdfctl keymanagement provider delete --id '04ba179c-2f77-4e0d-90c5-fe4d1c9aa3f7' +``` + +```shell +otdfctl keymanagement provider delete --id '04ba179c-2f77-4e0d-90c5-fe4d1c9aa3f7' --force +``` diff --git a/otdfctl/docs/man/policy/key-management/provider/get.md b/otdfctl/docs/man/policy/key-management/provider/get.md new file mode 100644 index 0000000000..231d0ad0b8 --- /dev/null +++ b/otdfctl/docs/man/policy/key-management/provider/get.md @@ -0,0 +1,22 @@ +--- +title: Get a Provider Config +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the provider config to retrieve + - name: name + shorthand: n + description: Name of the provider config to retrieve +--- + +Retrieves a provider config by its ID or name. + +## Examples + +```shell +otdfctl keymanagement provider get --id '04ba179c-2f77-4e0d-90c5-fe4d1c9aa3f7' +``` diff --git a/otdfctl/docs/man/policy/key-management/provider/list.md b/otdfctl/docs/man/policy/key-management/provider/list.md new file mode 100644 index 0000000000..a408260246 --- /dev/null +++ b/otdfctl/docs/man/policy/key-management/provider/list.md @@ -0,0 +1,24 @@ +--- +title: List Provider Configs +command: + name: list + aliases: + - l + flags: + - name: limit + shorthand: l + description: Maximum number of results to return + required: true + - name: offset + shorthand: o + description: Offset for pagination + required: true +--- + +Lists all provider configs with pagination support. + +## Examples + +```shell +otdfctl keymanagement provider list --limit 10 --offset 0 +``` diff --git a/otdfctl/docs/man/policy/key-management/provider/update.md b/otdfctl/docs/man/policy/key-management/provider/update.md new file mode 100644 index 0000000000..c11d4344d2 --- /dev/null +++ b/otdfctl/docs/man/policy/key-management/provider/update.md @@ -0,0 +1,36 @@ +--- +title: Update a Provider Config +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the provider config to update + required: true + - name: name + shorthand: n + description: New name for the provider config + - name: manager + shorthand: m + description: New key manager for the provider config + - name: config + shorthand: c + description: New JSON configuration for the provider + - name: label + shorthand: l + description: Metadata labels for the provider config +--- + +Updates an existing provider config with the specified parameters. + +## Examples + +```shell +otdfctl keymanagement provider update --id --name --config +``` + +```shell +otdfctl keymanagement provider update --id '04ba179c-2f77-4e0d-90c5-fe4d1c9aa3f7' --name 'gcp' --config `{"region": "us-west-2"}` +``` diff --git a/otdfctl/docs/man/policy/namespaces/_index.md b/otdfctl/docs/man/policy/namespaces/_index.md new file mode 100644 index 0000000000..26d17751b1 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/_index.md @@ -0,0 +1,18 @@ +--- +title: Manage attribute namespaces +command: + name: namespaces + aliases: + - ns + - namespace +--- + +A namespace is the root (parent) of a set of platform policy. Like an owner or an authority, it fully qualifies attributes and their values, +resource mapping groups, etc. As the various mappings of a platform are to attributes or values, a namespace effectively "owns" the +mappings as well (transitively if not directly). + +In an attribute or other FQN (Fully Qualified Name), the namespace is found after the scheme: `https://` + +Namespaces, like other FQN'd objects, are normalized to lower case both on create and in a decision request lookup. + +As the Namespace is the parent of policy, a namespace's existence is required to create attributes or resource mapping groups beneath. diff --git a/otdfctl/docs/man/policy/namespaces/create.md b/otdfctl/docs/man/policy/namespaces/create.md new file mode 100644 index 0000000000..7fbd064b72 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/create.md @@ -0,0 +1,31 @@ +--- +title: Create an attribute namespace +command: + name: create + aliases: + - c + - add + - new + flags: + - name: name + shorthand: n + description: Name of the attribute namespace (must be unique within Policy) + required: true + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Creation of a `namespace` is required to add attributes or any other policy objects beneath. + +A namespace `name` is normalized to lower case, may contain hyphens and underscores between other alphanumeric characters, and it +must contain two segments separated by a `.`, such as `example.com`. + +For more information, see the `namespaces` subcommand. + +## Example + +```shell +otdfctl policy namespaces create --name opentdf.io +``` diff --git a/otdfctl/docs/man/policy/namespaces/deactivate.md b/otdfctl/docs/man/policy/namespaces/deactivate.md new file mode 100644 index 0000000000..35de01d68e --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/deactivate.md @@ -0,0 +1,29 @@ +--- +title: Deactivate an attribute namespace +command: + name: deactivate + flags: + - name: id + shorthand: i + description: ID of the attribute namespace + required: true + - name: force + description: Force deactivation without interactive confirmation (dangerous) +--- + +Deactivating an Attribute Namespace will make the namespace name inactive as well as any attribute definitions and values beneath. + +Deactivation of a Namespace renders any existing TDFs of those attributes inaccessible. + +Deactivation will permanently reserve the Namespace name within a platform. Reactivation and deletion are both considered "unsafe" +behaviors. + +For information about reactivation, see the `unsafe reactivate` subcommand. + +For reactivation, see the `unsafe` command. + +## Example + +```shell +otdfctl policy namespaces deactivate --id 7650f02a-be00-4faa-a1d1-37cded5e23dc +``` diff --git a/otdfctl/docs/man/policy/namespaces/get.md b/otdfctl/docs/man/policy/namespaces/get.md new file mode 100644 index 0000000000..3bac5f2753 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/get.md @@ -0,0 +1,19 @@ +--- +title: Get an attribute namespace +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the attribute namespace +--- + +For more information, see the `namespaces` subcommand. + +## Example + +```shell +otdfctl policy namespaces get --id=7650f02a-be00-4faa-a1d1-37cded5e23dc +``` \ No newline at end of file diff --git a/otdfctl/docs/man/policy/namespaces/key/_index.md b/otdfctl/docs/man/policy/namespaces/key/_index.md new file mode 100644 index 0000000000..39b82e9b05 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/key/_index.md @@ -0,0 +1,7 @@ +--- +title: Key Management changes to attribute namespaces +command: + name: key +--- + +Manages KAS key associations for attribute namespaces. diff --git a/otdfctl/docs/man/policy/namespaces/key/assign.md b/otdfctl/docs/man/policy/namespaces/key/assign.md new file mode 100644 index 0000000000..489de5d88f --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/key/assign.md @@ -0,0 +1,26 @@ +--- +title: Assign a KAS key to an attribute namespace +command: + name: assign + flags: + - name: namespace + shorthand: n + description: Can be URI or ID of namespace + required: true + - name: key-id + shorthand: k + description: ID of the KAS key to assign + required: true +--- + +Assigns a KAS key to a policy attribute namespace. This enables the attribute namespace to be used with the specified KAS key for encryption and decryption operations. + +## Example + +```shell +otdfctl policy namespaces assign --namespace 3d25d33e-2469-4990-a9ed-fdd13ce74436 --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` + +```shell +otdfctl policy namespaces remove --namespace "https://example.com" --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` diff --git a/otdfctl/docs/man/policy/namespaces/key/remove.md b/otdfctl/docs/man/policy/namespaces/key/remove.md new file mode 100644 index 0000000000..6853dc2335 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/key/remove.md @@ -0,0 +1,26 @@ +--- +title: Remove a KAS key from an attribute namespace +command: + name: remove + flags: + - name: namespace + shorthand: n + description: Can be URI or ID of namespace + required: true + - name: key-id + shorthand: k + description: ID of the KAS key to remove + required: true +--- + +Removes a KAS key from a policy attribute namespace. After removing the key, the attribute namespace can no longer be used with the specified KAS key for encryption and decryption operations. + +## Example + +```shell +otdfctl policy namespaces remove --namespace 3d25d33e-2469-4990-a9ed-fdd13ce74436 --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` + +```shell +otdfctl policy namespaces remove --namespace "https://example.com" --key-id 8f7e6d5c-4b3a-2d1e-9f8d-7c6b5a432f1d +``` diff --git a/otdfctl/docs/man/policy/namespaces/list.md b/otdfctl/docs/man/policy/namespaces/list.md new file mode 100644 index 0000000000..8fa3e0802d --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/list.md @@ -0,0 +1,64 @@ +--- +title: List attribute namespaces +command: + name: list + aliases: + - ls + - l + flags: + - name: state + shorthand: s + description: Filter by state [active, inactive, any] + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc +--- + +For more general information, see the `namespaces` subcommand. + +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | Namespace name | No | +| `fqn` | Namespace FQN | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy namespaces list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy namespaces list --order asc +``` + +## Example + +```shell +otdfctl policy namespaces list +``` + +Sort namespaces by name ascending: + +```shell +otdfctl policy namespaces list --sort name --order asc +``` diff --git a/otdfctl/docs/man/policy/namespaces/unsafe/_index.md b/otdfctl/docs/man/policy/namespaces/unsafe/_index.md new file mode 100644 index 0000000000..b86cc14498 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/unsafe/_index.md @@ -0,0 +1,19 @@ +--- +title: Unsafe changes to attribute namespaces +command: + name: unsafe + flags: + - name: force + description: Force unsafe change without confirmation + required: false +--- + +Unsafe changes are dangerous mutations to Policy that can significantly change access behavior around existing attributes +and entitlement. + +Depending on the unsafe change introduced and already existing TDFs, TDFs might become inaccessible that were previously +accessible or vice versa. + +Make sure you know what you are doing. + +For more general information, see the `namespaces` subcommand. diff --git a/otdfctl/docs/man/policy/namespaces/unsafe/delete.md b/otdfctl/docs/man/policy/namespaces/unsafe/delete.md new file mode 100644 index 0000000000..74ef450328 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/unsafe/delete.md @@ -0,0 +1,26 @@ +--- +title: Delete an attribute namespace +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the attribute namespace + required: true +--- + +# Unsafe Delete Warning + +Deleting a Namespace cascades deletion of any Attribute Definitions, Values, and any associated mappings underneath. + +Any existing TDFs containing attributes under this namespace will be rendered inaccessible until it has been recreated. + +Make sure you know what you are doing. + +For more general information, see the `namespaces` subcommand. + +## Example + +```shell +otdfctl policy namespaces unsafe delete --id 7650f02a-be00-4faa-a1d1-37cded5e23dc +``` diff --git a/otdfctl/docs/man/policy/namespaces/unsafe/reactivate.md b/otdfctl/docs/man/policy/namespaces/unsafe/reactivate.md new file mode 100644 index 0000000000..2f4a6b973f --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/unsafe/reactivate.md @@ -0,0 +1,26 @@ +--- +title: Reactivate an attribute namespace +command: + name: reactivate + flags: + - name: id + shorthand: i + description: ID of the attribute namespace + required: true +--- + +# Unsafe Reactivate Warning + +Reactivating a Namespace can potentially open up an access path to any existing TDFs referencing attributes under that Namespace. + +The Active/Inactive state of any Attribute Definitions or Values under this Namespace will NOT be changed. + +Make sure you know what you are doing. + +For more general information, see the `namespaces` subcommand. + +## Example + +```shell +otdfctl policy namespaces unsafe reactivate --id 7650f02a-be00-4faa-a1d1-37cded5e23dc +``` diff --git a/otdfctl/docs/man/policy/namespaces/unsafe/update.md b/otdfctl/docs/man/policy/namespaces/unsafe/update.md new file mode 100644 index 0000000000..a2ead5a776 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/unsafe/update.md @@ -0,0 +1,31 @@ +--- +title: Update an attribute namespace +command: + name: update + flags: + - name: id + shorthand: i + description: ID of the attribute namespace + required: true + - name: name + shorthand: n + description: Name of the attribute namespace (new) + required: true +--- + +# Unsafe Update Warning + +Renaming a Namespace means any Attribute Definitions, Values, and any associated mappings underneath will now be tied to the new name. + +Any existing TDFs containing attributes under the old namespace will be rendered inaccessible, and any TDFs tied to the new namespace +and already created may now become accessible. + +Make sure you know what you are doing. + +For more general information, see the `namespaces` subcommand. + +## Example + +```shell +otdfctl policy namespaces unsafe update --id=7650f02a-be00-4faa-a1d1-37cded5e23dc --name opentdf2.io +``` diff --git a/otdfctl/docs/man/policy/namespaces/update.md b/otdfctl/docs/man/policy/namespaces/update.md new file mode 100644 index 0000000000..25cba980d1 --- /dev/null +++ b/otdfctl/docs/man/policy/namespaces/update.md @@ -0,0 +1,29 @@ +--- +title: Update a attribute namespace +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the attribute namespace + required: true + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Attribute Namespace changes can be dangerous, so this command is for updates considered "safe" (currently just mutations to metadata `labels`). + +For unsafe updates, see the dedicated `unsafe update` command. For more general information, see the `namespaces` subcommand. + +## Example + +```shell +otdfctl policy namespaces update --id=7650f02a-be00-4faa-a1d1-37cded5e23dc --label hello=world +``` diff --git a/otdfctl/docs/man/policy/obligations/_index.md b/otdfctl/docs/man/policy/obligations/_index.md new file mode 100644 index 0000000000..609636cf1c --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/_index.md @@ -0,0 +1,9 @@ +--- +title: Manage obligations +command: + name: obligations + aliases: + - obl +--- + +Obligations enable conditional access enforcement at the Policy Enforcement Point (PEP) level and allow security administrators to enforce additional restrictions beyond basic attribute-based access control (ABAC), such as requiring multi-factor authentication (MFA), enforcing watermarking, or applying time-based access expiration. diff --git a/otdfctl/docs/man/policy/obligations/create.md b/otdfctl/docs/man/policy/obligations/create.md new file mode 100644 index 0000000000..4f23e712a5 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/create.md @@ -0,0 +1,37 @@ +--- +title: Create an obligation definition +command: + name: create + aliases: + - c + - add + - new + flags: + - name: name + shorthand: n + description: Name of the obligation (must be unique within a Namespace) + required: true + - name: namespace + shorthand: s + description: Namespace ID or FQN + required: true + - name: value + shorthand: v + description: Value of the obligation (i.e. 'value1', must be unique within the Obligation) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Add an obligation definition to the platform Policy. + +For more information, see the `obligations` subcommand. + +## Examples + +Create an obligation definition named 'my_obligation' with value 'my_value': + +```shell +otdfctl policy obligations create --name my_obligation --value my_value +``` diff --git a/otdfctl/docs/man/policy/obligations/delete.md b/otdfctl/docs/man/policy/obligations/delete.md new file mode 100644 index 0000000000..605b9559e1 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/delete.md @@ -0,0 +1,34 @@ +--- +title: Delete an obligation definition +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the obligation + - name: fqn + shorthand: f + description: FQN of the obligation + - name: force + description: Force deletion without interactive confirmation +--- + +Removes an obligation definition from platform Policy. + +Obligation deletion cascades to the associated obligation values. + +For more information about obligations, see the manual for the `obligations` subcommand. + +## Example + +Delete by ID: + +```shell +otdfctl policy obligations delete --id 217b300a-47f9-4bee-be8c-d38c880053f7 +``` + +Delete by FQN: + +```shell +otdfctl policy obligations delete --fqn "https://namespace.com/obl/name/drm" +``` \ No newline at end of file diff --git a/otdfctl/docs/man/policy/obligations/get.md b/otdfctl/docs/man/policy/obligations/get.md new file mode 100644 index 0000000000..7c2320fa97 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/get.md @@ -0,0 +1,34 @@ +--- +title: Get an obligation definition +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the obligation + - name: fqn + shorthand: f + description: FQN of the obligation +--- + +Retrieve an obligation definition along with its metadata and values. + +If both `id` and `fqn` flag values are provided, `id` is preferred. + +For more information about obligations, see the manual for the `obligations` subcommand. + +## Example + +Get by ID: + +```shell +otdfctl policy obligations get --id=3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` + +Get by FQN: + +```shell +otdfctl policy obligations get --fqn=https://namespace.com/obl/drm +``` diff --git a/otdfctl/docs/man/policy/obligations/list.md b/otdfctl/docs/man/policy/obligations/list.md new file mode 100644 index 0000000000..3cae7e32e8 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/list.md @@ -0,0 +1,65 @@ +--- +title: List obligation definitions +command: + name: list + aliases: + - l + flags: + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + - name: namespace + shorthand: n + description: Namespace ID or FQN by which to filter results + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc +--- + +List obligations definitions (optionally by namespace). + +For more information about obligations, see the `obligations` subcommand. + +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | Obligation name | No | +| `fqn` | Obligation FQN | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy obligations list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy obligations list --order asc +``` + +## Example + +```shell +otdfctl policy obligations list --limit 10 --offset 0 +``` + +Sort obligations by name ascending: + +```shell +otdfctl policy obligations list --sort name --order asc +``` diff --git a/otdfctl/docs/man/policy/obligations/triggers/_index.md b/otdfctl/docs/man/policy/obligations/triggers/_index.md new file mode 100644 index 0000000000..0f98080299 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/triggers/_index.md @@ -0,0 +1,7 @@ +--- +title: Manage obligation triggers +command: + name: triggers +--- + +Obligations triggers are the link between an attribute value, a PEPs intended action on a TDF, and what the PEP is obliged to do. diff --git a/otdfctl/docs/man/policy/obligations/triggers/create.md b/otdfctl/docs/man/policy/obligations/triggers/create.md new file mode 100644 index 0000000000..8b1d95d88c --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/triggers/create.md @@ -0,0 +1,56 @@ +--- +title: Create an obligation trigger +command: + name: create + aliases: + - c + - add + - new + flags: + - name: attribute-value + description: Attribute value ID or FQN + required: true + - name: action + description: Action ID or Name + required: true + - name: obligation-value + description: Obligation value ID or FQN + required: true + - name: client-id + description: Create a scoped trigger. Optionally include the clientID for which this trigger should be scoped to. + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l +--- + +Add an obligation trigger to the platform Policy with our without a client identifier. + +>[!NOTE] +>Creating an obligation trigger with a client-id scopes the +>trigger to a specific policy enforcement point, which is identified +>through the requestor's authentication token. +>Scoping a trigger to a specific client does two things: +> +>1. If the requesting application is **NOT** scoped to the trigger, it will not be used in the authorization decisioning and the obligation does not need to be fulfilled. +>2. If the requesting application **IS** scoped to the trigger, the application must +>be able to fulfill the obligation the trigger is mapped to. + +## Examples + +Create an obligation trigger with FQNs/Names: + +```shell +otdfctl policy obligations triggers create --attribute-value "https://example.com/attr/classification/value/confidential" --action "read" --obligation-value "https://example.com/obl/test/value/mfa" +``` + +Create an obligation trigger with IDs + +```shell +otdfctl policy obligations triggers create --attribute-value "d10e0fb6-4b4a-4976-8036-33903ebc6be3" --action "f15f65db-6889-453a-b032-212f78e8eb18" --obligation-value "0cbbb9bb-ed2d-41c0-8efa-1bcdddc44771" +``` + +Create a scoped obligation trigger with IDs. + +```shell +otdfctl policy obligations triggers create --attribute-value "d10e0fb6-4b4a-4976-8036-33903ebc6be3" --action "f15f65db-6889-453a-b032-212f78e8eb18" --obligation-value "0cbbb9bb-ed2d-41c0-8efa-1bcdddc44771" --client-id "my-service" +``` diff --git a/otdfctl/docs/man/policy/obligations/triggers/delete.md b/otdfctl/docs/man/policy/obligations/triggers/delete.md new file mode 100644 index 0000000000..49581d4dd4 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/triggers/delete.md @@ -0,0 +1,27 @@ +--- +title: Delete an obligation trigger +command: + name: delete + flags: + - name: id + description: ID of the obligation trigger to delete + required: true + - name: force + description: Force deletion without interactive confirmation +--- + +Delete an obligation trigger. + +## Examples + +Delete an obligation trigger by its ID: + +```shell +otdfctl policy obligations triggers delete --id "79b798f2-50a4-4a6d-9c5d-0f0e3c8787e8" +``` + +Force the deletion of an obligation trigger: + +```shell +otdfctl policy obligations triggers delete --id "79b798f2-50a4-4a6d-9c5d-0f0e3c8787e8" --force +``` diff --git a/otdfctl/docs/man/policy/obligations/triggers/list.md b/otdfctl/docs/man/policy/obligations/triggers/list.md new file mode 100644 index 0000000000..377820f886 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/triggers/list.md @@ -0,0 +1,29 @@ +--- +title: List obligation triggers +command: + name: list + aliases: + - l + flags: + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + - name: namespace + shorthand: n + description: Namespace ID or FQN by which to filter results +--- + +List obligation triggers (optionally by namespace). + +## Example + +```shell +otdfctl policy obligations triggers list --limit 10 --offset 0 +``` + +```shell +otdfctl policy obligations triggers list --limit 10 --offset 0 --namespace "https://example.com" +``` diff --git a/otdfctl/docs/man/policy/obligations/update.md b/otdfctl/docs/man/policy/obligations/update.md new file mode 100644 index 0000000000..4db33b31fe --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/update.md @@ -0,0 +1,36 @@ +--- +title: Update an obligation definition +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the obligation to update + required: true + - name: name + shorthand: n + description: Optional updated name of the obligation (must be unique within the Namespace) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Update the `name` and/or metadata labels for an obligation definition. + +If PEPs rely on this obligation name, a name update could break access. + +Make sure you know what you are doing. + +For more information about obligations, see the `obligations` subcommand. + +## Example + +```shell +otdfctl policy obligations update --id 34c62145-5d99-45cb-a732-13cb16270e63 --name new_obligation_name +``` diff --git a/otdfctl/docs/man/policy/obligations/values/_index.md b/otdfctl/docs/man/policy/obligations/values/_index.md new file mode 100644 index 0000000000..c229bb1e9f --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/values/_index.md @@ -0,0 +1,10 @@ +--- +title: Manage obligation values +command: + name: values + aliases: + - val + - value +--- + +Obligation values are the values associated with an obligation. diff --git a/otdfctl/docs/man/policy/obligations/values/create.md b/otdfctl/docs/man/policy/obligations/values/create.md new file mode 100644 index 0000000000..b627f274d2 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/values/create.md @@ -0,0 +1,59 @@ +--- +title: Create an obligation value +command: + name: create + aliases: + - c + - add + - new + flags: + - name: obligation + shorthand: o + description: Identifier of the associated obligation (ID or FQN) + required: true + - name: value + shorthand: v + description: Value of the obligation (i.e. 'value1', must be unique within the definition) + required: true + - name: triggers + shorthand: t + description: Optional JSON array or file path of obligation trigger(s) to be created and stored on the obligation value. + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Add a value to an obligation in the platform Policy. + +For more information about obligation values, see the `obligations` subcommand. + +## Examples + +Create an obligation value for the obligation with ID '3c51a593-cbf8-419d-b7dc-b656d0bedfbb', and value 'my_value': + +```shell +otdfctl policy obligations values create --obligation 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value my_value +``` + +### Trigger examples + +You can also create multiple obligation triggers while creating an obligation value. + +Create an obligation value and create a non-scoped trigger that will map to the created value. + +```shell +otdfctl policy obligations values create --obligation 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value my_value --triggers '[{"action": "read", "attribute_value": "https://test.org/attr/test/value/red"}]' +``` + +Create an obligation value and create a scoped trigger that will map to the created value + +```shell +otdfctl policy obligations values create --obligation 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value my_value --triggers '[{"action": "read", "attribute_value": "https://test.org/attr/test/value/red", "context": {"pep": {"client_id": "a-pep" }}}]' +``` + +Create an obligation value and triggers, where the triggers come from a json file. + +```shell +otdfctl policy obligations values create --obligation 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value my_value --triggers "/path/to/file.json" +``` diff --git a/otdfctl/docs/man/policy/obligations/values/delete.md b/otdfctl/docs/man/policy/obligations/values/delete.md new file mode 100644 index 0000000000..75d586f764 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/values/delete.md @@ -0,0 +1,32 @@ +--- +title: Delete an obligation value +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the obligation value + - name: fqn + shorthand: f + description: FQN of the obligation value + - name: force + description: Force deletion without interactive confirmation +--- + +Removes an obligation value from platform Policy. + +For more information about obligation values, see the manual for the `values` subcommand. + +## Example + +Delete by ID: + +```shell +otdfctl policy obligations values delete --id 217b300a-47f9-4bee-be8c-d38c880053f7 +``` + +Delete by FQN: + +```shell +otdfctl policy obligations values delete --fqn "https://namespace.com/obl/name/drm/value/expiration" +``` \ No newline at end of file diff --git a/otdfctl/docs/man/policy/obligations/values/get.md b/otdfctl/docs/man/policy/obligations/values/get.md new file mode 100644 index 0000000000..41d994bb54 --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/values/get.md @@ -0,0 +1,34 @@ +--- +title: Get an obligation value +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the obligation value + - name: fqn + shorthand: f + description: FQN of the obligation value +--- + +Retrieve an obligation value along with its metadata. + +If both `id` and `fqn` flag values are provided, `id` is preferred. + +For more information about obligation values, see the manual for the `values` subcommand. + +## Example + +Get by ID: + +```shell +otdfctl policy obligations values get --id=3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` + +Get by FQN: + +```shell +otdfctl policy obligations values get --fqn=https://namespace.com/drm/value/watermark +``` diff --git a/otdfctl/docs/man/policy/obligations/values/update.md b/otdfctl/docs/man/policy/obligations/values/update.md new file mode 100644 index 0000000000..2c7d3711db --- /dev/null +++ b/otdfctl/docs/man/policy/obligations/values/update.md @@ -0,0 +1,58 @@ +--- +title: Update an obligation value +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the obligation value to update + required: true + - name: value + shorthand: v + description: Optional updated value of the obligation value (must be unique within the definition) + - name: triggers + shorthand: t + description: Optional JSON array or file path of obligation trigger(s) to be created and stored on the obligation value. + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Update the `value` and/or metadata labels for an obligation value. + +If PEPs rely on this value, a value update could break access. + +Make sure you know what you are doing. + +For more information about obligation values, see the manual for the `values` subcommand. + +## Example + +```shell +otdfctl policy obligations values update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value new_value --label "hello=world" +``` + +### Trigger Example + +>[!CAUTION] +>Updating a obligation value with triggers will replace all existing +>triggers, on the obligation value being updated, with the new list. + +Update an obligation value and assign one unscoped trigger to the new value. + +>[!NOTE] +>View the `create` command under obligation triggers to read +>more about `scoped` and `unscoped` triggers. + +```shell +otdfctl policy obligations values update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value new_value --label "hello=world" --triggers '[{"action": "read", "attribute_value": "https://test.org/attr/test/value/red"}]' +``` + +Update triggers on an obligation value via a json file. + +```shell +otdfctl policy obligations values update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value new_value --label "hello=world" --triggers "/path/to/file.json" +``` diff --git a/otdfctl/docs/man/policy/registered-resources/_index.md b/otdfctl/docs/man/policy/registered-resources/_index.md new file mode 100644 index 0000000000..39523e3c5f --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/_index.md @@ -0,0 +1,9 @@ +--- +title: Manage Registered Resources +command: + name: registered-resources + aliases: + - reg-res +--- + +Registered Resources are "non-data" resources (i.e. not a TDF data object) that are registered with the platform policy and may serve as the "Entity" or "Resource" in a decision request. diff --git a/otdfctl/docs/man/policy/registered-resources/create.md b/otdfctl/docs/man/policy/registered-resources/create.md new file mode 100644 index 0000000000..cb9d085c83 --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/create.md @@ -0,0 +1,38 @@ +--- +title: Create a Registered Resource +command: + name: create + aliases: + - c + - add + - new + flags: + - name: name + shorthand: n + description: Name of the registered resource (must be unique within a Namespace) + required: true + - name: namespace + shorthand: s + description: Namespace ID or FQN + - name: value + shorthand: v + description: Value of the registered resource (i.e. 'value1', must be unique within the Registered Resource) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Add a registered resource to the platform Policy. + +A registered resource `name` is normalized to lower case and may contain hyphens or dashes between other alphanumeric characters. + +For more information, see the `registered-resources` subcommand. + +## Examples + +Create a registered resource named 'my_resource' with value 'my_value': + +```shell +otdfctl policy registered-resources create --name my_resource --value my_value +``` diff --git a/otdfctl/docs/man/policy/registered-resources/delete.md b/otdfctl/docs/man/policy/registered-resources/delete.md new file mode 100644 index 0000000000..c6413b7343 --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/delete.md @@ -0,0 +1,24 @@ +--- +title: Delete a Registered Resource +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the registered resource + required: true + - name: force + description: Force deletion without interactive confirmation +--- + +Removes a Registered Resource from platform Policy. + +Registered resource deletion cascades to the associated Registered Resource Values and Action Attribute Values. + +For more information about Registered Resources, see the manual for the `registered-resources` subcommand. + +## Example + +```shell +otdfctl policy registered-resources delete --id 217b300a-47f9-4bee-be8c-d38c880053f7 +``` diff --git a/otdfctl/docs/man/policy/registered-resources/get.md b/otdfctl/docs/man/policy/registered-resources/get.md new file mode 100644 index 0000000000..f2c4f171f3 --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/get.md @@ -0,0 +1,37 @@ +--- +title: Get a Registered Resource +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the registered resource + - name: name + shorthand: n + description: Name of the registered resource + - name: namespace + shorthand: s + description: Namespace ID or FQN for name-based lookups (optional) +--- + +Retrieve a registered resource along with its metadata and values. + +If both `id` and `name` flag values are provided, `id` is preferred. + +For more information about Registered Resources, see the manual for the `registered-resources` subcommand. + +## Example + +Get by ID: + +```shell +otdfctl policy registered-resources get --id=3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` + +Get by Name: + +```shell +otdfctl policy registered-resources get --name=my_resource +``` diff --git a/otdfctl/docs/man/policy/registered-resources/list.md b/otdfctl/docs/man/policy/registered-resources/list.md new file mode 100644 index 0000000000..a51ee69357 --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/list.md @@ -0,0 +1,62 @@ +--- +title: List Registered Resources +command: + name: list + aliases: + - l + flags: + - name: namespace + shorthand: s + description: Namespace ID or FQN to filter results + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc +--- + +For more information about Registered Resources, see the `registered-resources` subcommand. + +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | Registered resource name | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy registered-resources list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy registered-resources list --order asc +``` + +## Example + +```shell +otdfctl policy registered-resources list +``` + +Sort registered resources by name ascending: + +```shell +otdfctl policy registered-resources list --sort name --order asc +``` diff --git a/otdfctl/docs/man/policy/registered-resources/update.md b/otdfctl/docs/man/policy/registered-resources/update.md new file mode 100644 index 0000000000..11b4d43aba --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/update.md @@ -0,0 +1,36 @@ +--- +title: Update a Registered Resource +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the registered resource to update + required: true + - name: name + shorthand: n + description: Optional updated name of the registered resource (must be unique within Policy) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Update the `name` and/or metadata labels for a Registered Resource. + +If PEPs rely on this registered resource name, a name update could break access. + +Make sure you know what you are doing. + +For more information about Registered Resources, see the `registered-resources` subcommand. + +## Example + +```shell +otdfctl policy registered-resources update --id 34c62145-5d99-45cb-a732-13cb16270e63 --name new_resource_name +``` diff --git a/otdfctl/docs/man/policy/registered-resources/values/_index.md b/otdfctl/docs/man/policy/registered-resources/values/_index.md new file mode 100644 index 0000000000..552d709af3 --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/values/_index.md @@ -0,0 +1,10 @@ +--- +title: Manage Registered Resource Values +command: + name: values + aliases: + - val + - value +--- + +Registered Resource Values are the values associated with a registered resource. diff --git a/otdfctl/docs/man/policy/registered-resources/values/create.md b/otdfctl/docs/man/policy/registered-resources/values/create.md new file mode 100644 index 0000000000..c22bf91aeb --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/values/create.md @@ -0,0 +1,43 @@ +--- +title: Create Registered Resource Value +command: + name: create + aliases: + - c + - add + - new + flags: + - name: resource + shorthand: r + description: Identifier of the associated registered resource (ID or name) + required: true + - name: value + shorthand: v + description: Value of the registered resource (i.e. 'value1', must be unique within the Registered Resource) + required: true + - name: namespace + shorthand: s + description: "Namespace ID or FQN (required when --resource is a name)" + - name: action-attribute-value + shorthand: a + description: "Optional action attribute values in the format: \";\"" + default: '' + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Add a value to a registered resource in the platform Policy. + +A registered resource value `value` is normalized to lower case and may contain hyphens or dashes between other alphanumeric characters. + +For more information, see the `registered-resources` subcommand. + +## Examples + +Create a registered resource value for the registered resource with ID '3c51a593-cbf8-419d-b7dc-b656d0bedfbb', value 'my_value', and action attribute values using action/attribute value IDs, action names, and attribute value FQNs: + +```shell +otdfctl policy registered-resources values create --resource 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value my_value --action-attribute-value "74a3eade-ef6c-4422-b764-fe0471f5c6c1;405a35a7-2051-49a6-9645-3a667b4739f3" --action-attribute-value "create;https://example.com/attr/my_attribute/value/my_value" +``` diff --git a/otdfctl/docs/man/policy/registered-resources/values/delete.md b/otdfctl/docs/man/policy/registered-resources/values/delete.md new file mode 100644 index 0000000000..a7d31ce3bf --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/values/delete.md @@ -0,0 +1,24 @@ +--- +title: Delete a Registered Resource Value +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the registered resource value + required: true + - name: force + description: Force deletion without interactive confirmation +--- + +Removes a Registered Resource Value from platform Policy. + +Registered resource value deletion cascades to the associated Action Attribute Values. + +For more information about Registered Resource Values, see the manual for the `values` subcommand. + +## Example + +```shell +otdfctl policy registered-resources values delete --id 217b300a-47f9-4bee-be8c-d38c880053f7 +``` diff --git a/otdfctl/docs/man/policy/registered-resources/values/get.md b/otdfctl/docs/man/policy/registered-resources/values/get.md new file mode 100644 index 0000000000..31cdac0925 --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/values/get.md @@ -0,0 +1,34 @@ +--- +title: Get a Registered Resource Value +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the registered resource value + - name: fqn + shorthand: f + description: FQN of the registered resource value +--- + +Retrieve a registered resource value along with its metadata. + +If both `id` and `fqn` flag values are provided, `id` is preferred. + +For more information about Registered Resource Values, see the manual for the `values` subcommand. + +## Example + +Get by ID: + +```shell +otdfctl policy registered-resources values get --id=3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` + +Get by FQN: + +```shell +otdfctl policy registered-resources values get --fqn=https://reg_res/my_name/value/my_value +``` diff --git a/otdfctl/docs/man/policy/registered-resources/values/list.md b/otdfctl/docs/man/policy/registered-resources/values/list.md new file mode 100644 index 0000000000..f6cac7c22c --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/values/list.md @@ -0,0 +1,30 @@ +--- +title: List Registered Resource Values +command: + name: list + aliases: + - l + flags: + - name: resource + shorthand: r + description: Identifier of the associated registered resource (ID or name) + - name: namespace + shorthand: s + description: "Namespace ID or FQN (required when --resource is a name)" + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +List registered resource values in the platform Policy. + +For more information about Registered Resource Values, see the manual for the `values` subcommand. + +## Example + +```shell +otdfctl policy registered-resources values list +``` diff --git a/otdfctl/docs/man/policy/registered-resources/values/update.md b/otdfctl/docs/man/policy/registered-resources/values/update.md new file mode 100644 index 0000000000..c578d7aaed --- /dev/null +++ b/otdfctl/docs/man/policy/registered-resources/values/update.md @@ -0,0 +1,40 @@ +--- +title: Update a Registered Resource Value +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the registered resource value to update + - name: value + shorthand: v + description: Optional updated value of the registered resource value (must be unique within the Registered Resource) + - name: action-attribute-value + shorthand: a + description: "Optional action attribute values in the format: \";\"" + default: '' + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force + description: Force update without interactive confirmation +--- + +Update any or all of the `value`, action attribute values, and metadata labels for a Registered Resource Value. + +If PEPs rely on this value, a value update could break access. + +Updating the action attribute values will remove and replace all existing action attribute values for this registered resource value. + +Make sure you know what you are doing. + +For more information about Registered Resource Values, see the manual for the `values` subcommand. + +## Example + +```shell +otdfctl policy registered-resources values update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value new_value --action-attribute-value "74a3eade-ef6c-4422-b764-fe0471f5c6c1;405a35a7-2051-49a6-9645-3a667b4739f3" --action-attribute-value "create;https://example.com/attr/my_attribute/value/my_value" +``` diff --git a/otdfctl/docs/man/policy/resource-mapping-groups/_index.md b/otdfctl/docs/man/policy/resource-mapping-groups/_index.md new file mode 100644 index 0000000000..ed0269362b --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mapping-groups/_index.md @@ -0,0 +1,11 @@ +--- +title: Manage resource mapping groups +command: + name: resource-mapping-groups + aliases: + - resmg + - remapgrp + - resource-mapping-group +--- + +Resource mapping groups allow you to organize multiple resource mappings into logical collections. By grouping related resource mappings, you can manage sets of resources more efficiently. This is useful for scenarios where resources share common access controls or need to be managed together as a unit. \ No newline at end of file diff --git a/otdfctl/docs/man/policy/resource-mapping-groups/create.md b/otdfctl/docs/man/policy/resource-mapping-groups/create.md new file mode 100644 index 0000000000..48756b1ee6 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mapping-groups/create.md @@ -0,0 +1,30 @@ +--- +title: Create a resource mapping group +command: + name: create + aliases: + - add + - new + - c + flags: + - name: namespace-id + description: The ID of the namespace of the group + default: '' + - name: name + description: The name of the group + default: '' + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Create a new group to organize resource mappings. Resource mapping groups belong to a namespace and are identified by a name. + +For more information about resource mapping groups, see the `resource-mapping-groups` subcommand. + +## Examples + +```shell +otdfctl policy resource-mapping-groups create --namespace-id 891cfe85-b381-4f85-9699-5f7dbfe2a9ab --name my-group +``` diff --git a/otdfctl/docs/man/policy/resource-mapping-groups/delete.md b/otdfctl/docs/man/policy/resource-mapping-groups/delete.md new file mode 100644 index 0000000000..1ef4be0875 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mapping-groups/delete.md @@ -0,0 +1,19 @@ +--- +title: Delete a resource mapping group +command: + name: delete + flags: + - name: id + description: The ID of the resource mapping group to delete + default: '' + - name: force + description: Force deletion without interactive confirmation (dangerous) +--- + +For more information about resource mapping groups, see the `resource-mapping-groups` subcommand. + +## Examples + +```shell +otdfctl policy resource-mapping-groups delete --id=3ff446fb-8fb1-4c04-8023-47592c90370c +``` diff --git a/otdfctl/docs/man/policy/resource-mapping-groups/get.md b/otdfctl/docs/man/policy/resource-mapping-groups/get.md new file mode 100644 index 0000000000..17bc422137 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mapping-groups/get.md @@ -0,0 +1,19 @@ +--- +title: Get a resource mapping group +command: + name: get + aliases: + - g + flags: + - name: id + description: The ID of the resource mapping group to get. + default: '' +--- + +For more information about resource mapping groups, see the `resource-mapping-groups` subcommand. + +## Examples + +```shell +otdfctl policy resource-mapping-groups get --id=3ff446fb-8fb1-4c04-8023-47592c90370c +``` diff --git a/otdfctl/docs/man/policy/resource-mapping-groups/list.md b/otdfctl/docs/man/policy/resource-mapping-groups/list.md new file mode 100644 index 0000000000..1e4d15833f --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mapping-groups/list.md @@ -0,0 +1,22 @@ +--- +title: List resource mapping groups +command: + name: list + aliases: + - l + flags: + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +For more information about resource mapping groups, see the `resource-mapping-groups` subcommand. + +## Examples + +```shell +otdfctl policy resource-mapping-groups list +``` diff --git a/otdfctl/docs/man/policy/resource-mapping-groups/update.md b/otdfctl/docs/man/policy/resource-mapping-groups/update.md new file mode 100644 index 0000000000..96d2e47679 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mapping-groups/update.md @@ -0,0 +1,34 @@ +--- +title: Update a resource mapping group +command: + name: update + aliases: + - u + flags: + - name: id + description: The ID of the resource mapping group to update. + default: '' + - name: namespace-id + description: The ID of the namespace of the group + default: '' + - name: name + description: The name of the group + default: '' + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Alter the namespace associated with a group, or update the group's name. + +For more information about resource mapping groups, see the `resource-mapping-groups` subcommand. + +## Examples + +```shell +otdfctl policy resource-mapping-groups update --id=3ff446fb-8fb1-4c04-8023-47592c90370c --name new-name +``` diff --git a/otdfctl/docs/man/policy/resource-mappings/_index.md b/otdfctl/docs/man/policy/resource-mappings/_index.md new file mode 100644 index 0000000000..cb7b3836a2 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mappings/_index.md @@ -0,0 +1,17 @@ +--- +title: Manage resource mappings +command: + name: resource-mappings + aliases: + - resm + - remap + - resource-mapping +--- + +Resource mappings are used to map resources to their respective attribute values based on the terms +that are related to the data. Alone, this service is not very useful, but when combined with a PEP +or PDP that can use the resource mappings it becomes a powerful tool for automating access control. + +As an example, Tagging PDP uses resource mappings to map resources based on the terms found within +the metadata and documents which are sent to it. Combined with the resource mappings it can then +determine which attributes should be applied to the TDF and return those attributes to the PEP. diff --git a/otdfctl/docs/man/policy/resource-mappings/create.md b/otdfctl/docs/man/policy/resource-mappings/create.md new file mode 100644 index 0000000000..f64563446c --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mappings/create.md @@ -0,0 +1,33 @@ +--- +title: Create a resource mapping +command: + name: create + aliases: + - add + - new + - c + flags: + - name: attribute-value-id + description: The ID of the attribute value to map to the resource. + default: '' + - name: terms + description: The synonym terms to match for the resource mapping. + default: '' + - name: group-id + description: The ID of the resource mapping group to assign this mapping to + default: '' + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Associate an attribute value with a set of plaintext string terms. + +For more information about resource mappings, see the `resource-mappings` subcommand. + +## Examples + +```shell +otdfctl policy resource-mappings create --attribute-value-id 891cfe85-b381-4f85-9699-5f7dbfe2a9ab --terms term1,term2 --group-id 3ff446fb-8fb1-4c04-8023-47592c90370c +``` diff --git a/otdfctl/docs/man/policy/resource-mappings/delete.md b/otdfctl/docs/man/policy/resource-mappings/delete.md new file mode 100644 index 0000000000..3640325b14 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mappings/delete.md @@ -0,0 +1,19 @@ +--- +title: Delete a resource mapping +command: + name: delete + flags: + - name: id + description: The ID of the resource mapping to delete + default: '' + - name: force + description: Force deletion without interactive confirmation (dangerous) +--- + +For more information about resource mappings, see the `resource-mappings` subcommand. + +## Examples + +```shell +otdfctl policy resource-mappings delete --id=3ff446fb-8fb1-4c04-8023-47592c90370c +``` diff --git a/otdfctl/docs/man/policy/resource-mappings/get.md b/otdfctl/docs/man/policy/resource-mappings/get.md new file mode 100644 index 0000000000..abc4a0fbb8 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mappings/get.md @@ -0,0 +1,19 @@ +--- +title: Get a resource mapping +command: + name: get + aliases: + - g + flags: + - name: id + description: The ID of the resource mapping to get. + default: '' +--- + +For more information about resource mappings, see the `resource-mappings` subcommand. + +## Examples + +```shell +otdfctl policy resource-mappings get --id=3ff446fb-8fb1-4c04-8023-47592c90370c +``` diff --git a/otdfctl/docs/man/policy/resource-mappings/list.md b/otdfctl/docs/man/policy/resource-mappings/list.md new file mode 100644 index 0000000000..402865bad6 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mappings/list.md @@ -0,0 +1,22 @@ +--- +title: List resource mappings +command: + name: list + aliases: + - l + flags: + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +For more information about resource mappings, see the `resource-mappings` subcommand. + +## Examples + +```shell +otdfctl policy resource-mappings get --id=3ff446fb-8fb1-4c04-8023-47592c90370c +``` diff --git a/otdfctl/docs/man/policy/resource-mappings/update.md b/otdfctl/docs/man/policy/resource-mappings/update.md new file mode 100644 index 0000000000..9d38812a10 --- /dev/null +++ b/otdfctl/docs/man/policy/resource-mappings/update.md @@ -0,0 +1,37 @@ +--- +title: Update a resource mapping +command: + name: update + aliases: + - u + flags: + - name: id + description: The ID of the resource mapping to update. + default: '' + - name: attribute-value-id + description: The ID of the attribute value to map to the resource. + default: '' + - name: terms + description: The synonym terms to match for the resource mapping. + default: '' + - name: group-id + description: The ID of the resource mapping group to assign this mapping to + default: '' + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Alter the attribute value associated with a resource mapping's terms, change its group, or fully replace the terms in a given resource mapping. + +For more information about resource mappings, see the `resource-mappings` subcommand. + +## Examples + +```shell +otdfctl policy resource-mappings update --id=3ff446fb-8fb1-4c04-8023-47592c90370c --terms newterm1,newterm2 +``` diff --git a/otdfctl/docs/man/policy/subject-condition-sets/_index.md b/otdfctl/docs/man/policy/subject-condition-sets/_index.md new file mode 100644 index 0000000000..b6f6717886 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-condition-sets/_index.md @@ -0,0 +1,21 @@ +--- +title: Subject condition sets +command: + name: subject-condition-sets + aliases: + - subcs + - scs + - subject-condition-set +--- + +Subject Condition Sets (SCSs) are the logical resolvers of entitlement to attributes. + +An SCS contains AND/OR groups of conditions with IN/NOT_IN/CONTAINS logic to be applied against +a Subject Entity Representation as either their OIDC Access Token claims or the platform's Entity +Resolution Service (ERS). + +They are applied to Attribute Values via Subject Mappings to determine a Subject's entitlement to +any given attribute on TDF'd data. + +For example structure and logical resolution, see `create` subcommand. For information about Subject +Mappings, see the `subject-mappings` command. diff --git a/otdfctl/docs/man/policy/subject-condition-sets/create.md b/otdfctl/docs/man/policy/subject-condition-sets/create.md new file mode 100644 index 0000000000..15df104087 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-condition-sets/create.md @@ -0,0 +1,135 @@ +--- +title: Create a Subject Condition Set + +command: + name: create + aliases: + - c + - add + - new + flags: + - name: subject-sets + description: A JSON array of subject sets, containing a list of condition groups, each with one or more conditions + shorthand: s + required: true + default: '' + - name: subject-sets-file-json + description: A JSON file with path from the current working directory containing an array of subject sets + shorthand: j + default: '' + required: false + - name: namespace + description: Namespace ID or FQN + shorthand: n + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +### Example Subject Condition Sets + +`--subject-sets` example input: + +```json +[ + { + "condition_groups": [ + { + "conditions": [ + { + "operator": 1, + "subject_external_values": ["CoolTool", "RadService", "ShinyThing"], + "subject_external_selector_value": ".team.name" + }, + { + "operator": 2, + "subject_external_values": ["marketing"], + "subject_external_selector_value": ".org.name" + } + ], + "boolean_operator": 1 + } + ] + } +] +``` + +ConditionGroup `boolean_operator` is driven through the API `CONDITION_BOOLEAN_TYPE_ENUM` definition: + +| CONDITION_BOOLEAN_TYPE_ENUM | index value | comparison | +| --------------------------- | ----------- | --------------------- | +| AND | 1 | all conditions met | +| OR | 2 | any one condition met | + +Condition `operator` is driven through the API `SUBJECT_MAPPING_OPERATOR_ENUM` definition, +and is evaluated by applying the `subject_external_selector_value` to the Subject entity +representation (token or Entity Resolution Service response) and comparing the logical operator +against the list of `subject_external_values`: + +| SUBJECT_MAPPING_OPERATOR_ENUM | index value | subject value at selector MUST | +| ----------------------------- | ----------- | ------------------------------ | +| IN | 1 | be any of the values | +| NOT_IN | 2 | not be any of the values | +| IN_CONTAINS | 3 | contain one of the values | + +In the example SCS above, the Subject entity MUST BE represented with a token claim or ERS response +containing a field at `.team.name` identifying them as team name "CoolTool", "RadService", or "ShinyThing", AND THEY MUST ALSO have a field `org.name` that is NOT "marketing". + +This structure if their team name was "CoolTool" and they were entitled might look like: + +```json +{ + "team": { + "name": "CoolTool" // could alternatively be RadService or ShinyThing + }, + "org": { + "name": "sales" + } +} +``` + +If any condition in the group is not met (such as if `.org.name` were `marketing` instead), +the condition set would not resolve to true, and the Subject would not be found to be entitled +to the Attribute Value applicable to this Subject Condition Set via Subject Mapping between. + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Examples + +The following subject condition set would resolve to true if the field at `.example.field.one` is +`myvalue` or `myothervalue1`, or the field at `.example.field.two` is not equal to `notpresentvalue`. +```shell +otdfctl policy subject-condition-set create --subject-sets '[ + { + "condition_groups": [ + { + "conditions": [ + { + "operator": 1, + "subject_external_values": ["myvalue", "myothervalue"], + "subject_external_selector_value": ".example.field.one" + }, + { + "operator": 2, + "subject_external_values": ["notpresentvalue"], + "subject_external_selector_value": ".example.field.two" + } + ], + "boolean_operator": 2 + } + ] + } +]' +``` + +You can perform the same action with the input contained in a file: +```shell +otdfctl policy subject-condition-set create --subject-sets-file-json scs.json + +# Namespaced subject condition set creation +otdfctl policy subject-condition-set create --subject-sets-file-json scs.json --namespace "https://example.com" +``` diff --git a/otdfctl/docs/man/policy/subject-condition-sets/delete.md b/otdfctl/docs/man/policy/subject-condition-sets/delete.md new file mode 100644 index 0000000000..3f269b9321 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-condition-sets/delete.md @@ -0,0 +1,21 @@ +--- +title: Delete a Subject Condition Set + +command: + name: delete + flags: + - name: id + description: The ID of the subject condition set to delete + shorthand: i + required: true + - name: force + description: Force deletion without interactive confirmation (dangerous) +--- + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Example + +```shell +otdfctl policy subject-condition-sets delete --id=bfade235-509a-4a6f-886a-812005c01db5 +``` diff --git a/otdfctl/docs/man/policy/subject-condition-sets/get.md b/otdfctl/docs/man/policy/subject-condition-sets/get.md new file mode 100644 index 0000000000..fefb284218 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-condition-sets/get.md @@ -0,0 +1,21 @@ +--- +title: Get a Subject Condition Set + +command: + name: get + aliases: + - g + flags: + - name: id + description: The ID of the subject condition set to get + shorthand: i + required: true +--- + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Example + +```shell +otdfctl policy subject-condition-sets get --id=bfade235-509a-4a6f-886a-812005c01db5 +``` diff --git a/otdfctl/docs/man/policy/subject-condition-sets/list.md b/otdfctl/docs/man/policy/subject-condition-sets/list.md new file mode 100644 index 0000000000..794fb17306 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-condition-sets/list.md @@ -0,0 +1,64 @@ +--- +title: List Subject Condition Set + +command: + name: list + aliases: + - l + flags: + - name: namespace + shorthand: n + description: Namespace ID or FQN to filter results + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc +--- + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy subject-condition-sets list --sort created_at +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy subject-condition-sets list --order asc +``` + +## Example + +```shell +otdfctl policy subject-condition-sets list + +otdfctl policy subject-condition-sets list --namespace https://example.com +``` + +Sort subject condition sets by creation time ascending: + +```shell +otdfctl policy subject-condition-sets list --sort created_at --order asc +``` diff --git a/otdfctl/docs/man/policy/subject-condition-sets/prune.md b/otdfctl/docs/man/policy/subject-condition-sets/prune.md new file mode 100644 index 0000000000..45806e7b16 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-condition-sets/prune.md @@ -0,0 +1,19 @@ +--- +title: Prune (delete all un-mapped Subject Condition Sets) + +command: + name: prune + flags: + - name: force + description: Force prune without interactive confirmation (dangerous) +--- + +This command will delete all Subject Condition Sets that are not utilized within any Subject Mappings and are therefore 'stranded'. + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Example + +```shell +otdfctl policy subject-condition-set prune +``` diff --git a/otdfctl/docs/man/policy/subject-condition-sets/update.md b/otdfctl/docs/man/policy/subject-condition-sets/update.md new file mode 100644 index 0000000000..f82783f687 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-condition-sets/update.md @@ -0,0 +1,61 @@ +--- +title: Update a Subject Condition Set + +command: + name: update + aliases: + - u + flags: + - name: id + description: The ID of the subject condition set to update + shorthand: i + required: true + - name: subject-sets + description: A JSON array of subject sets, containing a list of condition groups, each with one or more conditions + shorthand: s + default: '' + - name: subject-sets-file-json + description: A JSON file with path from the current working directory containing an array of subject sets + shorthand: j + default: '' + required: false + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Replace the existing conditional logic within an SCS with new conditional logic, passing either JSON directly or a JSON file. + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Example + +This updates the boolean_operator of the subject condition set created in the `create` example. The following subject condition set would resolve to true if the field at `.example.field.one` is +`myvalue` or `myothervalue` AND the field at `.example.field.two` is not equal to `notpresentvalue`. +```shell +otdfctl policy subject-condition-set update --id bfade235-509a-4a6f-886a-812005c01db5 --subject-sets '[ + { + "condition_groups": [ + { + "conditions": [ + { + "operator": 1, + "subject_external_values": ["myvalue", "myothervalue"], + "subject_external_selector_value": ".example.field.one" + }, + { + "operator": 2, + "subject_external_values": ["notpresentvalue"], + "subject_external_selector_value": ".example.field.two" + } + ], + "boolean_operator": 1 + } + ] + } +]' +``` diff --git a/otdfctl/docs/man/policy/subject-mappings/_index.md b/otdfctl/docs/man/policy/subject-mappings/_index.md new file mode 100644 index 0000000000..6ba912a9c3 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-mappings/_index.md @@ -0,0 +1,25 @@ +--- +title: Subject mappings +command: + name: subject-mappings + aliases: + - subm + - sm + - submap + - subject-mapping +--- + +Subject Mappings are the policy mechanism used to entitle Entities to take Actions on Attribute Values. + +In a TDF flow, the resource data is associated to Attribute Values within the TDF manifest policy, +and a Subject Mapping links a given entity (user, principal) to entitled Action(s) on an Attribute Value. + +A Subject Mapping (SM) relates: + 1. one Subject Condition Set (SCS, see `subject-condition-sets` command) + 2. one or more Actions (see `actions` command) + 3. one Attribute Value (see `attributes values` command) + +Within ABAC entitlement decisioning, the principal/agent/user/subject is known via an Entity Representation +provided by the Entity Resolution Service and identity provider, and that Entity Representation is logically +resolved against the Subject Mapping's contained Subject Condition set such that if it is logically true, +the entity is considered entitled to the contained Actions on the contained Attribute Value. diff --git a/otdfctl/docs/man/policy/subject-mappings/create.md b/otdfctl/docs/man/policy/subject-mappings/create.md new file mode 100644 index 0000000000..8349fdca0b --- /dev/null +++ b/otdfctl/docs/man/policy/subject-mappings/create.md @@ -0,0 +1,95 @@ +--- +title: Create a new subject mapping +command: + name: create + aliases: + - new + - add + - c + flags: + - name: attribute-value-id + description: The ID of the attribute value to map to a subject condition set + shorthand: a + required: true + - name: action + description: Each 'id' or 'name' of an Action to be entitled (i.e. 'create', 'read', 'update', 'delete') + - name: subject-condition-set-id + description: Known preexisting Subject Condition Set Id + - name: subject-condition-set-new + description: JSON array of Subject Sets to create a new Subject Condition Set associated with the created Subject Mapping + - name: namespace + description: Namespace ID or FQN + shorthand: n + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + - name: action-standard + description: Deprecated. Migrated to '--action'. + shorthand: s + - name: action-custom + description: Deprecated. Migrated to '--action'. + shorthand: c +--- + +Create a Subject Mapping to entitle an entity (via an existing or new Subject Condition Set) to Action(s) +on an Attribute Value. + +Subject Mappings may entitle Actions with standard names ('create', 'read', 'update', 'delete'), custom names, +or by their stored 'id' within policy. If the referenced Action name does not already exist within policy, +it will be created along with the new Subject Mapping. + +For more information about actions, see the `actions` subcommand. + +For more information about subject mappings, see the `subject-mappings` subcommand. + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Namespacing subject mappings + +The following rules must be applied when attempting to namespace a subject mapping: + +- Either all policy constructs (action, subject mappings, subject condition set, attribute value) are within the same + namespace +- Subject mapping, subject condition set, action are all not within a namespace. + +You cannot, for example: + +- Create a subject mapping that is not within the same namespace as an action that is passed in + +## Examples + +Create a subject mapping for a 'read' action linking to an existing subject condition set: +```shell +otdfctl policy subject-mapping create --attribute-value-id 891cfe85-b381-4f85-9699-5f7dbfe2a9ab --action read --subject-condition-set-id 8dc98f65-5f0a-4444-bfd1-6a818dc7b447 +``` + +Or you can create a mapping for 'read' or 'create' linking to a new subject condition set: +```shell +otdfctl policy subject-mapping create --attribute-value-id 891cfe85-b381-4f85-9699-5f7dbfe2a9ab --action create --action update --subject-condition-set-new '[ + { + "condition_groups": [ + { + "conditions": [ + { + "operator": 1, + "subject_external_values": ["myvalue", "myothervalue"], + "subject_external_selector_value": ".example.field.one" + }, + { + "operator": 2, + "subject_external_values": ["notpresentvalue"], + "subject_external_selector_value": ".example.field.two" + } + ], + "boolean_operator": 2 + } + ] + } +]' +``` + +Create a subject mapping under a namespace + +```shell +otdfctl policy subject-mapping create --attribute-value-id 891cfe85-b381-4f85-9699-5f7dbfe2a9ab --action read --subject-condition-set-id 8dc98f65-5f0a-4444-bfd1-6a818dc7b447 --namespace "https://example.com" +``` diff --git a/otdfctl/docs/man/policy/subject-mappings/delete.md b/otdfctl/docs/man/policy/subject-mappings/delete.md new file mode 100644 index 0000000000..1a00549d88 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-mappings/delete.md @@ -0,0 +1,25 @@ +--- +title: Delete a subject mapping by id +command: + name: delete + flags: + - name: id + description: The ID of the subject mapping to delete + shorthand: i + required: true + default: '' + - name: force + description: Force deletion without interactive confirmation (dangerous) +--- + +Delete a Subject Mapping to remove entitlement of an entity (via Subject Condition Set) to an Attribute Value. + +For more information about subject mappings, see the `subject-mappings` subcommand. + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Example + +```shell +otdfctl policy subject-mappings delete --id d71c4028-ce64-453b-8aa7-6edb45fbb848 +``` diff --git a/otdfctl/docs/man/policy/subject-mappings/get.md b/otdfctl/docs/man/policy/subject-mappings/get.md new file mode 100644 index 0000000000..bc52c6f2ba --- /dev/null +++ b/otdfctl/docs/man/policy/subject-mappings/get.md @@ -0,0 +1,21 @@ +--- +title: Get a subject mapping +command: + name: get + aliases: + - g + flags: + - name: id + description: The ID of the subject mapping to get + shorthand: i + required: true + default: '' +--- + +Retrieve the specifics of a Subject Mapping. + +For more information about subject mappings, see the `subject-mappings` subcommand. + +```shell +otdfctl policy subject-mappings get --id 39866dd2-368b-41f6-b292-b4b68c01888b +``` diff --git a/otdfctl/docs/man/policy/subject-mappings/list.md b/otdfctl/docs/man/policy/subject-mappings/list.md new file mode 100644 index 0000000000..359b3915cc --- /dev/null +++ b/otdfctl/docs/man/policy/subject-mappings/list.md @@ -0,0 +1,63 @@ +--- +title: List subject mappings +command: + name: list + aliases: + - l + flags: + - name: namespace + shorthand: n + description: Namespace ID or FQN to filter results + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc +--- + +For more information about subject mappings, see the `subject-mappings` subcommand. + +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy subject-mappings list --sort created_at +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy subject-mappings list --order asc +``` + +## Example + +```shell +otdfctl policy subject-mappings list + +otdfctl policy subject-mappings list --namespace "https://example.com" +``` + +Sort subject mappings by creation time ascending: + +```shell +otdfctl policy subject-mappings list --sort created_at --order asc +``` diff --git a/otdfctl/docs/man/policy/subject-mappings/match.md b/otdfctl/docs/man/policy/subject-mappings/match.md new file mode 100644 index 0000000000..4791b19a57 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-mappings/match.md @@ -0,0 +1,41 @@ +--- +title: Match a subject or set of selectors to relevant subject mappings +command: + name: match + flags: + - name: subject + shorthand: s + description: A Subject Entity Representation string (JSON or JWT, auto-detected) + default: '' + - name: selector + shorthand: x + description: "Individual selectors (i.e. '.department' or '.realm_access.roles[]') that may be found in SubjectConditionSets" +--- + +This tool queries platform policies for relevant Subject Mappings using either an Entity Representation or specific selectors. + +If an Entity Representation is provided via `--subject` (such as an OIDC JWT or JSON response from an Entity Resolution Service), the tool +parses all valid selectors and checks for matching Subject Condition Sets in Subject Mappings to Attribute Values. + +If selectors are provided directly with `--selector`, the tool searches for Subject Mappings with Subject Condition Sets that contain those selectors. + +## Examples + +Various ways to invoke the `match` command to query Subject Mappings to Attribute Values with relevant Subject Condition Sets. + +```shell +# matches either org name or department selectors +otdfctl policy subject-mappings match --selector '.org.name' --selector '.department' + +# parses subject entity representation as JSON and matches any selector (with this subject only '.emailAddress') +otdfctl policy subject-mappings match --subject '{"emailAddress":"user@email.com"}' + +# parses entity representation as JWT into all possicle claim selectors and matches any of them +otdfctl policy subject-mappings match --subject 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' +``` + +> [!NOTE] +> The values of the selectors and any `IN`/`NOT_IN`/`IN_CONTAINS` logic of Subject Condition Sets is irrelevant to this command. +> Evaluation of any matched conditions is handled by the Authorization Service to determine entitlements. This command +> is specifically for management of policy - to facilitate lookup of current conditions driven by known selectors as a +> precondition for administration of entitlement given the logical _operators_ of the matched conditions and their relations. diff --git a/otdfctl/docs/man/policy/subject-mappings/update.md b/otdfctl/docs/man/policy/subject-mappings/update.md new file mode 100644 index 0000000000..602544b659 --- /dev/null +++ b/otdfctl/docs/man/policy/subject-mappings/update.md @@ -0,0 +1,45 @@ +--- +title: Update a subject mapping +command: + name: update + aliases: + - u + flags: + - name: id + description: The ID of the subject mapping to update + shorthand: i + required: true + - name: action + description: Each 'id' or 'name' of an Action to be entitled (i.e. 'create', 'read', 'update', 'delete') + - name: action-standard + description: Deprecated. Migrated to '--action'. + shorthand: s + - name: action-custom + description: Deprecated. Migrated to '--action'. + shorthand: c + - name: subject-condition-set-id + description: Known preexisting Subject Condition Set Id + required: false + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Update a Subject Mapping to alter entitlement of an entity to an Attribute Value. + +`Actions` are updated in place, destructively replacing the current set. If you want to add or remove actions, you must provide the full set of actions on update. + +At this time, creation of a new SCS during update of a subject mapping is not supported. + +For more information about subject mappings, see the `subject-mappings` subcommand. + +For more information about subject condition sets, see the `subject-condition-sets` subcommand. + +## Example + +```shell +otdfctl policy subject-mappings update --id 39866dd2-368b-41f6-b292-b4b68c01888b --action read +``` diff --git a/otdfctl/e2e/action.yaml b/otdfctl/e2e/action.yaml new file mode 100644 index 0000000000..b95a21027c --- /dev/null +++ b/otdfctl/e2e/action.yaml @@ -0,0 +1,94 @@ +name: 'end-to-end' +description: 'Run end-to-end tests for the otdfctl CLI' +inputs: + testrail-run-name-for-cli-test: + required: false + description: 'The name to use for the TestRail test run created for the CLI tests' + default: '' + +runs: + using: 'composite' + steps: + # Build the CLI and run tests + - name: Set up Go + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + with: + go-version-file: go.work + - name: Build the CLI + shell: bash + run: go build . + working-directory: otdfctl + - name: Build the CLI in test mode + shell: bash + run: make build-test + working-directory: otdfctl + - name: Build v0.26.2 CLI for keyring profiles + shell: bash + working-directory: otdfctl + run: | + git worktree add ../otdfctl_v0.26.2 otdfctl/v0.26.2 + cd ../otdfctl_v0.26.2 + GOWORK=off go build -o ../otdfctl/otdfctl_v0.26.2 . + echo "LEGACY_OTDFCTL_BIN=./otdfctl_v0.26.2" >> $GITHUB_ENV + - name: Install keyring dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y gnome-keyring + working-directory: otdfctl + - name: Setup Bats and bats libs + uses: bats-core/bats-action@2.0.0 + - name: Unlock Gnome keyring + shell: bash + run: | + # This is a fake dummy password, that is not used + # anywhere else in the CI, but for bats testing + echo "somecredstorepass" | gnome-keyring-daemon --unlock + - name: Run Bats tests + shell: bash + working-directory: otdfctl + run: | + # Run the namespaced-policy migration suite separately before the rest + # of the e2e directory. Those tests discover legacy/global objects by + # scope and are not safe to overlap with the parallel remainder of the + # suite while other files still create unnamespaced policy fixtures. + bats --tap e2e --filter-tags namespaced_policy_migration | tee e2e/bats-results.tap + + if command -v parallel >/dev/null 2>&1; then + echo "GNU parallel found, running remaining tests in parallel" + bats --tap e2e --filter-tags '!namespaced_policy_migration' --jobs 4 --no-parallelize-within-files --no-tempdir-cleanup | tee -a e2e/bats-results.tap + else + echo "GNU parallel not found, running remaining tests sequentially" + bats --tap e2e --filter-tags '!namespaced_policy_migration' | tee -a e2e/bats-results.tap + fi + env: + # Define 'bats' install location in ubuntu + BATS_LIB_PATH: /usr/lib + # Terminal width for testing printed output + TEST_TERMINAL_WIDTH: 200 + - name: Upload Bats TAP results + if: always() # ensures test report is uploaded even if tests fail + uses: actions/upload-artifact@v4 + with: + name: bats-test-results + path: otdfctl/e2e/bats-results.tap + - name: Integrate Bats test results into TestRail + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + shell: bash + working-directory: otdfctl + run: | + cd e2e + cp ./testrail-integration/samples-for-virtru-instance/testrail-virtru.config.json ./testrail-integration/testrail.config.json + cp ./testrail-integration/samples-for-virtru-instance/testname-to-testrail-id.virtru.json ./testrail-integration/testname-to-testrail-id.json + ./testrail-integration/upload-bats-test-results-to-testrail.sh + continue-on-error: true + env: + TESTRAIL_USER: ${{ env.TESTRAIL_USER }} + TESTRAIL_PASS: ${{ env.TESTRAIL_PASS }} + TESTRAIL_CLI_RUN_NAME: ${{ inputs.testrail-run-name-for-cli-test }} + - name: Upload TestRail mapping report + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: test-cases-mapping-report + path: otdfctl/e2e/mapping-report.txt diff --git a/otdfctl/e2e/actions.bats b/otdfctl/e2e/actions.bats new file mode 100644 index 0000000000..c4aa90615a --- /dev/null +++ b/otdfctl/e2e/actions.bats @@ -0,0 +1,239 @@ +#!/usr/bin/env bats + +# Tests for actions + +setup_file() { + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + export ACTION_NAMESPACE_NAME='test-act.org' + export ACTION_NAMESPACE="https://$ACTION_NAMESPACE_NAME" + # create namespace first (needed for action creation) + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create --name "$ACTION_NAMESPACE_NAME" --json | jq -r '.id') +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_action () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy actions $*" + } +} + +teardown_file() { + # clear out all test env vars + # remove the namespace and cascade delete attributes and values used in registered resource values tests + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$NS_ID" --force + unset HOST WITH_CREDS ACTION_NAMESPACE ACTION_NAMESPACE_NAME NS_ID +} + + +@test "Create a new custom action - Good" { + # with a namespace + run_otdfctl_action create --name test_action_create_namespaced --namespace "$ACTION_NAMESPACE" + assert_output --partial "SUCCESS" + assert_line --regexp "Name.*test_action_create_namespaced" + assert_line --regexp "Namespace.*$ACTION_NAMESPACE" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + + # cleanup + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_action delete --id $created_id --force + + # without a namespace (should default to un-namespaced) + run_otdfctl_action create --name test_action_create + assert_output --partial "SUCCESS" + assert_line --regexp "Name.*test_action_create" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + # ensure namespace is empty for un-namespaced actions + refute_line --regexp "Namespace.*$ACTION_NAMESPACE" + + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_action delete --id $created_id --force +} + +@test "Create a new action - Bad" { + # bad action names + run_otdfctl_action create --name ends_underscored_ --namespace "$ACTION_NAMESPACE" + assert_failure + run_otdfctl_action create --name -first-char-hyphen --namespace "$ACTION_NAMESPACE" + assert_failure + run_otdfctl_action create --name inval!d.chars --namespace "$ACTION_NAMESPACE" + assert_failure + + # missing flag + run_otdfctl_action create --namespace "$ACTION_NAMESPACE" + assert_failure + assert_output --partial "Flag '--name' is required" + + # TODO: re-enable when namespace is required + # run_otdfctl_action create --name no_namespace + # assert_failure + # assert_output --partial "Flag '--namespace' is required" + + # conflict + run_otdfctl_action create -n "read" --namespace "$ACTION_NAMESPACE" + assert_failure + assert_output --partial "intended action would violate a restriction" + + run_otdfctl_action create -n "read" + assert_failure + assert_output --partial "intended action would violate a restriction" + + # duplicate custom action + run_otdfctl_action create --name test_action_conflict --namespace "$ACTION_NAMESPACE" --json + assert_success + conflict_action_id=$(echo "$output" | jq -er '.id') + assert_success + [ -n "$conflict_action_id" ] + + run_otdfctl_action create --name test_action_conflict --namespace "$ACTION_NAMESPACE" + assert_failure + assert_output --partial "already_exists" + + # cleanup + run_otdfctl_action delete --id "$conflict_action_id" --force +} + +@test "Get an action - Good" { + run_otdfctl_action get --name "read" --namespace "$ACTION_NAMESPACE" + assert_success + assert_line --partial "Id" + assert_line --regexp "Name.*read" + assert_line --regexp "Namespace.*$ACTION_NAMESPACE" + + # get by name to retrieve the ID + UPDATE_ACTION_ID=$(./otdfctl policy actions get --name update --namespace "$ACTION_NAMESPACE" --json $HOST $WITH_CREDS | jq -r '.id') + + # ensure getting by id does not require namespace + run_otdfctl_action get --id "$UPDATE_ACTION_ID" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$UPDATE_ACTION_ID" ] + [ "$(echo "$output" | jq -r '.name')" = "update" ] + + # ensure you can use the namespace id instead of the fqn + run_otdfctl_action get --name "read" --namespace "$NS_ID" + assert_success + assert_line --partial "Id" + assert_line --regexp "Name.*read" + assert_line --regexp "Namespace.*$ACTION_NAMESPACE" + + # ensure get without namespace still works for un-namespaced actions + run_otdfctl_action get --name "read" + assert_success + assert_line --partial "Id" + assert_line --regexp "Name.*read" + refute_line --regexp "Namespace.*$ACTION_NAMESPACE" +} + +@test "Get an action - Bad" { + run_otdfctl_action get + assert_failure + assert_output --partial "Either 'id' or 'name' must be provided" + + run_otdfctl_action get --id 'testing_get' + assert_failure + assert_output --partial "must be a valid UUID" + + # TODO: re-enable when namespace is required + # run_otdfctl_action get --name 'testing_get' + # assert_failure + # assert_output --partial "namespace' must be provided when using 'name'" +} + +@test "List actions" { + run_otdfctl_action create --name test_action_list_namespaced --namespace "$ACTION_NAMESPACE" --json + assert_success + created_id=$(echo "$output" | jq -r '.id') + run_otdfctl_action create --name test_action_list_unnamespaced --json + assert_success + created_id_2=$(echo "$output" | jq -r '.id') + + run_otdfctl_action list --namespace "$ACTION_NAMESPACE" + assert_output --partial "Namespace" + assert_output --partial "$ACTION_NAMESPACE" + assert_output --partial "create" + assert_output --partial "read" + assert_output --partial "update" + assert_output --partial "delete" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + assert_output --partial "test_action_list_namespaced" + refute_output --partial "test_action_list_unnamespaced" + + run_otdfctl_action list --namespace "$ACTION_NAMESPACE" --json + assert_success + assert_not_equal $(echo "$output" | jq -r 'pagination') "null" + assert_output --partial "create" + assert_output --partial "read" + assert_output --partial "update" + assert_output --partial "delete" + total=$(echo "$output" | jq -r '.pagination.total') + [[ "$total" -ge 1 ]] + + # listing without namespace should succeed and should include both namespaced and un-namespaced actions (namespace field should be empty for un-namespaced actions) + run_otdfctl_action list + assert_output --partial "create" + assert_output --partial "read" + assert_output --partial "update" + assert_output --partial "delete" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + assert_output --partial "test_action_list_namespaced" + assert_output --partial "test_action_list_unnamespaced" + + run_otdfctl_action delete --id $created_id --force + run_otdfctl_action delete --id $created_id_2 --force +} + +@test "Update action" { + ACTION_TO_UPDATE=$(./otdfctl policy actions create --name testing_updation --namespace "$ACTION_NAMESPACE" $HOST $WITH_CREDS --json | jq -r '.id') + # extend labels + run_otdfctl_action update --id "$ACTION_TO_UPDATE" -l key=value --label test=true + assert_success + assert_line --regexp "Id.*$ACTION_TO_UPDATE" + assert_line --regexp "Name.*testing_updation" + assert_line --regexp "Namespace.*$ACTION_NAMESPACE" + assert_line --regexp "Labels.*key: value" + assert_line --regexp "Labels.*test: true" + + # force replace labels + run_otdfctl_action update --id "$ACTION_TO_UPDATE" -l key=other --force-replace-labels + assert_success + assert_line --regexp "Id.*$ACTION_TO_UPDATE" + assert_line --regexp "Name.*testing_updation" + assert_line --regexp "Namespace.*$ACTION_NAMESPACE" + assert_line --regexp "Labels.*key: other" + refute_output --regexp "Labels.*key: value" + refute_output --regexp "Labels.*test: true" + refute_output --regexp "Labels.*test: true" + + # renamed + run_otdfctl_action update --id "$ACTION_TO_UPDATE" --name updated_action_in_test + assert_success + assert_line --regexp "Id.*$ACTION_TO_UPDATE" + assert_line --regexp "Name.*updated_action_in_test" + assert_line --regexp "Namespace.*$ACTION_NAMESPACE" + refute_output --regexp "Name.*testing_updation" + + # clean up + run_otdfctl_action delete --id "$ACTION_TO_UPDATE" --force +} + +@test "Delete action - bad" { + STANDARD_ACTION=$(./otdfctl policy actions get --name update --namespace "$ACTION_NAMESPACE" $HOST $WITH_CREDS --json | jq -r '.id') + run_otdfctl_action delete --id "$STANDARD_ACTION" --force + assert_failure +} + +@test "Delete action - good" { + DELETABLE_ACTION=$(./otdfctl policy actions create --name testing-delete --namespace "$ACTION_NAMESPACE" $HOST $WITH_CREDS --json | jq -r '.id') + run_otdfctl_action delete --id "$DELETABLE_ACTION" --force + assert_success + assert_line --regexp "Namespace.*$ACTION_NAMESPACE" +} diff --git a/otdfctl/e2e/attributes.bats b/otdfctl/e2e/attributes.bats new file mode 100755 index 0000000000..5d0680d7e9 --- /dev/null +++ b/otdfctl/e2e/attributes.bats @@ -0,0 +1,550 @@ +#!/usr/bin/env bats + +# Tests for attributes + +setup_file() { + bats_load_library bats-support + bats_load_library bats-assert + load "otdfctl-utils.sh" + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # Create the namespace to be used by other tests + + export NS_NAME="testing-attr.co" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$NS_NAME" --json | jq -r '.id') + + export KAS_URI="https://test-kas-for-attributes.com" + export KAS_REG_ID=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --uri "$KAS_URI" --json | jq -r '.id') + # Generate a valid RSA public key and base64 encode (single-line) + export PEM_B64=$(openssl genrsa 2048 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n') + export KAS_KEY_ID="test-key-for-attr" + export KAS_KEY_SYSTEM_ID=$(./otdfctl $HOST $WITH_CREDS policy kas-registry key create --kas "$KAS_REG_ID" --key-id "$KAS_KEY_ID" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json | jq -r '.key.id') + export PEM=$(echo "$PEM_B64" | base64 -d) +} + +# always create a randomly named attribute +setup() { + bats_load_library bats-support + bats_load_library bats-assert + # invoke binary with credentials + run_otdfctl_attr() { + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes $*" + } + + export ATTR_NAME_RANDOM=$(LC_ALL=C tr -dc 'a-zA-Z' = 10 attribute definitions + for i in {1..10}; do + random_name=$(LC_ALL=C tr -dc 'A-Za-z0-9' bad_creds.json + BAD_CREDS="--with-client-creds-file ./bad_creds.json" + run_otdfctl $HOST $BAD_CREDS policy attributes list + assert_failure + assert_output --partial "Failed to authenticate with flag-provided client credentials" + + # malformed JSON + BAD_CREDS="--with-client-creds '{clientId:"badClient",clientSecret:"badSecret"}'" + run_otdfctl $HOST $BAD_CREDS policy attributes list + assert_failure + assert_output --partial "Failed to get client credentials" +} + +@test "helpful error if missing client credentials" { + run_otdfctl $HOST policy attributes list + assert_failure + assert_output --partial "One of" + assert_output --partial "must be set: when using global flags" +} + +@test "helpful error if missing host" { + run_otdfctl $WITH_CREDS policy attributes list + assert_failure + assert_output --partial "Host must be set: when using global flags" +} \ No newline at end of file diff --git a/otdfctl/e2e/encrypt-decrypt.bats b/otdfctl/e2e/encrypt-decrypt.bats new file mode 100755 index 0000000000..32b336e676 --- /dev/null +++ b/otdfctl/e2e/encrypt-decrypt.bats @@ -0,0 +1,326 @@ +#!/usr/bin/env bats + +# Tests for encrypt decrypt + +setup_file() { + + # TODO: Remove this file-level skip once otdfctl passes namespace flags for the namespaced action and subject mapping APIs used by encrypt/decrypt entitlement setup. + skip "Temporarily disabled [namespaced-subject-mappings]: encrypt/decrypt BATS setup still depends on pre-namespace subject mapping APIs" + + export CREDSFILE=creds.json + echo -n '{"clientId":"opentdf","clientSecret":"secret"}' > $CREDSFILE + export WITH_CREDS="--with-client-creds-file $CREDSFILE" + export DEBUG_LEVEL="--log-level debug" + export HOST=http://localhost:8080 + + export INFILE_GO_MOD=go.mod + export OUTFILE_GO_MOD=go.mod.tdf + export RESULTFILE_GO_MOD=result.mod + export SESSION_KEY_ALGORITHM=ec:secp256r1 + export WRAPPING_KEY_ALGORITHM=ec:secp256r1 + + export SECRET_TEXT="my special secret" + export OUT_TXT=secret.txt + export OUTFILE_TXT=secret.txt.tdf + + NS_ID=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes namespaces create -n "testing-enc-dec.io" --json | jq -r '.id') + ATTR_ID=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes create --namespace "$NS_ID" -n attr1 -r ALL_OF --json | jq -r '.id') + VAL_ID=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes values create --attribute-id "$ATTR_ID" -v value1 --json | jq -r '.id') + ATTR_OBL_VAL_OUTPUT=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes values create --attribute-id "$ATTR_ID" -v test_attr_obligation_value --json) + export ATTR_OBL_VAL_ID=$(echo $ATTR_OBL_VAL_OUTPUT | jq -r '.id') + export ATTR_OBL_VAL_FQN=$(echo $ATTR_OBL_VAL_OUTPUT | jq -r '.fqn') + + # Create obligations + OBL_ID=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy obligations create -n test_obligation -s "$NS_ID" --json | jq -r '.id') + OBL_VAL_OUTPUT=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy obligations values create -o "$OBL_ID" -v test_obligation_value --json) + OBL_VAL_ID=$(echo $OBL_VAL_OUTPUT | jq -r '.id') + export OBL_VAL_FQN=$(echo $OBL_VAL_OUTPUT | jq -r '.fqn') + OBL_TRIG_ID=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy obligations triggers create --obligation-value "$OBL_VAL_ID" --attribute-value "$ATTR_OBL_VAL_FQN" --action "read" --json | jq -r '.id') + + # entitles opentdf client id for client credentials CLI user + SCS='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["opentdf"],"subject_external_selector_value":".clientId"}],"boolean_operator":2}]}]' + + # assertions setup + HS256_KEY=$(openssl rand -base64 32) + RS_PRIVATE_KEY=rs_private_key.pem + RS_PUBLIC_KEY=rs_public_key.pem + openssl genpkey -algorithm RSA -out $RS_PRIVATE_KEY -pkeyopt rsa_keygen_bits:2048 + openssl rsa -pubout -in $RS_PRIVATE_KEY -out $RS_PUBLIC_KEY + + export ASSERTIONS='[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"}}]' + + export SIGNED_ASSERTIONS_HS256=signed_assertions_hs256.json + export SIGNED_ASSERTION_VERIFICATON_HS256=assertion_verification_hs256.json + export SIGNED_ASSERTIONS_RS256=signed_assertion_rs256.json + export SIGNED_ASSERTION_VERIFICATON_RS256=assertion_verification_rs256.json + echo '[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"},"signingKey":{"alg":"HS256","key":"replace"}}]' > $SIGNED_ASSERTIONS_HS256 + jq --arg pem "$(echo $HS256_KEY)" '.[0].signingKey.key = $pem' $SIGNED_ASSERTIONS_HS256 > tmp.json && mv tmp.json $SIGNED_ASSERTIONS_HS256 + echo '{"keys":{"assertion1":{"alg":"HS256","key":"replace"}}}' > $SIGNED_ASSERTION_VERIFICATON_HS256 + jq --arg pem "$(echo $HS256_KEY)" '.keys.assertion1.key = $pem' $SIGNED_ASSERTION_VERIFICATON_HS256 > tmp.json && mv tmp.json $SIGNED_ASSERTION_VERIFICATON_HS256 + echo '[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"},"signingKey":{"alg":"RS256","key":"replace"}}]' > $SIGNED_ASSERTIONS_RS256 + jq --arg pem "$(<$RS_PRIVATE_KEY)" '.[0].signingKey.key = $pem' $SIGNED_ASSERTIONS_RS256 > tmp.json && mv tmp.json $SIGNED_ASSERTIONS_RS256 + echo '{"keys":{"assertion1":{"alg":"RS256","key":"replace"}}}' > $SIGNED_ASSERTION_VERIFICATON_RS256 + jq --arg pem "$(<$RS_PUBLIC_KEY)" '.keys.assertion1.key = $pem' $SIGNED_ASSERTION_VERIFICATON_RS256 > tmp.json && mv tmp.json $SIGNED_ASSERTION_VERIFICATON_RS256 + + + SM=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy subject-mappings create --action 'read' -a "$VAL_ID" --subject-condition-set-new "$SCS") + export FQN="https://testing-enc-dec.io/attr/attr1/value/value1" + export MIXED_CASE_FQN="https://Testing-Enc-Dec.io/attr/Attr1/value/VALUE1" +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert +} + +teardown() { + rm -f $OUTFILE_GO_MOD $RESULTFILE_GO_MOD $OUTFILE_TXT +} + +teardown_file(){ + rm -f $SIGNED_ASSERTIONS_HS256 $SIGNED_ASSERTION_VERIFICATON_HS256 $SIGNED_ASSERTIONS_RS256 $SIGNED_ASSERTION_VERIFICATON_RS256 +} + +@test "roundtrip TDF3, no attributes, file" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 $INFILE_GO_MOD + ./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 $OUTFILE_GO_MOD + diff $INFILE_GO_MOD $RESULTFILE_GO_MOD +} + +@test "roundtrip TDF3, no attributes, ec-wrapping, file" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 --wrapping-key-algorithm $WRAPPING_KEY_ALGORITHM $INFILE_GO_MOD + ./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 --session-key-algorithm $SESSION_KEY_ALGORITHM $OUTFILE_GO_MOD + diff $INFILE_GO_MOD $RESULTFILE_GO_MOD +} + +@test "roundtrip TDF3, one attribute, stdin" { + echo $SECRET_TEXT | ./otdfctl encrypt -o $OUT_TXT --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $FQN + ./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS $OUTFILE_TXT | grep "$SECRET_TEXT" +} + +@test "roundtrip TDF3, one attribute, mixed case FQN, stdin" { + echo $SECRET_TEXT | ./otdfctl encrypt -o $OUT_TXT --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $MIXED_CASE_FQN + ./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS $OUTFILE_TXT | grep "$SECRET_TEXT" +} + +@test "allow traversal with mapped key uses definition when value missing" { + local attr_name="attr-allow-traversal-${RANDOM}" + local kas_name="kas-allow-traversal-${RANDOM}" + local kas_uri="https://kas-allow-traversal-${RANDOM}.example.com" + local key_id="allow-traversal-key-${RANDOM}" + local ns_id="$NS_ID" + + if [[ -z "$ns_id" ]]; then + ns_id=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes namespaces list --json | jq -r '.namespaces[] | select(.name=="testing-enc-dec.io") | .id' | head -n 1) + fi + if [[ -z "$ns_id" ]]; then + echo "Failed to resolve namespace id for testing-enc-dec.io" + return 1 + fi + + attr_output=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes create --namespace "$ns_id" -n "$attr_name" -r HIERARCHY --allow-traversal --json) + attr_id=$(echo "$attr_output" | jq -r '.id') + attr_fqn=$(echo "$attr_output" | jq -r '.fqn') + missing_value_fqn="${attr_fqn}/value/missing" + + kas_output=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy kas-registry create --uri "$kas_uri" -n "$kas_name" --json) + kas_id=$(echo "$kas_output" | jq -r '.id') + + pem_b64=$(openssl genrsa 2048 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n') + key_output=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy kas-registry key create --kas "$kas_id" --key-id "$key_id" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "$pem_b64" --json) + key_system_id=$(echo "$key_output" | jq -r '.key.id') + + run sh -c "./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes key assign --attribute $attr_id --key-id $key_system_id --json" + assert_success + + echo $SECRET_TEXT | ./otdfctl encrypt -o $OUT_TXT --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a "$missing_value_fqn" + + inspect_output=$(./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_TXT) + policy_b64=$(echo "$inspect_output" | jq -r '.manifest.encryptionInformation.policy') + assert_not_equal "$policy_b64" "null" + assert_not_equal "$policy_b64" "" + run sh -c "printf '%s' \"$policy_b64\" | base64 -d" + assert_success + assert_output --partial "$missing_value_fqn" + assert_equal "$(echo "$inspect_output" | jq -r '.manifest.encryptionInformation.keyAccess | length')" "1" + assert_equal "$(echo "$inspect_output" | jq -r '.manifest.encryptionInformation.keyAccess[0].kid')" "$key_id" + assert_equal "$(echo "$inspect_output" | jq -r '.manifest.encryptionInformation.keyAccess[0].url')" "$kas_uri" + + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy attributes key remove --attribute $attr_id --key-id $key_system_id" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy attributes unsafe delete --id $attr_id --force" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy kas-registry key unsafe delete --id $key_system_id --key-id $key_id --kas-uri $kas_uri --force" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy kas-registry delete --id $kas_id --force" + assert_success +} + +@test "allow traversal uses attribute value mapping when value present" { + local attr_name="attr-allow-traversal-value-${RANDOM}" + local value_name="val-${RANDOM}" + local kas_name="kas-allow-traversal-value-${RANDOM}" + local kas_uri="https://kas-allow-traversal-value-${RANDOM}.example.com" + local def_key_id="def-key-${RANDOM}" + local val_key_id="val-key-${RANDOM}" + local ns_id="$NS_ID" + + if [[ -z "$ns_id" ]]; then + ns_id=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes namespaces list --json | jq -r '.namespaces[] | select(.name=="testing-enc-dec.io") | .id' | head -n 1) + fi + if [[ -z "$ns_id" ]]; then + echo "Failed to resolve namespace id for testing-enc-dec.io" + return 1 + fi + + attr_output=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes create --namespace "$ns_id" -n "$attr_name" -r HIERARCHY -v "$value_name" --allow-traversal --json) + attr_id=$(echo "$attr_output" | jq -r '.id') + value_id=$(echo "$attr_output" | jq -r '.values[0].id') + value_fqn=$(echo "$attr_output" | jq -r '.values[0].fqn') + + kas_output=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy kas-registry create --uri "$kas_uri" -n "$kas_name" --json) + kas_id=$(echo "$kas_output" | jq -r '.id') + + pem_b64=$(openssl genrsa 2048 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n') + def_key_output=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy kas-registry key create --kas "$kas_id" --key-id "$def_key_id" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "$pem_b64" --json) + def_key_system_id=$(echo "$def_key_output" | jq -r '.key.id') + val_key_output=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy kas-registry key create --kas "$kas_id" --key-id "$val_key_id" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "$pem_b64" --json) + val_key_system_id=$(echo "$val_key_output" | jq -r '.key.id') + + run sh -c "./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes key assign --attribute $attr_id --key-id $def_key_system_id --json" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes values key assign --value $value_id --key-id $val_key_system_id --json" + assert_success + + echo $SECRET_TEXT | ./otdfctl encrypt -o $OUT_TXT --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a "$value_fqn" + + inspect_output=$(./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_TXT) + assert_equal "$(echo "$inspect_output" | jq -r '.manifest.encryptionInformation.keyAccess | length')" "1" + assert_equal "$(echo "$inspect_output" | jq -r '.manifest.encryptionInformation.keyAccess[0].kid')" "$val_key_id" + assert_equal "$(echo "$inspect_output" | jq -r '.manifest.encryptionInformation.keyAccess[0].url')" "$kas_uri" + + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy attributes values key remove --value $value_id --key-id $val_key_system_id" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy attributes key remove --attribute $attr_id --key-id $def_key_system_id" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy attributes unsafe delete --id $attr_id --force" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy kas-registry key unsafe delete --id $def_key_system_id --key-id $def_key_id --kas-uri $kas_uri --force" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy kas-registry key unsafe delete --id $val_key_system_id --key-id $val_key_id --kas-uri $kas_uri --force" + assert_success + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy kas-registry delete --id $kas_id --force" + assert_success +} + +@test "allow traversal with inactive attribute value fails" { + local attr_name="attr-allow-traversal-inactive-${RANDOM}" + local value_name="val-inactive-${RANDOM}" + local ns_id="$NS_ID" + + if [[ -z "$ns_id" ]]; then + ns_id=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes namespaces list --json | jq -r '.namespaces[] | select(.name=="testing-enc-dec.io") | .id' | head -n 1) + fi + if [[ -z "$ns_id" ]]; then + echo "Failed to resolve namespace id for testing-enc-dec.io" + return 1 + fi + + attr_output=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes create --namespace "$ns_id" -n "$attr_name" -r HIERARCHY -v "$value_name" --allow-traversal --json) + attr_id=$(echo "$attr_output" | jq -r '.id') + value_id=$(echo "$attr_output" | jq -r '.values[0].id') + value_fqn=$(echo "$attr_output" | jq -r '.values[0].fqn') + + run sh -c "./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes values deactivate --id $value_id --force" + assert_success + + run sh -c "echo \"$SECRET_TEXT\" | ./otdfctl encrypt -o $OUT_TXT --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a \"$value_fqn\"" + assert_failure + + run sh -c "./otdfctl --host $HOST $WITH_CREDS policy attributes unsafe delete --id $attr_id --force" + assert_success +} + +@test "roundtrip TDF3, assertions, stdin" { + echo $SECRET_TEXT | ./otdfctl encrypt -o $OUT_TXT --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $FQN --with-assertions "$ASSERTIONS" + ./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS $OUTFILE_TXT | grep "$SECRET_TEXT" + ./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_TXT + assertions_present=$(./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_TXT | jq '.manifest.assertions[0].id') + [[ $assertions_present == "\"assertion1\"" ]] +} + +@test "roundtrip TDF3, assertions with HS256 keys and verification, file" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $FQN --with-assertions $SIGNED_ASSERTIONS_HS256 --tdf-type tdf3 $INFILE_GO_MOD + ./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --with-assertion-verification-keys $SIGNED_ASSERTION_VERIFICATON_HS256 --tdf-type tdf3 $OUTFILE_GO_MOD + diff $INFILE_GO_MOD $RESULTFILE_GO_MOD + ./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_GO_MOD + assertions_present=$(./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_GO_MOD | jq '.manifest.assertions[0].id') + [[ $assertions_present == "\"assertion1\"" ]] +} + +@test "roundtrip TDF3, assertions with RS256 keys and verification, file" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $FQN --with-assertions $SIGNED_ASSERTIONS_RS256 --tdf-type tdf3 $INFILE_GO_MOD + ./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --with-assertion-verification-keys $SIGNED_ASSERTION_VERIFICATON_RS256 --tdf-type tdf3 $OUTFILE_GO_MOD + diff $INFILE_GO_MOD $RESULTFILE_GO_MOD + ./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_GO_MOD + assertions_present=$(./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_GO_MOD | jq '.manifest.assertions[0].id') + [[ $assertions_present == "\"assertion1\"" ]] +} + +@test "roundtrip TDF3, with target version < 4.3.0" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 --target-mode v4.2.2 $INFILE_GO_MOD + ./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 $OUTFILE_GO_MOD + diff $INFILE_GO_MOD $RESULTFILE_GO_MOD + + schema_version_present=$(./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_GO_MOD | jq '.manifest | has("schemaVersion")') + [[ $schema_version_present == false ]] +} + +@test "roundtrip TDF3, with target version >= 4.3.0" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 --target-mode v4.3.1 $INFILE_GO_MOD + ./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 $OUTFILE_GO_MOD + diff $INFILE_GO_MOD $RESULTFILE_GO_MOD + + schema_version_present=$(./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_GO_MOD | jq '.manifest | has("schemaVersion")') + [[ $schema_version_present == true ]] +} + +@test "roundtrip TDF3, with allowlist containing platform kas" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 $INFILE_GO_MOD + run sh -c "./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 --kas-allowlist http://localhost:8080/kas $OUTFILE_GO_MOD" + assert_success +} + +@test "roundtrip TDF3, with allowlist containing non existent kas (should fail)" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 $INFILE_GO_MOD + run sh -c "./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 --kas-allowlist http://not-a-real-kas.com/kas $OUTFILE_GO_MOD" + assert_failure + assert_output --partial "KasAllowlist: kas url http://localhost:8080/kas is not allowed" +} + +@test "roundtrip TDF3, ignoring allowlist" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 $INFILE_GO_MOD + run sh -c "./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type tdf3 --kas-allowlist '*' $OUTFILE_GO_MOD" + assert_success + assert_output --partial "kasAllowlist is ignored" +} + +@test "roundtrip TDF3, not entitled to data, no required obligations returned" { + run sh -c "./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $ATTR_OBL_VAL_FQN $INFILE_GO_MOD" + assert_success + run sh -c "./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS $OUTFILE_GO_MOD" + assert_failure + refute_output --partial "required obligations" +} + +@test "roundtrip TDF3, entitled to data, required obligations returned" { + # Handle subject mapping + run sh -c "./otdfctl policy subject-mappings create --attribute-value-id $ATTR_OBL_VAL_ID --action read --subject-condition-set-new '[{\"conditionGroups\":[{\"conditions\":[{\"operator\":\"SUBJECT_MAPPING_OPERATOR_ENUM_IN\",\"subjectExternalValues\":[\"opentdf\"],\"subjectExternalSelectorValue\":\".clientId\"}], \"booleanOperator\":\"CONDITION_BOOLEAN_TYPE_ENUM_OR\"}]}]' --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS" + assert_success + + run sh -c "./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $ATTR_OBL_VAL_FQN $INFILE_GO_MOD" + assert_success + run sh -c "./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS $OUTFILE_GO_MOD" + assert_failure + assert_output --partial "required obligations: [$OBL_VAL_FQN]" +} diff --git a/otdfctl/e2e/kas-grants.bats b/otdfctl/e2e/kas-grants.bats new file mode 100755 index 0000000000..5864a26d20 --- /dev/null +++ b/otdfctl/e2e/kas-grants.bats @@ -0,0 +1,82 @@ +#!/usr/bin/env bats + +# Tests for KAS grants + +setup_file() { + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + export KAS_URI="https://e2etestkas.com" + export KAS_ID=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --uri "$KAS_URI" --json | jq -r '.id') + export KAS_ID_FLAG="--kas-id $KAS_ID" + + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "testing-kasg.uk" --json | jq -r '.id') + ATTR=$(./otdfctl $HOST $WITH_CREDS policy attributes create -n "attr1" --json --rule ANY_OF --namespace "$NS_ID" -v "val1") + export ATTR_ID=$(echo $ATTR | jq -r '.id') + export VAL_ID=$(echo $ATTR | jq -r '.values[0].id') +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_kasg() { + run sh -c "./otdfctl $HOST $WITH_CREDS policy kas-grants $*" + } +} + +teardown_file() { + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$NS_ID" --force + ./otdfctl $HOST $WITH_CREDS policy kas-registry delete --id "$KAS_ID" --force + + # clear out all test env vars + unset HOST WITH_CREDS KAS_ID KAS_ID_FLAG KAS_URI NS_ID NS_ID_FLAG ATTR_ID ATTR_ID_FLAG VAL_ID VAL_ID_FLAG +} + +@test "unassign rejects more than one type of grant at once" { + export NS_ID_FLAG='--namespace-id 258e69b7-9e61-46e1-8fd6-b4ba00898ec2' + export ATTR_ID_FLAG='--attribute-id 258e69b7-9e61-46e1-8fd6-b4ba00898ec1' + export VAL_ID_FLAG='--value-id 258e69b7-9e61-46e1-8fd6-b4ba00898ec3' + + run_otdfctl_kasg unassign $ATTR_ID_FLAG $VAL_ID_FLAG $KAS_ID_FLAG + assert_failure + assert_output --partial "Must specify exactly one Attribute Namespace ID, Definition ID, or Value ID to unassign" + + run_otdfctl_kasg unassign $NS_ID_FLAG $VAL_ID_FLAG $KAS_ID_FLAG + assert_failure + assert_output --partial "Must specify exactly one Attribute Namespace ID, Definition ID, or Value ID to unassign" + + run_otdfctl_kasg unassign $ATTR_ID_FLAG $NS_ID_FLAG $KAS_ID_FLAG + assert_failure + assert_output --partial "Must specify exactly one Attribute Namespace ID, Definition ID, or Value ID to unassign" +} + +@test "assign grant prints warning" { + # assign the namespace a grant + export NS_ID_FLAG="--namespace-id $NS_ID" + + run_otdfctl_kasg assign "$NS_ID_FLAG" "$KAS_ID_FLAG" + assert_output --partial "Grants are now Key Mappings." + + run_otdfctl_kasg unassign "$NS_ID_FLAG" "$KAS_ID_FLAG" + assert_output --partial "Grants are now Key Mappings." +} + +@test "optional ID flag string error message" { + export NS_ID_FLAG='--namespace-id hello' + export ATTR_ID_FLAG='--attribute-id world' + export VAL_ID_FLAG='--value-id goodnight' + + run_otdfctl_kasg unassign $NS_ID_FLAG $KAS_ID_FLAG + assert_failure + assert_output --partial "Optional flag '--namespace-id' received value 'hello' and must be a valid UUID if used" + + run_otdfctl_kasg unassign $ATTR_ID_FLAG $KAS_ID_FLAG + assert_failure + assert_output --partial "Optional flag '--attribute-id' received value 'world' and must be a valid UUID if used" + + run_otdfctl_kasg unassign $VAL_ID_FLAG $KAS_ID_FLAG + assert_failure + assert_output --partial "Optional flag '--value-id' received value 'goodnight' and must be a valid UUID if used" +} diff --git a/otdfctl/e2e/kas-keys-mappings.bats b/otdfctl/e2e/kas-keys-mappings.bats new file mode 100644 index 0000000000..4736c367e4 --- /dev/null +++ b/otdfctl/e2e/kas-keys-mappings.bats @@ -0,0 +1,246 @@ +#!/usr/bin/env bats + +# Tests listing key mappings + +# Helper functions for otdfctl commands +run_otdfctl_key() { + run sh -c "./otdfctl policy kas-registry key $HOST $WITH_CREDS $*" +} + +run_otdfctl_kas_registry_create() { + run sh -c "./otdfctl policy kas-registry create $HOST $WITH_CREDS $*" +} + +run_otdfctl_namespace_create() { + run sh -c "./otdfctl policy attributes namespaces create $HOST $WITH_CREDS $*" +} + +run_otdfctl_attribute_create() { + run sh -c "./otdfctl policy attributes create $HOST $WITH_CREDS $*" +} + +run_otdfctl_value_create() { + run sh -c "./otdfctl policy attributes values create $HOST $WITH_CREDS $*" +} + +run_otdfctl_namespace_assign_key() { + run sh -c "./otdfctl policy attributes namespaces key assign $HOST $WITH_CREDS $*" +} + +run_otdfctl_attribute_assign_key() { + run sh -c "./otdfctl policy attributes key assign $HOST $WITH_CREDS $*" +} + +run_otdfctl_value_assign_key() { + run sh -c "./otdfctl policy attributes values key assign $HOST $WITH_CREDS $*" +} + +run_otdfctl_namespace_remove_key() { + run sh -c "./otdfctl policy attributes namespaces key remove $HOST $WITH_CREDS $*" +} + +run_otdfctl_attribute_remove_key() { + run sh -c "./otdfctl policy attributes key remove $HOST $WITH_CREDS $*" +} + +run_otdfctl_value_remove_key() { + run sh -c "./otdfctl policy attributes values key remove $HOST $WITH_CREDS $*" +} + +run_otdfctl_value_delete() { + run sh -c "./otdfctl policy attributes values unsafe delete --force $HOST $WITH_CREDS $*" +} + +run_otdfctl_attribute_delete() { + run sh -c "./otdfctl policy attributes unsafe delete --force $HOST $WITH_CREDS $*" +} + +run_otdfctl_namespace_delete() { + run sh -c "./otdfctl policy namespaces unsafe delete --force $HOST $WITH_CREDS $*" +} + +setup_file() { + bats_load_library bats-support + bats_load_library bats-assert + load "otdfctl-utils.sh" + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + export KAS_URI="https://test-kas-for-mappings.com" + export KAS_NAME="kas-registry-for-mappings-test" + # Generate valid public keys for different algorithms and base64 encode (single-line) + export PEM_B64_RSA_2048=$(openssl genrsa 2048 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n') + export PEM_B64_EC_P256=$(openssl ecparam -name prime256v1 -genkey 2>/dev/null | openssl ec -pubout 2>/dev/null | base64 | tr -d '\n') + export PEM_B64_RSA_4096=$(openssl genrsa 4096 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n') + + run_otdfctl_kas_registry_create --name $KAS_NAME --uri "$KAS_URI" --json + assert_success + export KAS_REGISTRY_ID=$(echo "$output" | jq -r '.id') + + # Create three keys + export KEY_ID_1=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_1}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA_2048}" --json + assert_success + export SYSTEM_KEY_ID_1=$(echo "$output" | jq -r '.key.id') + + export KEY_ID_2=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_2}" --algorithm "ec:secp256r1" --mode "public_key" --public-key-pem "${PEM_B64_EC_P256}" --json + assert_success + export SYSTEM_KEY_ID_2=$(echo "$output" | jq -r '.key.id') + + export KEY_ID_3=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_3}" --algorithm "rsa:4096" --mode "public_key" --public-key-pem "${PEM_B64_RSA_4096}" --json + assert_success + export SYSTEM_KEY_ID_3=$(echo "$output" | jq -r '.key.id') + + # Create a namespace, attribute, and value for testing assignments + export NAMESPACE_NAME="test-namespace-for-mappings.com" + run_otdfctl_namespace_create --name "${NAMESPACE_NAME}" --json + assert_success + export NAMESPACE_ID=$(echo "$output" | jq -r '.id') + + export ATTRIBUTE_NAME=$(generate_kas_name) + run_otdfctl_attribute_create --name "${ATTRIBUTE_NAME}" --namespace "${NAMESPACE_ID}" --rule ALL_OF --json + assert_success + export ATTRIBUTE_ID=$(echo "$output" | jq -r '.id') + + export VALUE_NAME=$(generate_kas_name) + run_otdfctl_value_create --value "${VALUE_NAME}" --attribute-id "${ATTRIBUTE_ID}" --json + assert_success + export VALUE_ID=$(echo "$output" | jq -r '.id') + + # Assign all three keys to the namespace, attribute, and value + run_otdfctl_namespace_assign_key --namespace "${NAMESPACE_ID}" --key-id "${SYSTEM_KEY_ID_1}" + assert_success + run_otdfctl_namespace_assign_key --namespace "${NAMESPACE_ID}" --key-id "${SYSTEM_KEY_ID_2}" + assert_success + run_otdfctl_namespace_assign_key --namespace "${NAMESPACE_ID}" --key-id "${SYSTEM_KEY_ID_3}" + assert_success + + run_otdfctl_attribute_assign_key --attribute "${ATTRIBUTE_ID}" --key-id "${SYSTEM_KEY_ID_1}" + assert_success + run_otdfctl_attribute_assign_key --attribute "${ATTRIBUTE_ID}" --key-id "${SYSTEM_KEY_ID_2}" + assert_success + run_otdfctl_attribute_assign_key --attribute "${ATTRIBUTE_ID}" --key-id "${SYSTEM_KEY_ID_3}" + assert_success + + run_otdfctl_value_assign_key --value "${VALUE_ID}" --key-id "${SYSTEM_KEY_ID_1}" + assert_success + run_otdfctl_value_assign_key --value "${VALUE_ID}" --key-id "${SYSTEM_KEY_ID_2}" + assert_success + run_otdfctl_value_assign_key --value "${VALUE_ID}" --key-id "${SYSTEM_KEY_ID_3}" + assert_success +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + load "otdfctl-utils.sh" +} + +teardown_file() { + # Unassign the keys + run_otdfctl_namespace_remove_key --namespace "${NAMESPACE_ID}" --key-id "${SYSTEM_KEY_ID_1}" + run_otdfctl_namespace_remove_key --namespace "${NAMESPACE_ID}" --key-id "${SYSTEM_KEY_ID_2}" + run_otdfctl_namespace_remove_key --namespace "${NAMESPACE_ID}" --key-id "${SYSTEM_KEY_ID_3}" + run_otdfctl_attribute_remove_key --attribute "${ATTRIBUTE_ID}" --key-id "${SYSTEM_KEY_ID_1}" + run_otdfctl_attribute_remove_key --attribute "${ATTRIBUTE_ID}" --key-id "${SYSTEM_KEY_ID_2}" + run_otdfctl_attribute_remove_key --attribute "${ATTRIBUTE_ID}" --key-id "${SYSTEM_KEY_ID_3}" + run_otdfctl_value_remove_key --value "${VALUE_ID}" --key-id "${SYSTEM_KEY_ID_1}" + run_otdfctl_value_remove_key --value "${VALUE_ID}" --key-id "${SYSTEM_KEY_ID_2}" + run_otdfctl_value_remove_key --value "${VALUE_ID}" --key-id "${SYSTEM_KEY_ID_3}" + + # Delete the value, attribute, and namespace + run_otdfctl_value_delete --id "${VALUE_ID}" + run_otdfctl_attribute_delete --id "${ATTRIBUTE_ID}" + run_otdfctl_namespace_delete --id "${NAMESPACE_ID}" + + delete_all_keys_in_kas "$KAS_REGISTRY_ID" + delete_kas_registry "$KAS_REGISTRY_ID" + + unset HOST WITH_CREDS KAS_REGISTRY_ID KAS_NAME KAS_URI PEM_B64 KEY_ID_1 SYSTEM_KEY_ID_1 KEY_ID_2 SYSTEM_KEY_ID_2 KEY_ID_3 SYSTEM_KEY_ID_3 NAMESPACE_ID NAMESPACE_NAME ATTRIBUTE_ID ATTRIBUTE_NAME VALUE_ID VALUE_NAME +} + +# Helper function to generate a unique key ID +generate_key_id() { + local length="${1:-8}" + + if [ ! -c /dev/urandom ]; then + echo "Error: /dev/urandom not found. Cannot generate random string." >&2 + return 1 + fi + key_id=$(LC_ALL=C tr /dev/null | head -c "${length}") + echo "$key_id" +} + +generate_kas_name() { + local length="${1:-6}" + + if [ ! -c /dev/urandom ]; then + echo "Error: /dev/urandom not found. Cannot generate random string." >&2 + return 1 + fi + kas_name=$(LC_ALL=C tr /dev/null | head -c "${length}") + echo "$kas_name" +} + +format_kas_name_as_uri() { + local input="$1" + echo "http://${input}.org" +} + +# Helper function to assert key mapping details +assert_key_mapping_details() { + local key_id="$1" + assert_equal "$(echo "$output" | jq -r '.key_mappings | length')" "1" + assert_equal "$(echo "$output" | jq -r '.key_mappings.[0].kid')" "${key_id}" + assert_equal "$(echo "$output" | jq -r '.key_mappings.[0].kas_uri')" "${KAS_URI}" + assert_equal "$(echo "$output" | jq -r '.key_mappings.[0].namespace_mappings | length')" "1" + assert_equal "$(echo "$output" | jq -r '.key_mappings.[0].attribute_mappings | length')" "1" + assert_equal "$(echo "$output" | jq -r '.key_mappings.[0].value_mappings | length')" "1" + assert_equal "$(echo "$output" | jq -r '.key_mappings.[0].namespace_mappings[0].id')" "${NAMESPACE_ID}" + assert_equal "$(echo "$output" | jq -r '.key_mappings.[0].attribute_mappings[0].id')" "${ATTRIBUTE_ID}" + assert_equal "$(echo "$output" | jq -r '.key_mappings.[0].value_mappings[0].id')" "${VALUE_ID}" +} + +@test "kas-keys-mappings: list key mappings for a specific key by kas id" { + run_otdfctl_key list-mappings --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_1}" --json + assert_success + assert_key_mapping_details "${KEY_ID_1}" +} + +@test "kas-keys-mappings: list key mappings for a specific key by kas name" { + run_otdfctl_key list-mappings --kas "${KAS_NAME}" --key-id "${KEY_ID_1}" --json + assert_success + assert_key_mapping_details "${KEY_ID_1}" +} + +@test "kas-keys-mappings: list key mappings for a specific key by kas uri" { + run_otdfctl_key list-mappings --kas "${KAS_URI}" --key-id "${KEY_ID_1}" --json + assert_success + assert_key_mapping_details "${KEY_ID_1}" +} + +@test "kas-keys-mappings: list key mappings with pagination" { + run_otdfctl_key list-mappings --json --limit 1 --offset 0 + assert_success + assert_equal "$(echo "$output" | jq -r '.key_mappings | length')" "1" + assert_not_equal "$(echo "$output" | jq -r '.key_mappings[0].kid')" "null" + assert [ "$(echo "$output" | jq -r '.pagination.total')" -ge 3 ] + assert_equal "$(echo "$output" | jq -r '.pagination.next_offset')" "1" +} + +@test "kas-keys-mappings: list key mappings - required together are missing" { + run_otdfctl_key list-mappings --key-id "nonexistent-key" --json + assert_failure + assert_output --partial "--kas" + + run_otdfctl_key list-mappings --kas "${KAS_NAME}" --json + assert_failure + assert_output --partial "--kas" +} + +@test "kas-keys-mappings: list key mappings - mutually exclusive flags" { + run_otdfctl_key list-mappings --kas "${KAS_NAME}" --key-id "nonexistent-key" --id "${KEY_ID_1}" --json + assert_failure + assert_output --partial "Error: if any flags in the group [kas id] are set none of the others can be; [id kas] were all set" +} diff --git a/otdfctl/e2e/kas-keys.bats b/otdfctl/e2e/kas-keys.bats new file mode 100644 index 0000000000..ddb2b6d7f3 --- /dev/null +++ b/otdfctl/e2e/kas-keys.bats @@ -0,0 +1,1258 @@ +#!/usr/bin/env bats + +run_otdfctl_kas_registry_create() { + run sh -c "./otdfctl policy kas-registry create $HOST $WITH_CREDS $*" +} + +run_otdfctl_provider_create() { + run sh -c "./otdfctl policy keymanagement provider create $HOST $WITH_CREDS $*" +} + +setup_file() { + bats_load_library bats-support + bats_load_library bats-assert + load "otdfctl-utils.sh" + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + # This command is not a 'kas-registry key' subcommand, so it won't use run_otdfctl_key + export KAS_URI="https://test-kas-with-keys.com" + export KAS_NAME="kas-registry-for-keys-test" + + run_otdfctl_kas_registry_create --name $KAS_NAME --uri "$KAS_URI" --json + assert_success + export KAS_REGISTRY_ID=$(echo "$output" | jq -r '.id') + + if [ "$RUN_EXPERIMENTAL_TESTS" == "true" ]; then + run_otdfctl_provider_create --name "test-provider-config-kas-keys" --config '{}' --json + assert_success + export PC_ID=$(echo "$output" | jq -r '.id') + fi + export WRAPPING_KEY=$(openssl rand -hex 32) + # Generate valid public keys and base64 encode (single-line) + export PEM_B64_RSA=$(openssl genrsa 2048 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n') + export PEM_B64_EC_P256=$(openssl ecparam -name prime256v1 -genkey 2>/dev/null | openssl ec -pubout 2>/dev/null | base64 | tr -d '\n') + export PEM_B64_RSA_4096=$(openssl genrsa 4096 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n') + export PEM_B64=${PEM_B64_RSA} +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + load "otdfctl-utils.sh" +} + + +teardown_file() { + delete_all_keys_in_kas "$KAS_REGISTRY_ID" + delete_kas_registry "$KAS_REGISTRY_ID" + if [ -n "$PC_ID" ]; then + delete_provider_config "$PC_ID" + fi + + unset HOST WITH_CREDS KAS_REGISTRY_ID KAS_NAME KAS_URI PEM_B64 WRAPPING_KEY PC_ID +} + +# Helper function to generate a unique key ID +generate_key_id() { + local length="${1:-8}" + + # Check if /dev/urandom is available + if [ ! -c /dev/urandom ]; then + echo "Error: /dev/urandom not found. Cannot generate random string." >&2 + return 1 + fi + key_id=$(LC_ALL=C tr /dev/null | head -c "${length}") + echo "$key_id" +} + +generate_kas_name() { + local length="${1:-6}" + + # Check if /dev/urandom is available + if [ ! -c /dev/urandom ]; then + echo "Error: /dev/urandom not found. Cannot generate random string." >&2 + return 1 + fi + kas_name=$(LC_ALL=C tr /dev/null | head -c "${length}") + echo "$kas_name" +} + +format_kas_name_as_uri() { + local input="$1" + echo "http://${input}.org" +} + +@test "kas-keys: create key (local mode, rsa:2048)" { + KEY_ID=$(generate_key_id) + # For local mode, a public key is generated by otdfctl. + # Its exact value cannot be known before running the command without replicating the key generation logic. + # Thus, we assert its presence and that it\'s a non-empty base64 encoded string. This is the intended assertion. + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm rsa:2048 --mode local --wrapping-key-id wrapping-key-1 --wrapping-key "${WRAPPING_KEY}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "1" # local + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active (assuming default) + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" # False should be null + # Assert public_key_ctx.pem is present and not empty + assert_not_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "" + # Assert private_key_ctx for local mode + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.key_id)" "wrapping-key-1" + assert_not_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: create key (local mode, ec:secp256r1)" { + KEY_ID=$(generate_key_id) + # For local mode, a public key is generated by otdfctl. + # Its exact value cannot be known before running the command without replicating the key generation logic. + # Thus, we assert its presence and that it\'s a non-empty base64 encoded string. This is the intended assertion. + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "ec:secp256r1" --mode "local" --wrapping-key-id "wrapping-key-1" --wrapping-key "${WRAPPING_KEY}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "3" # ec:secp256r1 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "1" # local + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.key_id)" "wrapping-key-1" + assert_not_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: create key (public_key mode)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + # Assert private_key_ctx is null or not present for public_key mode + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: create key (remote mode)" { + if [ "$RUN_EXPERIMENTAL_TESTS" != "true" ]; then + skip "Skipping experimental test" + fi + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "remote" --public-key-pem "${PEM_B64}" --provider-config-id "${PC_ID}" --wrapping-key-id "wrapping-key-remote" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "3" # remote + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + # Assert private_key_ctx is not what it is for local mode, but check its key_id as per previous logic + # Based on kas-keys.go, remote mode sets privateKeyCtx.key-id = wrapping-key-id + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.key_id)" "wrapping-key-remote" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "null" # wrapped_key should not be set for remote + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: create key (provider mode)" { + if [ "$RUN_EXPERIMENTAL_TESTS" != "true" ]; then + skip "Skipping experimental test" + fi + KEY_ID=$(generate_key_id) + WRAPPING_KEY_ID="wrapping-key-for-provider" + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "provider" --provider-config-id "${PC_ID}" --wrapping-key-id "${WRAPPING_KEY_ID}" --public-key-pem "${PEM_B64}" --private-key-pem "${PEM_B64}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "2" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.key_id)" "${WRAPPING_KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: create key with labels" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --label "env=dev" --label "owner=test" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."env"')" "dev" + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."owner"')" "test" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: create key (missing key-id)" { + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --algorithm "rsa:2048" --mode "local" --wrapping-key-id "wrapping-key-1" --wrapping-key "${WRAPPING_KEY}" + assert_failure + assert_output --partial "Flag '--key-id' is required" +} + +@test "kas-keys: create key (missing algorithm)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --mode "local" --wrapping-key-id "wrapping-key-1" --wrapping-key "${WRAPPING_KEY}" + assert_failure + assert_output --partial "Flag '--algorithm' is required" +} + +@test "kas-keys: create key (missing mode)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --wrapping-key-id "wrapping-key-1" --wrapping-key "${WRAPPING_KEY}" + assert_failure + assert_output --partial "Flag '--mode' is required" +} + +@test "kas-keys: create key (local mode, missing wrapping-key-id)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "local" --wrapping-key "${WRAPPING_KEY}" + assert_failure + assert_output --partial "wrapping-key-id is required for mode local" +} + +@test "kas-keys: create key (local mode, missing wrapping-key)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "local" --wrapping-key-id "wrapping-key-1" + assert_failure + assert_output --partial "Flag '--wrapping-key' is required" +} + +@test "kas-keys: create key (public_key mode, missing pem)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" + assert_failure + # The error message might vary based on how --public-key-pem is validated if missing. + # Assuming it's caught by the CLI framework or the command logic. + assert_output --partial "Flag '--public-key-pem' is required" +} + +@test "kas-keys: create key (remote mode, missing pem)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "remote" --provider-config-id "pc-1" --wrapping-key-id "wk-1" + assert_failure + assert_output --partial "Flag '--public-key-pem' is required" +} + +@test "kas-keys: create key (remote mode, missing provider-config-id)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "remote" --public-key-pem "${PEM_B64}" --wrapping-key-id "wk-1" + assert_failure + assert_output --partial "Flag '--provider-config-id' is required" +} + +@test "kas-keys: create key (remote mode, missing wrapping-key-id)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "remote" --public-key-pem "${PEM_B64}" --provider-config-id "pc-1" + assert_failure + assert_output --partial "wrapping-key-id is required for mode remote" +} + +@test "kas-keys: create key (provider mode, missing wrapping-key-id)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "provider" --provider-config-id "pc-1" + assert_failure + assert_output --partial "wrapping-key-id is required for mode provider" +} + +@test "kas-keys: create key (provider mode, missing provider-config-id)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "provider" --wrapping-key-id "wk-1" + assert_failure + assert_output --partial "Flag '--provider-config-id' is required" +} + +@test "kas-keys: create key (remote mode, pem not base64)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "remote" --public-key-pem "not-base64-value" --provider-config-id "pc-1" --wrapping-key-id "wk-1" + assert_failure + assert_output --partial "pem must be base64 encoded" +} + +@test "kas-keys: create key (public_key mode, pem not base64)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "not-base64-value" + assert_failure + assert_output --partial "pem must be base64 encoded" +} + +@test "kas-keys: create key (public_key mode, invalid PEM content)" { + KEY_ID=$(generate_key_id) + # base64 of a non-PEM string + BAD_PEM_B64=$(echo "not a pem" | base64 | tr -d '\n') + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${BAD_PEM_B64}" + assert_failure + assert_output --partial "invalid public key pem" +} + +@test "kas-keys: create key (public_key mode, EC key with RSA algorithm)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_EC_P256}" + assert_failure + assert_output --partial "invalid public key pem" +} + +@test "kas-keys: create key (missing kas identifier)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" + assert_failure + assert_output --partial "Flag '--kas' is required" +} + +@test "kas-keys: create key (using kasName)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_NAME}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: create key (using kasUri)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_URI}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: create key (invalid algorithm value)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "invalid-algorithm-value" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_failure + assert_output --partial "invalid algorithm" +} + +@test "kas-keys: create key (invalid mode value)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "invalid-mode-value" --public-key-pem "${PEM_B64}" --json + assert_failure + assert_output --partial "invalid mode" +} + +@test "kas-keys: create key (duplicate key-id)" { + KEY_ID_DUPLICATE=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_DUPLICATE}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_DUPLICATE}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_failure + assert_output --partial "Failed to create kas key" +} + +@test "kas-keys: create key (invalid kas identifier)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "invalid-kas-id" --key-id "${KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_failure + assert_output --partial "Failed to resolve KAS identifier 'invalid-kas-id': not_found: resource not found" +} + +@test "kas-keys: create key (invalid hex encoded wrapping-key)" { + KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID}" --algorithm "ec:secp256r1" --mode "local" --wrapping-key-id "wrapping-key-1" --wrapping-key "not-hex-encoded" --json + assert_failure + + assert_output --partial "wrapping-key must be hex encoded" +} + +@test "kas-keys: get key by system ID" { + KEY_ID_GET=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_GET}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + CREATED_KEY_SYSTEM_ID=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key get --key "${CREATED_KEY_SYSTEM_ID}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.id)" "${CREATED_KEY_SYSTEM_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID_GET}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: get key by user key-id and kasId" { + KEY_ID_GET_USER=$(generate_key_id) + # Using ec:secp256r1 and public_key mode for variety + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_GET_USER}" --algorithm "ec:secp256r1" --mode "public_key" --public-key-pem "${PEM_B64_EC_P256}" --json + assert_success + local created_key_system_id_for_get=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key get --key "${KEY_ID_GET_USER}" --kas "${KAS_REGISTRY_ID}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.id)" "${created_key_system_id_for_get}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID_GET_USER}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "3" # ec:secp256r1 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64_EC_P256}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: get key by user key-id and kasName" { + KEY_ID_GET_USER_kas=$(generate_key_id) + run_otdfctl_key create --kas "kas-registry-for-keys-test" --key-id "${KEY_ID_GET_USER_kas}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + local created_key_system_id_for_kas_get=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key get --key "${KEY_ID_GET_USER_kas}" --kas "kas-registry-for-keys-test" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.id)" "${created_key_system_id_for_kas_get}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID_GET_USER_kas}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: get key by user key-id and kasUri" { + KEY_ID_GET_USER_kas=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_URI}" --key-id "${KEY_ID_GET_USER_kas}" --algorithm "ec:secp256r1" --mode "public_key" --public-key-pem "${PEM_B64_EC_P256}" --json + assert_success + local created_key_system_id_for_kas_get=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key get --key "${KEY_ID_GET_USER_kas}" --kas "${KAS_URI}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" # Should resolve to the same KAS + assert_equal "$(echo "$output" | jq -r .key.id)" "${created_key_system_id_for_kas_get}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID_GET_USER_kas}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "3" # ec:secp256r1 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64_EC_P256}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "null" +} + +@test "kas-keys: get key (failure: only key-id, missing KAS identifier)" { + KEY_ID_FAIL_GET=$(generate_key_id) + # Create a key first so it potentially exists, though the failure should be due to missing KAS context for the get + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_FAIL_GET}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + + run_otdfctl_key get --key "${KEY_ID_FAIL_GET}" --json + assert_failure + # Error message might vary, but it should indicate an issue with resolving the key or missing parameters + assert_output --partial "Flag '--kas' is required" # Or a more specific error about missing KAS identifier +} + +@test "kas-keys: get key (failure: only kas, missing key-id or system id)" { + run_otdfctl_key get --kas "${KAS_REGISTRY_ID}" --json + assert_failure + assert_output --partial "Flag '--key' is required" +} + +@test "kas-keys: get key (not found by system ID)" { + run_otdfctl_key get --key "39af808f-6cac-403f-90d7-6b88e865860d" --json + assert_failure + assert_output --partial "Failed to get kas key" # Error should indicate not found or similar +} + +@test "kas-keys: get key (not found by user key-id and kas)" { + run_otdfctl_key get --key "non-existent-key" --kas "${KAS_REGISTRY_ID}" --json + assert_failure + assert_output --partial "Failed to get kas key" # Error should indicate not found or similar +} + +@test "kas-keys: update key labels (add)" { + KEY_ID_UPDATE_LABEL=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UPDATE_LABEL}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --label "initial=true" --json + assert_success + UPDATE_KEY_LABEL_SYSTEM_ID=$(echo "$output" | jq -r .key.id) + local initial_created_at_seconds=$(echo "$output" | jq -r .key.metadata.created_at.seconds) + local initial_updated_at_seconds=$(echo "$output" | jq -r .key.metadata.updated_at.seconds) + + run_otdfctl_key update --id "${UPDATE_KEY_LABEL_SYSTEM_ID}" --label "added=true" --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.id)" "${UPDATE_KEY_LABEL_SYSTEM_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID_UPDATE_LABEL}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active (should not change) + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx)" "null" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."initial"')" "true" + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."added"')" "true" + assert_equal "$(echo "$output" | jq -r .key.metadata.created_at.seconds)" "${initial_created_at_seconds}" # created_at should not change + + # Verify with a subsequent get + run_otdfctl_key get --key "${UPDATE_KEY_LABEL_SYSTEM_ID}" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."initial"')" "true" + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."added"')" "true" +} + +@test "kas-keys: update key labels (replace)" { + KEY_ID_UPDATE_LABEL_REPLACE=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UPDATE_LABEL_REPLACE}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --label "initial=true" --json + assert_success + UPDATE_KEY_LABEL_REPLACE_SYSTEM_ID=$(echo "$output" | jq -r .key.id) + local initial_created_at_replace_seconds=$(echo "$output" | jq -r .key.metadata.created_at.seconds) + + run_otdfctl_key update --id "${UPDATE_KEY_LABEL_REPLACE_SYSTEM_ID}" --label "replaced=true" --force-replace-labels --json + assert_success + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.id)" "${UPDATE_KEY_LABEL_REPLACE_SYSTEM_ID}" + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID_UPDATE_LABEL_REPLACE}" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key + assert_equal "$(echo "$output" | jq -r .key.key_status)" "1" # active + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."replaced"')" "true" + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."initial" // "null"')" "null" + assert_equal "$(echo "$output" | jq -r .key.metadata.created_at.seconds)" "${initial_created_at_replace_seconds}" + + # Verify with a subsequent get + run_otdfctl_key get --key "${UPDATE_KEY_LABEL_REPLACE_SYSTEM_ID}" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."replaced"')" "true" + assert_equal "$(echo "$output" | jq -r '.key.metadata.labels."initial" // "null"')" "null" +} + +@test "kas-keys: update key (not found)" { + run_otdfctl_key update --id "39af808f-6cac-403f-90d7-6b88e865860d" --json + assert_failure + assert_output --partial "Failed to update kas key" +} + +@test "kas-keys: update key (missing id)" { + run_otdfctl_key update --json + assert_failure + assert_equal "$(echo "$output" | jq -r .status)" "ERROR" + assert_equal "$(echo "$output" | jq -r .message)" "Flag '--id' is required" +} + +# LIST Tests +@test "kas-keys: list keys (default limit and offset)" { + # Create a few keys to ensure there\'s something to list and to check structure + KEY_ID_LIST_1=$(generate_key_id) + + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_LIST_1}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + local key1_system_id=$(echo "$output" | jq -r .key.id) + + KEY_ID_LIST_2=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_LIST_2}" --algorithm "ec:secp256r1" --mode "public_key" --public-key-pem "${PEM_B64_EC_P256}" --json + assert_success + local key2_system_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key list --json + assert_success + + # For key1: + assert_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.id')" "${key1_system_id}" + assert_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_id')" "${KEY_ID_LIST_1}" + assert_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_algorithm')" "1" + assert_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_mode')" "4" + assert_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_status')" "1" + assert_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.legacy')" "null" + assert_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.public_key_ctx.pem')" "${PEM_B64_RSA}" + assert_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.private_key_ctx')" "null" + assert_not_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.metadata.created_at')" "null" + assert_not_equal "$(echo "$output" | jq -r --arg id "${key1_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.metadata.updated_at')" "null" + + # For key2: + assert_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.id')" "${key2_system_id}" + assert_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_id')" "${KEY_ID_LIST_2}" + assert_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_algorithm')" "3" + assert_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_mode')" "4" + assert_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.legacy')" "null" + assert_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_status')" "1" + assert_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.public_key_ctx.pem')" "${PEM_B64_EC_P256}" + assert_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.private_key_ctx')" "null" + assert_not_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.metadata.created_at')" "null" + assert_not_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.metadata.updated_at')" "null" +} + +@test "kas-keys: list keys supports sort and order flags" { + sort_prefix="sort-key-$BATS_TEST_NUMBER-$RANDOM" + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${sort_prefix}-alpha" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + key_a_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${sort_prefix}-bravo" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + key_b_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${sort_prefix}-charlie" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + key_c_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort key_id --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.kas_keys[] | select(.key.key_id | startswith($prefix)) | .key.id] | join(",")')" "$key_c_id,$key_b_id,$key_a_id" + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort key_id --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.kas_keys[] | select(.key.key_id | startswith($prefix)) | .key.id] | join(",")')" "$key_a_id,$key_b_id,$key_c_id" + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$key_a_id" --arg b "$key_b_id" --arg c "$key_c_id" '[.kas_keys[] | select(.key.id == $a or .key.id == $b or .key.id == $c) | .key.id] | join(",")')" "$key_a_id,$key_b_id,$key_c_id" + + run_otdfctl_key update --id "$key_a_id" --label sort=a --json + assert_success + run_otdfctl_key update --id "$key_b_id" --label sort=b --json + assert_success + run_otdfctl_key update --id "$key_c_id" --label sort=c --json + assert_success + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$key_a_id" --arg b "$key_b_id" --arg c "$key_c_id" '[.kas_keys[] | select(.key.id == $a or .key.id == $b or .key.id == $c) | .key.id] | join(",")')" "$key_a_id,$key_b_id,$key_c_id" + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort key_id --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.kas_keys[] | select(.key.key_id | startswith($prefix)) | .key.id] | join(",")')" "$key_c_id,$key_b_id,$key_a_id" + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$key_a_id" --arg b "$key_b_id" --arg c "$key_c_id" '[.kas_keys[] | select(.key.id == $a or .key.id == $b or .key.id == $c) | .key.id] | join(",")')" "$key_a_id,$key_b_id,$key_c_id" +} + +@test "kas-keys: list keys (pagination with limit and offset)" { + KAS_NAME_LIST=$(generate_kas_name) + KAS_URI_LIST=$(format_kas_name_as_uri "${KAS_NAME_LIST}") + KAS_ID_LIST=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$KAS_NAME_LIST" --uri "$KAS_URI_LIST" --json | jq -r '.id') + assert_not_equal "$KAS_ID_LIST" "" + + # Create a known set of keys for pagination testing + local key_p1_id=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_ID_LIST}" --key-id "${key_p1_id}" --algorithm "rsa:4096" --mode "public_key" --public-key-pem "${PEM_B64_RSA_4096}" --json + assert_success + local key_p1_sys_id=$(echo "$output" | jq -r .key.id) + + local key_p2_id=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_ID_LIST}" --key-id "${key_p2_id}" --algorithm "ec:secp256r1" --mode "public_key" --public-key-pem "${PEM_B64_EC_P256}" --json + assert_success + local key_p2_sys_id=$(echo "$output" | jq -r .key.id) + + local key_p3_id=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_ID_LIST}" --key-id "${key_p3_id}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + local key_p3_sys_id=$(echo "$output" | jq -r .key.id) + + # Test: limit 1, offset 0 - should get the first key (order dependent on server, so we check for one of them) + run_otdfctl_key list --kas "${KAS_ID_LIST}" --limit 1 --offset 0 --json + assert_success + assert_equal "$(echo "$output" | jq '.kas_keys | length')" "1" + local found_id_limit1_offset0=$(echo "$output" | jq -r '.kas_keys[0].key.id') + + # Test: limit 1, offset 1 - should get the second key + run_otdfctl_key list --kas "${KAS_ID_LIST}" --limit 1 --offset 1 --json + assert_success + assert_equal "$(echo "$output" | jq '.kas_keys | length')" "1" + local found_id_limit1_offset1=$(echo "$output" | jq -r '.kas_keys[0].key.id') + assert_not_equal "${found_id_limit1_offset1}" "${found_id_limit1_offset0}" + + run_otdfctl_key list --kas "${KAS_ID_LIST}" --limit 3 --offset 0 --json # Fetch up to 3 + assert_success + local count_limit3_offset0=$(echo "$output" | jq '.kas_keys | length') + assert_equal "${count_limit3_offset0}" 3 + assert_equal "$(echo "$output" | jq -r --arg id "${key_p1_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.id')" "${key_p1_sys_id}" + assert_equal "$(echo "$output" | jq -r --arg id "${key_p2_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.id')" "${key_p2_sys_id}" + assert_equal "$(echo "$output" | jq -r --arg id "${key_p3_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.id')" "${key_p3_sys_id}" + + # Test: limit 1, offset (large number, e.g., 100) - should get 0 keys + run_otdfctl_key list --kas "${KAS_ID_LIST}" --limit 1 --offset 100 --json + assert_success + assert_equal "$(echo $output | jq '.kas_keys | length')" "0" + + delete_all_keys_in_kas "$KAS_ID_LIST" + delete_kas_registry "$KAS_ID_LIST" +} + +@test "kas-keys: list keys (filter by algorithm rsa:2048)" { + KAS_NAME_LIST=$(generate_kas_name) + KAS_URI_LIST=$(format_kas_name_as_uri "${KAS_NAME_LIST}") + KAS_ID_LIST=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$KAS_NAME_LIST" --uri "$KAS_URI_LIST" --json | jq -r '.id') + assert_not_equal "$KAS_ID_LIST" "" + + # Ensure at least one rsa:2048 key exists for this KAS + KEY_ID_LIST_RSA=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_ID_LIST}" --key-id "${KEY_ID_LIST_RSA}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + local rsa_key_sys_id=$(echo "$output" | jq -r .key.id) + + # Ensure at least one non-rsa:2048 key exists for this KAS to test filtering + KEY_ID_LIST_EC=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_ID_LIST}" --key-id "${KEY_ID_LIST_EC}" --algorithm "ec:secp256r1" --mode "public_key" --public-key-pem "${PEM_B64_EC_P256}" --json + assert_success + local ec_key_sys_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key list --kas "${KAS_ID_LIST}" --algorithm "rsa:2048" --json + assert_success + # Every key in the list should be rsa:2048 + # And our specific RSA key should be present + assert_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.id')" "${rsa_key_sys_id}" + assert_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_id')" "${KEY_ID_LIST_RSA}" + assert_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_algorithm')" "1" + assert_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_mode')" "4" + assert_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.key_status')" "1" + assert_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.public_key_ctx.pem')" "${PEM_B64_RSA}" + assert_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.private_key_ctx')" "null" + assert_not_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.metadata.created_at')" "null" + assert_not_equal "$(echo "$output" | jq -r --arg id "${rsa_key_sys_id}" '.kas_keys[] | select(.key.id == $id) | .key.metadata.updated_at')" "null" + + # Check that all listed keys have key_algorithm 1 (algorithmORITHM_RSA_2048) + local count_non_rsa=$(echo "$output" | jq '[.kas_keys[] | select(.key.key_algorithm != 1)] | length') + assert_equal "$count_non_rsa" "0" + + delete_all_keys_in_kas "$KAS_ID_LIST" + delete_kas_registry "$KAS_ID_LIST" +} + +@test "kas-keys: list keys (filter by kas)" { + KAS_NAME_LIST=$(generate_kas_name) + KAS_URI_LIST=$(format_kas_name_as_uri "${KAS_NAME_LIST}") + KAS_ID_LIST=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$KAS_NAME_LIST" --uri "$KAS_URI_LIST" --json | jq -r '.id') + assert_not_equal "$KAS_ID_LIST" "" + + KEY_ID_LIST_KAS_FILTER=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_ID_LIST}" --key-id "${KEY_ID_LIST_KAS_FILTER}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + local kas_filter_key_sys_id=$(echo "$output" | jq -r .key.id) + + # List keys for the new KAS + run_otdfctl_key list --kas "${KAS_ID_LIST}" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].kas_id')" "${KAS_ID_LIST}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.id')" "${kas_filter_key_sys_id}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_id')" "${KEY_ID_LIST_KAS_FILTER}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_algorithm')" "1" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_mode')" "4" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_status')" "1" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.legacy')" "null" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.public_key_ctx.pem')" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.private_key_ctx')" "null" + assert_not_equal "$(echo "$output" | jq -r '.kas_keys[0].key.metadata.created_at')" "null" + assert_not_equal "$(echo "$output" | jq -r '.kas_keys[0].key.metadata.updated_at')" "null" + assert_equal "$(echo "$output" | jq '.kas_keys | length')" "1" + + # List keys for the default KAS_REGISTRY_ID and ensure the new key is not present + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg id "${kas_filter_key_sys_id}" '[.kas_keys[] | select(.key.id == $id)] | length')" "0" + + delete_all_keys_in_kas "$KAS_ID_LIST" + delete_kas_registry "$KAS_ID_LIST" +} + +@test "kas-keys: list keys (filter by kasName)" { + KAS_NAME_LIST=$(generate_kas_name) + echo "DEBUG: KAS_NAME_LIST: ${KAS_NAME_LIST}" >&2 + KAS_URI_LIST=$(format_kas_name_as_uri "${KAS_NAME_LIST}") + KAS_ID_LIST=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$KAS_NAME_LIST" --uri "$KAS_URI_LIST" --json | jq -r '.id') + assert_not_equal "$KAS_ID_LIST" "" + + KEY_ID_LIST_KAS_NAME_FILTER=$(generate_key_id) + local lower_kas_name=$(echo "${KAS_NAME_LIST}" | tr '[:upper:]' '[:lower:]') + run_otdfctl_key create --kas "${lower_kas_name}" --key-id "${KEY_ID_LIST_KAS_NAME_FILTER}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + local kas_name_filter_key_sys_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key list --kas "${lower_kas_name}" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].kas_id')" "${KAS_ID_LIST}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.id')" "${kas_name_filter_key_sys_id}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_id')" "${KEY_ID_LIST_KAS_NAME_FILTER}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_algorithm')" "1" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_mode')" "4" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_status')" "1" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.public_key_ctx.pem')" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.private_key_ctx')" "null" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.legacy')" "null" + assert_not_equal "$(echo "$output" | jq -r '.kas_keys[0].key.metadata.created_at')" "null" + assert_not_equal "$(echo "$output" | jq -r '.kas_keys[0].key.metadata.updated_at')" "null" + + delete_all_keys_in_kas "$KAS_ID_LIST" + delete_kas_registry "$KAS_ID_LIST" +} + +@test "kas-keys: list keys (filter by kasUri)" { + # This command is not a 'kas-registry key' subcommand, so it won't use run_otdfctl_key + KAS_NAME_LIST=$(generate_kas_name) + KAS_URI_LIST=$(format_kas_name_as_uri "${KAS_NAME_LIST}") + KAS_ID_LIST=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$KAS_NAME_LIST" --uri "$KAS_URI_LIST" --json | jq -r '.id') + assert_not_equal "$KAS_ID_LIST" "" + + KEY_ID_LIST_KAS_URI_FILTER=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_URI_LIST}" --key-id "${KEY_ID_LIST_KAS_URI_FILTER}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + local kas_uri_filter_key_sys_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key list --kas "${KAS_URI_LIST}" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].kas_id')" "${KAS_ID_LIST}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.id')" "${kas_uri_filter_key_sys_id}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_id')" "${KEY_ID_LIST_KAS_URI_FILTER}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_algorithm')" "1" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_mode')" "4" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_status')" "1" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.legacy')" "null" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.public_key_ctx.pem')" "${PEM_B64}" + + delete_all_keys_in_kas "$KAS_ID_LIST" + delete_kas_registry "$KAS_ID_LIST" +} + + +@test "kas-keys: list legacy keys" { + KAS_NAME_LIST=$(generate_kas_name) + KAS_URI_LIST=$(format_kas_name_as_uri "${KAS_NAME_LIST}") + KAS_ID_LIST=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$KAS_NAME_LIST" --uri "$KAS_URI_LIST" --json | jq -r '.id') + assert_not_equal "$KAS_ID_LIST" "" + + NON_LEGACY_KEY_ID="imported-key-$(generate_key_id)" + run_otdfctl_key import --key-id "${NON_LEGACY_KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_ID_LIST}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" \ + --legacy false \ + --json + + # Create a key that should be returned when legacy=true + KEY_ID_LEGACY=$(generate_key_id) + run_otdfctl_key import --key-id "${KEY_ID_LEGACY}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_ID_LIST}" \ + --wrapping-key-id "test-wrapping-key-legacy" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" \ + --legacy true \ + --json + assert_success + + # List keys with legacy=true + run_otdfctl_key list --legacy true --kas "${KAS_ID_LIST}" --json + assert_success + assert_equal "$(echo "$output" | jq '.kas_keys | length')" "1" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_id')" "${KEY_ID_LEGACY}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.legacy')" "true" + + run_otdfctl_key list --legacy false --kas "${KAS_ID_LIST}" --json + assert_success + assert_equal "$(echo "$output" | jq '.kas_keys | length')" "1" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.key_id')" "${NON_LEGACY_KEY_ID}" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].key.legacy')" "null" + + run_otdfctl_key list --kas "${KAS_ID_LIST}" --json + assert_success + assert_equal "$(echo "$output" | jq '.kas_keys | length')" "2" + assert_equal "$(echo "$output" | jq -r --arg id "${NON_LEGACY_KEY_ID}" '.kas_keys[] | select(.key.key_id == $id) | .key.key_id')" "${NON_LEGACY_KEY_ID}" + assert_equal "$(echo "$output" | jq -r --arg id "${KEY_ID_LEGACY}" '.kas_keys[] | select(.key.key_id == $id) | .key.key_id')" "${KEY_ID_LEGACY}" + + delete_all_keys_in_kas "$KAS_ID_LIST" + delete_kas_registry "$KAS_ID_LIST" +} + +@test "kas-keys: list keys (invalid algorithm)" { + run_otdfctl_key list --algorithm "invalid-algorithm" --json + assert_failure + assert_output --partial "Invalid algorithm" +} + +@test "kas-keys: list keys (legacy=invalid)" { + run_otdfctl_key list --legacy invalid --json + assert_failure + assert_output --partial "Invalid legacy flag" +} + +@test "kas-keys: rotate key" { + # Create a key first + OLD_KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${OLD_KEY_ID}" --algorithm "rsa:2048" --mode "local" --wrapping-key-id "wrapping-key-1" --wrapping-key "${WRAPPING_KEY}" --json + assert_success + OLD_KEY_SYSTEM_ID=$(echo "$output" | jq -r .key.id) + + # Rotate the key + NEW_KEY_ID=$(generate_key_id) + run_otdfctl_key rotate --key "${OLD_KEY_SYSTEM_ID}" --key-id "${NEW_KEY_ID}" --algorithm "rsa:2048" --mode "local" --wrapping-key-id "wrapping-key-2" --wrapping-key "${WRAPPING_KEY}" --json + assert_success + + # Verify the new key in kas_key section + NEW_KEY_SYSTEM_ID=$(echo "$output" | jq -r .kas_key.key.id) + assert_not_equal "${OLD_KEY_SYSTEM_ID}" "${NEW_KEY_SYSTEM_ID}" + assert_equal "$(echo "$output" | jq -r .kas_key.key.key_id)" "${NEW_KEY_ID}" + assert_equal "$(echo "$output" | jq -r .kas_key.key.key_algorithm)" "1" # rsa:2048 + assert_equal "$(echo "$output" | jq -r .kas_key.key.key_mode)" "1" # local + assert_equal "$(echo "$output" | jq -r .kas_key.key.key_status)" "1" # active (new key should be active) + assert_not_equal "$(echo "$output" | jq -r .kas_key.key.public_key_ctx.pem)" "null" + assert_not_equal "$(echo "$output" | jq -r .kas_key.key.public_key_ctx.pem)" "" + assert_equal "$(echo "$output" | jq -r .kas_key.key.private_key_ctx.key_id)" "wrapping-key-2" + assert_not_equal "$(echo "$output" | jq -r .kas_key.key.private_key_ctx.wrapped_key)" "null" + assert_not_equal "$(echo "$output" | jq -r .kas_key.key.private_key_ctx.wrapped_key)" "" + assert_not_equal "$(echo "$output" | jq -r .kas_key.key.metadata.created_at)" "null" + assert_not_equal "$(echo "$output" | jq -r .kas_key.key.metadata.updated_at)" "null" + + # Verify the old rotated key in rotated_resources section + assert_equal "$(echo "$output" | jq -r .rotated_resources.rotated_out_key.key.id)" "${OLD_KEY_SYSTEM_ID}" + assert_equal "$(echo "$output" | jq -r .rotated_resources.rotated_out_key.key.key_id)" "${OLD_KEY_ID}" + assert_equal "$(echo "$output" | jq -r .rotated_resources.rotated_out_key.key.key_status)" "2" # rotated (old key should be marked as rotated) +} + +@test "kas-keys: rotate key (missing key)" { + run_otdfctl_key rotate --key-id "new-key-id" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" + assert_failure + assert_output --partial "Flag '--key' is required" +} + +@test "kas-keys: rotate key (missing key-id)" { + run_otdfctl_key rotate --key "old-key-id" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" + assert_failure + assert_output --partial "Flag '--key-id' is required" +} + +@test "kas-keys: rotate key (missing algorithm)" { + run_otdfctl_key rotate --key "old-key-id" --key-id "new-key-id" --mode "public_key" --public-key-pem "${PEM_B64}" + assert_failure + assert_output --partial "Flag '--algorithm' is required" +} + +@test "kas-keys: rotate key (missing mode)" { + run_otdfctl_key rotate --key "old-key-id" --key-id "new-key-id" --algorithm "rsa:2048" --public-key-pem "${PEM_B64}" + assert_failure + assert_output --partial "Flag '--mode' is required" +} + +@test "kas-keys: rotate key (local mode, missing wrapping-key-id)" { + run_otdfctl_key rotate --key "old-key-id" --key-id "new-key-id" --algorithm "rsa:2048" --mode "local" --wrapping-key "${WRAPPING_KEY}" + assert_failure + assert_output --partial "wrapping-key-id is required for mode local" +} + +@test "kas-keys: rotate key (local mode, missing wrapping-key)" { + run_otdfctl_key rotate --key "old-key-id" --key-id "new-key-id" --algorithm "rsa:2048" --mode "local" --wrapping-key-id "wrapping-key-1" + assert_failure + assert_output --partial "Flag '--wrapping-key' is required" +} + +@test "kas-keys: rotate key (public_key mode, missing public-key-pem)" { + run_otdfctl_key rotate --key "old-key-id" --key-id "new-key-id" --algorithm "rsa:2048" --mode "public_key" + assert_failure + assert_output --partial "Flag '--public-key-pem' is required" +} + +@test "kas-keys: rotate key (remote mode, missing provider-config-id)" { + run_otdfctl_key rotate --key "old-key-id" --key-id "new-key-id" --algorithm "rsa:2048" --mode "remote" --public-key-pem "${PEM_B64}" --wrapping-key-id "wk-1" + assert_failure + assert_output --partial "Flag '--provider-config-id' is required" +} + +@test "kas-keys: rotate key (invalid algorithm)" { + # Create a key first that we can try to rotate + OLD_KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${OLD_KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + + # Try to rotate with invalid algorithm + NEW_KEY_ID=$(generate_key_id) + run_otdfctl_key rotate --key "${OLD_KEY_ID}" --key-id "${NEW_KEY_ID}" --algorithm "invalid-algorithm" --mode "public_key" --public-key-pem "${PEM_B64}" + assert_failure + assert_output --partial "invalid algorithm" +} + +@test "kas-keys: rotate key (invalid mode)" { + # Create a key first that we can try to rotate + OLD_KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${OLD_KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + + # Try to rotate with invalid mode + NEW_KEY_ID=$(generate_key_id) + run_otdfctl_key rotate --key "${OLD_KEY_ID}" --key-id "${NEW_KEY_ID}" --algorithm "rsa:2048" --mode "invalid-mode" --public-key-pem "${PEM_B64}" + assert_failure + assert_output --partial "invalid mode" +} + +@test "kas-keys: rotate key (invalid hex encoded wrapping-key)" { + # Create a key first that we can try to rotate + OLD_KEY_ID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${OLD_KEY_ID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + + # Try to rotate with invalid wrapping-key + NEW_KEY_ID=$(generate_key_id) + run_otdfctl_key rotate --key "${OLD_KEY_ID}" --key-id "${NEW_KEY_ID}" --algorithm "rsa:2048" --mode "local" --public-key-pem "${PEM_B64}" --wrapping-key "not-hex-encoded" --wrapping-key-id "wrapping-key-1" + assert_failure + assert_output --partial "wrapping-key must be hex encoded" +} + +@test "kas-keys: import key successful" { + KEY_ID="imported-key-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" \ + --json + assert_success + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.key_id)" "test-wrapping-key" + assert_not_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "null" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "1" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" +} + + +@test "kas-keys: import key successful (legacy=true)" { + KEY_ID="imported-key-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" \ + --legacy true \ + --json + assert_success + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.key_id)" "test-wrapping-key" + assert_not_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "null" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "1" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "true" +} + +@test "kas-keys: import key successful (legacy=false)" { + KEY_ID="imported-key-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" \ + --legacy false \ + --json + assert_success + + assert_equal "$(echo "$output" | jq -r .key.key_id)" "${KEY_ID}" + assert_equal "$(echo "$output" | jq -r .kas_id)" "${KAS_REGISTRY_ID}" + assert_equal "$(echo "$output" | jq -r .key.public_key_ctx.pem)" "${PEM_B64}" + assert_equal "$(echo "$output" | jq -r .key.private_key_ctx.key_id)" "test-wrapping-key" + assert_not_equal "$(echo "$output" | jq -r .key.private_key_ctx.wrapped_key)" "null" + assert_equal "$(echo "$output" | jq -r .key.key_algorithm)" "1" + assert_equal "$(echo "$output" | jq -r .key.key_mode)" "1" + assert_equal "$(echo "$output" | jq -r .key.legacy)" "null" +} + +@test "kas-keys: import key failure (legacy=invalid)" { + KEY_ID="imported-key-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" \ + --legacy invalid \ + --json + assert_failure + assert_output --partial "Invalid legacy flag" +} + +@test "kas-keys: import key failure - missing required private key" { + KEY_ID="import-fail-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" + + assert_failure + assert_output --partial "'--private-key-pem' is required" +} + +@test "kas-keys: import key failure - invalid wrapping key" { + KEY_ID="import-fail-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "not-a-valid-hex-string" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" + + assert_failure + assert_output --partial "wrapping-key must be hex encoded" +} + +@test "kas-keys: import key failure - invalid public key PEM" { + KEY_ID="import-fail-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "not-base64-encoded" \ + --private-key-pem "${PEM_B64}" + + assert_failure + assert_output --partial "public-key-pem must be base64 encoded" +} + +@test "kas-keys: import key failure - invalid private key PEM" { + KEY_ID="import-fail-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "not-base64-encoded" + + assert_failure + assert_output --partial "private-key-pem must be base64 encoded" +} + +@test "kas-keys: import key failure - invalid algorithm" { + KEY_ID="import-fail-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "invalid-algorithm" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" + + assert_failure + assert_output --partial "Invalid algorithm" +} + +@test "kas-keys: import key failure - missing wrapping key ID" { + KEY_ID="import-fail-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key "${WRAPPING_KEY}" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" + + assert_failure + assert_output --partial "'--wrapping-key-id' is required" +} + +@test "kas-keys: import key failure - missing wrapping key" { + KEY_ID="import-fail-$(generate_key_id)" + + run_otdfctl_key import --key-id "${KEY_ID}" \ + --algorithm "rsa:2048" \ + --kas "${KAS_REGISTRY_ID}" \ + --wrapping-key-id "test-wrapping-key" \ + --public-key-pem "${PEM_B64}" \ + --private-key-pem "${PEM_B64}" + + assert_failure + assert_output --partial "'--wrapping-key' is required" +} + +@test "kas-keys: delete key" { + KID=$(generate_key_id) + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KID}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json + assert_success + CREATED_KEY_SYSTEM_ID=$(echo "$output" | jq -r .key.id) + + + run_otdfctl_key unsafe delete --id "${CREATED_KEY_SYSTEM_ID}" --key-id "${KID}" --kas-uri ${KAS_URI} --json --force + assert_success + + run_otdfctl_key get --key "${CREATED_KEY_SYSTEM_ID}" --json + assert_failure + assert_output --partial "Failed to get kas key" +} + +@test "kas-keys: delete key failure - (missing id)" { + run_otdfctl_key unsafe delete --kas-uri "a-uri" --key-id "key" --force + assert_failure + assert_output --partial "Flag '--id' is required" +} + +@test "kas-keys: delete key failure - (missing key-id)" { + run_otdfctl_key unsafe delete --id "ded32e6d-9fec-4a4c-a391-13158c52e5f2" --kas-uri "a-uri" --force + assert_failure + assert_output --partial "Flag '--key-id' is required" +} + +@test "kas-keys: delete key failure - (missing kas-uri)" { + run_otdfctl_key unsafe delete --id "ded32e6d-9fec-4a4c-a391-13158c52e5f2" --key-id "kid" --force + assert_failure + assert_output --partial "Flag '--kas-uri' is required" +} diff --git a/otdfctl/e2e/kas-registry.bats b/otdfctl/e2e/kas-registry.bats new file mode 100755 index 0000000000..f97eda9de6 --- /dev/null +++ b/otdfctl/e2e/kas-registry.bats @@ -0,0 +1,214 @@ +#!/usr/bin/env bats + +# Tests for kas registry + +setup_file() { + export CREDSFILE=creds.json + echo -n '{"clientId":"opentdf","clientSecret":"secret"}' >$CREDSFILE + export WITH_CREDS="--with-client-creds-file $CREDSFILE" + export HOST='--host http://localhost:8080' + export DEBUG_LEVEL="--log-level debug" +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_kasr() { + run sh -c "./otdfctl policy kas-registry $HOST $WITH_CREDS $*" + } +} + +teardown() { + if [[ -z "${CREATED:-}" ]]; then + return + fi + + ID=$(echo "$CREATED" | jq -r '.id') + if [[ -n "$ID" ]]; then + run_otdfctl_kasr delete --id "$ID" --force + fi +} + +@test "create KAS registration with invalid URI - fails" { + BAD_URIS=( + "no-scheme.co" + "localhost" + "http://example.com:abc" + "https ://example.com" + ) + + for URI in "${BAD_URIS[@]}"; do + run_otdfctl_kasr create --uri "$URI" + assert_failure + assert_output --partial "Failed to create Registered KAS" + assert_output --partial "uri: " + done +} + +@test "create KAS registration with duplicate URI - fails" { + URI="https://testing-duplication.io" + run_otdfctl_kasr create --uri "$URI" + assert_success + export CREATED="$output" + run_otdfctl_kasr create --uri "$URI" + assert_failure + assert_output --partial "Failed to create Registered KAS entry" + assert_output --partial "already_exists" +} + +@test "create KAS registration with duplicate name - fails" { + NAME="duplicate_name_kas" + run_otdfctl_kasr create --uri "https://testing-duplication.name.io" -n "$NAME" + assert_success + run_otdfctl_kasr create --uri "https://testing-duplication.name.net" -n "$NAME" + assert_failure + assert_output --partial "Failed to create Registered KAS entry" + assert_output --partial "already_exists" +} + +@test "create KAS registration with invalid name - fails" { + URI="http://creating.kas.invalid.name/kas" + BAD_NAMES=( + "-bad-name" + "bad-name-" + "_bad_name" + "bad_name_" + "name@with!special#chars" + "$(printf 'a%.0s' {1..254})" # Generates a string of 254 'a' characters + ) + + for NAME in "${BAD_NAMES[@]}"; do + echo "testing $NAME" + run_otdfctl_kasr create --uri "$URI" -n "$NAME" + assert_failure + assert_output --partial "Failed to create Registered KAS" + assert_output --partial "name: " + done +} + +@test "update registered KAS" { + URI="https://testing-update.net" + NAME="new-kas-testing-update" + export CREATED=$(./otdfctl $HOST $DEBUG_LEVEL $WITH_CREDS policy kas-registry create --uri "$URI" -n "$NAME" --json) + ID=$(echo "$CREATED" | jq -r '.id') + run_otdfctl_kasr update --id "$ID" -u "https://newuri.com" -n "newer-name" --json + assert_output --partial "$ID" + assert_output --partial "https://newuri.com" + assert_output --partial "newer-name" + refute_output --partial "$NAME" + refute_output --partial "$URI" +} + +@test "update registered KAS with invalid URI - fails" { + export CREATED=$(./otdfctl $HOST $DEBUG_LEVEL $WITH_CREDS policy kas-registry create --uri "https://bad-update.uri.kas" --json) + ID=$(echo "$CREATED" | jq -r '.id') + BAD_URIS=( + "no-scheme.co" + "localhost" + "http://example.com:abc" + "https ://example.com" + ) + + for URI in "${BAD_URIS[@]}"; do + run_otdfctl_kasr update -i "$ID" --uri "$URI" + assert_failure + assert_output --partial "$ID" + assert_output --partial "Failed to update Registered KAS entry" + assert_output --partial "uri: " + done +} + +@test "update registered KAS with invalid name - fails" { + export CREATED=$(./otdfctl $HOST $DEBUG_LEVEL $WITH_CREDS policy kas-registry create --uri "https://bad-update.name.kas" --json) + ID=$(echo "$CREATED" | jq -r '.id') + BAD_NAMES=( + "-bad-name" + "bad-name-" + "_bad_name" + "bad_name_" + "name@with!special#chars" + "$(printf 'a%.0s' {1..254})" # Generates a string of 254 'a' characters + ) + + for NAME in "${BAD_NAMES[@]}"; do + run_otdfctl_kasr update --id "$ID" --name "$NAME" + assert_failure + assert_output --partial "Failed to update Registered KAS" + assert_output --partial "name: " + done +} + +@test "list registered KASes" { + URI="https://testing-list.io" + NAME="listed-kas" + export CREATED=$(./otdfctl $HOST $DEBUG_LEVEL $WITH_CREDS policy kas-registry create --uri "$URI" -n "$NAME" --json) + ID=$(echo "$CREATED" | jq -r '.id') + run_otdfctl_kasr list --json + assert_output --partial "$ID" + assert_output --partial "uri" + assert_output --partial "$URI" + assert_output --partial "name" + assert_output --partial "$NAME" + + run_otdfctl_kasr list + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + run_otdfctl_kasr list --json + assert_success + assert_not_equal $(echo "$output" | jq -r ".pagination") "null" + total=$(echo "$output" | jq -r ".pagination.total") + [[ $total -ge 1 ]] +} + +@test "list registered KASes supports sort and order flags" { + export CREATED="" + sort_prefix="sort-kas-$BATS_TEST_NUMBER-$RANDOM" + kas_a=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$sort_prefix-alpha" --uri "https://$sort_prefix-alpha.example.com" --json) + kas_b=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$sort_prefix-bravo" --uri "https://$sort_prefix-bravo.example.com" --json) + kas_c=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$sort_prefix-charlie" --uri "https://$sort_prefix-charlie.example.com" --json) + kas_a_id=$(echo "$kas_a" | jq -r '.id') + kas_b_id=$(echo "$kas_b" | jq -r '.id') + kas_c_id=$(echo "$kas_c" | jq -r '.id') + + run_otdfctl_kasr list --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.key_access_servers[] | select((.name // "") | startswith($prefix)) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr list --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.key_access_servers[] | select((.name // "") | startswith($prefix)) | .id] | join(",")')" "$kas_c_id,$kas_b_id,$kas_a_id" + + run_otdfctl_kasr list --sort uri --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "https://$sort_prefix" '[.key_access_servers[] | select(.uri | startswith($prefix)) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr list --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$kas_a_id" --arg b "$kas_b_id" --arg c "$kas_c_id" '[.key_access_servers[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr update --id "$kas_a_id" --label sort=a --json + assert_success + run_otdfctl_kasr update --id "$kas_b_id" --label sort=b --json + assert_success + run_otdfctl_kasr update --id "$kas_c_id" --label sort=c --json + assert_success + + run_otdfctl_kasr list --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$kas_a_id" --arg b "$kas_b_id" --arg c "$kas_c_id" '[.key_access_servers[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr list --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.key_access_servers[] | select((.name // "") | startswith($prefix)) | .id] | join(",")')" "$kas_c_id,$kas_b_id,$kas_a_id" + + run_otdfctl_kasr list --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$kas_a_id" --arg b "$kas_b_id" --arg c "$kas_c_id" '[.key_access_servers[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr delete --id "$kas_a_id" --force + run_otdfctl_kasr delete --id "$kas_b_id" --force + run_otdfctl_kasr delete --id "$kas_c_id" --force +} diff --git a/otdfctl/e2e/key-base.bats b/otdfctl/e2e/key-base.bats new file mode 100644 index 0000000000..a3517cd1b9 --- /dev/null +++ b/otdfctl/e2e/key-base.bats @@ -0,0 +1,170 @@ +#!/usr/bin/env bats + +# NEEDS TO RUN AFTER encrypt-decrypt.bats + +setup_file() { + bats_load_library bats-support + bats_load_library bats-assert + load "otdfctl-utils.sh" + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # Create a KAS registry entry for testing base keys + export KAS_NAME_BASE_KEY_TEST="kas-registry-for-base-key-tests" + export KAS_URI_BASE_KEY_TEST="https://test-kas-for-base-keys.com" + export KAS_REGISTRY_ID_BASE_KEY_TEST=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "${KAS_NAME_BASE_KEY_TEST}" --uri "${KAS_URI_BASE_KEY_TEST}" --json | jq -r '.id') + + # Create a regular KAS key to be set as a base key + # This key will be used by the 'set' command tests + export REGULAR_KEY_ID_FOR_BASE_TEST="regular-key-for-base-$(date +%s)" + export WRAPPING_KEY="9453b4d7cc55cf27926ae8f98a9d5aa159d51b7a4d478e440271ab261792a2bd" + export KAS_KEY_SYSTEM_ID=$(./otdfctl $HOST $WITH_CREDS policy kas-registry key create --kas "${KAS_REGISTRY_ID_BASE_KEY_TEST}" --key-id "${REGULAR_KEY_ID_FOR_BASE_TEST}" --algorithm rsa:2048 --mode local --wrapping-key "${WRAPPING_KEY}" --wrapping-key-id "wrapping-key-id" --json | jq -r '.key.id') +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + # invoke binary with credentials for base key commands + run_otdfctl_base_key() { + run sh -c "./otdfctl policy kas-registry key base $HOST $WITH_CREDS $*" + } +} + +teardown_file() { + # Note: A key will be present still, due to a FK where we do + # not allow keys to be deleted if they are currently set as the base key. + delete_all_keys_in_kas "$KAS_REGISTRY_ID_BASE_KEY_TEST" + + unset HOST WITH_CREDS KAS_REGISTRY_ID_BASE_KEY_TEST KAS_NAME_BASE_KEY_TEST KAS_URI_BASE_KEY_TEST REGULAR_KEY_ID_FOR_BASE_TEST WRAPPING_KEY KAS_KEY_SYSTEM_ID +} + +# --- get base key tests --- + +@test "base-key: get (initially no base key should be set for a new KAS)" { + run_otdfctl_base_key get + assert_failure # Expecting failure or specific message indicating no base key + assert_output --partial "No base key found" # Or similar error message +} + +# --- set base key tests --- + +@test "base-key: set by --key (uuid)" { + run_otdfctl_base_key set --key "${KAS_KEY_SYSTEM_ID}" --json + assert_success + # Verify the new base key part of the response + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.kid)" "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .new_base_key.kas_uri)" "${KAS_URI_BASE_KEY_TEST}" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.algorithm)" 1 + # Verify previous base key is null or not present if this is the first set + assert_equal "$(echo "$output" | jq -r .previous_base_key)" "null" +} + +@test "base-key: set by --key(id) and --kas(id)" { + run_otdfctl_base_key set --key "${REGULAR_KEY_ID_FOR_BASE_TEST}" --kas "${KAS_REGISTRY_ID_BASE_KEY_TEST}" --json + assert_success + # Verify the new base key part of the response + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.kid)" "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .new_base_key.kas_uri)" "${KAS_URI_BASE_KEY_TEST}" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.algorithm)" 1 +} + +@test "base-key: get (after setting a base key)" { + run_otdfctl_base_key set --key "${KAS_KEY_SYSTEM_ID}" --json + assert_success + + run_otdfctl_base_key get --json + assert_success + assert_equal "$(echo "$output" | jq -r .public_key.kid)" "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .kas_uri)" "${KAS_URI_BASE_KEY_TEST}" + assert_not_equal "$(echo "$output" | jq -r .public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .public_key.algorithm)" 1 +} + +@test "base-key: set by --key(id) and --kas(name)" { + run_otdfctl_base_key set --key "${REGULAR_KEY_ID_FOR_BASE_TEST}" --kas "${KAS_NAME_BASE_KEY_TEST}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.kid)" "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .new_base_key.kas_uri)" "${KAS_URI_BASE_KEY_TEST}" # KAS URI should remain the same for the KAS Name + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.algorithm)" 1 +} + +@test "base-key: set by --key(id) and --kas(uri)" { + # This will set REGULAR_KEY_ID_FOR_BASE_TEST back as the base key + run_otdfctl_base_key set --key "${REGULAR_KEY_ID_FOR_BASE_TEST}" --kas "${KAS_URI_BASE_KEY_TEST}" --json + assert_success + # Verify the new base key + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.kid)" "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .new_base_key.kas_uri)" "${KAS_URI_BASE_KEY_TEST}" # KAS URI should remain the same for the KAS Name + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.algorithm)" 1 +} + +@test "base-key: set, get, and verify previous base key" { + run_otdfctl_base_key set --key "${KAS_KEY_SYSTEM_ID}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.kid)" "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .new_base_key.kas_uri)" "${KAS_URI_BASE_KEY_TEST}" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.algorithm)" 1 + + run_otdfctl_base_key get --json + assert_success + assert_equal "$(echo "$output" | jq -r .public_key.kid)" "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .kas_uri)" "${KAS_URI_BASE_KEY_TEST}" + assert_not_equal "$(echo "$output" | jq -r .public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .public_key.algorithm)" 1 + + SECOND_KEY_ID_FOR_BASE_TEST="second-key-for-base-$(date +%s)" + SECOND_KAS_KEY_SYSTEM_ID=$(./otdfctl $HOST $WITH_CREDS policy kas-registry key create --kas "${KAS_REGISTRY_ID_BASE_KEY_TEST}" --key-id "${SECOND_KEY_ID_FOR_BASE_TEST}" --algorithm ec:secp256r1 --mode local --wrapping-key "${WRAPPING_KEY}" --wrapping-key-id "test-key" --json | jq -r '.key.id') + + run_otdfctl_base_key set --key "${SECOND_KAS_KEY_SYSTEM_ID}" --json + assert_success + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.kid)" "${SECOND_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .new_base_key.kas_uri)" "${KAS_URI_BASE_KEY_TEST}" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .new_base_key.public_key.algorithm)" 3 + # Verify previous base key + assert_equal "$(echo "$output" | jq -r .previous_base_key.public_key.kid)" "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_equal "$(echo "$output" | jq -r .previous_base_key.kas_uri)" "${KAS_URI_BASE_KEY_TEST}" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "" + assert_not_equal "$(echo "$output" | jq -r .new_base_key.public_key.pem)" "null" + assert_equal "$(echo "$output" | jq -r .previous_base_key.public_key.algorithm)" 1 +} + +@test "base-key: set (missing kas identifier)" { + run_otdfctl_base_key set --key "${REGULAR_KEY_ID_FOR_BASE_TEST}" + assert_failure + assert_output --partial "Flag '--kas' is required" +} + +@test "base-key: set (missing key identifier: id or keyId)" { + run_otdfctl_base_key set --kas "${KAS_REGISTRY_ID_BASE_KEY_TEST}" + assert_failure + assert_output --partial "Flag '--key' is required" +} + +@test "base-key: set (using non-existent keyId)" { + NON_EXISTENT_KEY_ID="this-key-does-not-exist-12345" + run_otdfctl_base_key set --key "${NON_EXISTENT_KEY_ID}" --kas "${KAS_REGISTRY_ID_BASE_KEY_TEST}" + assert_failure + # The exact error message might depend on the backend implementation + assert_output --partial "not_found" # Or a more specific "key not found" error +} + +@test "base-key: set (using non-existent kasId)" { + NON_EXISTENT_KAS_ID="a1b2c3d4-e5f6-7890-1234-567890abcdef" + run_otdfctl_base_key set --key "${REGULAR_KEY_ID_FOR_BASE_TEST}" --kas "${NON_EXISTENT_KAS_ID}" + assert_failure + assert_output --partial "not_found" # Or a more specific "KAS not found" error +} diff --git a/otdfctl/e2e/logging.bats b/otdfctl/e2e/logging.bats new file mode 100644 index 0000000000..b0634a8d71 --- /dev/null +++ b/otdfctl/e2e/logging.bats @@ -0,0 +1,24 @@ +#!/usr/bin/env bats + +setup() { + bats_load_library bats-support + bats_load_library bats-assert +} + +@test "version is logged to stderr when debug logging enabled" { + run --separate-stderr -- ./otdfctl --version --log-level debug + + assert_success + assert_output --partial "otdfctl version" + [[ "$stderr" == *"otdfctl version"* ]] + [[ "$stderr" == *"\"level\":\"DEBUG\""* ]] +} + +@test "version is logged to stderr when debug enabled" { + run --separate-stderr -- ./otdfctl --version --debug + + assert_success + assert_output --partial "otdfctl version" + [[ "$stderr" == *"otdfctl version"* ]] + [[ "$stderr" == *"\"level\":\"DEBUG\""* ]] +} diff --git a/otdfctl/e2e/migrate-namespaced-policy.bats b/otdfctl/e2e/migrate-namespaced-policy.bats new file mode 100644 index 0000000000..78e907a4de --- /dev/null +++ b/otdfctl/e2e/migrate-namespaced-policy.bats @@ -0,0 +1,2695 @@ +#!/usr/bin/env bats + +# bats file_tags=namespaced_policy_migration + +# Tests for namespaced-policy migration +# This file needs isolated execution while the rest of otdfctl/e2e is still +# running in parallel. The migration planner discovers legacy/global policy +# objects by scope, so overlapping unnamespaced fixtures from other BATS files +# can pollute these migration assertions. +# CI should run this tag in a separate invocation, then run the remaining suite +# with this tag filtered out. +# +# Paths intentionally covered here: +# - action-scope migration creates only namespaced action targets, including +# custom-action fanout across namespaces from RR and trigger anchors +# - standard read action resolves to the canonical namespaced target where +# downstream migrations depend on it +# - SCS migration handles single-namespace placement, cross-namespace fanout, +# and reuse of an already-existing canonical target +# - subject-mapping migration rewrites action and SCS dependencies correctly +# and can reuse an already-existing canonical target +# - registered-resource migration rewrites action bindings and reuses canonical +# targets when they already exist +# - obligation-trigger migration rewrites action dependencies and reuses +# canonical targets when they already exist +# - combined all-scope migration creates one target per supported object type +# - every covered scope verifies idempotent reruns and that legacy source +# objects remain in place after migration +# +# Paths that are not in these e2e tests: +# - planner-only or dry-run output, summary formatting, and status bucket +# assertions such as create/already_migrated/existing_standard/unresolved +# - unresolved or interactive-review flows, especially conflicting registered +# resources and manual namespace selection +# - unused legacy objects being skipped entirely by planning and execution +# - subject mappings with multiple actions +# - RRs with multiple action-attribute values + +run_otdfctl_migrate() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json migrate "$@" + assert_success +} + +run_otdfctl_action() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy actions "$@" + assert_success +} + +run_otdfctl_sm() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy subject-mappings "$@" + assert_success +} + +run_otdfctl_scs() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy scs "$@" + assert_success +} + +run_otdfctl_registered_resources() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy registered-resources "$@" + assert_success +} + +run_otdfctl_registered_resource_values() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy registered-resources values "$@" + assert_success +} + +run_otdfctl_obligations() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy obligations "$@" + assert_success +} + +run_otdfctl_obligation_values() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy obligations values "$@" + assert_success +} + +run_otdfctl_obligation_triggers() { + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy obligations triggers "$@" + assert_success +} + +sql_escape_literal() { + printf "%s" "$1" | sed "s/'/''/g" +} + +run_policy_db_sql() { + local sql="$1" + + run env \ + PGPASSWORD="${OPENTDF_DB_PASSWORD:-changeme}" \ + psql \ + -h "${OPENTDF_DB_HOST:-localhost}" \ + -p "${OPENTDF_DB_PORT:-5432}" \ + -U "${OPENTDF_DB_USER:-postgres}" \ + -d "${OPENTDF_DB_DATABASE:-opentdf}" \ + -X \ + -v ON_ERROR_STOP=1 \ + -Atqc "SET search_path TO \"opentdf_policy\", public; ${sql}" +} + +build_metadata_json_from_labels() { + if [ "$#" -eq 0 ]; then + printf '{}' + return + fi + + local metadata_json='{"labels":{}}' + local label + local key + local value + + for label in "$@"; do + key="${label%%=*}" + if [ "$key" = "$label" ]; then + value="" + else + value="${label#*=}" + fi + + metadata_json=$(jq -c --arg key "$key" --arg value "$value" '.labels[$key] = $value' <<< "$metadata_json") + done + + printf '%s' "$metadata_json" +} + +track_action_id() { + local action_id="$1" + TRACKED_ACTION_IDS="${TRACKED_ACTION_IDS}${action_id}"$'\n' +} + +track_registered_resource_id() { + local resource_id="$1" + TRACKED_REGISTERED_RESOURCE_IDS="${TRACKED_REGISTERED_RESOURCE_IDS}${resource_id}"$'\n' +} + +track_registered_resource_value_id() { + local resource_value_id="$1" + TRACKED_REGISTERED_RESOURCE_VALUE_IDS="${TRACKED_REGISTERED_RESOURCE_VALUE_IDS}${resource_value_id}"$'\n' +} + +track_scs_id() { + local scs_id="$1" + TRACKED_SCS_IDS="${TRACKED_SCS_IDS}${scs_id}"$'\n' +} + +track_subject_mapping_id() { + local subject_mapping_id="$1" + TRACKED_SUBJECT_MAPPING_IDS="${TRACKED_SUBJECT_MAPPING_IDS}${subject_mapping_id}"$'\n' +} + +track_obligation_trigger_id() { + local obligation_trigger_id="$1" + TRACKED_OBLIGATION_TRIGGER_IDS="${TRACKED_OBLIGATION_TRIGGER_IDS}${obligation_trigger_id}"$'\n' +} + +remove_tracked_id() { + local remove_id="$1" + local tracked_ids="$2" + local remaining_ids="" + local tracked_id + + while IFS= read -r tracked_id; do + [ -n "$tracked_id" ] || continue + [ "$tracked_id" != "$remove_id" ] || continue + remaining_ids="${remaining_ids}${tracked_id}"$'\n' + done <<< "$tracked_ids" + + printf '%s' "$remaining_ids" +} + +untrack_action_id() { + local action_id="$1" + TRACKED_ACTION_IDS="$(remove_tracked_id "$action_id" "$TRACKED_ACTION_IDS")" + [ -z "$TRACKED_ACTION_IDS" ] || TRACKED_ACTION_IDS="${TRACKED_ACTION_IDS}"$'\n' +} + +untrack_registered_resource_id() { + local resource_id="$1" + TRACKED_REGISTERED_RESOURCE_IDS="$(remove_tracked_id "$resource_id" "$TRACKED_REGISTERED_RESOURCE_IDS")" + [ -z "$TRACKED_REGISTERED_RESOURCE_IDS" ] || TRACKED_REGISTERED_RESOURCE_IDS="${TRACKED_REGISTERED_RESOURCE_IDS}"$'\n' +} + +untrack_registered_resource_value_id() { + local resource_value_id="$1" + TRACKED_REGISTERED_RESOURCE_VALUE_IDS="$(remove_tracked_id "$resource_value_id" "$TRACKED_REGISTERED_RESOURCE_VALUE_IDS")" + [ -z "$TRACKED_REGISTERED_RESOURCE_VALUE_IDS" ] || TRACKED_REGISTERED_RESOURCE_VALUE_IDS="${TRACKED_REGISTERED_RESOURCE_VALUE_IDS}"$'\n' +} + +untrack_scs_id() { + local scs_id="$1" + TRACKED_SCS_IDS="$(remove_tracked_id "$scs_id" "$TRACKED_SCS_IDS")" + [ -z "$TRACKED_SCS_IDS" ] || TRACKED_SCS_IDS="${TRACKED_SCS_IDS}"$'\n' +} + +untrack_subject_mapping_id() { + local subject_mapping_id="$1" + TRACKED_SUBJECT_MAPPING_IDS="$(remove_tracked_id "$subject_mapping_id" "$TRACKED_SUBJECT_MAPPING_IDS")" + [ -z "$TRACKED_SUBJECT_MAPPING_IDS" ] || TRACKED_SUBJECT_MAPPING_IDS="${TRACKED_SUBJECT_MAPPING_IDS}"$'\n' +} + +untrack_obligation_trigger_id() { + local obligation_trigger_id="$1" + TRACKED_OBLIGATION_TRIGGER_IDS="$(remove_tracked_id "$obligation_trigger_id" "$TRACKED_OBLIGATION_TRIGGER_IDS")" + [ -z "$TRACKED_OBLIGATION_TRIGGER_IDS" ] || TRACKED_OBLIGATION_TRIGGER_IDS="${TRACKED_OBLIGATION_TRIGGER_IDS}"$'\n' +} + +create_action() { + local result_var="$1" + local action_name="$2" + shift 2 + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy actions create --name "$action_name" "$@" --json + assert_success + + local created_action_id + created_action_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_action_id" "" + + track_action_id "$created_action_id" + printf -v "$result_var" '%s' "$created_action_id" +} + +create_global_action() { + create_action "$@" +} + +create_namespaced_action() { + local result_var="$1" + local namespace_id="$2" + local action_name="$3" + shift 3 + + create_action "$result_var" "$action_name" --namespace "$namespace_id" "$@" +} + +create_global_scs() { + local result_var="$1" + local subject_sets_json="$2" + shift 2 + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy scs create --subject-sets "$subject_sets_json" "$@" --json + assert_success + + local created_scs_id + created_scs_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_scs_id" "" + + track_scs_id "$created_scs_id" + printf -v "$result_var" '%s' "$created_scs_id" +} + +create_namespaced_scs() { + local result_var="$1" + local namespace_id="$2" + local subject_sets_json="$3" + shift 3 + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy scs create --namespace "$namespace_id" --subject-sets "$subject_sets_json" "$@" --json + assert_success + + local created_scs_id + created_scs_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_scs_id" "" + + track_scs_id "$created_scs_id" + printf -v "$result_var" '%s' "$created_scs_id" +} + +create_legacy_subject_mapping() { + local result_var="$1" + local attribute_value_id="$2" + local action_id="$3" + local subject_condition_set_id="$4" + shift 4 + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy subject-mappings create --attribute-value-id "$attribute_value_id" --action "$action_id" --subject-condition-set-id "$subject_condition_set_id" "$@" --json + assert_success + + local created_subject_mapping_id + created_subject_mapping_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_subject_mapping_id" "" + + track_subject_mapping_id "$created_subject_mapping_id" + printf -v "$result_var" '%s' "$created_subject_mapping_id" +} + +create_namespaced_subject_mapping() { + local result_var="$1" + local namespace_id="$2" + local attribute_value_id="$3" + local action_id="$4" + local subject_condition_set_id="$5" + shift 5 + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy subject-mappings create --namespace "$namespace_id" --attribute-value-id "$attribute_value_id" --action "$action_id" --subject-condition-set-id "$subject_condition_set_id" "$@" --json + assert_success + + local created_subject_mapping_id + created_subject_mapping_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_subject_mapping_id" "" + + track_subject_mapping_id "$created_subject_mapping_id" + printf -v "$result_var" '%s' "$created_subject_mapping_id" +} + +create_global_registered_resource() { + local result_var="$1" + local resource_name="$2" + shift 2 + + run_otdfctl_registered_resources create --name "$resource_name" "$@" --json + assert_success + + local created_resource_id + created_resource_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_resource_id" "" + + track_registered_resource_id "$created_resource_id" + printf -v "$result_var" '%s' "$created_resource_id" +} + +create_namespaced_registered_resource() { + local result_var="$1" + local namespace_id="$2" + local resource_name="$3" + shift 3 + + run_otdfctl_registered_resources create --name "$resource_name" --namespace "$namespace_id" "$@" --json + assert_success + + local created_resource_id + created_resource_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_resource_id" "" + + track_registered_resource_id "$created_resource_id" + printf -v "$result_var" '%s' "$created_resource_id" +} + +create_registered_resource_value() { + local result_var="$1" + local resource_id="$2" + local resource_value="$3" + shift 3 + + run_otdfctl_registered_resource_values create --resource "$resource_id" --value "$resource_value" "$@" --json + assert_success + + local created_resource_value_id + created_resource_value_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_resource_value_id" "" + + track_registered_resource_value_id "$created_resource_value_id" + printf -v "$result_var" '%s' "$created_resource_value_id" +} + +create_namespaced_obligation() { + local result_var="$1" + local namespace_id="$2" + local obligation_name="$3" + shift 3 + + run_otdfctl_obligations create --namespace "$namespace_id" --name "$obligation_name" "$@" --json + assert_success + + local created_obligation_id + created_obligation_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_obligation_id" "" + + printf -v "$result_var" '%s' "$created_obligation_id" +} + +create_obligation_value() { + local result_var="$1" + local obligation_id="$2" + local obligation_value="$3" + shift 3 + + run_otdfctl_obligation_values create --obligation "$obligation_id" --value "$obligation_value" "$@" --json + assert_success + + local created_obligation_value_id + created_obligation_value_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$created_obligation_value_id" "" + + printf -v "$result_var" '%s' "$created_obligation_value_id" +} + +create_legacy_obligation_trigger() { + local result_var="$1" + local attribute_value_id="$2" + local action_id="$3" + local obligation_value_id="$4" + shift 4 + + local client_id="" + local labels=() + while [ "$#" -gt 0 ]; do + case "$1" in + --client-id) + client_id="$2" + shift 2 + ;; + --label) + labels+=("$2") + shift 2 + ;; + *) + echo "unsupported create_legacy_obligation_trigger arg: $1" >&2 + return 1 + ;; + esac + done + + local metadata_json + metadata_json=$(build_metadata_json_from_labels "${labels[@]}") + + local client_id_sql="NULL" + if [ -n "$client_id" ]; then + client_id_sql="'$(sql_escape_literal "$client_id")'" + fi + + # The API now rejects action/obligation namespace mismatches, but this test + # needs a legacy/global action bound to a namespaced obligation. Seed the row + # directly to exercise the migration flow against persisted legacy data. + run_policy_db_sql " + INSERT INTO obligation_triggers ( + obligation_value_id, + action_id, + attribute_value_id, + metadata, + client_id + ) + VALUES ( + '$(sql_escape_literal "$obligation_value_id")'::uuid, + '$(sql_escape_literal "$action_id")'::uuid, + '$(sql_escape_literal "$attribute_value_id")'::uuid, + '$(sql_escape_literal "$metadata_json")'::jsonb, + ${client_id_sql} + ) + RETURNING id; + " + assert_success + + local created_obligation_trigger_id + created_obligation_trigger_id=$(echo "$output" | tail -n 1) + assert_not_equal "$created_obligation_trigger_id" "" + + track_obligation_trigger_id "$created_obligation_trigger_id" + printf -v "$result_var" '%s' "$created_obligation_trigger_id" +} + +lookup_namespaced_action_id() { + local result_var="$1" + local action_name="$2" + local namespace_id="$3" + + local looked_up_action_json + run_otdfctl_action get --name "$action_name" --namespace "$namespace_id" --json + looked_up_action_json="$output" + + local looked_up_action_id + looked_up_action_id=$(echo "$looked_up_action_json" | jq -r '.id // empty') + assert_not_equal "$looked_up_action_id" "" + + printf -v "$result_var" '%s' "$looked_up_action_id" +} + +namespace_state_json() { + local namespace_filter="$1" + local actions_total + local subject_mappings_total + local scs_total + local registered_resources_total + local obligation_triggers_total + local actions_json + local subject_mappings_json + local scs_json + local registered_resources_json + local obligation_triggers_json + + run_otdfctl_action list --namespace "$namespace_filter" --limit 1 --offset 0 --json + actions_json="$output" + actions_total=$(echo "$actions_json" | jq -r '.pagination.total // 0') + + run_otdfctl_sm list --namespace "$namespace_filter" --limit 1 --offset 0 --json + subject_mappings_json="$output" + subject_mappings_total=$(echo "$subject_mappings_json" | jq -r '.pagination.total // 0') + + run_otdfctl_scs list --namespace "$namespace_filter" --limit 1 --offset 0 --json + scs_json="$output" + scs_total=$(echo "$scs_json" | jq -r '.pagination.total // 0') + + run_otdfctl_registered_resources list --namespace "$namespace_filter" --limit 1 --offset 0 --json + registered_resources_json="$output" + registered_resources_total=$(echo "$registered_resources_json" | jq -r '.pagination.total // 0') + + run_otdfctl_obligation_triggers list --namespace "$namespace_filter" --limit 1 --offset 0 --json + obligation_triggers_json="$output" + obligation_triggers_total=$(echo "$obligation_triggers_json" | jq -r '.pagination.total // 0') + + jq -cn \ + --argjson actions "$actions_total" \ + --argjson subject_mappings "$subject_mappings_total" \ + --argjson scs "$scs_total" \ + --argjson registered_resources "$registered_resources_total" \ + --argjson obligation_triggers "$obligation_triggers_total" \ + '{ + actions: $actions, + subject_mappings: $subject_mappings, + scs: $scs, + registered_resources: $registered_resources, + obligation_triggers: $obligation_triggers + }' +} + +assert_namespace_state_delta() { + local before_state="$1" + local after_state="$2" + local expected_actions_delta="$3" + local expected_subject_mappings_delta="$4" + local expected_scs_delta="$5" + local expected_registered_resources_delta="$6" + local expected_obligation_triggers_delta="$7" + local actual_delta_json + local expected_delta_json + + actual_delta_json=$( + jq -cn \ + --argjson before "$before_state" \ + --argjson after "$after_state" \ + '{ + actions: ($after.actions - $before.actions), + subject_mappings: ($after.subject_mappings - $before.subject_mappings), + scs: ($after.scs - $before.scs), + registered_resources: ($after.registered_resources - $before.registered_resources), + obligation_triggers: ($after.obligation_triggers - $before.obligation_triggers) + }' + ) + expected_delta_json=$( + jq -cn \ + --argjson actions "$expected_actions_delta" \ + --argjson subject_mappings "$expected_subject_mappings_delta" \ + --argjson scs "$expected_scs_delta" \ + --argjson registered_resources "$expected_registered_resources_delta" \ + --argjson obligation_triggers "$expected_obligation_triggers_delta" \ + '{ + actions: $actions, + subject_mappings: $subject_mappings, + scs: $scs, + registered_resources: $registered_resources, + obligation_triggers: $obligation_triggers + }' + ) + + assert_equal "$actual_delta_json" "$expected_delta_json" +} + +subject_mapping_json_by_migrated_from() { + local namespace_filter="$1" + local source_mapping_id="$2" + local subject_mappings_json + + run_otdfctl_sm list --namespace "$namespace_filter" --limit 100 --offset 0 --json + subject_mappings_json="$output" + + echo "$subject_mappings_json" \ + | jq -cer --arg source_mapping_id "$source_mapping_id" ' + [ + (.subject_mappings // [])[] + | select((.metadata.labels.migrated_from // "") == $source_mapping_id) + ] | if length == 1 then .[0] else empty end + ' +} + +subject_mapping_id_by_migrated_from() { + local namespace_filter="$1" + local source_mapping_id="$2" + local migrated_mapping_id + + migrated_mapping_id=$(subject_mapping_json_by_migrated_from "$namespace_filter" "$source_mapping_id" | jq -r '.id // empty') + assert_not_equal "$migrated_mapping_id" "" + printf '%s\n' "$migrated_mapping_id" +} + +assert_subject_mapping_created_in_namespace() { + local source_mapping_id="$1" + local namespace_id="$2" + local attribute_value_id="$3" + local action_name="$4" + local source_action_id="$5" + local expected_action_status="$6" + local source_scs_id="$7" + + case "$expected_action_status" in + create) + assert_custom_action_created_in_namespace "$action_name" "$source_action_id" "$namespace_id" + ;; + existing_standard) + assert_standard_action_resolved_in_namespace "$action_name" "$namespace_id" + ;; + *) + false + ;; + esac + + assert_scs_created_in_namespace "$source_scs_id" "$namespace_id" + + local expected_action_target_id + expected_action_target_id=$(action_id_by_name_in_namespace "$action_name" "$namespace_id") + + local expected_scs_target_id + expected_scs_target_id=$(scs_id_by_migrated_from "$namespace_id" "$source_scs_id") + + local source_mapping_json + run_otdfctl_sm get --id "$source_mapping_id" --json + source_mapping_json="$output" + + local created_mapping_json + created_mapping_json=$(subject_mapping_json_by_migrated_from "$namespace_id" "$source_mapping_id") + assert_not_equal "$created_mapping_json" "" + + local created_target_id + created_target_id=$(echo "$created_mapping_json" | jq -r '.id // empty') + assert_not_equal "$created_target_id" "" + assert_not_equal "$created_target_id" "$source_mapping_id" + + assert_equal "$(echo "$created_mapping_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(echo "$created_mapping_json" | jq -r '.attribute_value.id')" "$attribute_value_id" + assert_equal "$(echo "$created_mapping_json" | jq -r '.actions[0].id')" "$expected_action_target_id" + assert_equal "$(echo "$created_mapping_json" | jq -r '.subject_condition_set.id')" "$expected_scs_target_id" + assert_metadata_labels_preserved "$source_mapping_json" "$created_mapping_json" + assert_equal "$(echo "$created_mapping_json" | jq -r '.metadata.labels.migrated_from')" "$source_mapping_id" +} + +assert_subject_mapping_already_migrated_in_namespace() { + local source_mapping_id="$1" + local namespace_id="$2" + local existing_mapping_id="$3" + + assert_not_equal "$existing_mapping_id" "" + + local source_mapping_json + run_otdfctl_sm get --id "$source_mapping_id" --json + source_mapping_json="$output" + + local existing_mapping_json + run_otdfctl_sm get --id "$existing_mapping_id" --json + existing_mapping_json="$output" + + assert_equal "$(echo "$existing_mapping_json" | jq -r '.id // empty')" "$existing_mapping_id" + assert_equal "$(echo "$existing_mapping_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(echo "$existing_mapping_json" | jq -r '.attribute_value.id')" "$(echo "$source_mapping_json" | jq -r '.attribute_value.id')" +} + +assert_legacy_subject_mapping_still_exists() { + local attribute_value_id="$1" + local source_mapping_id="$2" + + assert_not_equal "$attribute_value_id" "" + assert_not_equal "$source_mapping_id" "" + + local legacy_mapping_json + run_otdfctl_sm get --id "$source_mapping_id" --json + legacy_mapping_json="$output" + + assert_equal "$(echo "$legacy_mapping_json" | jq -r '.id // empty')" "$source_mapping_id" + assert_equal "$(echo "$legacy_mapping_json" | jq -r '.namespace.id // empty')" "" + assert_equal "$(echo "$legacy_mapping_json" | jq -r '.attribute_value.id')" "$attribute_value_id" +} + +assert_legacy_subject_mapping_pruned() { + local source_mapping_id="$1" + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy subject-mappings get --id "$source_mapping_id" --json + assert_failure +} + +assert_subject_mapping_target_still_exists() { + local target_mapping_id="$1" + local namespace_id="$2" + local source_mapping_id="$3" + + local target_mapping_json + run_otdfctl_sm get --id "$target_mapping_id" --json + target_mapping_json="$output" + + assert_equal "$(echo "$target_mapping_json" | jq -r '.id // empty')" "$target_mapping_id" + assert_equal "$(echo "$target_mapping_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(echo "$target_mapping_json" | jq -r '.metadata.labels.migrated_from')" "$source_mapping_id" +} + +assert_no_subject_mappings_in_namespace() { + local namespace_id="$1" + local namespace_state + + namespace_state=$(namespace_state_json "$namespace_id") + assert_equal "$(echo "$namespace_state" | jq -r '.subject_mappings')" "0" +} + +obligation_trigger_json_by_id() { + local trigger_id="$1" + local namespace_filter="$2" + local triggers_json + + run_otdfctl_obligation_triggers list --namespace "$namespace_filter" --limit 100 --offset 0 --json + triggers_json="$output" + + echo "$triggers_json" \ + | jq -cer --arg trigger_id "$trigger_id" '(.triggers // [])[] | select(.id == $trigger_id)' +} + +obligation_trigger_json_by_migrated_from() { + local namespace_filter="$1" + local source_trigger_id="$2" + local triggers_json + + run_otdfctl_obligation_triggers list --namespace "$namespace_filter" --limit 100 --offset 0 --json + triggers_json="$output" + + echo "$triggers_json" \ + | jq -cer --arg source_trigger_id "$source_trigger_id" ' + [ + (.triggers // [])[] + | select((.metadata.labels.migrated_from // "") == $source_trigger_id) + ] | if length == 1 then .[0] else empty end + ' +} + +obligation_trigger_id_by_migrated_from() { + local namespace_filter="$1" + local source_trigger_id="$2" + local migrated_trigger_id + + migrated_trigger_id=$(obligation_trigger_json_by_migrated_from "$namespace_filter" "$source_trigger_id" | jq -r '.id // empty') + assert_not_equal "$migrated_trigger_id" "" + printf '%s\n' "$migrated_trigger_id" +} + +assert_metadata_labels_preserved() { + local source_json="$1" + local target_json="$2" + + local source_labels + source_labels=$(echo "$source_json" | jq -c '.metadata.labels // {}') + assert_not_equal "$source_labels" "{}" + + local target_labels + target_labels=$(echo "$target_json" | jq -c '(.metadata.labels // {}) | del(.migrated_from)') + + assert_equal "$target_labels" "$source_labels" +} + +action_json_by_name_in_namespace() { + local action_name="$1" + local namespace_filter="$2" + + run_otdfctl_action get --name "$action_name" --namespace "$namespace_filter" --json + printf '%s\n' "$output" +} + +action_id_by_name_in_namespace() { + local action_name="$1" + local namespace_filter="$2" + local action_id + + action_id=$(action_json_by_name_in_namespace "$action_name" "$namespace_filter" | jq -r '.id // empty') + assert_not_equal "$action_id" "" + printf '%s\n' "$action_id" +} + +scs_json_by_migrated_from() { + local namespace_filter="$1" + local source_scs_id="$2" + local scs_json + + run_otdfctl_scs list --namespace "$namespace_filter" --limit 100 --offset 0 --json + scs_json="$output" + + echo "$scs_json" \ + | jq -cer --arg source_scs_id "$source_scs_id" ' + [ + (.subject_condition_sets // [])[] + | select((.metadata.labels.migrated_from // "") == $source_scs_id) + ] | if length == 1 then .[0] else empty end + ' +} + +scs_id_by_migrated_from() { + local namespace_filter="$1" + local source_scs_id="$2" + local migrated_scs_id + + migrated_scs_id=$(scs_json_by_migrated_from "$namespace_filter" "$source_scs_id" | jq -r '.id // empty') + assert_not_equal "$migrated_scs_id" "" + printf '%s\n' "$migrated_scs_id" +} + +registered_resource_json_by_migrated_from() { + local namespace_filter="$1" + local source_resource_id="$2" + local resources_json + + run_otdfctl_registered_resources list --namespace "$namespace_filter" --limit 100 --offset 0 --json + resources_json="$output" + + echo "$resources_json" \ + | jq -cer --arg source_resource_id "$source_resource_id" ' + [ + (.resources // [])[] + | select((.metadata.labels.migrated_from // "") == $source_resource_id) + ] | if length == 1 then .[0] else empty end + ' +} + +registered_resource_id_by_migrated_from() { + local namespace_filter="$1" + local source_resource_id="$2" + local migrated_resource_id + + migrated_resource_id=$(registered_resource_json_by_migrated_from "$namespace_filter" "$source_resource_id" | jq -r '.id // empty') + assert_not_equal "$migrated_resource_id" "" + printf '%s\n' "$migrated_resource_id" +} + +registered_resource_value_json_by_migrated_from() { + local resource_id="$1" + local source_value_id="$2" + local resource_values_json + + run_otdfctl_registered_resource_values list --resource "$resource_id" --limit 100 --offset 0 --json + resource_values_json="$output" + + echo "$resource_values_json" \ + | jq -cer --arg source_value_id "$source_value_id" ' + [ + (.values // [])[] + | select((.metadata.labels.migrated_from // "") == $source_value_id) + ] | if length == 1 then .[0] else empty end + ' +} + +registered_resource_value_id_by_migrated_from() { + local resource_id="$1" + local source_value_id="$2" + local migrated_resource_value_id + + migrated_resource_value_id=$(registered_resource_value_json_by_migrated_from "$resource_id" "$source_value_id" | jq -r '.id // empty') + assert_not_equal "$migrated_resource_value_id" "" + printf '%s\n' "$migrated_resource_value_id" +} + +assert_action_absent_in_namespace() { + local action_name="$1" + local namespace_filter="$2" + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy actions get --name "$action_name" --namespace "$namespace_filter" --json + assert_failure +} + +assert_scs_absent_in_namespace() { + local source_scs_id="$1" + local namespace_filter="$2" + local scs_list_json + + run_otdfctl_scs list --namespace "$namespace_filter" --limit 100 --offset 0 --json + scs_list_json="$output" + assert_equal "$(echo "$scs_list_json" | jq -r --arg source_scs_id "$source_scs_id" '[(.subject_condition_sets // [])[] | select((.metadata.labels.migrated_from // "") == $source_scs_id)] | length')" "0" +} + +registered_resource_values_signature() { + local resource_json="$1" + + echo "$resource_json" | jq -c ' + def normalized_bindings: + (.action_attribute_values // []) + | map("\(.action.name | ascii_downcase)|\(.attribute_value.fqn)") + | sort; + + (.values // []) + | map({ + value: (.value | ascii_downcase), + bindings: normalized_bindings + }) + | sort_by([.value, (.bindings | join(","))]) + ' +} + +subject_sets_signature() { + local scs_json="$1" + + echo "$scs_json" | jq -c ' + def normalized_condition: + { + selector: (.subject_external_selector_value // ""), + operator: (.operator // 0), + values: ((.subject_external_values // []) | sort) + }; + + def normalized_group: + { + conditions: ( + (.conditions // []) + | map(select(. != null) | normalized_condition) + | sort_by([.selector, .operator, (.values | join(","))]) + ), + boolean_operator: (.boolean_operator // 0) + }; + + (.subject_sets // []) + | map( + select(. != null) + | { + condition_groups: ( + (.condition_groups // []) + | map(select(. != null) | normalized_group) + | sort_by(tojson) + ) + } + ) + | sort_by(tojson) + ' +} + +assert_standard_action_resolved_in_namespace() { + local action_name="$1" + local namespace_id="$2" + + local live_action_json + live_action_json=$(action_json_by_name_in_namespace "$action_name" "$namespace_id") + assert_not_equal "$live_action_json" "" + assert_not_equal "$(echo "$live_action_json" | jq -r '.id // empty')" "" + assert_equal "$(echo "$live_action_json" | jq -r '.namespace.id')" "$namespace_id" +} + +assert_action_already_migrated_in_namespace() { + local action_name="$1" + local namespace_id="$2" + local existing_action_id="$3" + + local existing_action_json + run_otdfctl_action get --id "$existing_action_id" --json + existing_action_json="$output" + + assert_equal "$(echo "$existing_action_json" | jq -r '.id // empty')" "$existing_action_id" + assert_equal "$(echo "$existing_action_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(echo "$existing_action_json" | jq -r '.name')" "$action_name" +} + +assert_custom_action_created_in_namespace() { + local action_name="$1" + local source_action_id="$2" + local namespace_id="$3" + + local source_action_json + run_otdfctl_action get --id "$source_action_id" --json + source_action_json="$output" + + local created_action_json + created_action_json=$(action_json_by_name_in_namespace "$action_name" "$namespace_id") + assert_not_equal "$created_action_json" "" + + local created_target_id + created_target_id=$(echo "$created_action_json" | jq -r '.id // empty') + assert_not_equal "$created_target_id" "" + assert_not_equal "$created_target_id" "$source_action_id" + + assert_equal "$(echo "$created_action_json" | jq -r '.name')" "$action_name" + assert_equal "$(echo "$created_action_json" | jq -r '.namespace.id')" "$namespace_id" + assert_metadata_labels_preserved "$source_action_json" "$created_action_json" + assert_equal "$(echo "$created_action_json" | jq -r '.metadata.labels.migrated_from')" "$source_action_id" +} + +assert_legacy_custom_action_still_exists() { + local action_id="$1" + local action_name="$2" + + assert_not_equal "$action_id" "" + assert_not_equal "$action_name" "" + + local legacy_action_json + run_otdfctl_action get --id "$action_id" --json + legacy_action_json="$output" + + assert_equal "$(echo "$legacy_action_json" | jq -r '.id // empty')" "$action_id" + assert_equal "$(echo "$legacy_action_json" | jq -r '.name')" "$action_name" + assert_equal "$(echo "$legacy_action_json" | jq -r '.namespace.id // empty')" "" +} + +assert_legacy_custom_action_pruned() { + local action_id="$1" + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy actions get --id "$action_id" --json + assert_failure +} + +assert_scs_created_in_namespace() { + local source_scs_id="$1" + local namespace_id="$2" + + local source_scs_json + run_otdfctl_scs get --id "$source_scs_id" --json + source_scs_json="$output" + + local created_scs_json + created_scs_json=$(scs_json_by_migrated_from "$namespace_id" "$source_scs_id") + assert_not_equal "$created_scs_json" "" + + local created_target_id + created_target_id=$(echo "$created_scs_json" | jq -r '.id // empty') + assert_not_equal "$created_target_id" "" + assert_not_equal "$created_target_id" "$source_scs_id" + + assert_equal "$(echo "$created_scs_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(subject_sets_signature "$created_scs_json")" "$(subject_sets_signature "$source_scs_json")" + assert_metadata_labels_preserved "$source_scs_json" "$created_scs_json" + assert_equal "$(echo "$created_scs_json" | jq -r '.metadata.labels.migrated_from')" "$source_scs_id" +} + +assert_registered_resource_created_in_namespace() { + local source_resource_id="$1" + local source_value_id="$2" + local namespace_id="$3" + local resource_name="$4" + local resource_value="$5" + local action_name="$6" + local source_action_id="$7" + local expected_action_status="$8" + local attribute_value_id="$9" + + case "$expected_action_status" in + create) + assert_custom_action_created_in_namespace "$action_name" "$source_action_id" "$namespace_id" + ;; + existing_standard) + assert_standard_action_resolved_in_namespace "$action_name" "$namespace_id" + ;; + *) + false + ;; + esac + + local expected_action_target_id + expected_action_target_id=$(action_id_by_name_in_namespace "$action_name" "$namespace_id") + + local source_resource_json + run_otdfctl_registered_resources get --id "$source_resource_id" --json + source_resource_json="$output" + + local created_resource_json + created_resource_json=$(registered_resource_json_by_migrated_from "$namespace_id" "$source_resource_id") + assert_not_equal "$created_resource_json" "" + + local created_resource_id + created_resource_id=$(echo "$created_resource_json" | jq -r '.id // empty') + assert_not_equal "$created_resource_id" "" + assert_not_equal "$created_resource_id" "$source_resource_id" + + assert_equal "$(echo "$created_resource_json" | jq -r '.name')" "$resource_name" + assert_equal "$(echo "$created_resource_json" | jq -r '.namespace.id')" "$namespace_id" + assert_metadata_labels_preserved "$source_resource_json" "$created_resource_json" + assert_equal "$(echo "$created_resource_json" | jq -r '.metadata.labels.migrated_from')" "$source_resource_id" + + local source_resource_value_json + run_otdfctl_registered_resource_values get --id "$source_value_id" --json + source_resource_value_json="$output" + + local created_resource_value_json + created_resource_value_json=$(registered_resource_value_json_by_migrated_from "$created_resource_id" "$source_value_id") + assert_not_equal "$created_resource_value_json" "" + + local created_resource_value_id + created_resource_value_id=$(echo "$created_resource_value_json" | jq -r '.id // empty') + assert_not_equal "$created_resource_value_id" "" + assert_not_equal "$created_resource_value_id" "$source_value_id" + + assert_equal "$(echo "$created_resource_value_json" | jq -r '.value')" "$resource_value" + assert_equal "$(echo "$created_resource_value_json" | jq -r '.action_attribute_values | length')" "1" + assert_equal "$(echo "$created_resource_value_json" | jq -r '.action_attribute_values[0].action.id')" "$expected_action_target_id" + assert_equal "$(echo "$created_resource_value_json" | jq -r '.action_attribute_values[0].attribute_value.id')" "$attribute_value_id" + assert_metadata_labels_preserved "$source_resource_value_json" "$created_resource_value_json" + assert_equal "$(echo "$created_resource_value_json" | jq -r '.metadata.labels.migrated_from')" "$source_value_id" +} + +assert_registered_resource_already_migrated_in_namespace() { + local source_resource_id="$1" + local namespace_id="$2" + local existing_resource_id="$3" + + local source_resource_json + run_otdfctl_registered_resources get --id "$source_resource_id" --json + source_resource_json="$output" + + local existing_resource_json + run_otdfctl_registered_resources get --id "$existing_resource_id" --json + existing_resource_json="$output" + + assert_equal "$(echo "$existing_resource_json" | jq -r '.id // empty')" "$existing_resource_id" + assert_equal "$(echo "$existing_resource_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(echo "$existing_resource_json" | jq -r '.name')" "$(echo "$source_resource_json" | jq -r '.name')" + assert_equal "$(registered_resource_values_signature "$existing_resource_json")" "$(registered_resource_values_signature "$source_resource_json")" +} + +assert_registered_resource_value_uses_action() { + local value_id="$1" + local expected_action_id="$2" + local attribute_value_id="$3" + + local value_json + run_otdfctl_registered_resource_values get --id "$value_id" --json + value_json="$output" + + assert_equal "$(echo "$value_json" | jq -r '.action_attribute_values | length')" "1" + assert_equal "$(echo "$value_json" | jq -r '.action_attribute_values[0].action.id')" "$expected_action_id" + assert_equal "$(echo "$value_json" | jq -r '.action_attribute_values[0].attribute_value.id')" "$attribute_value_id" +} + +assert_legacy_registered_resource_still_exists() { + local source_resource_id="$1" + local source_value_id="$2" + local resource_name="$3" + local resource_value="$4" + + local legacy_resource_json + run_otdfctl_registered_resources get --id "$source_resource_id" --json + legacy_resource_json="$output" + + assert_equal "$(echo "$legacy_resource_json" | jq -r '.id // empty')" "$source_resource_id" + assert_equal "$(echo "$legacy_resource_json" | jq -r '.name')" "$resource_name" + assert_equal "$(echo "$legacy_resource_json" | jq -r '.namespace.id // empty')" "" + + local legacy_resource_value_json + run_otdfctl_registered_resource_values get --id "$source_value_id" --json + legacy_resource_value_json="$output" + + assert_equal "$(echo "$legacy_resource_value_json" | jq -r '.id // empty')" "$source_value_id" + assert_equal "$(echo "$legacy_resource_value_json" | jq -r '.value')" "$resource_value" +} + +assert_legacy_registered_resource_pruned() { + local source_resource_id="$1" + local source_value_id="$2" + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy registered-resources get --id "$source_resource_id" --json + assert_failure + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy registered-resources values get --id "$source_value_id" --json + assert_failure +} + +assert_registered_resource_target_still_exists() { + local target_resource_id="$1" + local target_value_id="$2" + local namespace_id="$3" + local source_resource_id="$4" + local source_value_id="$5" + + local target_resource_json + run_otdfctl_registered_resources get --id "$target_resource_id" --json + target_resource_json="$output" + + assert_equal "$(echo "$target_resource_json" | jq -r '.id // empty')" "$target_resource_id" + assert_equal "$(echo "$target_resource_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(echo "$target_resource_json" | jq -r '.metadata.labels.migrated_from')" "$source_resource_id" + + local target_value_json + run_otdfctl_registered_resource_values get --id "$target_value_id" --json + target_value_json="$output" + + assert_equal "$(echo "$target_value_json" | jq -r '.id // empty')" "$target_value_id" + assert_equal "$(echo "$target_value_json" | jq -r '.metadata.labels.migrated_from')" "$source_value_id" +} + +assert_registered_resource_unlabeled_target_still_exists() { + local target_resource_id="$1" + local target_value_id="$2" + local namespace_id="$3" + local resource_name="$4" + local resource_value="$5" + local action_id="$6" + local attribute_value_id="$7" + + local target_resource_json + run_otdfctl_registered_resources get --id "$target_resource_id" --json + target_resource_json="$output" + + assert_equal "$(echo "$target_resource_json" | jq -r '.id // empty')" "$target_resource_id" + assert_equal "$(echo "$target_resource_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(echo "$target_resource_json" | jq -r '.name')" "$resource_name" + assert_equal "$(echo "$target_resource_json" | jq -r '.metadata.labels.migrated_from // empty')" "" + + local target_value_json + run_otdfctl_registered_resource_values get --id "$target_value_id" --json + target_value_json="$output" + + assert_equal "$(echo "$target_value_json" | jq -r '.id // empty')" "$target_value_id" + assert_equal "$(echo "$target_value_json" | jq -r '.value')" "$resource_value" + assert_equal "$(echo "$target_value_json" | jq -r '.action_attribute_values | length')" "1" + assert_equal "$(echo "$target_value_json" | jq -r '.action_attribute_values[0].action.id')" "$action_id" + assert_equal "$(echo "$target_value_json" | jq -r '.action_attribute_values[0].attribute_value.id')" "$attribute_value_id" + assert_equal "$(echo "$target_value_json" | jq -r '.metadata.labels.migrated_from // empty')" "" +} + +assert_obligation_trigger_created_in_namespace() { + local source_trigger_id="$1" + local namespace_id="$2" + local attribute_value_id="$3" + local obligation_value_id="$4" + local action_name="$5" + local source_action_id="$6" + local expected_action_status="$7" + local client_id="$8" + + case "$expected_action_status" in + create) + assert_custom_action_created_in_namespace "$action_name" "$source_action_id" "$namespace_id" + ;; + existing_standard) + assert_standard_action_resolved_in_namespace "$action_name" "$namespace_id" + ;; + *) + false + ;; + esac + + local expected_action_target_id + expected_action_target_id=$(action_id_by_name_in_namespace "$action_name" "$namespace_id") + + local source_trigger_json + source_trigger_json=$(obligation_trigger_json_by_id "$source_trigger_id" "$namespace_id") + + local created_trigger_json + created_trigger_json=$(obligation_trigger_json_by_migrated_from "$namespace_id" "$source_trigger_id") + assert_not_equal "$created_trigger_json" "" + + local created_trigger_id + created_trigger_id=$(echo "$created_trigger_json" | jq -r '.id // empty') + assert_not_equal "$created_trigger_id" "" + assert_not_equal "$created_trigger_id" "$source_trigger_id" + + assert_equal "$(echo "$created_trigger_json" | jq -r '.attribute_value.id')" "$attribute_value_id" + assert_equal "$(echo "$created_trigger_json" | jq -r '.action.id')" "$expected_action_target_id" + assert_equal "$(echo "$created_trigger_json" | jq -r '.obligation_value.id')" "$obligation_value_id" + assert_equal "$(echo "$created_trigger_json" | jq -r '.context | length')" "1" + assert_equal "$(echo "$created_trigger_json" | jq -r '.context[0].pep.client_id')" "$client_id" + assert_metadata_labels_preserved "$source_trigger_json" "$created_trigger_json" + assert_equal "$(echo "$created_trigger_json" | jq -r '.metadata.labels.migrated_from')" "$source_trigger_id" +} + +assert_obligation_trigger_already_migrated_in_namespace() { + local source_trigger_id="$1" + local namespace_id="$2" + local existing_trigger_id="$3" + + assert_not_equal "$existing_trigger_id" "" + + local source_trigger_json + source_trigger_json=$(obligation_trigger_json_by_id "$source_trigger_id" "$namespace_id") + + local existing_trigger_json + existing_trigger_json=$(obligation_trigger_json_by_id "$existing_trigger_id" "$namespace_id") + + assert_equal "$(echo "$existing_trigger_json" | jq -r '.id // empty')" "$existing_trigger_id" + assert_equal "$(echo "$existing_trigger_json" | jq -r '.attribute_value.id')" "$(echo "$source_trigger_json" | jq -r '.attribute_value.id')" + assert_equal "$(echo "$existing_trigger_json" | jq -r '.obligation_value.id')" "$(echo "$source_trigger_json" | jq -r '.obligation_value.id')" + assert_equal \ + "$(echo "$existing_trigger_json" | jq -c '(.context // []) | map(.pep.client_id // "") | sort')" \ + "$(echo "$source_trigger_json" | jq -c '(.context // []) | map(.pep.client_id // "") | sort')" + + local existing_action_id + existing_action_id=$(echo "$existing_trigger_json" | jq -r '.action.id // empty') + assert_not_equal "$existing_action_id" "" + + local existing_action_json + run_otdfctl_action get --id "$existing_action_id" --json + existing_action_json="$output" + + assert_equal "$(echo "$existing_action_json" | jq -r '.id // empty')" "$existing_action_id" + assert_equal "$(echo "$existing_action_json" | jq -r '.namespace.id')" "$namespace_id" +} + +assert_legacy_obligation_trigger_still_exists() { + local source_trigger_id="$1" + local namespace_id="$2" + local attribute_value_id="$3" + local action_id="$4" + local obligation_value_id="$5" + local client_id="$6" + + assert_not_equal "$source_trigger_id" "" + + local legacy_trigger_json + legacy_trigger_json=$(obligation_trigger_json_by_id "$source_trigger_id" "$namespace_id") + + assert_equal "$(echo "$legacy_trigger_json" | jq -r '.id // empty')" "$source_trigger_id" + assert_equal "$(echo "$legacy_trigger_json" | jq -r '.attribute_value.id')" "$attribute_value_id" + assert_equal "$(echo "$legacy_trigger_json" | jq -r '.action.id')" "$action_id" + assert_equal "$(echo "$legacy_trigger_json" | jq -r '.action.namespace.id // empty')" "" + assert_equal "$(echo "$legacy_trigger_json" | jq -r '.obligation_value.id')" "$obligation_value_id" + assert_equal "$(echo "$legacy_trigger_json" | jq -r '.context[0].pep.client_id')" "$client_id" +} + +assert_legacy_obligation_trigger_pruned() { + local source_trigger_id="$1" + local namespace_id="$2" + local triggers_json + + run_otdfctl_obligation_triggers list --namespace "$namespace_id" --limit 100 --offset 0 --json + triggers_json="$output" + + assert_equal "$(echo "$triggers_json" | jq -r --arg source_trigger_id "$source_trigger_id" '[(.triggers // [])[] | select(.id == $source_trigger_id)] | length')" "0" +} + +assert_obligation_trigger_target_still_exists() { + local target_trigger_id="$1" + local namespace_id="$2" + local source_trigger_id="$3" + + local target_trigger_json + target_trigger_json=$(obligation_trigger_json_by_id "$target_trigger_id" "$namespace_id") + + assert_equal "$(echo "$target_trigger_json" | jq -r '.id // empty')" "$target_trigger_id" + assert_equal "$(echo "$target_trigger_json" | jq -r '.metadata.labels.migrated_from')" "$source_trigger_id" +} + +assert_obligation_trigger_unlabeled_target_still_exists() { + local target_trigger_id="$1" + local namespace_id="$2" + local attribute_value_id="$3" + local action_id="$4" + local obligation_value_id="$5" + local client_id="$6" + + local target_trigger_json + target_trigger_json=$(obligation_trigger_json_by_id "$target_trigger_id" "$namespace_id") + + assert_equal "$(echo "$target_trigger_json" | jq -r '.id // empty')" "$target_trigger_id" + assert_equal "$(echo "$target_trigger_json" | jq -r '.attribute_value.id')" "$attribute_value_id" + assert_equal "$(echo "$target_trigger_json" | jq -r '.action.id')" "$action_id" + assert_equal "$(echo "$target_trigger_json" | jq -r '.obligation_value.id')" "$obligation_value_id" + assert_equal "$(echo "$target_trigger_json" | jq -r '.context[0].pep.client_id')" "$client_id" + assert_equal "$(echo "$target_trigger_json" | jq -r '.metadata.labels.migrated_from // empty')" "" +} + +assert_scs_already_migrated_in_namespace() { + local source_scs_id="$1" + local namespace_id="$2" + local existing_scs_id="$3" + + local source_scs_json + run_otdfctl_scs get --id "$source_scs_id" --json + source_scs_json="$output" + + local existing_scs_json + run_otdfctl_scs get --id "$existing_scs_id" --json + existing_scs_json="$output" + + assert_equal "$(echo "$existing_scs_json" | jq -r '.id // empty')" "$existing_scs_id" + assert_equal "$(echo "$existing_scs_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(subject_sets_signature "$existing_scs_json")" "$(subject_sets_signature "$source_scs_json")" +} + +assert_legacy_scs_still_exists() { + local source_scs_id="$1" + + local legacy_scs_json + run_otdfctl_scs get --id "$source_scs_id" --json + legacy_scs_json="$output" + + assert_equal "$(echo "$legacy_scs_json" | jq -r '.id // empty')" "$source_scs_id" + assert_equal "$(echo "$legacy_scs_json" | jq -r '.namespace.id // empty')" "" +} + +assert_legacy_scs_pruned() { + local source_scs_id="$1" + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json policy scs get --id "$source_scs_id" --json + assert_failure +} + +assert_scs_target_still_exists() { + local target_scs_id="$1" + local namespace_id="$2" + local source_scs_id="$3" + + local target_scs_json + run_otdfctl_scs get --id "$target_scs_id" --json + target_scs_json="$output" + + assert_equal "$(echo "$target_scs_json" | jq -r '.id // empty')" "$target_scs_id" + assert_equal "$(echo "$target_scs_json" | jq -r '.namespace.id')" "$namespace_id" + assert_equal "$(echo "$target_scs_json" | jq -r '.metadata.labels.migrated_from')" "$source_scs_id" +} + +run_namespaced_policy_commit() { + local scope="$1" + + run_otdfctl_migrate --commit namespaced-policy --scope "$scope" +} + +run_namespaced_policy_prune_commit() { + local scope="$1" + + run_otdfctl_migrate --commit prune namespaced-policy --scope "$scope" +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + export TEST_PREFIX="${MIGRATION_TEST_PREFIX}-t${BATS_TEST_NUMBER}" + export TRACKED_ACTION_IDS="" + export TRACKED_REGISTERED_RESOURCE_IDS="" + export TRACKED_REGISTERED_RESOURCE_VALUE_IDS="" + export TRACKED_SCS_IDS="" + export TRACKED_SUBJECT_MAPPING_IDS="" + export TRACKED_OBLIGATION_TRIGGER_IDS="" +} + +setup_file() { + bats_load_library bats-support + bats_load_library bats-assert + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + export MIGRATION_TEST_PREFIX="np-migrate-$(date +%s)" + export NS_A_NAME="${MIGRATION_TEST_PREFIX}-a.test" + export NS_B_NAME="${MIGRATION_TEST_PREFIX}-b.test" + export NS_A_FQN="https://${NS_A_NAME}" + export NS_B_FQN="https://${NS_B_NAME}" + + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes namespaces create --name \"$NS_A_NAME\" --json" + assert_success + export NS_A_ID + NS_A_ID=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$NS_A_ID" "" + + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes namespaces create --name \"$NS_B_NAME\" --json" + assert_success + export NS_B_ID + NS_B_ID=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$NS_B_ID" "" + + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes create --name \"${MIGRATION_TEST_PREFIX}-attr-a\" --namespace \"$NS_A_ID\" --rule ANY_OF -v \"${MIGRATION_TEST_PREFIX}-a1\" --json" + assert_success + attr_a_json="$output" + export ATTR_A_ID ATTR_A_VAL_1_ID + ATTR_A_ID=$(echo "$attr_a_json" | jq -r '.id // empty') + ATTR_A_VAL_1_ID=$(echo "$attr_a_json" | jq -r '.values[0].id // empty') + assert_not_equal "$ATTR_A_ID" "" + assert_not_equal "$ATTR_A_VAL_1_ID" "" + + # ATTR_A values resolve under the namespace FQN: + # ${NS_A_FQN}/attr/${MIGRATION_TEST_PREFIX}-attr-a/value/${MIGRATION_TEST_PREFIX}-a1 + # ${NS_A_FQN}/attr/${MIGRATION_TEST_PREFIX}-attr-a/value/${MIGRATION_TEST_PREFIX}-a2 + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id \"$ATTR_A_ID\" --value \"${MIGRATION_TEST_PREFIX}-a2\" --json" + assert_success + export ATTR_A_VAL_2_ID + ATTR_A_VAL_2_ID=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$ATTR_A_VAL_2_ID" "" + + # ATTR_B values resolve under the namespace FQN: + # ${NS_B_FQN}/attr/${MIGRATION_TEST_PREFIX}-attr-b/value/${MIGRATION_TEST_PREFIX}-b1 + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes create --name \"${MIGRATION_TEST_PREFIX}-attr-b\" --namespace \"$NS_B_ID\" --rule ANY_OF -v \"${MIGRATION_TEST_PREFIX}-b1\" --json" + assert_success + attr_b_json="$output" + export ATTR_B_ID ATTR_B_VAL_1_ID + ATTR_B_ID=$(echo "$attr_b_json" | jq -r '.id // empty') + ATTR_B_VAL_1_ID=$(echo "$attr_b_json" | jq -r '.values[0].id // empty') + assert_not_equal "$ATTR_B_ID" "" + assert_not_equal "$ATTR_B_VAL_1_ID" "" + + local global_read_json + run_otdfctl_action get --name read --json + global_read_json="$output" + export GLOBAL_READ_ID + GLOBAL_READ_ID=$(echo "$global_read_json" | jq -r '.id // empty') + assert_not_equal "$GLOBAL_READ_ID" "" +} + +teardown() { + local obligation_trigger_id + local delete_output + local delete_status + while IFS= read -r obligation_trigger_id; do + [ -n "$obligation_trigger_id" ] || continue + if delete_output=$(./otdfctl $HOST $WITH_CREDS policy obligations triggers delete --id "$obligation_trigger_id" --force 2>&1); then + : + else + delete_status=$? + echo "warning: failed to delete obligation trigger fixture $obligation_trigger_id during teardown (exit $delete_status): $delete_output" >&2 + fi + done <<< "$TRACKED_OBLIGATION_TRIGGER_IDS" + + local resource_value_id + while IFS= read -r resource_value_id; do + [ -n "$resource_value_id" ] || continue + if delete_output=$(./otdfctl $HOST $WITH_CREDS policy registered-resources values delete --id "$resource_value_id" --force 2>&1); then + : + else + delete_status=$? + echo "warning: failed to delete registered resource value fixture $resource_value_id during teardown (exit $delete_status): $delete_output" >&2 + fi + done <<< "$TRACKED_REGISTERED_RESOURCE_VALUE_IDS" + + local resource_id + while IFS= read -r resource_id; do + [ -n "$resource_id" ] || continue + if delete_output=$(./otdfctl $HOST $WITH_CREDS policy registered-resources delete --id "$resource_id" --force 2>&1); then + : + else + delete_status=$? + echo "warning: failed to delete registered resource fixture $resource_id during teardown (exit $delete_status): $delete_output" >&2 + fi + done <<< "$TRACKED_REGISTERED_RESOURCE_IDS" + + local subject_mapping_id + while IFS= read -r subject_mapping_id; do + [ -n "$subject_mapping_id" ] || continue + if delete_output=$(./otdfctl $HOST $WITH_CREDS policy subject-mappings delete --id "$subject_mapping_id" --force 2>&1); then + : + else + delete_status=$? + echo "warning: failed to delete subject mapping fixture $subject_mapping_id during teardown (exit $delete_status): $delete_output" >&2 + fi + done <<< "$TRACKED_SUBJECT_MAPPING_IDS" + + local scs_id + while IFS= read -r scs_id; do + [ -n "$scs_id" ] || continue + if delete_output=$(./otdfctl $HOST $WITH_CREDS policy scs delete --id "$scs_id" --force 2>&1); then + : + else + delete_status=$? + echo "warning: failed to delete subject condition set fixture $scs_id during teardown (exit $delete_status): $delete_output" >&2 + fi + done <<< "$TRACKED_SCS_IDS" + + local action_id + while IFS= read -r action_id; do + [ -n "$action_id" ] || continue + if delete_output=$(./otdfctl $HOST $WITH_CREDS policy actions delete --id "$action_id" --force 2>&1); then + : + else + delete_status=$? + echo "warning: failed to delete action fixture $action_id during teardown (exit $delete_status): $delete_output" >&2 + fi + done <<< "$TRACKED_ACTION_IDS" +} + +teardown_file() { + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$NS_A_ID" --force + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$NS_B_ID" --force + + unset HOST WITH_CREDS MIGRATION_TEST_PREFIX TEST_PREFIX + unset NS_A_NAME NS_B_NAME NS_A_FQN NS_B_FQN NS_A_ID NS_B_ID + unset ATTR_A_ID ATTR_A_VAL_1_ID ATTR_A_VAL_2_ID ATTR_B_ID ATTR_B_VAL_1_ID + unset GLOBAL_READ_ID + unset TRACKED_ACTION_IDS TRACKED_REGISTERED_RESOURCE_IDS TRACKED_REGISTERED_RESOURCE_VALUE_IDS + unset TRACKED_SCS_IDS TRACKED_SUBJECT_MAPPING_IDS TRACKED_OBLIGATION_TRIGGER_IDS +} + +# Asserts action-scope migration can fan out one legacy custom action into +# multiple namespaces when registered-resource and obligation-trigger anchors +# reference it across those namespaces, does not create unrelated namespaced +# objects as a side effect, and is idempotent on rerun. +@test "migrate namespaced-policy actions fans out custom actions from RR and trigger anchors" { + local custom_action_name="${TEST_PREFIX}-download" + local shared_scs='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-shared"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local custom_action_labels=(--label "test_case=actions" --label "fixture=${TEST_PREFIX}-custom-action") + local rr_a_name="${TEST_PREFIX}-repo-a" + local rr_a_value="${TEST_PREFIX}-repo-a-main" + local rr_a_labels=(--label "test_case=actions" --label "fixture=${TEST_PREFIX}-rr-a") + local rr_a_value_labels=(--label "test_case=actions" --label "fixture=${TEST_PREFIX}-rr-a-value") + local obligation_b_name="${TEST_PREFIX}-notify-b" + local obligation_b_value="${TEST_PREFIX}-notify-b-default" + local trigger_b_client_id="${TEST_PREFIX}-client-b" + local trigger_b_labels=(--label "test_case=actions" --label "fixture=${TEST_PREFIX}-trigger-b") + local custom_action_id + local shared_scs_id + local read_anchor_mapping_id + local rr_a_id + local rr_a_value_id + local obligation_b_id + local obligation_b_value_id + local trigger_b_id + local ns_a_state_before + local ns_b_state_before + local ns_a_state_after + local ns_b_state_after + + create_global_action custom_action_id "$custom_action_name" "${custom_action_labels[@]}" + create_global_scs shared_scs_id "$shared_scs" + create_legacy_subject_mapping read_anchor_mapping_id "$ATTR_A_VAL_1_ID" "$GLOBAL_READ_ID" "$shared_scs_id" + create_global_registered_resource rr_a_id "$rr_a_name" "${rr_a_labels[@]}" + create_registered_resource_value rr_a_value_id "$rr_a_id" "$rr_a_value" --action-attribute-value "$custom_action_id;$ATTR_A_VAL_2_ID" "${rr_a_value_labels[@]}" + + create_namespaced_obligation obligation_b_id "$NS_B_ID" "$obligation_b_name" --label "test_case=actions" --label "fixture=${TEST_PREFIX}-obligation-b" + create_obligation_value obligation_b_value_id "$obligation_b_id" "$obligation_b_value" --label "test_case=actions" --label "fixture=${TEST_PREFIX}-obligation-b-value" + create_legacy_obligation_trigger trigger_b_id "$ATTR_B_VAL_1_ID" "$custom_action_id" "$obligation_b_value_id" --client-id "$trigger_b_client_id" "${trigger_b_labels[@]}" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + ns_b_state_before=$(namespace_state_json "$NS_B_ID") + + run_namespaced_policy_commit "actions" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 1 0 0 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 1 0 0 0 0 + + assert_custom_action_created_in_namespace "$custom_action_name" "$custom_action_id" "$NS_A_ID" + assert_custom_action_created_in_namespace "$custom_action_name" "$custom_action_id" "$NS_B_ID" + + assert_legacy_custom_action_still_exists "$custom_action_id" "$custom_action_name" + assert_legacy_scs_still_exists "$shared_scs_id" + assert_legacy_subject_mapping_still_exists "$ATTR_A_VAL_1_ID" "$read_anchor_mapping_id" + assert_legacy_registered_resource_still_exists "$rr_a_id" "$rr_a_value_id" "$rr_a_name" "$rr_a_value" + assert_legacy_obligation_trigger_still_exists "$trigger_b_id" "$NS_B_ID" "$ATTR_B_VAL_1_ID" "$custom_action_id" "$obligation_b_value_id" "$trigger_b_client_id" + + # Re-running the same migration should be idempotent. No namespace-scoped + # counts should change on the second pass. + local custom_action_ns_a_target_id + local custom_action_ns_b_target_id + custom_action_ns_a_target_id=$(action_id_by_name_in_namespace "$custom_action_name" "$NS_A_ID") + custom_action_ns_b_target_id=$(action_id_by_name_in_namespace "$custom_action_name" "$NS_B_ID") + + ns_a_state_before="$ns_a_state_after" + ns_b_state_before="$ns_b_state_after" + + run_namespaced_policy_commit "actions" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_action_already_migrated_in_namespace "$custom_action_name" "$NS_A_ID" "$custom_action_ns_a_target_id" + assert_action_already_migrated_in_namespace "$custom_action_name" "$NS_B_ID" "$custom_action_ns_b_target_id" +} + +# Asserts SCS-scope migration creates missing namespaced SCS targets, reuses an +# already-migrated canonical target when present, preserves subject_sets and +# metadata, does not create unrelated namespaced objects as a side effect, and +# is idempotent on rerun. +@test "migrate namespaced-policy subject-condition-sets creates single-namespace targets and reuses existing fanout targets" { + local fanout_scs='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-shared"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local single_namespace_scs='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-a-only"],"subject_external_selector_value":".team.name"}],"boolean_operator":1}]}]' + local fanout_scs_labels=(--label "test_case=scs" --label "fixture=${TEST_PREFIX}-fanout-scs") + local single_namespace_scs_labels=(--label "test_case=scs" --label "fixture=${TEST_PREFIX}-single-scs") + local fanout_scs_id + local single_namespace_scs_id + local existing_fanout_ns_b_scs_id + local ns_a_state_before + local ns_b_state_before + local ns_a_state_after + local ns_b_state_after + + create_global_scs fanout_scs_id "$fanout_scs" "${fanout_scs_labels[@]}" + create_global_scs single_namespace_scs_id "$single_namespace_scs" "${single_namespace_scs_labels[@]}" + create_namespaced_scs existing_fanout_ns_b_scs_id "$NS_B_ID" "$fanout_scs" + + local ignored_mapping_id + create_legacy_subject_mapping ignored_mapping_id "$ATTR_A_VAL_1_ID" "$GLOBAL_READ_ID" "$fanout_scs_id" + create_legacy_subject_mapping ignored_mapping_id "$ATTR_B_VAL_1_ID" "$GLOBAL_READ_ID" "$fanout_scs_id" + create_legacy_subject_mapping ignored_mapping_id "$ATTR_A_VAL_2_ID" "$GLOBAL_READ_ID" "$single_namespace_scs_id" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + ns_b_state_before=$(namespace_state_json "$NS_B_ID") + + run_namespaced_policy_commit "subject-condition-sets" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 2 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_scs_created_in_namespace "$fanout_scs_id" "$NS_A_ID" + assert_scs_already_migrated_in_namespace "$fanout_scs_id" "$NS_B_ID" "$existing_fanout_ns_b_scs_id" + + assert_scs_created_in_namespace "$single_namespace_scs_id" "$NS_A_ID" + assert_scs_absent_in_namespace "$single_namespace_scs_id" "$NS_B_ID" + + assert_legacy_scs_still_exists "$fanout_scs_id" + assert_legacy_scs_still_exists "$single_namespace_scs_id" + + # Re-running the same migration should be idempotent. The previously created + # SCS targets should now be marked already_migrated, and the pre-existing + # canonical target should continue to resolve as already_migrated. + local fanout_ns_a_target_id + local single_namespace_target_id + fanout_ns_a_target_id=$(scs_id_by_migrated_from "$NS_A_ID" "$fanout_scs_id") + single_namespace_target_id=$(scs_id_by_migrated_from "$NS_A_ID" "$single_namespace_scs_id") + + ns_a_state_before="$ns_a_state_after" + ns_b_state_before="$ns_b_state_after" + + run_namespaced_policy_commit "subject-condition-sets" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_scs_already_migrated_in_namespace "$fanout_scs_id" "$NS_A_ID" "$fanout_ns_a_target_id" + assert_scs_already_migrated_in_namespace "$fanout_scs_id" "$NS_B_ID" "$existing_fanout_ns_b_scs_id" + assert_scs_already_migrated_in_namespace "$single_namespace_scs_id" "$NS_A_ID" "$single_namespace_target_id" + assert_scs_absent_in_namespace "$single_namespace_scs_id" "$NS_B_ID" +} + +# Asserts subject-mapping migration creates missing namespaced mappings, +# rewrites both custom-action and standard-action dependencies to the correct +# target IDs, reuses an already-migrated canonical mapping when present, +# rewrites SCS dependencies, preserves source metadata on created mappings, and +# is idempotent on rerun. +@test "migrate namespaced-policy subject-mappings rewrite dependencies and reuse canonical targets" { + local custom_action_name="${TEST_PREFIX}-download" + local sm_a_scs='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-sm-a"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local sm_b_scs='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-sm-b"],"subject_external_selector_value":".team.name"}],"boolean_operator":1}]}]' + local custom_action_labels=(--label "test_case=subject-mappings" --label "fixture=${TEST_PREFIX}-custom-action") + local sm_a_scs_labels=(--label "test_case=subject-mappings" --label "fixture=${TEST_PREFIX}-sm-a-scs") + local sm_b_scs_labels=(--label "test_case=subject-mappings" --label "fixture=${TEST_PREFIX}-sm-b-scs") + local mapping_custom_labels=(--label "test_case=subject-mappings" --label "fixture=${TEST_PREFIX}-mapping-custom") + local mapping_standard_labels=(--label "test_case=subject-mappings" --label "fixture=${TEST_PREFIX}-mapping-standard") + local custom_action_id + local sm_a_scs_id + local sm_b_scs_id + local mapping_custom_id + local mapping_standard_id + local ns_b_read_action_id + local existing_sm_b_scs_id + local existing_mapping_standard_id + local ns_a_state_before + local ns_b_state_before + local ns_a_state_after + local ns_b_state_after + + create_global_action custom_action_id "$custom_action_name" "${custom_action_labels[@]}" + create_global_scs sm_a_scs_id "$sm_a_scs" "${sm_a_scs_labels[@]}" + create_global_scs sm_b_scs_id "$sm_b_scs" "${sm_b_scs_labels[@]}" + + create_legacy_subject_mapping mapping_custom_id "$ATTR_A_VAL_1_ID" "$custom_action_id" "$sm_a_scs_id" "${mapping_custom_labels[@]}" + create_legacy_subject_mapping mapping_standard_id "$ATTR_B_VAL_1_ID" "$GLOBAL_READ_ID" "$sm_b_scs_id" "${mapping_standard_labels[@]}" + + lookup_namespaced_action_id ns_b_read_action_id "read" "$NS_B_ID" + create_namespaced_scs existing_sm_b_scs_id "$NS_B_ID" "$sm_b_scs" + create_namespaced_subject_mapping existing_mapping_standard_id "$NS_B_ID" "$ATTR_B_VAL_1_ID" "$ns_b_read_action_id" "$existing_sm_b_scs_id" --label "test_case=subject-mappings" --label "fixture=${TEST_PREFIX}-existing-mapping-standard" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + ns_b_state_before=$(namespace_state_json "$NS_B_ID") + + run_namespaced_policy_commit "subject-mappings" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 1 1 1 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_subject_mapping_created_in_namespace "$mapping_custom_id" "$NS_A_ID" "$ATTR_A_VAL_1_ID" "$custom_action_name" "$custom_action_id" "create" "$sm_a_scs_id" + assert_standard_action_resolved_in_namespace "read" "$NS_B_ID" + assert_scs_already_migrated_in_namespace "$sm_b_scs_id" "$NS_B_ID" "$existing_sm_b_scs_id" + assert_subject_mapping_already_migrated_in_namespace "$mapping_standard_id" "$NS_B_ID" "$existing_mapping_standard_id" + + assert_legacy_subject_mapping_still_exists "$ATTR_A_VAL_1_ID" "$mapping_custom_id" + assert_legacy_subject_mapping_still_exists "$ATTR_B_VAL_1_ID" "$mapping_standard_id" + + # Re-running the same migration should be idempotent. The custom action, + # migrated SCS targets, and migrated subject mappings should all resolve as + # already_migrated on the second pass. Standard read remains existing_standard. + local custom_action_target_id + local sm_a_scs_target_id + local mapping_custom_target_id + custom_action_target_id=$(action_id_by_name_in_namespace "$custom_action_name" "$NS_A_ID") + sm_a_scs_target_id=$(scs_id_by_migrated_from "$NS_A_ID" "$sm_a_scs_id") + mapping_custom_target_id=$(subject_mapping_id_by_migrated_from "$NS_A_ID" "$mapping_custom_id") + + ns_a_state_before="$ns_a_state_after" + ns_b_state_before="$ns_b_state_after" + + run_namespaced_policy_commit "subject-mappings" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_action_already_migrated_in_namespace "$custom_action_name" "$NS_A_ID" "$custom_action_target_id" + assert_standard_action_resolved_in_namespace "read" "$NS_B_ID" + assert_scs_already_migrated_in_namespace "$sm_a_scs_id" "$NS_A_ID" "$sm_a_scs_target_id" + assert_scs_already_migrated_in_namespace "$sm_b_scs_id" "$NS_B_ID" "$existing_sm_b_scs_id" + assert_subject_mapping_already_migrated_in_namespace "$mapping_custom_id" "$NS_A_ID" "$mapping_custom_target_id" + assert_subject_mapping_already_migrated_in_namespace "$mapping_standard_id" "$NS_B_ID" "$existing_mapping_standard_id" +} + +# Asserts registered-resource migration creates missing namespaced targets, +# rewrites action-attribute-value bindings to migrated action IDs, reuses an +# already-migrated canonical RR target when present, preserves metadata on both +# the parent RR and value, does not create unrelated namespaced objects as a +# side effect, and is idempotent on rerun. +@test "migrate namespaced-policy registered-resources rewrites action bindings and reuses canonical targets" { + local custom_action_name="${TEST_PREFIX}-download" + local rr_a_name="${TEST_PREFIX}-repo-a" + local rr_b_name="${TEST_PREFIX}-repo-b" + local rr_a_value="${TEST_PREFIX}-repo-a-main" + local rr_b_value="${TEST_PREFIX}-repo-b-main" + local custom_action_labels=(--label "test_case=registered-resources" --label "fixture=${TEST_PREFIX}-custom-action") + local rr_a_labels=(--label "test_case=registered-resources" --label "fixture=${TEST_PREFIX}-rr-a") + local rr_b_labels=(--label "test_case=registered-resources" --label "fixture=${TEST_PREFIX}-rr-b") + local rr_a_value_labels=(--label "test_case=registered-resources" --label "fixture=${TEST_PREFIX}-rr-a-value") + local rr_b_value_labels=(--label "test_case=registered-resources" --label "fixture=${TEST_PREFIX}-rr-b-value") + local custom_action_id + local rr_a_id + local rr_b_id + local rr_a_value_id + local rr_b_value_id + local ns_b_read_action_id + local existing_rr_b_id + local existing_rr_b_value_id + local ns_a_state_before + local ns_b_state_before + local ns_a_state_after + local ns_b_state_after + + create_global_action custom_action_id "$custom_action_name" "${custom_action_labels[@]}" + create_global_registered_resource rr_a_id "$rr_a_name" "${rr_a_labels[@]}" + create_global_registered_resource rr_b_id "$rr_b_name" "${rr_b_labels[@]}" + create_registered_resource_value rr_a_value_id "$rr_a_id" "$rr_a_value" --action-attribute-value "$custom_action_id;$ATTR_A_VAL_1_ID" "${rr_a_value_labels[@]}" + create_registered_resource_value rr_b_value_id "$rr_b_id" "$rr_b_value" --action-attribute-value "$GLOBAL_READ_ID;$ATTR_B_VAL_1_ID" "${rr_b_value_labels[@]}" + + lookup_namespaced_action_id ns_b_read_action_id "read" "$NS_B_ID" + + run_otdfctl_registered_resources create --name "$rr_b_name" --namespace "$NS_B_ID" --label "test_case=registered-resources" --label "fixture=${TEST_PREFIX}-existing-rr-b" --json + assert_success + existing_rr_b_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$existing_rr_b_id" "" + + run_otdfctl_registered_resource_values create --resource "$existing_rr_b_id" --value "$rr_b_value" --action-attribute-value "$ns_b_read_action_id;$ATTR_B_VAL_1_ID" --label "test_case=registered-resources" --label "fixture=${TEST_PREFIX}-existing-rr-b-value" --json + assert_success + existing_rr_b_value_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$existing_rr_b_value_id" "" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + ns_b_state_before=$(namespace_state_json "$NS_B_ID") + + run_namespaced_policy_commit "registered-resources" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 1 0 0 1 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_registered_resource_created_in_namespace "$rr_a_id" "$rr_a_value_id" "$NS_A_ID" "$rr_a_name" "$rr_a_value" "$custom_action_name" "$custom_action_id" "create" "$ATTR_A_VAL_1_ID" + + assert_registered_resource_already_migrated_in_namespace "$rr_b_id" "$NS_B_ID" "$existing_rr_b_id" + assert_standard_action_resolved_in_namespace "read" "$NS_B_ID" + assert_registered_resource_value_uses_action "$existing_rr_b_value_id" "$ns_b_read_action_id" "$ATTR_B_VAL_1_ID" + + assert_legacy_registered_resource_still_exists "$rr_a_id" "$rr_a_value_id" "$rr_a_name" "$rr_a_value" + assert_legacy_registered_resource_still_exists "$rr_b_id" "$rr_b_value_id" "$rr_b_name" "$rr_b_value" + + # Re-running the same migration should be idempotent. The previously created + # RR target should now resolve as already_migrated, while the existing + # canonical RR target continues to resolve as already_migrated. + local custom_action_target_id + local rr_a_target_id + custom_action_target_id=$(action_id_by_name_in_namespace "$custom_action_name" "$NS_A_ID") + rr_a_target_id=$(registered_resource_id_by_migrated_from "$NS_A_ID" "$rr_a_id") + + ns_a_state_before="$ns_a_state_after" + ns_b_state_before="$ns_b_state_after" + + run_namespaced_policy_commit "registered-resources" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_action_already_migrated_in_namespace "$custom_action_name" "$NS_A_ID" "$custom_action_target_id" + assert_standard_action_resolved_in_namespace "read" "$NS_B_ID" + assert_registered_resource_already_migrated_in_namespace "$rr_a_id" "$NS_A_ID" "$rr_a_target_id" + assert_registered_resource_already_migrated_in_namespace "$rr_b_id" "$NS_B_ID" "$existing_rr_b_id" +} + +# Asserts obligation-trigger migration creates missing namespaced trigger +# targets, rewrites the referenced action to the migrated action target, +# reuses an already-migrated canonical trigger when present, preserves source +# metadata, does not create unrelated namespaced objects as a side effect, and +# is idempotent on rerun. +@test "migrate namespaced-policy obligation-triggers rewrites action dependencies and reuses canonical targets" { + local custom_action_name="${TEST_PREFIX}-download" + local obligation_a_name="${TEST_PREFIX}-notify-a" + local obligation_b_name="${TEST_PREFIX}-notify-b" + local obligation_a_value="${TEST_PREFIX}-notify-a-default" + local obligation_b_value="${TEST_PREFIX}-notify-b-default" + local trigger_a_client_id="${TEST_PREFIX}-client-a" + local trigger_b_client_id="${TEST_PREFIX}-client-b" + local custom_action_labels=(--label "test_case=obligation-triggers" --label "fixture=${TEST_PREFIX}-custom-action") + local trigger_a_labels=(--label "test_case=obligation-triggers" --label "fixture=${TEST_PREFIX}-trigger-a") + local custom_action_id + local obligation_a_id + local obligation_b_id + local obligation_a_value_id + local obligation_b_value_id + local trigger_a_id + local trigger_b_id + local ns_b_read_action_id + local existing_trigger_b_id + local ns_a_state_before + local ns_b_state_before + local ns_a_state_after + local ns_b_state_after + + create_global_action custom_action_id "$custom_action_name" "${custom_action_labels[@]}" + create_namespaced_obligation obligation_a_id "$NS_A_ID" "$obligation_a_name" --label "test_case=obligation-triggers" --label "fixture=${TEST_PREFIX}-obligation-a" + create_namespaced_obligation obligation_b_id "$NS_B_ID" "$obligation_b_name" --label "test_case=obligation-triggers" --label "fixture=${TEST_PREFIX}-obligation-b" + create_obligation_value obligation_a_value_id "$obligation_a_id" "$obligation_a_value" --label "test_case=obligation-triggers" --label "fixture=${TEST_PREFIX}-obligation-a-value" + create_obligation_value obligation_b_value_id "$obligation_b_id" "$obligation_b_value" --label "test_case=obligation-triggers" --label "fixture=${TEST_PREFIX}-obligation-b-value" + + create_legacy_obligation_trigger trigger_a_id "$ATTR_A_VAL_1_ID" "$custom_action_id" "$obligation_a_value_id" --client-id "$trigger_a_client_id" "${trigger_a_labels[@]}" + create_legacy_obligation_trigger trigger_b_id "$ATTR_B_VAL_1_ID" "$GLOBAL_READ_ID" "$obligation_b_value_id" --client-id "$trigger_b_client_id" --label "test_case=obligation-triggers" --label "fixture=${TEST_PREFIX}-trigger-b" + + lookup_namespaced_action_id ns_b_read_action_id "read" "$NS_B_ID" + + run_otdfctl_obligation_triggers create --attribute-value "$ATTR_B_VAL_1_ID" --action "$ns_b_read_action_id" --obligation-value "$obligation_b_value_id" --client-id "$trigger_b_client_id" --label "test_case=obligation-triggers" --label "fixture=${TEST_PREFIX}-existing-trigger-b" --json + assert_success + existing_trigger_b_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$existing_trigger_b_id" "" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + ns_b_state_before=$(namespace_state_json "$NS_B_ID") + + run_namespaced_policy_commit "obligation-triggers" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 1 0 0 0 1 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_obligation_trigger_created_in_namespace "$trigger_a_id" "$NS_A_ID" "$ATTR_A_VAL_1_ID" "$obligation_a_value_id" "$custom_action_name" "$custom_action_id" "create" "$trigger_a_client_id" + + assert_obligation_trigger_already_migrated_in_namespace "$trigger_b_id" "$NS_B_ID" "$existing_trigger_b_id" + assert_standard_action_resolved_in_namespace "read" "$NS_B_ID" + + assert_legacy_obligation_trigger_still_exists "$trigger_a_id" "$NS_A_ID" "$ATTR_A_VAL_1_ID" "$custom_action_id" "$obligation_a_value_id" "$trigger_a_client_id" + assert_legacy_obligation_trigger_still_exists "$trigger_b_id" "$NS_B_ID" "$ATTR_B_VAL_1_ID" "$GLOBAL_READ_ID" "$obligation_b_value_id" "$trigger_b_client_id" + + # Re-running the same migration should be idempotent. The previously created + # custom action target and trigger target should resolve as already_migrated, + # while the pre-existing canonical trigger remains already_migrated. + local custom_action_target_id + local trigger_a_target_id + custom_action_target_id=$(action_id_by_name_in_namespace "$custom_action_name" "$NS_A_ID") + trigger_a_target_id=$(obligation_trigger_id_by_migrated_from "$NS_A_ID" "$trigger_a_id") + + ns_a_state_before="$ns_a_state_after" + ns_b_state_before="$ns_b_state_after" + + run_namespaced_policy_commit "obligation-triggers" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_action_already_migrated_in_namespace "$custom_action_name" "$NS_A_ID" "$custom_action_target_id" + assert_standard_action_resolved_in_namespace "read" "$NS_B_ID" + assert_obligation_trigger_already_migrated_in_namespace "$trigger_a_id" "$NS_A_ID" "$trigger_a_target_id" + assert_obligation_trigger_already_migrated_in_namespace "$trigger_b_id" "$NS_B_ID" "$existing_trigger_b_id" +} + +# Asserts selecting every migration scope together creates one namespaced +# target for each supported object type in a simple single-namespace graph and +# is idempotent on rerun. +@test "migrate namespaced-policy all scopes creates one target for each object type" { + local custom_action_name="${TEST_PREFIX}-download" + local all_scopes_scs='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-all-scopes"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local rr_name="${TEST_PREFIX}-repo" + local rr_value="${TEST_PREFIX}-repo-main" + local obligation_name="${TEST_PREFIX}-notify" + local obligation_value="${TEST_PREFIX}-notify-default" + local trigger_client_id="${TEST_PREFIX}-client" + local custom_action_id + local scs_id + local mapping_id + local rr_id + local rr_value_id + local obligation_id + local obligation_value_id + local trigger_id + local ns_a_state_before + local ns_b_state_before + local ns_a_state_after + local ns_b_state_after + + create_global_action custom_action_id "$custom_action_name" --label "test_case=all-scopes" --label "fixture=${TEST_PREFIX}-custom-action" + create_global_scs scs_id "$all_scopes_scs" --label "test_case=all-scopes" --label "fixture=${TEST_PREFIX}-scs" + create_legacy_subject_mapping mapping_id "$ATTR_A_VAL_1_ID" "$custom_action_id" "$scs_id" --label "test_case=all-scopes" --label "fixture=${TEST_PREFIX}-mapping" + create_global_registered_resource rr_id "$rr_name" --label "test_case=all-scopes" --label "fixture=${TEST_PREFIX}-rr" + create_registered_resource_value rr_value_id "$rr_id" "$rr_value" --action-attribute-value "$custom_action_id;$ATTR_A_VAL_2_ID" --label "test_case=all-scopes" --label "fixture=${TEST_PREFIX}-rr-value" + create_namespaced_obligation obligation_id "$NS_A_ID" "$obligation_name" --label "test_case=all-scopes" --label "fixture=${TEST_PREFIX}-obligation" + create_obligation_value obligation_value_id "$obligation_id" "$obligation_value" --label "test_case=all-scopes" --label "fixture=${TEST_PREFIX}-obligation-value" + create_legacy_obligation_trigger trigger_id "$ATTR_A_VAL_1_ID" "$custom_action_id" "$obligation_value_id" --client-id "$trigger_client_id" --label "test_case=all-scopes" --label "fixture=${TEST_PREFIX}-trigger" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + ns_b_state_before=$(namespace_state_json "$NS_B_ID") + + run_namespaced_policy_commit "actions,subject-condition-sets,subject-mappings,registered-resources,obligation-triggers" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 1 1 1 1 1 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_custom_action_created_in_namespace "$custom_action_name" "$custom_action_id" "$NS_A_ID" + + assert_scs_created_in_namespace "$scs_id" "$NS_A_ID" + + assert_subject_mapping_created_in_namespace "$mapping_id" "$NS_A_ID" "$ATTR_A_VAL_1_ID" "$custom_action_name" "$custom_action_id" "create" "$scs_id" + + assert_registered_resource_created_in_namespace "$rr_id" "$rr_value_id" "$NS_A_ID" "$rr_name" "$rr_value" "$custom_action_name" "$custom_action_id" "create" "$ATTR_A_VAL_2_ID" + + assert_obligation_trigger_created_in_namespace "$trigger_id" "$NS_A_ID" "$ATTR_A_VAL_1_ID" "$obligation_value_id" "$custom_action_name" "$custom_action_id" "create" "$trigger_client_id" + + assert_legacy_custom_action_still_exists "$custom_action_id" "$custom_action_name" + assert_legacy_scs_still_exists "$scs_id" + assert_legacy_subject_mapping_still_exists "$ATTR_A_VAL_1_ID" "$mapping_id" + assert_legacy_registered_resource_still_exists "$rr_id" "$rr_value_id" "$rr_name" "$rr_value" + assert_legacy_obligation_trigger_still_exists "$trigger_id" "$NS_A_ID" "$ATTR_A_VAL_1_ID" "$custom_action_id" "$obligation_value_id" "$trigger_client_id" + + # Re-running the combined migration should be idempotent. Every target created + # above should now resolve as already_migrated, and no namespace counts + # should change on the second pass. + local custom_action_target_id + local scs_target_id + local mapping_target_id + local rr_target_id + local trigger_target_id + custom_action_target_id=$(action_id_by_name_in_namespace "$custom_action_name" "$NS_A_ID") + scs_target_id=$(scs_id_by_migrated_from "$NS_A_ID" "$scs_id") + mapping_target_id=$(subject_mapping_id_by_migrated_from "$NS_A_ID" "$mapping_id") + rr_target_id=$(registered_resource_id_by_migrated_from "$NS_A_ID" "$rr_id") + trigger_target_id=$(obligation_trigger_id_by_migrated_from "$NS_A_ID" "$trigger_id") + + ns_a_state_before="$ns_a_state_after" + ns_b_state_before="$ns_b_state_after" + + run_namespaced_policy_commit "actions,subject-condition-sets,subject-mappings,registered-resources,obligation-triggers" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + ns_b_state_after=$(namespace_state_json "$NS_B_ID") + + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + assert_namespace_state_delta "$ns_b_state_before" "$ns_b_state_after" 0 0 0 0 0 + + assert_action_already_migrated_in_namespace "$custom_action_name" "$NS_A_ID" "$custom_action_target_id" + assert_scs_already_migrated_in_namespace "$scs_id" "$NS_A_ID" "$scs_target_id" + assert_subject_mapping_already_migrated_in_namespace "$mapping_id" "$NS_A_ID" "$mapping_target_id" + assert_registered_resource_already_migrated_in_namespace "$rr_id" "$NS_A_ID" "$rr_target_id" + assert_obligation_trigger_already_migrated_in_namespace "$trigger_id" "$NS_A_ID" "$trigger_target_id" +} + +# Paths intentionally covered here for prune: +# - prune command validation rejects empty, invalid, and multi-scope CSV input +# and leaves otherwise-prunable fixtures untouched +# - action prune deletes labeled migrated legacy actions and retains actions +# that are still in use or were not migrated +# - SCS prune deletes labeled migrated legacy SCS and retains in-use, +# not-migrated, and unlabeled-target cases +# - subject-mapping prune deletes labeled migrated legacy mappings and retains +# not-migrated and unlabeled-target cases +# - registered-resource prune deletes labeled migrated legacy resources and +# values and retains not-migrated, unlabeled-target, and multi-namespace +# source cases +# - obligation-trigger prune deletes labeled migrated legacy triggers and +# retains not-migrated and unlabeled-target cases +# - every covered prune scope verifies idempotent reruns and uses namespace +# delta checks to confirm no unexpected target churn +# +# Paths that are not in these e2e prune tests: +# - planner-only or dry-run prune output, summary formatting, and explicit +# status bucket assertions such as delete/blocked/unresolved +# - interactive prune review and backup-confirmation flows + +# Covers prune scope validation paths: +# - reject an explicitly empty scope value +# - leave a prunable legacy source and migrated target untouched +@test "prune namespaced-policy rejects an empty scope" { + local action_name="${TEST_PREFIX}-prune-empty-scope" + local action_id + local action_target_id + local ns_a_state_before + local ns_a_state_after + + create_global_action action_id "$action_name" --label "test_case=prune-empty-scope" --label "fixture=${TEST_PREFIX}-source" + create_namespaced_action action_target_id "$NS_A_ID" "$action_name" --label "test_case=prune-empty-scope" --label "fixture=${TEST_PREFIX}-target" --label "migrated_from=$action_id" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json migrate prune namespaced-policy --commit --scope "" + assert_failure + assert_output --partial "Flag '--scope' is required" + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_custom_action_still_exists "$action_id" "$action_name" + assert_action_already_migrated_in_namespace "$action_name" "$NS_A_ID" "$action_target_id" +} + +# Covers prune scope validation paths: +# - reject an invalid scope value +# - leave a prunable legacy source and migrated target untouched +@test "prune namespaced-policy rejects an invalid scope" { + local action_name="${TEST_PREFIX}-prune-invalid-scope" + local action_id + local action_target_id + local ns_a_state_before + local ns_a_state_after + + create_global_action action_id "$action_name" --label "test_case=prune-invalid-scope" --label "fixture=${TEST_PREFIX}-source" + create_namespaced_action action_target_id "$NS_A_ID" "$action_name" --label "test_case=prune-invalid-scope" --label "fixture=${TEST_PREFIX}-target" --label "migrated_from=$action_id" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json migrate prune namespaced-policy --commit --scope "not-a-real-scope" + assert_failure + assert_output --partial "invalid migration scope: not-a-real-scope" + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_custom_action_still_exists "$action_id" "$action_name" + assert_action_already_migrated_in_namespace "$action_name" "$NS_A_ID" "$action_target_id" +} + +# Covers prune scope validation paths: +# - reject CSV scope values containing more than one scope +# - leave a prunable legacy source and migrated target untouched +@test "prune namespaced-policy rejects multiple CSV scopes" { + local action_name="${TEST_PREFIX}-prune-multiple-scopes" + local action_id + local action_target_id + local ns_a_state_before + local ns_a_state_after + + create_global_action action_id "$action_name" --label "test_case=prune-multiple-scopes" --label "fixture=${TEST_PREFIX}-source" + create_namespaced_action action_target_id "$NS_A_ID" "$action_name" --label "test_case=prune-multiple-scopes" --label "fixture=${TEST_PREFIX}-target" --label "migrated_from=$action_id" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + + run ./otdfctl --host http://localhost:8080 --with-client-creds-file ./creds.json migrate prune namespaced-policy --commit --scope "actions,registered-resources" + assert_failure + assert_output --partial "prune planner accepts exactly one scope" + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_custom_action_still_exists "$action_id" "$action_name" + assert_action_already_migrated_in_namespace "$action_name" "$NS_A_ID" "$action_target_id" +} + + +# Prune mixed-state coverage: each scope is pruned once while the plan contains +# deletable candidates and candidates that must remain. +# Covers action prune paths: +# - delete legacy custom actions with labeled migrated targets +# - retain legacy actions still referenced by subject mappings, registered resources, or obligation triggers +# - retain actions that were not migrated +# - check idempotency +@test "prune namespaced-policy actions handles delete, in-use, and not-migrated states together" { + local delete_a_name="${TEST_PREFIX}-prune-action-delete-a" + local delete_b_name="${TEST_PREFIX}-prune-action-delete-b" + local used_by_mapping_name="${TEST_PREFIX}-prune-action-used-by-mapping" + local used_by_rr_name="${TEST_PREFIX}-prune-action-used-by-rr" + local used_by_trigger_name="${TEST_PREFIX}-prune-action-used-by-trigger" + local not_migrated_name="${TEST_PREFIX}-prune-action-not-migrated" + local shared_scs='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-action"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local delete_a_id + local delete_b_id + local used_by_mapping_id + local used_by_rr_id + local used_by_trigger_id + local not_migrated_id + local delete_a_target_id + local delete_b_target_id + local shared_scs_id + local mapping_id + local rr_id + local rr_value_id + local obligation_id + local obligation_value_id + local trigger_id + local ns_a_state_before + local ns_a_state_after + + create_global_action delete_a_id "$delete_a_name" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-delete-a-source" + create_global_action delete_b_id "$delete_b_name" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-delete-b-source" + create_global_action used_by_mapping_id "$used_by_mapping_name" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-used-by-mapping-source" + create_global_action used_by_rr_id "$used_by_rr_name" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-used-by-rr-source" + create_global_action used_by_trigger_id "$used_by_trigger_name" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-used-by-trigger-source" + create_global_action not_migrated_id "$not_migrated_name" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-not-migrated-source" + + create_namespaced_action delete_a_target_id "$NS_A_ID" "$delete_a_name" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-delete-a-target" --label "migrated_from=$delete_a_id" + create_namespaced_action delete_b_target_id "$NS_A_ID" "$delete_b_name" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-delete-b-target" --label "migrated_from=$delete_b_id" + + create_global_scs shared_scs_id "$shared_scs" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-shared-scs" + create_legacy_subject_mapping mapping_id "$ATTR_A_VAL_1_ID" "$used_by_mapping_id" "$shared_scs_id" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-mapping-reference" + create_global_registered_resource rr_id "${TEST_PREFIX}-prune-action-rr" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-rr-reference" + create_registered_resource_value rr_value_id "$rr_id" "${TEST_PREFIX}-prune-action-rr-value" --action-attribute-value "$used_by_rr_id;$ATTR_A_VAL_1_ID" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-rr-value-reference" + create_namespaced_obligation obligation_id "$NS_A_ID" "${TEST_PREFIX}-prune-action-obligation" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-obligation-reference" + create_obligation_value obligation_value_id "$obligation_id" "${TEST_PREFIX}-prune-action-obligation-value" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-obligation-value-reference" + create_legacy_obligation_trigger trigger_id "$ATTR_A_VAL_1_ID" "$used_by_trigger_id" "$obligation_value_id" --client-id "${TEST_PREFIX}-prune-action-client" --label "test_case=prune-actions" --label "fixture=${TEST_PREFIX}-trigger-reference" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + + run_namespaced_policy_prune_commit "actions" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_custom_action_pruned "$delete_a_id" + assert_legacy_custom_action_pruned "$delete_b_id" + untrack_action_id "$delete_a_id" + untrack_action_id "$delete_b_id" + assert_action_already_migrated_in_namespace "$delete_a_name" "$NS_A_ID" "$delete_a_target_id" + assert_action_already_migrated_in_namespace "$delete_b_name" "$NS_A_ID" "$delete_b_target_id" + assert_legacy_custom_action_still_exists "$used_by_mapping_id" "$used_by_mapping_name" + assert_legacy_custom_action_still_exists "$used_by_rr_id" "$used_by_rr_name" + assert_legacy_custom_action_still_exists "$used_by_trigger_id" "$used_by_trigger_name" + assert_legacy_custom_action_still_exists "$not_migrated_id" "$not_migrated_name" + + ns_a_state_before="$ns_a_state_after" + + run_namespaced_policy_prune_commit "actions" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_custom_action_pruned "$delete_a_id" + assert_legacy_custom_action_pruned "$delete_b_id" + assert_action_already_migrated_in_namespace "$delete_a_name" "$NS_A_ID" "$delete_a_target_id" + assert_action_already_migrated_in_namespace "$delete_b_name" "$NS_A_ID" "$delete_b_target_id" + assert_legacy_custom_action_still_exists "$used_by_mapping_id" "$used_by_mapping_name" + assert_legacy_custom_action_still_exists "$used_by_rr_id" "$used_by_rr_name" + assert_legacy_custom_action_still_exists "$used_by_trigger_id" "$used_by_trigger_name" + assert_legacy_custom_action_still_exists "$not_migrated_id" "$not_migrated_name" +} + +# Covers SCS prune paths: +# - delete legacy SCS with labeled migrated targets +# - retain SCS still referenced by a subject mapping +# - retain SCS that were not migrated +# - retain SCS with an unlabeled target +# - check idempotency +@test "prune namespaced-policy subject-condition-sets handles delete, in-use, not-migrated, and unlabeled-target states together" { + local delete_a_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-scs-delete-a"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local delete_b_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-scs-delete-b"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local used_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-scs-used"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local not_migrated_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-scs-not-migrated"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local unlabeled_target_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-scs-unlabeled-target"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local delete_a_id + local delete_b_id + local used_id + local not_migrated_id + local unlabeled_id + local delete_a_target_id + local delete_b_target_id + local used_target_id + local unlabeled_target_id + local action_id + local mapping_id + local ns_a_state_before + local ns_a_state_after + + create_global_scs delete_a_id "$delete_a_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-delete-a-source" + create_global_scs delete_b_id "$delete_b_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-delete-b-source" + create_global_scs used_id "$used_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-used-source" + create_global_scs not_migrated_id "$not_migrated_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-not-migrated-source" + create_global_scs unlabeled_id "$unlabeled_target_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-unlabeled-target-source" + + create_namespaced_scs delete_a_target_id "$NS_A_ID" "$delete_a_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-delete-a-target" --label "migrated_from=$delete_a_id" + create_namespaced_scs delete_b_target_id "$NS_A_ID" "$delete_b_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-delete-b-target" --label "migrated_from=$delete_b_id" + create_namespaced_scs used_target_id "$NS_A_ID" "$used_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-used-target" --label "migrated_from=$used_id" + create_namespaced_scs unlabeled_target_id "$NS_A_ID" "$unlabeled_target_sets" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-unlabeled-target" + + create_global_action action_id "${TEST_PREFIX}-prune-scs-action" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-action-reference" + create_legacy_subject_mapping mapping_id "$ATTR_A_VAL_1_ID" "$action_id" "$used_id" --label "test_case=prune-scs" --label "fixture=${TEST_PREFIX}-mapping-reference" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + + run_namespaced_policy_prune_commit "subject-condition-sets" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_scs_pruned "$delete_a_id" + assert_legacy_scs_pruned "$delete_b_id" + untrack_scs_id "$delete_a_id" + untrack_scs_id "$delete_b_id" + assert_scs_target_still_exists "$delete_a_target_id" "$NS_A_ID" "$delete_a_id" + assert_scs_target_still_exists "$delete_b_target_id" "$NS_A_ID" "$delete_b_id" + assert_legacy_scs_still_exists "$used_id" + assert_legacy_scs_still_exists "$not_migrated_id" + assert_legacy_scs_still_exists "$unlabeled_id" + assert_scs_target_still_exists "$used_target_id" "$NS_A_ID" "$used_id" + + local unlabeled_target_json + run_otdfctl_scs get --id "$unlabeled_target_id" --json + unlabeled_target_json="$output" + assert_equal "$(echo "$unlabeled_target_json" | jq -r '.id // empty')" "$unlabeled_target_id" + assert_equal "$(echo "$unlabeled_target_json" | jq -r '.namespace.id')" "$NS_A_ID" + assert_equal "$(echo "$unlabeled_target_json" | jq -r '.metadata.labels.migrated_from // empty')" "" + + ns_a_state_before="$ns_a_state_after" + + run_namespaced_policy_prune_commit "subject-condition-sets" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_scs_pruned "$delete_a_id" + assert_legacy_scs_pruned "$delete_b_id" + assert_scs_target_still_exists "$delete_a_target_id" "$NS_A_ID" "$delete_a_id" + assert_scs_target_still_exists "$delete_b_target_id" "$NS_A_ID" "$delete_b_id" + assert_legacy_scs_still_exists "$used_id" + assert_legacy_scs_still_exists "$not_migrated_id" + assert_legacy_scs_still_exists "$unlabeled_id" + assert_scs_target_still_exists "$used_target_id" "$NS_A_ID" "$used_id" + run_otdfctl_scs get --id "$unlabeled_target_id" --json + unlabeled_target_json="$output" + assert_equal "$(echo "$unlabeled_target_json" | jq -r '.id // empty')" "$unlabeled_target_id" + assert_equal "$(echo "$unlabeled_target_json" | jq -r '.namespace.id')" "$NS_A_ID" + assert_equal "$(echo "$unlabeled_target_json" | jq -r '.metadata.labels.migrated_from // empty')" "" +} + +# Covers subject-mapping prune paths: +# - delete migrated legacy mappings +# - retain mappings that were not migrated +# - retain mappings whose matching target is not labeled as migrated_from the source +# - check idempotency +@test "prune namespaced-policy subject-mappings handles delete, not-migrated, and unlabeled-target states together" { + local delete_a_action_name="${TEST_PREFIX}-prune-sm-delete-a" + local delete_b_action_name="${TEST_PREFIX}-prune-sm-delete-b" + local not_migrated_global_action_name="${TEST_PREFIX}-prune-sm-not-migrated" + local unlabeled_global_action_name="${TEST_PREFIX}-prune-sm-unlabeled-target" + local delete_a_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-sm-delete-a"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local delete_b_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-sm-delete-b"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local not_migrated_global_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-sm-not-migrated"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local unlabeled_global_sets='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["'"${TEST_PREFIX}"'-prune-sm-unlabeled-target"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + local delete_a_action_id + local delete_b_action_id + local not_migrated_global_action_id + local unlabeled_global_action_id + local unlabeled_action_target_id + local delete_a_scs_id + local delete_b_scs_id + local not_migrated_global_scs_id + local unlabeled_global_scs_id + local unlabeled_scs_target_id + local delete_a_mapping_id + local delete_b_mapping_id + local not_migrated_global_mapping_id + local unlabeled_global_mapping_id + local delete_a_mapping_target_id + local delete_b_mapping_target_id + local unlabeled_mapping_target_id + local ns_a_state_before + local ns_a_state_after + + create_global_action delete_a_action_id "$delete_a_action_name" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-delete-a-action" + create_global_action delete_b_action_id "$delete_b_action_name" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-delete-b-action" + create_global_scs delete_a_scs_id "$delete_a_sets" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-delete-a-scs" + create_global_scs delete_b_scs_id "$delete_b_sets" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-delete-b-scs" + create_legacy_subject_mapping delete_a_mapping_id "$ATTR_A_VAL_1_ID" "$delete_a_action_id" "$delete_a_scs_id" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-delete-a-mapping" + create_legacy_subject_mapping delete_b_mapping_id "$ATTR_A_VAL_2_ID" "$delete_b_action_id" "$delete_b_scs_id" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-delete-b-mapping" + + run_namespaced_policy_commit "subject-mappings" + assert_success + + delete_a_mapping_target_id=$(subject_mapping_id_by_migrated_from "$NS_A_ID" "$delete_a_mapping_id") + delete_b_mapping_target_id=$(subject_mapping_id_by_migrated_from "$NS_A_ID" "$delete_b_mapping_id") + + create_global_action not_migrated_global_action_id "$not_migrated_global_action_name" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-not-migrated-action" + create_global_scs not_migrated_global_scs_id "$not_migrated_global_sets" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-not-migrated-scs" + create_legacy_subject_mapping not_migrated_global_mapping_id "$ATTR_A_VAL_1_ID" "$not_migrated_global_action_id" "$not_migrated_global_scs_id" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-not-migrated-mapping" + + create_global_action unlabeled_global_action_id "$unlabeled_global_action_name" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-unlabeled-target-action" + create_global_scs unlabeled_global_scs_id "$unlabeled_global_sets" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-unlabeled-target-scs" + create_legacy_subject_mapping unlabeled_global_mapping_id "$ATTR_A_VAL_2_ID" "$unlabeled_global_action_id" "$unlabeled_global_scs_id" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-unlabeled-target-mapping" + create_namespaced_action unlabeled_action_target_id "$NS_A_ID" "$unlabeled_global_action_name" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-unlabeled-target-action-target" --label "migrated_from=$unlabeled_global_action_id" + create_namespaced_scs unlabeled_scs_target_id "$NS_A_ID" "$unlabeled_global_sets" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-unlabeled-target-scs-target" --label "migrated_from=$unlabeled_global_scs_id" + create_namespaced_subject_mapping unlabeled_mapping_target_id "$NS_A_ID" "$ATTR_A_VAL_2_ID" "$unlabeled_action_target_id" "$unlabeled_scs_target_id" --label "test_case=prune-subject-mappings" --label "fixture=${TEST_PREFIX}-unlabeled-target-mapping-target" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + + run_namespaced_policy_prune_commit "subject-mappings" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_subject_mapping_pruned "$delete_a_mapping_id" + assert_legacy_subject_mapping_pruned "$delete_b_mapping_id" + untrack_subject_mapping_id "$delete_a_mapping_id" + untrack_subject_mapping_id "$delete_b_mapping_id" + assert_subject_mapping_target_still_exists "$delete_a_mapping_target_id" "$NS_A_ID" "$delete_a_mapping_id" + assert_subject_mapping_target_still_exists "$delete_b_mapping_target_id" "$NS_A_ID" "$delete_b_mapping_id" + assert_legacy_subject_mapping_still_exists "$ATTR_A_VAL_1_ID" "$not_migrated_global_mapping_id" + assert_legacy_subject_mapping_still_exists "$ATTR_A_VAL_2_ID" "$unlabeled_global_mapping_id" + assert_legacy_custom_action_still_exists "$delete_a_action_id" "$delete_a_action_name" + assert_legacy_scs_still_exists "$delete_a_scs_id" + assert_legacy_custom_action_still_exists "$delete_b_action_id" "$delete_b_action_name" + assert_legacy_scs_still_exists "$delete_b_scs_id" + assert_legacy_custom_action_still_exists "$not_migrated_global_action_id" "$not_migrated_global_action_name" + assert_legacy_scs_still_exists "$not_migrated_global_scs_id" + assert_legacy_custom_action_still_exists "$unlabeled_global_action_id" "$unlabeled_global_action_name" + assert_legacy_scs_still_exists "$unlabeled_global_scs_id" + + local unlabeled_mapping_target_json + run_otdfctl_sm get --id "$unlabeled_mapping_target_id" --json + unlabeled_mapping_target_json="$output" + assert_equal "$(echo "$unlabeled_mapping_target_json" | jq -r '.id // empty')" "$unlabeled_mapping_target_id" + assert_equal "$(echo "$unlabeled_mapping_target_json" | jq -r '.namespace.id')" "$NS_A_ID" + assert_equal "$(echo "$unlabeled_mapping_target_json" | jq -r '.metadata.labels.migrated_from // empty')" "" + + ns_a_state_before="$ns_a_state_after" + + run_namespaced_policy_prune_commit "subject-mappings" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_subject_mapping_pruned "$delete_a_mapping_id" + assert_legacy_subject_mapping_pruned "$delete_b_mapping_id" + assert_subject_mapping_target_still_exists "$delete_a_mapping_target_id" "$NS_A_ID" "$delete_a_mapping_id" + assert_subject_mapping_target_still_exists "$delete_b_mapping_target_id" "$NS_A_ID" "$delete_b_mapping_id" + assert_legacy_subject_mapping_still_exists "$ATTR_A_VAL_1_ID" "$not_migrated_global_mapping_id" + assert_legacy_subject_mapping_still_exists "$ATTR_A_VAL_2_ID" "$unlabeled_global_mapping_id" + assert_legacy_custom_action_still_exists "$delete_a_action_id" "$delete_a_action_name" + assert_legacy_scs_still_exists "$delete_a_scs_id" + assert_legacy_custom_action_still_exists "$delete_b_action_id" "$delete_b_action_name" + assert_legacy_scs_still_exists "$delete_b_scs_id" + assert_legacy_custom_action_still_exists "$not_migrated_global_action_id" "$not_migrated_global_action_name" + assert_legacy_scs_still_exists "$not_migrated_global_scs_id" + assert_legacy_custom_action_still_exists "$unlabeled_global_action_id" "$unlabeled_global_action_name" + assert_legacy_scs_still_exists "$unlabeled_global_scs_id" + + run_otdfctl_sm get --id "$unlabeled_mapping_target_id" --json + unlabeled_mapping_target_json="$output" + assert_equal "$(echo "$unlabeled_mapping_target_json" | jq -r '.id // empty')" "$unlabeled_mapping_target_id" + assert_equal "$(echo "$unlabeled_mapping_target_json" | jq -r '.namespace.id')" "$NS_A_ID" + assert_equal "$(echo "$unlabeled_mapping_target_json" | jq -r '.metadata.labels.migrated_from // empty')" "" +} + +# Covers registered-resource prune paths: +# - delete migrated legacy resources and values +# - retain RRs that were not migrated +# - retain resources whose matching target is not labeled as migrated_from the source +# - retain a source resource with values from multiple namespaces +# - check idempotency +@test "prune namespaced-policy registered-resources handles delete, not-migrated, unlabeled-target, and multi-namespace-source states together" { + local delete_a_action_name="${TEST_PREFIX}-prune-rr-delete-a" + local delete_b_action_name="${TEST_PREFIX}-prune-rr-delete-b" + local not_migrated_global_action_name="${TEST_PREFIX}-prune-rr-not-migrated" + local unlabeled_global_action_name="${TEST_PREFIX}-prune-rr-unlabeled-target" + local delete_a_action_id + local delete_b_action_id + local not_migrated_global_action_id + local unlabeled_global_action_id + local unlabeled_action_target_id + local ns_a_read_action_id + local delete_a_rr_id + local delete_b_rr_id + local not_migrated_global_rr_id + local unlabeled_global_rr_id + local multi_namespace_rr_id + local delete_a_value_id + local delete_b_value_id + local not_migrated_global_value_id + local unlabeled_global_value_id + local multi_namespace_value_a_id + local multi_namespace_value_b_id + local delete_a_rr_target_id + local delete_b_rr_target_id + local delete_a_value_target_id + local delete_b_value_target_id + local unlabeled_rr_target_id + local unlabeled_value_target_id + local multi_namespace_rr_target_id + local multi_namespace_value_target_id + local ns_a_state_before + local ns_a_state_after + + create_global_action delete_a_action_id "$delete_a_action_name" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-delete-a-action" + create_global_action delete_b_action_id "$delete_b_action_name" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-delete-b-action" + create_global_registered_resource delete_a_rr_id "${TEST_PREFIX}-prune-rr-delete-a" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-delete-a-rr" + create_global_registered_resource delete_b_rr_id "${TEST_PREFIX}-prune-rr-delete-b" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-delete-b-rr" + create_registered_resource_value delete_a_value_id "$delete_a_rr_id" "${TEST_PREFIX}-delete-a-value" --action-attribute-value "$delete_a_action_id;$ATTR_A_VAL_1_ID" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-delete-a-value" + create_registered_resource_value delete_b_value_id "$delete_b_rr_id" "${TEST_PREFIX}-delete-b-value" --action-attribute-value "$delete_b_action_id;$ATTR_A_VAL_2_ID" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-delete-b-value" + + run_namespaced_policy_commit "registered-resources" + assert_success + + delete_a_rr_target_id=$(registered_resource_id_by_migrated_from "$NS_A_ID" "$delete_a_rr_id") + delete_b_rr_target_id=$(registered_resource_id_by_migrated_from "$NS_A_ID" "$delete_b_rr_id") + delete_a_value_target_id=$(registered_resource_value_id_by_migrated_from "$delete_a_rr_target_id" "$delete_a_value_id") + delete_b_value_target_id=$(registered_resource_value_id_by_migrated_from "$delete_b_rr_target_id" "$delete_b_value_id") + + create_global_action not_migrated_global_action_id "$not_migrated_global_action_name" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-not-migrated-action" + create_global_registered_resource not_migrated_global_rr_id "${TEST_PREFIX}-prune-rr-not-migrated" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-not-migrated-rr" + create_registered_resource_value not_migrated_global_value_id "$not_migrated_global_rr_id" "${TEST_PREFIX}-not-migrated-value" --action-attribute-value "$not_migrated_global_action_id;$ATTR_A_VAL_1_ID" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-not-migrated-value" + + create_global_action unlabeled_global_action_id "$unlabeled_global_action_name" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-unlabeled-target-action" + create_global_registered_resource unlabeled_global_rr_id "${TEST_PREFIX}-prune-rr-unlabeled-target" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-unlabeled-target-rr" + create_registered_resource_value unlabeled_global_value_id "$unlabeled_global_rr_id" "${TEST_PREFIX}-unlabeled-target-value" --action-attribute-value "$unlabeled_global_action_id;$ATTR_A_VAL_2_ID" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-unlabeled-target-value" + create_namespaced_action unlabeled_action_target_id "$NS_A_ID" "$unlabeled_global_action_name" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-unlabeled-target-action-target" --label "migrated_from=$unlabeled_global_action_id" + create_namespaced_registered_resource unlabeled_rr_target_id "$NS_A_ID" "${TEST_PREFIX}-prune-rr-unlabeled-target" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-unlabeled-target-rr-target" + create_registered_resource_value unlabeled_value_target_id "$unlabeled_rr_target_id" "${TEST_PREFIX}-unlabeled-target-value" --action-attribute-value "$unlabeled_action_target_id;$ATTR_A_VAL_2_ID" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-unlabeled-target-value-target" + + lookup_namespaced_action_id ns_a_read_action_id "read" "$NS_A_ID" + create_global_registered_resource multi_namespace_rr_id "${TEST_PREFIX}-prune-rr-multi-namespace" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-multi-namespace-rr" + create_registered_resource_value multi_namespace_value_a_id "$multi_namespace_rr_id" "${TEST_PREFIX}-multi-namespace-a" --action-attribute-value "$GLOBAL_READ_ID;$ATTR_A_VAL_1_ID" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-multi-namespace-value-a" + create_registered_resource_value multi_namespace_value_b_id "$multi_namespace_rr_id" "${TEST_PREFIX}-multi-namespace-b" --action-attribute-value "$GLOBAL_READ_ID;$ATTR_B_VAL_1_ID" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-multi-namespace-value-b" + create_namespaced_registered_resource multi_namespace_rr_target_id "$NS_A_ID" "${TEST_PREFIX}-prune-rr-multi-namespace" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-multi-namespace-rr-target" --label "migrated_from=$multi_namespace_rr_id" + create_registered_resource_value multi_namespace_value_target_id "$multi_namespace_rr_target_id" "${TEST_PREFIX}-multi-namespace-a" --action-attribute-value "$ns_a_read_action_id;$ATTR_A_VAL_1_ID" --label "test_case=prune-registered-resources" --label "fixture=${TEST_PREFIX}-multi-namespace-value-target" --label "migrated_from=$multi_namespace_value_a_id" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + + run_namespaced_policy_prune_commit "registered-resources" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_registered_resource_pruned "$delete_a_rr_id" "$delete_a_value_id" + assert_legacy_registered_resource_pruned "$delete_b_rr_id" "$delete_b_value_id" + untrack_registered_resource_id "$delete_a_rr_id" + untrack_registered_resource_id "$delete_b_rr_id" + untrack_registered_resource_value_id "$delete_a_value_id" + untrack_registered_resource_value_id "$delete_b_value_id" + assert_registered_resource_target_still_exists "$delete_a_rr_target_id" "$delete_a_value_target_id" "$NS_A_ID" "$delete_a_rr_id" "$delete_a_value_id" + assert_registered_resource_target_still_exists "$delete_b_rr_target_id" "$delete_b_value_target_id" "$NS_A_ID" "$delete_b_rr_id" "$delete_b_value_id" + assert_legacy_registered_resource_still_exists "$not_migrated_global_rr_id" "$not_migrated_global_value_id" "${TEST_PREFIX}-prune-rr-not-migrated" "${TEST_PREFIX}-not-migrated-value" + assert_legacy_registered_resource_still_exists "$unlabeled_global_rr_id" "$unlabeled_global_value_id" "${TEST_PREFIX}-prune-rr-unlabeled-target" "${TEST_PREFIX}-unlabeled-target-value" + assert_legacy_registered_resource_still_exists "$multi_namespace_rr_id" "$multi_namespace_value_a_id" "${TEST_PREFIX}-prune-rr-multi-namespace" "${TEST_PREFIX}-multi-namespace-a" + assert_legacy_registered_resource_still_exists "$multi_namespace_rr_id" "$multi_namespace_value_b_id" "${TEST_PREFIX}-prune-rr-multi-namespace" "${TEST_PREFIX}-multi-namespace-b" + assert_registered_resource_target_still_exists "$multi_namespace_rr_target_id" "$multi_namespace_value_target_id" "$NS_A_ID" "$multi_namespace_rr_id" "$multi_namespace_value_a_id" + assert_registered_resource_unlabeled_target_still_exists "$unlabeled_rr_target_id" "$unlabeled_value_target_id" "$NS_A_ID" "${TEST_PREFIX}-prune-rr-unlabeled-target" "${TEST_PREFIX}-unlabeled-target-value" "$unlabeled_action_target_id" "$ATTR_A_VAL_2_ID" + + ns_a_state_before="$ns_a_state_after" + + run_namespaced_policy_prune_commit "registered-resources" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_registered_resource_pruned "$delete_a_rr_id" "$delete_a_value_id" + assert_legacy_registered_resource_pruned "$delete_b_rr_id" "$delete_b_value_id" + assert_registered_resource_target_still_exists "$delete_a_rr_target_id" "$delete_a_value_target_id" "$NS_A_ID" "$delete_a_rr_id" "$delete_a_value_id" + assert_registered_resource_target_still_exists "$delete_b_rr_target_id" "$delete_b_value_target_id" "$NS_A_ID" "$delete_b_rr_id" "$delete_b_value_id" + assert_legacy_registered_resource_still_exists "$not_migrated_global_rr_id" "$not_migrated_global_value_id" "${TEST_PREFIX}-prune-rr-not-migrated" "${TEST_PREFIX}-not-migrated-value" + assert_legacy_registered_resource_still_exists "$unlabeled_global_rr_id" "$unlabeled_global_value_id" "${TEST_PREFIX}-prune-rr-unlabeled-target" "${TEST_PREFIX}-unlabeled-target-value" + assert_legacy_registered_resource_still_exists "$multi_namespace_rr_id" "$multi_namespace_value_a_id" "${TEST_PREFIX}-prune-rr-multi-namespace" "${TEST_PREFIX}-multi-namespace-a" + assert_legacy_registered_resource_still_exists "$multi_namespace_rr_id" "$multi_namespace_value_b_id" "${TEST_PREFIX}-prune-rr-multi-namespace" "${TEST_PREFIX}-multi-namespace-b" + assert_registered_resource_unlabeled_target_still_exists "$unlabeled_rr_target_id" "$unlabeled_value_target_id" "$NS_A_ID" "${TEST_PREFIX}-prune-rr-unlabeled-target" "${TEST_PREFIX}-unlabeled-target-value" "$unlabeled_action_target_id" "$ATTR_A_VAL_2_ID" + assert_registered_resource_target_still_exists "$multi_namespace_rr_target_id" "$multi_namespace_value_target_id" "$NS_A_ID" "$multi_namespace_rr_id" "$multi_namespace_value_a_id" +} + +# Covers obligation-trigger prune paths: +# - delete migrated legacy triggers +# - retain triggers that were not migrated +# - retain triggers whose matching target is not labeled as migrated_from the source +# - check idempotency +@test "prune namespaced-policy obligation-triggers handles delete, not-migrated, and unlabeled-target states together" { + local delete_a_action_name="${TEST_PREFIX}-prune-trigger-delete-a" + local delete_b_action_name="${TEST_PREFIX}-prune-trigger-delete-b" + local not_migrated_global_action_name="${TEST_PREFIX}-prune-trigger-not-migrated" + local unlabeled_global_action_name="${TEST_PREFIX}-prune-trigger-unlabeled-target" + local delete_a_action_id + local delete_b_action_id + local not_migrated_global_action_id + local unlabeled_global_action_id + local unlabeled_action_target_id + local delete_a_obligation_id + local delete_b_obligation_id + local not_migrated_source_obligation_id + local unlabeled_source_obligation_id + local delete_a_value_id + local delete_b_value_id + local not_migrated_source_value_id + local unlabeled_source_value_id + local delete_a_trigger_id + local delete_b_trigger_id + local not_migrated_source_trigger_id + local unlabeled_source_trigger_id + local delete_a_trigger_target_id + local delete_b_trigger_target_id + local unlabeled_trigger_target_id + local ns_a_state_before + local ns_a_state_after + + create_global_action delete_a_action_id "$delete_a_action_name" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-delete-a-action" + create_global_action delete_b_action_id "$delete_b_action_name" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-delete-b-action" + create_namespaced_obligation delete_a_obligation_id "$NS_A_ID" "${TEST_PREFIX}-prune-trigger-delete-a" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-delete-a-obligation" + create_namespaced_obligation delete_b_obligation_id "$NS_A_ID" "${TEST_PREFIX}-prune-trigger-delete-b" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-delete-b-obligation" + create_obligation_value delete_a_value_id "$delete_a_obligation_id" "${TEST_PREFIX}-delete-a-value" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-delete-a-value" + create_obligation_value delete_b_value_id "$delete_b_obligation_id" "${TEST_PREFIX}-delete-b-value" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-delete-b-value" + create_legacy_obligation_trigger delete_a_trigger_id "$ATTR_A_VAL_1_ID" "$delete_a_action_id" "$delete_a_value_id" --client-id "${TEST_PREFIX}-delete-a-client" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-delete-a-trigger" + create_legacy_obligation_trigger delete_b_trigger_id "$ATTR_A_VAL_2_ID" "$delete_b_action_id" "$delete_b_value_id" --client-id "${TEST_PREFIX}-delete-b-client" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-delete-b-trigger" + + run_namespaced_policy_commit "obligation-triggers" + assert_success + + delete_a_trigger_target_id=$(obligation_trigger_id_by_migrated_from "$NS_A_ID" "$delete_a_trigger_id") + delete_b_trigger_target_id=$(obligation_trigger_id_by_migrated_from "$NS_A_ID" "$delete_b_trigger_id") + + create_global_action not_migrated_global_action_id "$not_migrated_global_action_name" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-not-migrated-action" + create_namespaced_obligation not_migrated_source_obligation_id "$NS_A_ID" "${TEST_PREFIX}-prune-trigger-not-migrated" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-not-migrated-obligation" + create_obligation_value not_migrated_source_value_id "$not_migrated_source_obligation_id" "${TEST_PREFIX}-not-migrated-value" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-not-migrated-value" + create_legacy_obligation_trigger not_migrated_source_trigger_id "$ATTR_A_VAL_1_ID" "$not_migrated_global_action_id" "$not_migrated_source_value_id" --client-id "${TEST_PREFIX}-not-migrated-client" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-not-migrated-trigger" + + create_global_action unlabeled_global_action_id "$unlabeled_global_action_name" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-unlabeled-target-action" + create_namespaced_obligation unlabeled_source_obligation_id "$NS_A_ID" "${TEST_PREFIX}-prune-trigger-unlabeled-target" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-unlabeled-target-obligation" + create_obligation_value unlabeled_source_value_id "$unlabeled_source_obligation_id" "${TEST_PREFIX}-unlabeled-target-value" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-unlabeled-target-value" + create_legacy_obligation_trigger unlabeled_source_trigger_id "$ATTR_A_VAL_2_ID" "$unlabeled_global_action_id" "$unlabeled_source_value_id" --client-id "${TEST_PREFIX}-unlabeled-target-client" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-unlabeled-target-trigger" + create_namespaced_action unlabeled_action_target_id "$NS_A_ID" "$unlabeled_global_action_name" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-unlabeled-target-action-target" --label "migrated_from=$unlabeled_global_action_id" + run_otdfctl_obligation_triggers create --attribute-value "$ATTR_A_VAL_2_ID" --action "$unlabeled_action_target_id" --obligation-value "$unlabeled_source_value_id" --client-id "${TEST_PREFIX}-unlabeled-target-client" --label "test_case=prune-obligation-triggers" --label "fixture=${TEST_PREFIX}-unlabeled-target-trigger-target" --json + unlabeled_trigger_target_id=$(echo "$output" | jq -r '.id // empty') + assert_not_equal "$unlabeled_trigger_target_id" "" + track_obligation_trigger_id "$unlabeled_trigger_target_id" + + ns_a_state_before=$(namespace_state_json "$NS_A_ID") + + run_namespaced_policy_prune_commit "obligation-triggers" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 -2 # Obligation triggers are already namespaced, they just need to be deleted/recreated; that's why there should be 2 less after prune. + + assert_legacy_obligation_trigger_pruned "$delete_a_trigger_id" "$NS_A_ID" + assert_legacy_obligation_trigger_pruned "$delete_b_trigger_id" "$NS_A_ID" + untrack_obligation_trigger_id "$delete_a_trigger_id" + untrack_obligation_trigger_id "$delete_b_trigger_id" + assert_obligation_trigger_target_still_exists "$delete_a_trigger_target_id" "$NS_A_ID" "$delete_a_trigger_id" + assert_obligation_trigger_target_still_exists "$delete_b_trigger_target_id" "$NS_A_ID" "$delete_b_trigger_id" + assert_legacy_obligation_trigger_still_exists "$not_migrated_source_trigger_id" "$NS_A_ID" "$ATTR_A_VAL_1_ID" "$not_migrated_global_action_id" "$not_migrated_source_value_id" "${TEST_PREFIX}-not-migrated-client" + assert_legacy_obligation_trigger_still_exists "$unlabeled_source_trigger_id" "$NS_A_ID" "$ATTR_A_VAL_2_ID" "$unlabeled_global_action_id" "$unlabeled_source_value_id" "${TEST_PREFIX}-unlabeled-target-client" + assert_obligation_trigger_unlabeled_target_still_exists "$unlabeled_trigger_target_id" "$NS_A_ID" "$ATTR_A_VAL_2_ID" "$unlabeled_action_target_id" "$unlabeled_source_value_id" "${TEST_PREFIX}-unlabeled-target-client" + + ns_a_state_before="$ns_a_state_after" + + run_namespaced_policy_prune_commit "obligation-triggers" + assert_success + + ns_a_state_after=$(namespace_state_json "$NS_A_ID") + assert_namespace_state_delta "$ns_a_state_before" "$ns_a_state_after" 0 0 0 0 0 + + assert_legacy_obligation_trigger_pruned "$delete_a_trigger_id" "$NS_A_ID" + assert_legacy_obligation_trigger_pruned "$delete_b_trigger_id" "$NS_A_ID" + assert_obligation_trigger_target_still_exists "$delete_a_trigger_target_id" "$NS_A_ID" "$delete_a_trigger_id" + assert_obligation_trigger_target_still_exists "$delete_b_trigger_target_id" "$NS_A_ID" "$delete_b_trigger_id" + assert_legacy_obligation_trigger_still_exists "$not_migrated_source_trigger_id" "$NS_A_ID" "$ATTR_A_VAL_1_ID" "$not_migrated_global_action_id" "$not_migrated_source_value_id" "${TEST_PREFIX}-not-migrated-client" + assert_legacy_obligation_trigger_still_exists "$unlabeled_source_trigger_id" "$NS_A_ID" "$ATTR_A_VAL_2_ID" "$unlabeled_global_action_id" "$unlabeled_source_value_id" "${TEST_PREFIX}-unlabeled-target-client" + assert_obligation_trigger_unlabeled_target_still_exists "$unlabeled_trigger_target_id" "$NS_A_ID" "$ATTR_A_VAL_2_ID" "$unlabeled_action_target_id" "$unlabeled_source_value_id" "${TEST_PREFIX}-unlabeled-target-client" +} diff --git a/otdfctl/e2e/namespaces.bats b/otdfctl/e2e/namespaces.bats new file mode 100755 index 0000000000..2e0c30e63c --- /dev/null +++ b/otdfctl/e2e/namespaces.bats @@ -0,0 +1,357 @@ +#!/usr/bin/env bats + +# Tests for namespaces + +setup_file() { + bats_load_library bats-support + bats_load_library bats-assert + load "otdfctl-utils.sh" + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # Create the namespace to be used by other tests + + export NS_NAME="creating-test-ns.net" + export NS_NAME_UPDATE="updated-test-ns.net" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$NS_NAME" --json | jq -r '.id') + export NS_ID_FLAG="--id $NS_ID" + + export KAS_URI="https://test-kas-for-namespace.com" + export KAS_REG_ID=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --uri "$KAS_URI" --json | jq -r '.id') + # Generate a valid RSA public key and base64 encode (single-line) + export PEM_B64=$(openssl genrsa 2048 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n') + export KAS_KEY_ID="test-key-for-namespace" + export KAS_KEY_SYSTEM_ID=$(./otdfctl $HOST $WITH_CREDS policy kas-registry key create --kas "$KAS_REG_ID" --key-id "$KAS_KEY_ID" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json | jq -r '.key.id') + export PEM=$(echo "$PEM_B64" | base64 -d) +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + # invoke binary with credentials under 'policy attributes namespaces' + run_otdfctl_ns() { + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes namespaces $*" + } + # invoke binary with credentials under 'policy namespaces' (direct path) + run_otdfctl_nsd() { + run sh -c "./otdfctl $HOST $WITH_CREDS policy namespaces $*" + } +} + +teardown_file() { + ./otdfctl $HOST $WITH_CREDS policy attributes namespace unsafe delete --id "$NS_ID" --force + + delete_all_keys_in_kas "$KAS_REG_ID" + delete_kas_registry "$KAS_REG_ID" + + # clear out all test env vars + unset HOST WITH_CREDS NS_NAME NS_FQN NS_ID NS_ID_FLAG KAS_REG_ID KAS_KEY_ID KAS_URI PEM_B64 PEM KAS_KEY_SYSTEM_ID +} + +@test "Create a namespace - Good" { + run_otdfctl_ns create --name throwaway.test + assert_output --partial "SUCCESS" + assert_line --regexp "Name.*throwaway.test" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + + # cleanup + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_ns unsafe delete --id $created_id --force +} + +@test "Create a namespace - Bad" { + # bad namespace names + run_otdfctl_ns create --name no_domain_extension + assert_failure + run_otdfctl_ns create --name -first-char-hyphen.co + assert_failure + run_otdfctl_ns create --name last-char-hyphen-.co + assert_failure + + # missing flag + run_otdfctl_ns create + assert_failure + assert_output --partial "Flag '--name' is required" + + # conflict + run_otdfctl_ns create -n "$NS_NAME" + assert_failure + assert_output --partial "already_exists" +} + +@test "Get a namespace - Good" { + run_otdfctl_ns get "$NS_ID_FLAG" + assert_success + assert_line --regexp "Id.*$NS_ID" + assert_line --regexp "Name.*$NS_NAME" + + run_otdfctl_ns get "$NS_ID_FLAG" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$NS_ID" ] + [ "$(echo "$output" | jq -r '.name')" = "$NS_NAME" ] +} + +@test "Get a namespace - Bad" { + run_otdfctl_ns get + assert_failure + assert_output --partial "Flag '--id' is required" + + run_otdfctl_ns get --id 'example.com' + assert_failure + assert_output --partial "Flag '--id' received value 'example.com' must be a valid UUID" + + run_otdfctl_ns get --id 'demo.com' --json + assert_failure + assert_output --partial "Flag '--id' received value 'demo.com' must be a valid UUID" +} + +@test "List namespaces - when active" { + run_otdfctl_ns list --json + echo $output | jq --arg id "$NS_ID" '.namespaces[] | select(.id == $id)' + assert_not_equal $(echo $output | jq '.pagination') "null" + + run_otdfctl_ns list --state inactive --json + refute_output --partial "$NS_ID" + assert_not_equal $(echo $output | jq '.pagination') "null" + + run_otdfctl_ns list --state active + assert_output --partial "$NS_ID" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" +} + +@test "List namespaces supports sort and order flags" { + sort_prefix="sort-ns-$BATS_TEST_NUMBER-$RANDOM" + ns_a_name="$sort_prefix-alpha.test" + ns_b_name="$sort_prefix-bravo.test" + ns_c_name="$sort_prefix-charlie.test" + ns_a_id=$(./otdfctl $HOST $WITH_CREDS policy namespaces create --name "$ns_a_name" --json | jq -r '.id') + ns_b_id=$(./otdfctl $HOST $WITH_CREDS policy namespaces create --name "$ns_b_name" --json | jq -r '.id') + ns_c_id=$(./otdfctl $HOST $WITH_CREDS policy namespaces create --name "$ns_c_name" --json | jq -r '.id') + + run_otdfctl_nsd list --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.namespaces[] | select(.name | startswith($prefix)) | .name] | join(",")')" "$ns_a_name,$ns_b_name,$ns_c_name" + + run_otdfctl_nsd list --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.namespaces[] | select(.name | startswith($prefix)) | .name] | join(",")')" "$ns_c_name,$ns_b_name,$ns_a_name" + + run_otdfctl_nsd list --sort fqn --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "https://$sort_prefix" '[.namespaces[] | select(.fqn | startswith($prefix)) | .id] | join(",")')" "$ns_a_id,$ns_b_id,$ns_c_id" + + run_otdfctl_nsd list --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$ns_a_id" --arg b "$ns_b_id" --arg c "$ns_c_id" '[.namespaces[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$ns_a_id,$ns_b_id,$ns_c_id" + + run_otdfctl_nsd update --id "$ns_a_id" --label sort=a --json + assert_success + run_otdfctl_nsd update --id "$ns_b_id" --label sort=b --json + assert_success + run_otdfctl_nsd update --id "$ns_c_id" --label sort=c --json + assert_success + + run_otdfctl_nsd list --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$ns_a_id" --arg b "$ns_b_id" --arg c "$ns_c_id" '[.namespaces[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$ns_a_id,$ns_b_id,$ns_c_id" + + run_otdfctl_nsd list --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.namespaces[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$ns_c_id,$ns_b_id,$ns_a_id" + + run_otdfctl_nsd list --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$ns_a_id" --arg b "$ns_b_id" --arg c "$ns_c_id" '[.namespaces[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$ns_a_id,$ns_b_id,$ns_c_id" + + run_otdfctl_nsd unsafe delete --id "$ns_a_id" --force + run_otdfctl_nsd unsafe delete --id "$ns_b_id" --force + run_otdfctl_nsd unsafe delete --id "$ns_c_id" --force +} + +@test "Update namespace - Safe" { + # extend labels + run_otdfctl_ns update "$NS_ID_FLAG" -l key=value --label test=true + assert_success + assert_line --regexp "Id.*$NS_ID" + assert_line --regexp "Name.*$NS_NAME" + assert_line --regexp "Labels.*key: value" + assert_line --regexp "Labels.*test: true" + + # force replace labels + run_otdfctl_ns update "$NS_ID_FLAG" -l key=other --force-replace-labels + assert_success + assert_line --regexp "Id.*$NS_ID" + assert_line --regexp "Name.*$NS_NAME" + assert_line --regexp "Labels.*key: other" + refute_output --regexp "Labels.*key: value" + refute_output --regexp "Labels.*test: true" +} + +@test "Update namespace - Unsafe" { + run_otdfctl_ns unsafe update "$NS_ID_FLAG" -n "$NS_NAME_UPDATE" --force + assert_success + assert_line --regexp "Id.*$NS_ID" + run_otdfctl_ns get "$NS_ID_FLAG" + assert_line --regexp "Name.*$NS_NAME_UPDATE" + refute_output --regexp "Name.*$NS_NAME" +} + +@test "Assign/Remove KAS key from namespace - With Namespace ID" { + run_otdfctl_ns key assign --namespace "$NS_ID" --key-id "$KAS_KEY_SYSTEM_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.namespace_id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.key_id')" "$KAS_KEY_SYSTEM_ID" + + run_otdfctl_ns get --id "$NS_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].kas_uri')" "$KAS_URI" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].kas_id')" "$KAS_REG_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].public_key.kid')" "$KAS_KEY_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].public_key.pem')" "$PEM" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].public_key.algorithm')" 1 + + run_otdfctl_ns key remove --namespace "$NS_ID" --key-id "$KAS_KEY_SYSTEM_ID" --json + assert_success + + run_otdfctl_ns get --id "$NS_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys | length')" 0 +} + +@test "Assign/Remove KAS key from namespace - With Namespace FQN" { + run_otdfctl_ns get --id "$NS_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys | length')" 0 + NS_FQN=$(echo "$output" | jq -r '.fqn') + + run_otdfctl_ns key assign --namespace "$NS_FQN" --key-id "$KAS_KEY_SYSTEM_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.namespace_id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.key_id')" "$KAS_KEY_SYSTEM_ID" + + run_otdfctl_ns get --id "$NS_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].kas_uri')" "$KAS_URI" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].kas_id')" "$KAS_REG_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].public_key.kid')" "$KAS_KEY_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].public_key.pem')" "$PEM" + assert_equal "$(echo "$output" | jq -r '.kas_keys[0].public_key.algorithm')" 1 + + run_otdfctl_ns key remove --namespace "$NS_ID" --key-id "$KAS_KEY_SYSTEM_ID" --json + assert_success + + run_otdfctl_ns get --id "$NS_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.kas_keys | length')" 0 +} + +@test "KAS key assignment error handling - namespace" { + # Test with non-existent namespace ID + run_otdfctl_ns key assign --namespace "00000000-0000-0000-0000-000000000000" --key-id "$KAS_KEY_SYSTEM_ID" + assert_failure + assert_output --partial "ERROR" + + # Test with missing required flags + run_otdfctl_ns key assign --namespace "$NS_ID" + assert_failure + assert_output --partial "Flag '--key-id' is required" + + run_otdfctl_ns key assign --key-id "$KAS_KEY_SYSTEM_ID" + assert_failure + assert_output --partial "Flag '--namespace' is required" +} + +@test "Deactivate namespace" { + run_otdfctl_ns deactivate "$NS_ID_FLAG" --force + assert_success + assert_line --regexp "Id.*$NS_ID" + assert_line --regexp "Name.*$NS_NAME_UPDATE" +} + +@test "List namespaces - when inactive" { + run_otdfctl_ns list --json + echo $output | jq --arg id "$NS_ID" '.namespaces[] | select(.id == $id)' + assert_not_equal $(echo $output | jq '.pagination') "null" + + # json + run_otdfctl_ns list --state inactive --json + echo $output | assert_output --partial "$NS_ID" + assert_not_equal $(echo $output | jq '.pagination') "null" + + run_otdfctl_ns list --state active --json + echo $output | refute_output --partial "$NS_ID" + assert_not_equal $(echo $output | jq '.pagination') "null" + # table + run_otdfctl_ns list --state inactive + echo $output | assert_output --partial "$NS_ID" + + run_otdfctl_ns list --state active + echo $output | refute_output --partial "$NS_ID" +} + +@test "Unsafe reactivate namespace" { + run_otdfctl_ns unsafe reactivate "$NS_ID_FLAG" --force + assert_success + assert_line --regexp "Id.*$NS_ID" +} + +@test "List namespaces - when reactivated" { + run_otdfctl_ns list --json + echo $output | jq --arg id "$NS_ID" '.namespaces[] | select(.id == $id)' + assert_not_equal $(echo $output | jq '.pagination') "null" + + run_otdfctl_ns list --state inactive --json + echo $output | refute_output --partial "$NS_ID" + assert_not_equal $(echo $output | jq '.pagination') "null" + + run_otdfctl_ns list --state active + echo $output | assert_output --partial "$NS_ID" +} + +@test "Unsafe delete namespace" { + run_otdfctl_ns unsafe delete "$NS_ID_FLAG" --force + assert_success + assert_line --regexp "Id.*$NS_ID" + assert_line --regexp "Name.*$NS_NAME_UPDATE" +} + +@test "List namespaces - when deleted" { + run_otdfctl_ns list --json + echo $output | refute_output --partial "$NS_ID" + assert_not_equal $(echo $output | jq '.pagination') "null" + + run_otdfctl_ns list --state inactive --json + echo $output | refute_output --partial "$NS_ID" + assert_not_equal $(echo $output | jq '.pagination') "null" + + run_otdfctl_ns list --state active + echo $output | refute_output --partial "$NS_ID" +} + +# ── policy namespaces (direct path) ────────────────────────────────────────── + +@test "Direct path: policy namespaces commands are accessible" { + run_otdfctl_nsd create --name direct-path-test.net --json + assert_success + DIRECT_NS_ID=$(echo "$output" | jq -r '.id') + + run_otdfctl_nsd get --id "$DIRECT_NS_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.name')" "direct-path-test.net" + + run_otdfctl_nsd list --json + assert_success + assert_output --partial "$DIRECT_NS_ID" + + # cleanup + run_otdfctl_nsd unsafe delete --id "$DIRECT_NS_ID" --force + assert_success +} diff --git a/otdfctl/e2e/obligations.bats b/otdfctl/e2e/obligations.bats new file mode 100644 index 0000000000..2fe9fffe82 --- /dev/null +++ b/otdfctl/e2e/obligations.bats @@ -0,0 +1,1082 @@ +#!/usr/bin/env bats + +# Tests for obligations + +setup_file() { + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # create attribute value to be used in obligation values tests + export NS_NAME="test-obl.org" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create --name "$NS_NAME" --json | jq -r '.id') + + # create obligation used in obligation values tests + export OBL_NAME="test_obl_for_values" + export OBL_ID=$(./otdfctl $HOST $WITH_CREDS policy obligations create --name "$OBL_NAME" --namespace "$NS_ID" --json | jq -r '.id') + + # shared triggers file for tests + export SHARED_TRIGGERS_FILE="/tmp/shared_test_triggers.json" + + # create shared actions for tests + export ACTION_1_NAME="test_action_1" + export ACTION_1_ID=$(./otdfctl $HOST $WITH_CREDS policy actions create --name "$ACTION_1_NAME" --namespace "$NS_ID" --json | jq -r '.id') + export ACTION_2_NAME="test_action_2" + export ACTION_2_ID=$(./otdfctl $HOST $WITH_CREDS policy actions create --name "$ACTION_2_NAME" --namespace "$NS_ID" --json | jq -r '.id') + + # create shared attributes for tests + export ATTR_NAME="test_attr_for_triggers" + export ATTR_VAL_NAME="test_val_for_triggers" + attr_result=$(./otdfctl $HOST $WITH_CREDS policy attributes create --name "$ATTR_NAME" --namespace "$NS_ID" --rule "HIERARCHY" -v "$ATTR_VAL_NAME" --json) + export ATTR_ID=$(echo "$attr_result" | jq -r '.id') + export ATTR_VAL_ID=$(echo "$attr_result" | jq -r '.values[0].id') + export ATTR_VAL_FQN=$(echo "$attr_result" | jq -r '.values[0].fqn') + + export ATTR_2_NAME="test_attr_for_triggers_2" + export ATTR_2_VAL_NAME="test_val_for_triggers_2" + attr_2_result=$(./otdfctl $HOST $WITH_CREDS policy attributes create --name "$ATTR_2_NAME" --namespace "$NS_ID" --rule "HIERARCHY" -v "$ATTR_2_VAL_NAME" --json) + export ATTR_2_ID=$(echo "$attr_2_result" | jq -r '.id') + export ATTR_2_VAL_ID=$(echo "$attr_2_result" | jq -r '.values[0].id') + export ATTR_2_VAL_FQN=$(echo "$attr_2_result" | jq -r '.values[0].fqn') + + # Create namespaces and attributes for list triggers tests + export LIST_NS_1_NAME="list-test-ns1.org" + export LIST_NS_1_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create --name "$LIST_NS_1_NAME" --json | jq -r '.id') + export LIST_NS_1_FQN="https://$LIST_NS_1_NAME" + + export LIST_NS_2_NAME="list-test-ns2.org" + export LIST_NS_2_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create --name "$LIST_NS_2_NAME" --json | jq -r '.id') + export LIST_NS_2_FQN="https://$LIST_NS_2_NAME" + + # Create actions in each list namespace for trigger creation + export LIST_ACTION_1_NAME="list_test_action_1" + export LIST_ACTION_1_ID=$(./otdfctl $HOST $WITH_CREDS policy actions create --name "$LIST_ACTION_1_NAME" --namespace "$LIST_NS_1_ID" --json | jq -r '.id') + + export LIST_ACTION_2_NAME="list_test_action_2" + export LIST_ACTION_2_ID=$(./otdfctl $HOST $WITH_CREDS policy actions create --name "$LIST_ACTION_2_NAME" --namespace "$LIST_NS_2_ID" --json | jq -r '.id') + + # Create attributes for list triggers tests + # Namespace 1 attributes + list_attr_1_result=$(./otdfctl $HOST $WITH_CREDS policy attributes create --name "list_test_attr" --namespace "$LIST_NS_1_ID" --rule "HIERARCHY" -v "val1" --json) + export LIST_ATTR_1_ID=$(echo "$list_attr_1_result" | jq -r '.id') + export LIST_ATTR_1_VAL_1_ID=$(echo "$list_attr_1_result" | jq -r '.values[0].id') + export LIST_ATTR_1_VAL_1_FQN=$(echo "$list_attr_1_result" | jq -r '.values[0].fqn') + + # Namespace 2 attributes + list_attr_2_result=$(./otdfctl $HOST $WITH_CREDS policy attributes create --name "list_test_attr" --namespace "$LIST_NS_2_ID" --rule "HIERARCHY" -v "val1" --json) + export LIST_ATTR_2_ID=$(echo "$list_attr_2_result" | jq -r '.id') + export LIST_ATTR_2_VAL_1_ID=$(echo "$list_attr_2_result" | jq -r '.values[0].id') + export LIST_ATTR_2_VAL_1_FQN=$(echo "$list_attr_2_result" | jq -r '.values[0].fqn') + + # Set global vars for list triggers tests that will get populated in setup_triggers_test_data + export CLIENT_ID_LIST="test-client-list" +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_obl () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations $*" + } + run_otdfctl_obl_values () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations values $*" + } + run_otdfctl_obl_triggers () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations triggers $*" + } + + run_otdfctl_action () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy actions $*" + } + + run_otdfctl_attr() { + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes $*" + } + + # Cleanup helper functions + cleanup_obligation_value() { + local value_id="$1" + if [ -n "$value_id" ] && [ "$value_id" != "null" ]; then + run_otdfctl_obl_values delete --id "$value_id" --force + fi + } + + cleanup_action() { + local action_id="$1" + if [ -n "$action_id" ] && [ "$action_id" != "null" ]; then + run_otdfctl_action delete --id "$action_id" --force + fi + } + + cleanup_attribute() { + local attr_id="$1" + if [ -n "$attr_id" ] && [ "$attr_id" != "null" ]; then + run_otdfctl_attr unsafe delete --id "$attr_id" --force + fi + } + + cleanup_trigger() { + local trigger_id="$1" + if [ -n "$trigger_id" ] && [ "$trigger_id" != "null" ]; then + run_otdfctl_obl_triggers delete --id "$trigger_id" --force + fi + } + + cleanup_temp_file() { + local file_path="$1" + if [ -n "$file_path" ] && [ -f "$file_path" ]; then + rm -f "$file_path" + fi + } + + # Validate triggers in JSON response + validate_triggers() { + local json_output="$1" + local expected_count="$2" + shift 2 + local expected_triggers=("$@") # Array of expected trigger specs: "attr_val_id;attr_val_fqn;action_id;action_name;client_id" + + # Validate trigger count + local actual_count=$(echo "$json_output" | jq -r '.triggers | length') + assert_equal "$actual_count" "$expected_count" + + # Validate each expected trigger exists in the response + for expected_trigger in "${expected_triggers[@]}"; do + IFS=';' read -ra TRIGGER_SPEC <<< "$expected_trigger" + local exp_attr_val_id="${TRIGGER_SPEC[0]}" + local exp_attr_val_fqn="${TRIGGER_SPEC[1]}" + local exp_action_id="${TRIGGER_SPEC[2]}" + local exp_action_name="${TRIGGER_SPEC[3]}" + local exp_client_id="${TRIGGER_SPEC[4]}" + local exp_obl_val_id="${TRIGGER_SPEC[5]}" + local exp_obl_val_fqn="${TRIGGER_SPEC[6]}" + + # Find if this expected trigger exists in the response + local found=false + for ((i=0; i 0 then .triggers[$i].context[0].pep.client_id // \"\" else \"\" end") + if [ "$actual_client_id" != "$exp_client_id" ]; then + match=false + fi + fi + + # Check obligation value ID if specified + if [ "$match" = true ] && [ -n "$exp_obl_val_id" ] && [ "$exp_obl_val_id" != "null" ]; then + local actual_obl_val_id=$(echo "$json_output" | jq -r ".triggers[$i].obligation_value.id") + if [ "$actual_obl_val_id" != "$exp_obl_val_id" ]; then + match=false + fi + fi + + # Check obligation value FQN if specified + if [ "$match" = true ] && [ -n "$exp_obl_val_fqn" ] && [ "$exp_obl_val_fqn" != "null" ]; then + local actual_obl_val_fqn=$(echo "$json_output" | jq -r ".triggers[$i].obligation_value.fqn") + if [ "$actual_obl_val_fqn" != "$exp_obl_val_fqn" ]; then + match=false + fi + fi + + if [ "$match" = true ]; then + found=true + break + fi + done + + # Assert that we found this expected trigger + if [ "$found" = false ]; then + echo "Expected trigger not found: attr_val_id=$exp_attr_val_id, attr_val_fqn=$exp_attr_val_fqn, action_id=$exp_action_id, action_name=$exp_action_name, client_id=$exp_client_id, obl_val_id=$exp_obl_val_id, obl_val_fqn=$exp_obl_val_fqn" + return 1 + fi + done + } + + setup_triggers_test_data() { + export LIST_OBL_1_NAME="list_test_obl" + export LIST_OBL_1_VAL="list_test_val" + export LIST_OBL_1_FQN="https://$LIST_NS_1_NAME/obl/$LIST_OBL_1_NAME" + export LIST_OBL_VAL_1_FQN="$LIST_OBL_1_FQN/value/$LIST_OBL_1_VAL" + export LIST_OBL_1_ID="" + export LIST_OBL_2_NAME="list_test_obl" + export LIST_OBL_2_VAL="list_test_val" + export LIST_OBL_2_FQN="https://$LIST_NS_2_NAME/obl/$LIST_OBL_2_NAME" + export LIST_OBL_VAL_2_FQN="$LIST_OBL_2_FQN/value/$LIST_OBL_2_VAL" + export LIST_OBL_2_ID="" + + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations get --fqn $LIST_OBL_1_FQN --json" + if [ $status -ne 0 ]; then + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations create --name "$LIST_OBL_1_NAME" --namespace "$LIST_NS_1_ID" --json" + assert_success + export LIST_OBL_1_ID=$(echo "$output" | jq -r '.id') + + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$LIST_OBL_1_ID" --value "$LIST_OBL_1_VAL" --json" + assert_success + export LIST_OBL_VAL_1_ID=$(echo "$output" | jq -r '.id') + + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations triggers create --attribute-value "$LIST_ATTR_1_VAL_1_ID" --action "$LIST_ACTION_1_ID" --obligation-value "$LIST_OBL_VAL_1_ID" --client-id "$CLIENT_ID_LIST" --json" + assert_success + export LIST_TRIGGER_1_ID=$(echo "$output" | jq -r '.id') + else + export LIST_OBL_1_ID=$(echo "$output" | jq -r '.id') + export LIST_OBL_VAL_1_ID=$(echo "$output" | jq -r '.values[0].id') + export LIST_TRIGGER_1_ID=$(echo "$output" | jq -r '.values[0].triggers[0].id') + fi + + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations get --fqn $LIST_OBL_2_FQN --json" + if [ $status -ne 0 ]; then + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations create --name "$LIST_OBL_2_NAME" --namespace "$LIST_NS_2_ID" --json" + assert_success + export LIST_OBL_2_ID=$(echo "$output" | jq -r '.id') + + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$LIST_OBL_2_ID" --value "$LIST_OBL_2_VAL" --json" + assert_success + export LIST_OBL_VAL_2_ID=$(echo "$output" | jq -r '.id') + + run sh -c "./otdfctl $HOST $WITH_CREDS policy obligations triggers create --attribute-value "$LIST_ATTR_2_VAL_1_ID" --action "$LIST_ACTION_2_ID" --obligation-value "$LIST_OBL_VAL_2_ID" --client-id "$CLIENT_ID_LIST" --json" + assert_success + export LIST_TRIGGER_2_ID=$(echo "$output" | jq -r '.id') + else + export LIST_OBL_2_ID=$(echo "$output" | jq -r '.id') + export LIST_OBL_VAL_2_ID=$(echo "$output" | jq -r '.values[0].id') + export LIST_TRIGGER_2_ID=$(echo "$output" | jq -r '.values[0].triggers[0].id') + fi + } + + validate_pagination() { + local json_output="$1" + local expected_offset="$2" + local expected_total="$3" + local expected_next_offset="$4" + assert_equal "$(echo "$json_output" | jq -r '.pagination.current_offset')" "$expected_offset" + assert_equal "$(echo "$json_output" | jq -r '.pagination.total')" "$expected_total" + assert_equal "$(echo "$json_output" | jq -r '.pagination.next_offset')" "$expected_next_offset" + } +} + +teardown_file() { + # remove the obligation used in obligation values tests + ./otdfctl $HOST $WITH_CREDS policy obligations delete --id "$OBL_ID" --force + + # remove shared actions + ./otdfctl $HOST $WITH_CREDS policy actions delete --id "$ACTION_1_ID" --force + ./otdfctl $HOST $WITH_CREDS policy actions delete --id "$ACTION_2_ID" --force + + # remove shared attributes + ./otdfctl $HOST $WITH_CREDS policy attributes unsafe delete --id "$ATTR_ID" --force + ./otdfctl $HOST $WITH_CREDS policy attributes unsafe delete --id "$ATTR_2_ID" --force + + # remove the namespace used in obligation values tests + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$NS_ID" --force + + # remove list triggers test namespaces + ./otdfctl $HOST $WITH_CREDS policy actions delete --id "$LIST_ACTION_1_ID" --force + ./otdfctl $HOST $WITH_CREDS policy actions delete --id "$LIST_ACTION_2_ID" --force + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$LIST_NS_1_ID" --force + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$LIST_NS_2_ID" --force + + # cleanup shared triggers file + rm -f "$SHARED_TRIGGERS_FILE" + + # clear out all test env vars + unset HOST WITH_CREDS OBL_NAME OBL_ID NS_NAME NS_ID ACTION_1_NAME ACTION_1_ID ACTION_2_NAME ACTION_2_ID ATTR_NAME ATTR_VAL_NAME ATTR_ID ATTR_VAL_ID ATTR_VAL_FQN ATTR_2_NAME ATTR_2_VAL_NAME ATTR_2_ID ATTR_2_VAL_ID ATTR_2_VAL_FQN + unset CLIENT_ID_LIST LIST_NS_1_NAME LIST_NS_1_ID LIST_NS_1_FQN LIST_NS_2_NAME LIST_NS_2_ID LIST_NS_2_FQN + unset LIST_ACTION_1_NAME LIST_ACTION_1_ID LIST_ACTION_2_NAME LIST_ACTION_2_ID + unset LIST_ATTR_1_ID LIST_ATTR_1_VAL_1_ID LIST_ATTR_1_VAL_1_FQN LIST_ATTR_2_ID LIST_ATTR_2_VAL_1_ID LIST_ATTR_2_VAL_1_FQN +} + + +@test "Create a obligation - Good" { + run_otdfctl_obl create --name test_create_obl --namespace "$NS_ID" --json + assert_success + [ "$(echo "$output" | jq -r '.name')" = "test_create_obl" ] + [ -n "$(echo "$output" | jq -r '.id')" ] + [ -n "$(echo "$output" | jq -r '.created_at')" ] + [ -n "$(echo "$output" | jq -r '.updated_at')" ] + + # cleanup + created_id="$(echo "$output" | jq -r '.id')" + run_otdfctl_obl delete --id "$created_id" --force +} + +@test "Create a obligation - Bad" { + # bad obligation names + run_otdfctl_obl create --name ends_underscored_ --namespace "$NS_ID" + assert_failure + run_otdfctl_obl create --name -first-char-hyphen --namespace "$NS_ID" + assert_failure + run_otdfctl_obl create --name inval!d.chars --namespace "$NS_ID" + assert_failure + + # missing flag + run_otdfctl_obl create + assert_failure + assert_output --partial "Flag '--name' is required" + + # conflict + run_otdfctl_obl create --name test_create_obl_conflict --namespace "$NS_ID" --json + assert_success + created_id="$(echo "$output" | jq -r '.id')" + run_otdfctl_obl create --name test_create_obl_conflict --namespace "$NS_ID" + assert_failure + assert_output --partial "already_exists" + + # cleanup + run_otdfctl_obl delete --id $created_id --force +} + +@test "Get an obligation - Good" { + # setup an obligation to get + run_otdfctl_obl create --name test_get_obl --namespace "$NS_ID" --json + assert_success + created_id="$(echo "$output" | jq -r '.id')" + + # get by id + run_otdfctl_obl get --id "$created_id" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_get_obl" ] + + # get by fqn + run_otdfctl_obl get --fqn "https://${NS_NAME}/obl/test_get_obl" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_get_obl" ] + + # cleanup + run_otdfctl_obl delete --id $created_id --force +} + +@test "Get an obligation - Bad" { + run_otdfctl_obl get + assert_failure + assert_output --partial "Error: at least one of the flags in the group" + assert_output --partial "id" + assert_output --partial "fqn" + + run_otdfctl_obl get --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" + + run_otdfctl_obl get --id '08db7417-bd97-4455-b308-7d9e94e43440' --fqn 'https://example.com/obl/example' + assert_failure + assert_output --partial "Error: if any flags in the group" + assert_output --partial "id" + assert_output --partial "fqn" +} + +@test "List obligations" { + # setup obligations to list + run_otdfctl_obl create --name test_list_obl_1 --namespace "$NS_ID" --json + obl1_id="$(echo "$output" | jq -r '.id')" + run_otdfctl_obl create --name test_list_obl_2 --namespace "$NS_ID" --json + obl2_id="$(echo "$output" | jq -r '.id')" + + run_otdfctl_obl list + assert_success + assert_output --partial "$obl1_id" + assert_output --partial "test_list_obl_1" + assert_output --partial "$obl2_id" + assert_output --partial "test_list_obl_2" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + run_otdfctl_obl list --json + assert_success + assert_not_equal $(echo "$output" | jq -r 'pagination') "null" + total=$(echo "$output" | jq -r '.pagination.total') + [[ "$total" -ge 1 ]] + + # cleanup + run_otdfctl_obl delete --id $obl1_id --force + run_otdfctl_obl delete --id $obl2_id --force +} + +@test "List obligations supports sort and order flags" { + sort_prefix="sort_obl_${BATS_TEST_NUMBER}_$RANDOM" + run_otdfctl_obl create --name "${sort_prefix}_alpha" --namespace "$NS_ID" --json + obl_a_id="$(echo "$output" | jq -r '.id')" + run_otdfctl_obl create --name "${sort_prefix}_bravo" --namespace "$NS_ID" --json + obl_b_id="$(echo "$output" | jq -r '.id')" + run_otdfctl_obl create --name "${sort_prefix}_charlie" --namespace "$NS_ID" --json + obl_c_id="$(echo "$output" | jq -r '.id')" + + run_otdfctl_obl list --namespace "$NS_ID" --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.obligations[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl list --namespace "$NS_ID" --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.obligations[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$obl_c_id,$obl_b_id,$obl_a_id" + + run_otdfctl_obl list --namespace "$NS_ID" --sort fqn --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "https://$NS_NAME/obl/$sort_prefix" '[.obligations[] | select(.fqn | startswith($prefix)) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl list --namespace "$NS_ID" --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$obl_a_id" --arg b "$obl_b_id" --arg c "$obl_c_id" '[.obligations[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl update --id "$obl_a_id" --label sort=a --json + assert_success + run_otdfctl_obl update --id "$obl_b_id" --label sort=b --json + assert_success + run_otdfctl_obl update --id "$obl_c_id" --label sort=c --json + assert_success + + run_otdfctl_obl list --namespace "$NS_ID" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$obl_a_id" --arg b "$obl_b_id" --arg c "$obl_c_id" '[.obligations[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl list --namespace "$NS_ID" --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.obligations[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$obl_c_id,$obl_b_id,$obl_a_id" + + run_otdfctl_obl list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$obl_a_id" --arg b "$obl_b_id" --arg c "$obl_c_id" '[.obligations[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl delete --id "$obl_a_id" --force + run_otdfctl_obl delete --id "$obl_b_id" --force + run_otdfctl_obl delete --id "$obl_c_id" --force +} + +@test "Update obligation" { + # setup an obligation to update + run_otdfctl_obl create --name test_update_obl --namespace "$NS_ID" --json + assert_success + created_id="$(echo "$output" | jq -r '.id')" + + # force replace labels + run_otdfctl_obl update --id "$created_id" -l key=other --force-replace-labels --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_update_obl" ] + [ "$(echo "$output" | jq -r '.metadata.labels | keys | length')" = "1" ] + [ "$(echo "$output" | jq -r '.metadata.labels.key')" = "other" ] + + # renamed + run_otdfctl_obl update --id "$created_id" --name test_renamed_obl --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_renamed_obl" ] + [ "$(echo "$output" | jq -r '.name')" != "test_update_obl" ] + + # cleanup + run_otdfctl_obl delete --id $created_id --force +} + +@test "Delete obligation - Good" { + # setup an obligation to delete + run_otdfctl_obl create --name test_delete_obl --namespace "$NS_ID" --json + created_id="$(echo "$output" | jq -r '.id')" + + run_otdfctl_obl delete --id "$created_id" --force + assert_success +} + +@test "Delete obligation - Bad" { + # no id + run_otdfctl_obl delete + assert_failure + assert_output --partial "Error: at least one of the flags in the group" + assert_output --partial "id" + assert_output --partial "fqn" + + # invalid id + run_otdfctl_obl delete --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" + + # id and fqn exclusive + run_otdfctl_obl delete --id '08db7417-bd97-4455-b308-7d9e94e43440' --fqn 'https://example.com/obl/example' + assert_failure + assert_output --partial "Error: if any flags in the group" + assert_output --partial "id" + assert_output --partial "fqn" +} + +# Tests for obligation values + +@test "Create an obligation value - Good" { + # simple by obligation ID + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_create_obl_val --json + assert_success + [ "$(echo "$output" | jq -r '.value')" = "test_create_obl_val" ] + [ -n "$(echo "$output" | jq -r '.id')" ] + [ -n "$(echo "$output" | jq -r '.created_at')" ] + [ -n "$(echo "$output" | jq -r '.updated_at')" ] + created_id_simple="$(echo "$output" | jq -r '.id')" + + # simple by obligation FQN + run_otdfctl_obl_values create --obligation "https://$NS_NAME/obl/$OBL_NAME" --value test_create_obl_val_by_obl_fqn --json + assert_success + [ "$(echo "$output" | jq -r '.value')" = "test_create_obl_val_by_obl_fqn" ] + [ -n "$(echo "$output" | jq -r '.id')" ] + [ -n "$(echo "$output" | jq -r '.created_at')" ] + [ -n "$(echo "$output" | jq -r '.updated_at')" ] + created_id_simple_by_fqn=$(echo "$output" | jq -r '.id') + # cleanup + run_otdfctl_obl_values delete --id $created_id_simple --force + run_otdfctl_obl_values delete --id $created_id_simple_by_fqn --force +} + +@test "Create an obligation value - Bad" { + # bad obligation value names + run_otdfctl_obl_values create --obligation "$OBL_ID" --value ends_underscored_ + assert_failure + run_otdfctl_obl_values create --obligation "$OBL_ID" --value -first-char-hyphen + assert_failure + run_otdfctl_obl_values create --obligation "$OBL_ID" --value inval!d.chars + assert_failure + + # missing flag + run_otdfctl_obl_values create + assert_failure + assert_output --partial "Flag '--obligation' is required" + run_otdfctl_obl_values create --obligation "$OBL_ID" + assert_failure + assert_output --partial "Flag '--value' is required" + + # non-existent obligation fqn + run_otdfctl_obl_values create --obligation invalid_fqn --value test_create_obl_val + assert_failure + assert_output --regexp "obligation_fqn: (value )?must be a valid URI \[string\.uri\]" + + # conflict + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_create_obl_val_conflict --json + assert_success + created_id="$(echo "$output" | jq -r '.id')" + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_create_obl_val_conflict + assert_failure + assert_output --partial "already_exists" + + # cleanup + run_otdfctl_obl_values delete --id $created_id --force +} + +@test "Create an obligation value with triggers - JSON Array - Success" { + # test with single trigger (new nested format) + triggers_json='[{"action": "'$ACTION_1_NAME'", "attribute_value": "'$ATTR_VAL_FQN'", "context": {"pep": {"client_id": "test-client"}}}]' + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_single_trigger --triggers "$triggers_json" --json + assert_success + single_trigger_val_id=$(echo "$output" | jq -r '.id') + assert_equal "$(echo "$output" | jq -r '.value')" "test_val_single_trigger" + assert_not_equal "$(echo "$output" | jq -r '.id')" "null" + validate_triggers "$output" "1" "$ATTR_VAL_ID;$ATTR_VAL_FQN;$ACTION_1_ID;$ACTION_1_NAME;test-client" + cleanup_obligation_value "$single_trigger_val_id" + assert_success + + # test with multiple triggers (scoped and unscoped) + triggers_json='[{"action": "'$ACTION_1_NAME'", "attribute_value": "'$ATTR_VAL_FQN'", "context": {"pep": {"client_id": "test-client"}}}, {"action": "'$ACTION_2_NAME'", "attribute_value": "'$ATTR_VAL_FQN'"}]' + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_multiple_triggers --triggers "$triggers_json" --json + assert_success + multiple_trigger_val_id=$(echo "$output" | jq -r '.id') + assert_equal "$(echo "$output" | jq -r '.value')" "test_val_multiple_triggers" + assert_not_equal "$(echo "$output" | jq -r '.id')" "null" + validate_triggers "$output" "2" "$ATTR_VAL_ID;$ATTR_VAL_FQN;$ACTION_1_ID;$ACTION_1_NAME;test-client" "$ATTR_VAL_ID;$ATTR_VAL_FQN;$ACTION_2_ID;$ACTION_2_NAME;" + cleanup_obligation_value "$multiple_trigger_val_id" + assert_success + + # test with unscoped trigger + triggers_json='[{"action": "'$ACTION_1_NAME'", "attribute_value": "'$ATTR_VAL_FQN'"}]' + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_unscoped_trigger --triggers "$triggers_json" --json + assert_success + unscoped_trigger_val_id=$(echo "$output" | jq -r '.id') + assert_equal "$(echo "$output" | jq -r '.value')" "test_val_unscoped_trigger" + assert_not_equal "$(echo "$output" | jq -r '.id')" "null" + validate_triggers "$output" "1" "$ATTR_VAL_ID;$ATTR_VAL_FQN;$ACTION_1_ID;$ACTION_1_NAME;" + cleanup_obligation_value "$unscoped_trigger_val_id" + assert_success +} + +@test "Create an obligation value with triggers - JSON File - Success" { + # create a temporary triggers file + cat > "$SHARED_TRIGGERS_FILE" << EOF +[ + { + "action": "$ACTION_1_NAME", + "attribute_value": "$ATTR_VAL_FQN", + "context": { + "pep": { + "client_id": "file-client-1" + } + } + }, + { + "action": "$ACTION_2_NAME", + "attribute_value": "$ATTR_VAL_FQN" + } +] +EOF + + # test with triggers from file + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_val_file_triggers --triggers "$SHARED_TRIGGERS_FILE" --json + assert_success + file_trigger_val_id=$(echo "$output" | jq -r '.id') + assert_equal "$(echo "$output" | jq -r '.value')" "test_val_file_triggers" + assert_not_equal "$(echo "$output" | jq -r '.id')" "null" + validate_triggers "$output" "2" "$ATTR_VAL_ID;$ATTR_VAL_FQN;$ACTION_1_ID;$ACTION_1_NAME;file-client-1" "$ATTR_VAL_ID;$ATTR_VAL_FQN;$ACTION_2_ID;$ACTION_2_NAME;" + + # cleanup + cleanup_obligation_value "$file_trigger_val_id" +} + +@test "Create an obligation value with triggers - Bad" { + # test with invalid JSON + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_bad_json --triggers '{"invalid": json}' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "failed to parse trigger JSON" + + # test with missing required fields + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_missing_action --triggers '[{"attribute_value": "https://test.com/attr/test/value/test"}]' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "action is required" + + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_missing_attr --triggers '[{"action": "read"}]' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "attribute_value is required" + + # test with empty required fields + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_empty_action --triggers '[{"action": "", "attribute_value": "https://test.com/attr/test/value/test"}]' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "action is required" + + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_empty_attr --triggers '[{"action": "read", "attribute_value": ""}]' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "attribute_value is required" + + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_empty_attr --triggers '[{"attribute_value": "https://test.com/attr/test/value/test", "action": "read"}, {"action": "write"}]' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "attribute_value is required" + + # test with non-existent file + run ./otdfctl $HOST $WITH_CREDS policy obligations values create --obligation "$OBL_ID" --value test_val_nonexistent_file --triggers "/nonexistent/file.json" + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "failed to parse trigger JSON" + + # test with invalid file content + invalid_file="/tmp/invalid_triggers_$$.json" + echo "invalid json content" > "$invalid_file" + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_val_invalid_file --triggers "$invalid_file" + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "failed to parse trigger JSON" + rm -f "$invalid_file" +} + +@test "Get an obligation value - Good" { + # setup an obligation value to get + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_get_obl_val --json + assert_success + created_id=$(echo "$output" | jq -r '.id') + + # get by id + run_otdfctl_obl_values get --id "$created_id" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.value')" = "test_get_obl_val" ] + + # get by fqn + run_otdfctl_obl_values get --fqn "https://$NS_NAME/obl/$OBL_NAME/value/test_get_obl_val" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.value')" = "test_get_obl_val" ] + + # cleanup + run_otdfctl_obl_values delete --id $created_id --force +} + +@test "Get an obligation value - Bad" { + run_otdfctl_obl_values get + assert_failure + assert_output --partial "Error: at least one of the flags in the group" + assert_output --partial "id" + assert_output --partial "fqn" + + # invalid id + run_otdfctl_obl_values get --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" + + # invalid fqn + run_otdfctl_obl_values get --fqn 'not_a_fqn' + assert_failure + assert_output --partial "must be a valid URI" + + # id and fqn exclusive + run_otdfctl_obl_values get --id '08db7417-bd97-4455-b308-7d9e94e43440' --fqn 'https://example.com/obl/example/value/value1' + assert_failure + assert_output --partial "Error: if any flags in the group" + assert_output --partial "id" + assert_output --partial "fqn" +} + +@test "Update obligation values" { + # setup an obligation value to update + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_update_obl_val --json + assert_success + created_id="$(echo "$output" | jq -r '.id')" + + # force replace labels + run_otdfctl_obl_values update --id "$created_id" -l key=other --force-replace-labels --json + assert_success + # Check that metadata.labels has exactly one key + [ "$(echo "$output" | jq -r '.metadata.labels | keys | length')" = "1" ] + # Check that the key "key" exists and has value "other" + [ "$(echo "$output" | jq -r '.metadata.labels.key')" = "other" ] + + # renamed + run_otdfctl_obl_values update --id "$created_id" --value test_renamed_obl_val --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.value')" = "test_renamed_obl_val" ] + [ "$(echo "$output" | jq -r '.value')" != "test_update_obl_val" ] + + # cleanup + run_otdfctl_obl_values delete --id $created_id --force +} + +@test "Update obligation values with triggers - Success" { + # create an obligation value to update + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_update_with_triggers --json + assert_success + created_id="$(echo "$output" | jq -r '.id')" + + # verify obligation value has no triggers initially + run_otdfctl_obl_values get --id "$created_id" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.triggers | length')" "0" + + # update with triggers (new nested format) + triggers_json='[{"action": "'$ACTION_1_NAME'", "attribute_value": "'$ATTR_2_VAL_FQN'", "context": {"pep": {"client_id": "update-client"}}}]' + run ./otdfctl $HOST $WITH_CREDS policy obligations values update --id "$created_id" --value test_updated_with_triggers --triggers "$triggers_json" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.id')" "$created_id" + assert_equal "$(echo "$output" | jq -r '.value')" "test_updated_with_triggers" + validate_triggers "$output" "1" "$ATTR_2_VAL_ID;$ATTR_2_VAL_FQN;$ACTION_1_ID;$ACTION_1_NAME;update-client" + + run_otdfctl_obl_values get --id "$created_id" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.triggers | length')" "1" + + # update with triggers from file + cat > "$SHARED_TRIGGERS_FILE" << EOF +[ + { + "action": "$ACTION_2_NAME", + "attribute_value": "$ATTR_VAL_FQN" + }, + { + "action": "$ACTION_1_NAME", + "attribute_value": "$ATTR_VAL_FQN" + } +] +EOF + + run_otdfctl_obl_values update --id "$created_id" --value test_updated_from_file --triggers "$SHARED_TRIGGERS_FILE" --json + assert_success + validate_triggers "$output" "2" "$ATTR_VAL_ID;$ATTR_VAL_FQN;$ACTION_2_ID;$ACTION_2_NAME;" "$ATTR_VAL_ID;$ATTR_VAL_FQN;$ACTION_1_ID;$ACTION_1_NAME;" + + run_otdfctl_obl_values get --id "$created_id" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.triggers | length')" "2" + + # cleanup + cleanup_obligation_value "$created_id" +} + +@test "Update obligation values with triggers - Bad" { + # create an obligation value to update + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_update_bad_triggers --json + assert_success + created_id="$(echo "$output" | jq -r '.id')" + + # test with invalid JSON + run ./otdfctl $HOST $WITH_CREDS policy obligations values update --id "$created_id" --triggers '{"invalid": json}' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "failed to parse trigger JSON" + + # test with missing required fields + run ./otdfctl $HOST $WITH_CREDS policy obligations values update --id "$created_id" --triggers '[{"attribute_value": "https://test.com/attr/test/value/test"}]' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "action is required" + + # Missing required fields many + run ./otdfctl $HOST $WITH_CREDS policy obligations values update --id "$created_id" --triggers '[{"attribute_value": "https://test.com/attr/test/value/test", "action": "read"}, {"action": "write"}]' + assert_failure + assert_output --partial "Invalid trigger configuration" + assert_output --partial "attribute_value is required" + + # cleanup + cleanup_obligation_value "$created_id" +} + +@test "Delete obligation value - Good" { + # setup a value to delete + run_otdfctl_obl_values create --obligation "$OBL_ID" --value test_delete_obl_val --json + created_id="$(echo "$output" | jq -r '.id')" + + run_otdfctl_obl_values delete --id "$created_id" --force + assert_success +} + +@test "Delete obligation value - Bad" { + # no id + run_otdfctl_obl_values delete + assert_failure + assert_output --partial "Error: at least one of the flags in the group" + assert_output --partial "id" + assert_output --partial "fqn" + + # invalid id + run_otdfctl_obl_values delete --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" + + # id and fqn exclusive + run_otdfctl_obl_values delete --id '08db7417-bd97-4455-b308-7d9e94e43440' --fqn 'https://example.com/obl/example/value/value1' + assert_failure + assert_output --partial "Error: if any flags in the group" + assert_output --partial "id" + assert_output --partial "fqn" +} + +# Tests for obligation triggers + +@test "Create an obligation trigger - Required Only - IDs - Success" { + # setup an obligation value to use + run_otdfctl_obl_values create --obligation "$OBL_ID" --value "test_obl_val_for_trigger" --json + obl_val_id=$(echo "$output" | jq -r '.id') + + # create trigger + run_otdfctl_obl_triggers create --attribute-value "$ATTR_VAL_ID" --action "$ACTION_1_ID" --obligation-value "$obl_val_id" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" != "null" ] + trigger_id=$(echo "$output" | jq -r '.id') + assert_equal "$(echo "$output" | jq -r '.attribute_value.id')" "$ATTR_VAL_ID" + assert_equal "$(echo "$output" | jq -r '.attribute_value.fqn')" "$ATTR_VAL_FQN" + assert_equal "$(echo "$output" | jq -r '.action.id')" "$ACTION_1_ID" + assert_equal "$(echo "$output" | jq -r '.action.name')" "$ACTION_1_NAME" + assert_equal "$(echo "$output" | jq -r '.obligation_value.id')" "$obl_val_id" + assert_equal "$(echo "$output" | jq -r '.obligation_value.value')" "test_obl_val_for_trigger" + assert_equal "$(echo "$output" | jq -r '.obligation_value.obligation.id')" "$OBL_ID" + assert_equal "$(echo "$output" | jq -r '.obligation_value.obligation.namespace.fqn')" "https://$NS_NAME" + assert_equal "$(echo "$output" | jq -r '.obligation_value.fqn')" "https://$NS_NAME/obl/$OBL_NAME/value/test_obl_val_for_trigger" + + # cleanup + cleanup_trigger "$trigger_id" + cleanup_obligation_value "$obl_val_id" +} + +@test "Create an obligation trigger - Required Only - FQNs - Success" { + # setup an obligation value to use + run_otdfctl_obl_values create --obligation "$OBL_ID" --value "test_obl_val_for_trigger" --json + obl_val_id=$(echo "$output" | jq -r '.id') + obl_val_fqn="https://$NS_NAME/obl/$OBL_NAME/value/test_obl_val_for_trigger" + + # create trigger + run_otdfctl_obl_triggers create --attribute-value "$ATTR_VAL_FQN" --action "$ACTION_1_NAME" --obligation-value "$obl_val_fqn" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" != "null" ] + trigger_id=$(echo "$output" | jq -r '.id') + assert_equal "$(echo "$output" | jq -r '.attribute_value.id')" "$ATTR_VAL_ID" + assert_equal "$(echo "$output" | jq -r '.attribute_value.fqn')" "$ATTR_VAL_FQN" + assert_equal "$(echo "$output" | jq -r '.action.id')" "$ACTION_1_ID" + assert_equal "$(echo "$output" | jq -r '.action.name')" "$ACTION_1_NAME" + assert_equal "$(echo "$output" | jq -r '.obligation_value.id')" "$obl_val_id" + assert_equal "$(echo "$output" | jq -r '.obligation_value.value')" "test_obl_val_for_trigger" + assert_equal "$(echo "$output" | jq -r '.obligation_value.obligation.id')" "$OBL_ID" + assert_equal "$(echo "$output" | jq -r '.obligation_value.obligation.namespace.fqn')" "https://$NS_NAME" + assert_equal "$(echo "$output" | jq -r '.obligation_value.fqn')" "https://$NS_NAME/obl/$OBL_NAME/value/test_obl_val_for_trigger" + assert_equal "$(echo "$output" | jq -r '.metadata.labels')" "null" + assert_equal "$(echo "$output" | jq -r '.context.pep')" "null" + + # cleanup + cleanup_trigger "$trigger_id" + cleanup_obligation_value "$obl_val_id" +} + +@test "Create an obligation trigger - Optional Fields - Success" { + # setup an obligation value to use + run_otdfctl_obl_values create --obligation "$OBL_ID" --value "test_obl_val_for_trigger" --json + obl_val_id=$(echo "$output" | jq -r '.id') + + # create trigger + client_id="a-pep" + run_otdfctl_obl_triggers create --attribute-value "$ATTR_VAL_ID" --action "$ACTION_2_ID" --obligation-value "$obl_val_id" --client-id "$client_id" --label "my=label" --json + assert_success + assert_not_equal "$(echo "$output" | jq -r '.id')" "null" + trigger_id=$(echo "$output" | jq -r '.id') + assert_equal "$(echo "$output" | jq -r '.attribute_value.id')" "$ATTR_VAL_ID" + assert_equal "$(echo "$output" | jq -r '.attribute_value.fqn')" "$ATTR_VAL_FQN" + assert_equal "$(echo "$output" | jq -r '.action.id')" "$ACTION_2_ID" + assert_equal "$(echo "$output" | jq -r '.action.name')" "$ACTION_2_NAME" + assert_equal "$(echo "$output" | jq -r '.obligation_value.id')" "$obl_val_id" + assert_equal "$(echo "$output" | jq -r '.obligation_value.value')" "test_obl_val_for_trigger" + assert_equal "$(echo "$output" | jq -r '.obligation_value.obligation.id')" "$OBL_ID" + assert_equal "$(echo "$output" | jq -r '.obligation_value.obligation.namespace.fqn')" "https://$NS_NAME" + assert_equal "$(echo "$output" | jq -r '.metadata.labels.my')" "label" + assert_equal "$(echo "$output" | jq -r '.context | length')" "1" + assert_equal "$(echo "$output" | jq -r '.context[0].pep.client_id')" "$client_id" + assert_equal "$(echo "$output" | jq -r '.obligation_value.fqn')" "https://$NS_NAME/obl/$OBL_NAME/value/test_obl_val_for_trigger" + + # cleanup + cleanup_trigger "$trigger_id" + cleanup_obligation_value "$obl_val_id" +} + +@test "Create an obligation trigger - Same tuple different client IDs - Success" { + # setup an obligation value to use + run_otdfctl_obl_values create --obligation "$OBL_ID" --value "test_obl_val_for_multi_peps" --json + assert_success + obl_val_id=$(echo "$output" | jq -r '.id') + + # create first client-scoped trigger + client_id_1="a-pep" + run_otdfctl_obl_triggers create --attribute-value "$ATTR_VAL_ID" --action "$ACTION_2_ID" --obligation-value "$obl_val_id" --client-id "$client_id_1" --json + assert_success + trigger_id_1=$(echo "$output" | jq -r '.id') + assert_not_equal "$trigger_id_1" "null" + assert_equal "$(echo "$output" | jq -r '.context[0].pep.client_id')" "$client_id_1" + + # create second client-scoped trigger with same tuple but different client id + client_id_2="b-pep" + run_otdfctl_obl_triggers create --attribute-value "$ATTR_VAL_ID" --action "$ACTION_2_ID" --obligation-value "$obl_val_id" --client-id "$client_id_2" --json + assert_success + trigger_id_2=$(echo "$output" | jq -r '.id') + assert_not_equal "$trigger_id_2" "null" + assert_not_equal "$trigger_id_1" "$trigger_id_2" + assert_equal "$(echo "$output" | jq -r '.context[0].pep.client_id')" "$client_id_2" + + # cleanup + cleanup_trigger "$trigger_id_1" + cleanup_trigger "$trigger_id_2" + cleanup_obligation_value "$obl_val_id" +} + +@test "Create an obligation trigger - Bad" { + # missing flags + run_otdfctl_obl_triggers create --attribute-value "http://example.com/attr/attr_name/value/attr_value" --action "read" + assert_failure + assert_output --partial "Flag '--obligation-value' is required" + + run_otdfctl_obl_triggers create --obligation-value "http://example.com/attr/attr_name/value/attr_value" --action "read" + assert_failure + assert_output --partial "Flag '--attribute-value' is required" + + run_otdfctl_obl_triggers create --obligation-value "http://example.com/attr/attr_name/value/attr_value" --attribute-value "http://example.com/attr/attr_name/value/attr_value" + assert_failure + assert_output --partial "Flag '--action' is required" +} + +@test "Delete an obligation trigger - Good" { + # setup an obligation value to use + run_otdfctl_obl_values create --obligation "$OBL_ID" --value "test_obl_val_for_del_trigger" --json + assert_success + obl_val_id=$(echo "$output" | jq -r '.id') + + # create trigger + run_otdfctl_obl_triggers create --attribute-value "$ATTR_2_VAL_ID" --action "$ACTION_2_ID" --obligation-value "$obl_val_id" --json + assert_success + assert_not_equal "$(echo "$output" | jq -r '.id')" "null" + trigger_id=$(echo "$output" | jq -r '.id') + + # delete trigger + run_otdfctl_obl_triggers delete --id "$trigger_id" --force --json + assert_success + assert_equal "$(echo "$output" | jq -r '.id')" "$trigger_id" + + # cleanup + cleanup_obligation_value "$obl_val_id" +} + +@test "List obligation triggers - No filters" { + setup_triggers_test_data + + run_otdfctl_obl_triggers list --json + assert_success + + # Verify all our triggers are present + actual_triggers=$(echo "$output" | jq -r '.triggers | length') + assert [ "$actual_triggers" -ge 2 ] + validate_triggers "$output" "2" "$LIST_ATTR_2_VAL_1_ID;$LIST_ATTR_2_VAL_1_FQN;$LIST_ACTION_2_ID;$LIST_ACTION_2_NAME;$CLIENT_ID_LIST;$LIST_OBL_2_VAL_ID;$LIST_OBL_VAL_2_FQN" "$LIST_ATTR_1_VAL_1_ID;$LIST_ATTR_1_VAL_1_FQN;$LIST_ACTION_1_ID;$LIST_ACTION_1_NAME;$CLIENT_ID_LIST;$LIST_OBL_1_VAL_ID;$LIST_OBL_VAL_1_FQN" + validate_pagination "$output" "null" "2" "null" +} + +@test "List obligation triggers - Limit and Offset" { + setup_triggers_test_data + run_otdfctl_obl_triggers list --limit 1 --offset 0 --json + assert_success + assert_equal "$(echo "$output" | jq -r '.triggers | length')" "1" + validate_triggers "$output" "1" "$LIST_ATTR_2_VAL_1_ID;$LIST_ATTR_2_VAL_1_FQN;$LIST_ACTION_2_ID;$LIST_ACTION_2_NAME;$CLIENT_ID_LIST;$LIST_OBL_2_VAL_ID;$LIST_OBL_VAL_2_FQN" + validate_pagination "$output" "null" "2" "1" + + run_otdfctl_obl_triggers list --limit 1 --offset 1 --json + assert_success + assert_equal "$(echo "$output" | jq -r '.triggers | length')" "1" + validate_triggers "$output" "1" "$LIST_ATTR_1_VAL_1_ID;$LIST_ATTR_1_VAL_1_FQN;$LIST_ACTION_1_ID;$LIST_ACTION_1_NAME;$CLIENT_ID_LIST;$LIST_OBL_1_VAL_ID;$LIST_OBL_VAL_1_FQN" + validate_pagination "$output" "1" "2" "null" +} + +@test "List obligation triggers - Filter by Namespace ID" { + setup_triggers_test_data + run_otdfctl_obl_triggers list --namespace "$LIST_NS_1_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.triggers | length')" "1" + validate_triggers "$output" "1" "$LIST_ATTR_1_VAL_1_ID;$LIST_ATTR_1_VAL_1_FQN;$LIST_ACTION_1_ID;$LIST_ACTION_1_NAME;$CLIENT_ID_LIST;$LIST_OBL_1_VAL_ID;$LIST_OBL_VAL_1_FQN" + validate_pagination "$output" "null" "1" "null" +} + +@test "List obligation triggers - Filter by Namespace FQN" { + setup_triggers_test_data + run_otdfctl_obl_triggers list --namespace "https://$LIST_NS_2_NAME" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.triggers | length')" "1" + validate_triggers "$output" "1" "$LIST_ATTR_2_VAL_1_ID;$LIST_ATTR_2_VAL_1_FQN;$LIST_ACTION_2_ID;$LIST_ACTION_2_NAME;$CLIENT_ID_LIST;$LIST_OBL_2_VAL_ID;$LIST_OBL_VAL_2_FQN" + validate_pagination "$output" "null" "1" "null" +} diff --git a/otdfctl/e2e/otdfctl-utils.sh b/otdfctl/e2e/otdfctl-utils.sh new file mode 100644 index 0000000000..3040454106 --- /dev/null +++ b/otdfctl/e2e/otdfctl-utils.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + + +run_otdfctl_key() { + run sh -c "./otdfctl policy kas-registry key $HOST $WITH_CREDS $*" +} + +delete_all_keys_in_kas() { + local kas_id="$1" + echo "Attempting to delete all keys in KAS registry: $kas_id" + + # List all keys in the specified KAS registry + run_otdfctl_key list --kas "$kas_id" --json + assert_success + + local key_ids=() + local keys_to_delete=$(echo "$output" | jq -c '.kas_keys[] | {id: .key.id, key_id: .key.key_id, kas_uri: .kas_uri}') + + if [ -z "$keys_to_delete" ]; then + echo "No keys found to delete in KAS registry: $kas_id" + return 0 + fi + + echo "Found $(echo "$keys_to_delete" | wc -l | xargs) keys to delete in KAS registry: $kas_id" + echo "$keys_to_delete" | while read -r key_info; do + local key_system_id=$(echo "$key_info" | jq -r '.id') + local kid=$(echo "$key_info" | jq -r '.key_id') + local key_kas_uri=$(echo "$key_info" | jq -r '.kas_uri') + + echo "Deleting key: $key_user_id (system ID: $key_system_id) from KAS URI: $key_kas_uri" + run_otdfctl_key unsafe delete --id "$key_system_id" --kas-uri "$key_kas_uri" --key-id "$kid" --force + assert_success + if [ "$status" -ne 0 ]; then + echo "Warning: Failed to delete key $key_system_id. Error: $output" >&2 + else + echo "Successfully deleted key: $key_system_id" + fi + done +} + + +delete_kas_registry() { + local kas_id="$1" + run sh -c "./otdfctl $HOST $WITH_CREDS policy kas-registry delete --id "$kas_id" --force" + assert_success +} + +delete_provider_config() { + local pc_id="$1" + run sh -c "./otdfctl $HOST $WITH_CREDS policy keymanagement provider delete --id "$pc_id" --force" + assert_success +} diff --git a/otdfctl/e2e/profile.bats b/otdfctl/e2e/profile.bats new file mode 100755 index 0000000000..92880284b1 --- /dev/null +++ b/otdfctl/e2e/profile.bats @@ -0,0 +1,319 @@ +#!/usr/bin/env bats + +setup_file() { + # Prefix for all profiles created in this file to avoid clashing + PROFILE_TEST_PREFIX="bats-profile-$(date +%s)" + export PROFILE_TEST_PREFIX +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + run_otdfctl() { + run bash -c "./otdfctl profile $*" + } + + run_otdfctl_profile_keyring() { + # LEGACY_OTDFCTL_BIN gets set in the action.yaml + # It is v0.26.2 of otdfctl + run bash -c "$LEGACY_OTDFCTL_BIN profile $*" + } +} + +teardown() { + run_otdfctl profile delete-all --force + run_otdfctl profile delete-all --store keyring --force +} + +@test "profile create" { + profile="${PROFILE_TEST_PREFIX}-create" + run_otdfctl create "$profile" http://localhost:8080 + assert_success + assert_output --partial "Profile ${profile} created" + + # Invalid endpoint should fail with a helpful message + run_otdfctl create "$profile" localhost:8080 + assert_failure + assert_output --partial "Failed to create profile" + assert_output --partial "invalid scheme" +} + +@test "profile list shows profiles and default" { + profile1="${PROFILE_TEST_PREFIX}-list-1" + profile2="${PROFILE_TEST_PREFIX}-list-2" + + run_otdfctl create "$profile1" http://localhost:8080 + assert_success + + run_otdfctl create "$profile2" http://localhost:8080 --set-default + assert_success + + run_otdfctl list + assert_success + assert_output --partial "Listing profiles from filesystem" + assert_output --partial " ${profile1}" + assert_output --partial "* ${profile2}" + + profile1_keyring="${PROFILE_TEST_PREFIX}-list-keyring-1" + profile2_keyring="${PROFILE_TEST_PREFIX}-list-keyring-2" + + run_otdfctl_profile_keyring create "$profile1_keyring" http://localhost:8080 + assert_success + + run_otdfctl_profile_keyring create --set-default "$profile2_keyring" http://localhost:8080 + assert_success + + run_otdfctl list --store keyring + assert_success + assert_output --partial "Listing profiles from keyring" + assert_output --partial " ${profile1_keyring}" + assert_output --partial "* ${profile2_keyring}" +} + +@test "profile get shows profile details" { + profile="${PROFILE_TEST_PREFIX}-get" + + run_otdfctl create "$profile" http://localhost:8080 --set-default + assert_success + + run_otdfctl get "$profile" + assert_success + assert_output --partial "Profile" + assert_output --partial "$profile" + assert_output --partial "Endpoint" + assert_output --partial "http://localhost:8080" + assert_output --partial "Is default" + assert_output --partial "true" + + profile_keyring="${PROFILE_TEST_PREFIX}-get-keyring" + + run_otdfctl_profile_keyring create --set-default "$profile_keyring" http://localhost:8080 + assert_success + + run_otdfctl get "$profile_keyring" --store keyring + assert_success + assert_output --partial "Profile" + assert_output --partial "$profile_keyring" + assert_output --partial "Endpoint" + assert_output --partial "http://localhost:8080" + assert_output --partial "Is default" + assert_output --partial "true" +} + +@test "profile delete removes profile" { + base="${PROFILE_TEST_PREFIX}-delete" + default_profile="${base}-default" + target_profile="${base}-target" + + run_otdfctl create "$default_profile" http://localhost:8080 --set-default + assert_success + + run_otdfctl create "$target_profile" http://localhost:8080 + assert_success + + run_otdfctl delete "$target_profile" + assert_success + assert_output --partial "Deleted profile ${target_profile} from filesystem" + + run_otdfctl profile list + assert_success + refute_output --partial "$target_profile" + + base_keyring="${PROFILE_TEST_PREFIX}-delete-keyring" + default_profile_keyring="${base_keyring}-default" + target_profile_keyring="${base_keyring}-target" + + run_otdfctl_profile_keyring create --set-default "$default_profile_keyring" http://localhost:8080 + assert_success + + run_otdfctl_profile_keyring create "$target_profile_keyring" http://localhost:8080 + assert_success + + run_otdfctl delete "$target_profile_keyring" --store keyring + assert_success + assert_output --partial "Deleted profile ${target_profile_keyring} from keyring" + + run_otdfctl list --store keyring + assert_success + refute_output --partial "$target_profile_keyring" +} + +@test "profile list supports json output" { + profile1="${PROFILE_TEST_PREFIX}-list-json-1" + profile2="${PROFILE_TEST_PREFIX}-list-json-2" + + run_otdfctl create "$profile1" http://localhost:8080 + assert_success + + run_otdfctl create "$profile2" http://localhost:8080 --set-default + assert_success + + run_otdfctl list --json + assert_success + assert_equal "$(echo "$output" | jq -r .store)" "filesystem" + echo "$output" | jq -e --arg profile1 "$profile1" --arg profile2 "$profile2" \ + 'any(.profiles[]; .name == $profile1 and .is_default == false) and any(.profiles[]; .name == $profile2 and .is_default == true)' +} + +@test "profile get supports json output" { + profile="${PROFILE_TEST_PREFIX}-get-json" + + run_otdfctl create "$profile" http://localhost:8080 --set-default --output-format json + assert_success + + run_otdfctl get "$profile" --json + assert_success + assert_equal "$(echo "$output" | jq -r .profile)" "$profile" + assert_equal "$(echo "$output" | jq -r .endpoint)" "http://localhost:8080" + assert_equal "$(echo "$output" | jq -r .is_default)" "true" + assert_equal "$(echo "$output" | jq -r .output_format)" "json" +} + +@test "profile errors support json output" { + profile="${PROFILE_TEST_PREFIX}-missing-json" + + run_otdfctl get "$profile" --json + assert_failure + assert_equal "$(echo "$output" | jq -r .status)" "ERROR" + echo "$output" | jq -e --arg profile "$profile" '.message | contains($profile)' +} + +@test "profile set-default updates default profile" { + base="${PROFILE_TEST_PREFIX}-set-default" + profile1="${base}-1" + profile2="${base}-2" + + run_otdfctl create "$profile1" http://localhost:8080 --set-default + assert_success + + run_otdfctl create "$profile2" http://localhost:8081 + assert_success + + run_otdfctl set-default "$profile2" + assert_success + assert_output --partial "Set profile ${profile2} as default" + + run_otdfctl list + assert_success + assert_output --partial "* ${profile2}" +} + +@test "profile set-endpoint updates endpoint" { + profile="${PROFILE_TEST_PREFIX}-set-endpoint" + + run_otdfctl create "$profile" http://localhost:8080 + assert_success + + run_otdfctl set-endpoint "$profile" http://localhost:8081 + assert_success + assert_output --partial "Set endpoint http://localhost:8081 for profile ${profile}" + + run_otdfctl get "$profile" + assert_success + assert_output --partial "http://localhost:8081" +} + +@test "profile delete-all deletes all profiles" { + base="${PROFILE_TEST_PREFIX}-delete-all" + profile1="${base}-1" + profile2="${base}-2" + + run_otdfctl create "$profile1" http://localhost:8080 --set-default + assert_success + + run_otdfctl create "$profile2" http://localhost:8081 + assert_success + + run_otdfctl delete-all --force + assert_success + assert_output --regexp '^Deleted [0-9]+ profiles from filesystem$' + + run_otdfctl list + assert_success + refute_output --partial "$profile1" + refute_output --partial "$profile2" + + base_keyring="${PROFILE_TEST_PREFIX}-delete-all-keyring" + profile1_keyring="${base_keyring}-1" + profile2_keyring="${base_keyring}-2" + + run_otdfctl_profile_keyring create --set-default "$profile1_keyring" http://localhost:8080 + assert_success + + run_otdfctl_profile_keyring create "$profile2_keyring" http://localhost:8081 + assert_success + + run_otdfctl delete-all --store keyring --force + assert_success + assert_output --regexp '^Deleted [0-9]+ profiles from keyring$' + + run_otdfctl list --store keyring + assert_success + refute_output --partial "$profile1_keyring" + refute_output --partial "$profile2_keyring" +} + +@test "profile migrate moves keyring profiles to filesystem" { + base="${PROFILE_TEST_PREFIX}-migrate" + profile1="${base}-1" + profile2="${base}-2" + + run_otdfctl_profile_keyring create --set-default "$profile1" http://localhost:8080 + assert_success + + run_otdfctl_profile_keyring create "$profile2" http://localhost:8081 + assert_success + + run_otdfctl list --store keyring + assert_success + assert_output --partial "$profile1" + assert_output --partial "$profile2" + + run_otdfctl list + assert_success + refute_output --partial "$profile1" + refute_output --partial "$profile2" + + run_otdfctl migrate + assert_success + assert_output --partial "Migration complete." + + run_otdfctl list + assert_success + assert_output --partial "Listing profiles from filesystem" + assert_output --partial "* ${profile1}" + assert_output --partial " ${profile2}" + + run_otdfctl list --store keyring + assert_success + assert_output --partial "Listing profiles from keyring" + refute_output --partial "$profile1" + refute_output --partial "$profile2" +} + +@test "profile keyring cleanup removes all keyring profiles" { + base="${PROFILE_TEST_PREFIX}-cleanup" + profile1="${base}-1" + profile2="${base}-2" + + run_otdfctl_profile_keyring create "$profile1" http://localhost:8080 + assert_success + + run_otdfctl_profile_keyring create "$profile2" http://localhost:8081 + assert_success + + run_otdfctl list --store keyring + assert_success + assert_output --partial "$profile1" + assert_output --partial "$profile2" + + run_otdfctl cleanup --force + assert_success + assert_output --partial "Keyring profile store cleanup complete" + + run_otdfctl list --store keyring + assert_success + refute_output --partial "$profile1" + refute_output --partial "$profile2" +} diff --git a/otdfctl/e2e/provider-config.bats b/otdfctl/e2e/provider-config.bats new file mode 100755 index 0000000000..faf6c4d5e8 --- /dev/null +++ b/otdfctl/e2e/provider-config.bats @@ -0,0 +1,181 @@ +#!/usr/bin/env bats + +setup_file() { + export CREDSFILE=creds.json + echo -n '{"clientId":"opentdf","clientSecret":"secret"}' > $CREDSFILE + export WITH_CREDS="--with-client-creds-file $CREDSFILE" + export HOST='--host http://localhost:8080' + export DEBUG_LEVEL="--log-level debug" + export VALID_CONFIG='{"cached":"key"}' + export BASE64_CONFIG="eyJjYWNoZWQiOiAia2V5In0=" +} + +setup() { + if [ "$RUN_EXPERIMENTAL_TESTS" != "true" ]; then + skip "Skipping experimental test" + fi + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_key_pc () { + run sh -c "./otdfctl policy keymanagement provider $HOST $WITH_CREDS $*" + } +} + +delete_pc_by_id() { + run_otdfctl_key_pc delete --id "$1" --force + assert_success +} + +######### +# Create Provider Configuration +######### +@test "fail to create provider configuration without config" { + run_otdfctl_key_pc create --name test-value + assert_failure + assert_output --partial "Flag '--config' is required" +} + +@test "fail to create provider configuration without name" { + run_otdfctl_key_pc create --config '{}' + assert_failure + assert_output --partial "Flag '--name' is required" +} + +@test "fail to create provider configuration with invalid config" { + run_otdfctl_key_pc create --name test-config --config test-value + assert_failure + assert_output --partial "invalid_argument" +} + +@test "create provider configuration" { + CONFIG_NAME="test-config" + run_otdfctl_key_pc create --name "$CONFIG_NAME" --config '"$VALID_CONFIG"' --json + assert_success + assert_equal "$(echo "$output" | jq -r .name)" "$CONFIG_NAME" + assert_equal "$(echo "$output" | jq -r .config_json)" "$BASE64_CONFIG" + delete_pc_by_id "$(echo "$output" | jq -r .id)" +} + +@test "get provider configuration by id" { + CONFIG_NAME="test-config-2" + run_otdfctl_key_pc create --name "$CONFIG_NAME" --config '"$VALID_CONFIG"' --json + assert_success + ID=$(echo "$output" | jq -r '.id') + run_otdfctl_key_pc get --id "$ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r .name)" "$CONFIG_NAME" + assert_equal "$(echo "$output" | jq -r .config_json)" "$BASE64_CONFIG" + delete_pc_by_id "$(echo "$output" | jq -r .id)" + } + + +@test "get provider configuration by name" { + CONFIG_NAME="test-config-3" + run_otdfctl_key_pc create --name "$CONFIG_NAME" --config '"$VALID_CONFIG"' --json + assert_success + NAME=$(echo "$output" | jq -r '.name') + run_otdfctl_key_pc get --name "$NAME" --json + assert_success + assert_equal "$(echo "$output" | jq -r .name)" "$CONFIG_NAME" + assert_equal "$(echo "$output" | jq -r .config_json)" "$BASE64_CONFIG" + delete_pc_by_id "$(echo "$output" | jq -r .id)" +} + +@test "fail to get provider configuration - no required flags" { + run_otdfctl_key_pc get + assert_failure +} + +@test "fail to get provider configuration with non-existent name" { + run_otdfctl_key_pc get --name non-existent-config + assert_failure + assert_output --partial "Failed to get provider config: not_found" +} +@test "list provider configurations" { + NAME="tst-config-4" + run_otdfctl_key_pc create --name "$NAME" --config '"$VALID_CONFIG"' --manager "fake-manager" --json + assert_success + ID=$(echo "$output" | jq -r '.id') + run_otdfctl_key_pc list --json + assert_success + assert_equal "$(echo "$output" | jq '.provider_configs | length')" "1" + assert_equal "$(echo "$output" | jq '.pagination.total')" "1" + run_otdfctl_key_pc list + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + delete_pc_by_id "$ID" +} + +@test "update provider configuration - success" { + NAME="test-config-5" + UPDATED_NAME="test-config-5-updated" + UPDATED_CONFIG='{"cached": "key-updated"}' + BASE64_UPDATED_CONFIG='eyJjYWNoZWQiOiAia2V5LXVwZGF0ZWQifQ==' + run_otdfctl_key_pc create --name "$NAME" --config '"$VALID_CONFIG"' --json + assert_success + ID=$(echo "$output" | jq -r '.id') + run_otdfctl_key_pc update --id "$ID" --name "$UPDATED_NAME" --config "'$UPDATED_CONFIG'" --json + assert_success + assert_equal "$(echo "$output" | jq -r .id)" "$ID" + assert_equal "$(echo "$output" | jq -r .name)" "$UPDATED_NAME" + assert_equal "$(echo "$output" | jq -r .config_json)" "$BASE64_UPDATED_CONFIG" + delete_pc_by_id "$ID" +} + +@test "fail to update provider configuration - missing id" { + run_otdfctl_key_pc update --name test-config + assert_failure + assert_output --partial "Flag '--id' is required" +} + +@test "fail to update provider configuration - no optional flags" { + NAME="test-config-6" + run_otdfctl_key_pc create --name "$NAME" --config '"$VALID_CONFIG"' --json + ID=$(echo "$output" | jq -r '.id') + run_otdfctl_key_pc update --id "$ID" + assert_failure + assert_output --partial "At least one field (name, config, or metadata labels) must be updated" + delete_pc_by_id "$ID" +} + +@test "fail to update provider configuration - invalid config format" { + NAME="test-config-7" + run_otdfctl_key_pc create --name "$NAME" --config '"$VALID_CONFIG"' --json + assert_success + ID=$(echo "$output" | jq -r '.id') + run_otdfctl_key_pc update --id "$ID" --config "{invalid: json}" + assert_failure + assert_output --partial "invalid_argument" + delete_pc_by_id "$ID" +} + +@test "delete provider configuration -- success" { + NAME="test-config-8" + run_otdfctl_key_pc create --name "$NAME" --config '"$VALID_CONFIG"' --json + ID=$(echo "$output" | jq -r '.id') + run_otdfctl_key_pc delete --id "$ID" --force + assert_success +} + +@test "delete provider configuration fail -- no id" { + run_otdfctl_key_pc delete + assert_failure + assert_output --partial "Flag '--id' is required" +} + +@test "delete provider configuration fail -- no force" { + NAME="test-config-9" + run_otdfctl_key_pc create --name "$NAME" --config '"$VALID_CONFIG"' --json + ID=$(echo "$output" | jq -r '.id') + run_otdfctl_key_pc delete --id "$ID" + assert_failure + assert_output --partial "The '--force' flag is required for this operation" + delete_pc_by_id "$ID" +} + +teardown_file() { + # clear out all test env vars + unset HOST WITH_CREDS DEBUG_LEVEL VALID_CONFIG BASE64_CONFIG +} \ No newline at end of file diff --git a/otdfctl/e2e/registered-resources.bats b/otdfctl/e2e/registered-resources.bats new file mode 100644 index 0000000000..4bc095bed6 --- /dev/null +++ b/otdfctl/e2e/registered-resources.bats @@ -0,0 +1,538 @@ +#!/usr/bin/env bats + +# Tests for registered resources + +setup_file() { + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # create namespace first (needed for registered resource creation) + export NS_NAME="test-rr.org" + NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create --name "$NS_NAME" --json | jq -r '.id') + export NS_ID + + # create registered resource used in registered resource values tests + export RR_NAME="test_rr_for_values" + RR_ID=$(./otdfctl $HOST $WITH_CREDS policy registered-resources create --name "$RR_NAME" --namespace "$NS_ID" --json | jq -r '.id') + export RR_ID + + # create custom action to be used in registered resource values tests + export CUSTOM_ACTION_NAME="test_action_for_values" + CUSTOM_ACTION_ID=$(./otdfctl $HOST $WITH_CREDS policy actions create --name "$CUSTOM_ACTION_NAME" --namespace "$NS_ID" --json | jq -r '.id') + export CUSTOM_ACTION_ID + + # get standard read action id in the same namespace used for RR tests + export READ_ACTION_NAME="read" + READ_ACTION_ID=$(./otdfctl $HOST $WITH_CREDS policy actions get --name "$READ_ACTION_NAME" --namespace "$NS_ID" --json | jq -r '.id') + export READ_ACTION_ID + export ATTR_NAME=test_rr_attr + attr_id=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name "$ATTR_NAME" --rule ANY_OF -l key=value --json | jq -r '.id') + ATTR_VAL_1_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$attr_id" --value test_reg_res_attr__val_1 --json | jq -r '.id') + export ATTR_VAL_1_ID + ATTR_VAL_1_FQN=$(./otdfctl $HOST $WITH_CREDS policy attributes values get --id "$ATTR_VAL_1_ID" --json | jq -r '.fqn') + export ATTR_VAL_1_FQN + ATTR_VAL_2_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$attr_id" --value test_reg_res_attr__val_2 --json | jq -r '.id') + export ATTR_VAL_2_ID + ATTR_VAL_2_FQN=$(./otdfctl $HOST $WITH_CREDS policy attributes values get --id "$ATTR_VAL_2_ID" --json | jq -r '.fqn') + export ATTR_VAL_2_FQN + + echo "FQN: $ATTR_VAL_1_FQN" +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_reg_res () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy registered-resources $*" + } + run_otdfctl_reg_res_values () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy registered-resources values $*" + } +} + +teardown_file() { + # remove the registered resource used in registered resource values tests + ./otdfctl $HOST $WITH_CREDS policy registered-resources delete --id "$RR_ID" --force + + # remove the custom action used in registered resource values tests + ./otdfctl $HOST $WITH_CREDS policy actions delete --id "$CUSTOM_ACTION_ID" --force + + # remove the namespace and cascade delete attributes and values used in registered resource values tests + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$NS_ID" --force + + # clear out all test env vars + unset HOST WITH_CREDS RR_NAME RR_ID CUSTOM_ACTION_NAME CUSTOM_ACTION_ID READ_ACTION_NAME READ_ACTION_ID NS_NAME NS_ID ATTR_NAME ATTR_VAL_1_ID ATTR_VAL_1_FQN ATTR_VAL_2_ID ATTR_VAL_2_FQN +} + +@test "Create a registered resource - Good" { + # with a namespace + run_otdfctl_reg_res create --name test_create_rr --namespace "$NS_ID" + assert_output --partial "SUCCESS" + assert_line --regexp "Name.*test_create_rr" + assert_line --regexp "Namespace.*https://$NS_NAME" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + + # cleanup + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res delete --id $created_id --force + + # without a namespace (should default to un-namespaced) + run_otdfctl_reg_res create --name test_create_rr_no_ns --json + assert_success + [ "$(echo "$output" | jq -r '.name')" = "test_create_rr_no_ns" ] + # ensure namespace is empty for un-namespaced resources + [ "$(echo "$output" | jq -r '.namespace.fqn // empty')" = "" ] + + created_id=$(echo "$output" | jq -r '.id') + run_otdfctl_reg_res delete --id $created_id --force +} + +@test "Create a registered resource - Bad" { + # bad resource names + run_otdfctl_reg_res create --name ends_underscored_ --namespace "$NS_ID" + assert_failure + run_otdfctl_reg_res create --name -first-char-hyphen --namespace "$NS_ID" + assert_failure + run_otdfctl_reg_res create --name inval!d.chars --namespace "$NS_ID" + assert_failure + + # missing flags + run_otdfctl_reg_res create + assert_failure + assert_output --partial "Flag '--name' is required" + + # conflict + run_otdfctl_reg_res create --name test_create_rr_conflict --namespace "$NS_ID" + assert_output --partial "SUCCESS" + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res create --name test_create_rr_conflict --namespace "$NS_ID" + assert_failure + assert_output --partial "already_exists" + + # cleanup + run_otdfctl_reg_res delete --id $created_id --force +} + +@test "Get a registered resource - Good" { + # setup a resource to get + run_otdfctl_reg_res create --name test_get_rr --namespace "$NS_ID" + assert_success + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # get by id + run_otdfctl_reg_res get --id "$created_id" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_get_rr" ] + + # get by name + run_otdfctl_reg_res get --name test_get_rr --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_get_rr" ] + + # get by name + namespace ID + run_otdfctl_reg_res get --name test_get_rr --namespace "$NS_ID" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_get_rr" ] + [ "$(echo "$output" | jq -r '.namespace.fqn')" = "https://$NS_NAME" ] + + # get by name + namespace FQN + run_otdfctl_reg_res get --name test_get_rr --namespace "https://$NS_NAME" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_get_rr" ] + + # cleanup + run_otdfctl_reg_res delete --id $created_id --force +} + +@test "Get a registered resource - Bad" { + run_otdfctl_reg_res get + assert_failure + assert_output --partial "Either 'id' or 'name' must be provided" + + run_otdfctl_reg_res get --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" +} + +@test "List registered resources" { + # setup registered resources to list + run_otdfctl_reg_res create --name test_list_rr_1 --namespace "$NS_ID" + reg_res1_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res create --name test_list_rr_2 --namespace "$NS_ID" + reg_res2_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + run_otdfctl_reg_res list + assert_success + assert_output --partial "$reg_res1_id" + assert_output --partial "test_list_rr_1" + assert_output --partial "$reg_res2_id" + assert_output --partial "test_list_rr_2" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + run_otdfctl_reg_res list --json + assert_success + assert_output --partial "$reg_res1_id" + assert_output --partial "test_list_rr_1" + assert_output --partial "$reg_res2_id" + assert_output --partial "test_list_rr_2" + [[ $(echo "$output" | jq -r '.pagination.total') -ge 1 ]] + + # cleanup + run_otdfctl_reg_res delete --id $reg_res1_id --force + run_otdfctl_reg_res delete --id $reg_res2_id --force +} + +@test "List registered resources supports sort and order flags" { + sort_prefix="sort_rr_${BATS_TEST_NUMBER}_$RANDOM" + run_otdfctl_reg_res create --name "${sort_prefix}_alpha" --namespace "$NS_ID" --json + rr_a_id=$(echo "$output" | jq -r '.id') + run_otdfctl_reg_res create --name "${sort_prefix}_bravo" --namespace "$NS_ID" --json + rr_b_id=$(echo "$output" | jq -r '.id') + run_otdfctl_reg_res create --name "${sort_prefix}_charlie" --namespace "$NS_ID" --json + rr_c_id=$(echo "$output" | jq -r '.id') + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.resources[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$rr_a_id,$rr_b_id,$rr_c_id" + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.resources[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$rr_c_id,$rr_b_id,$rr_a_id" + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$rr_a_id" --arg b "$rr_b_id" --arg c "$rr_c_id" '[.resources[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$rr_a_id,$rr_b_id,$rr_c_id" + + run_otdfctl_reg_res update --id "$rr_a_id" --label sort=a --json + assert_success + run_otdfctl_reg_res update --id "$rr_b_id" --label sort=b --json + assert_success + run_otdfctl_reg_res update --id "$rr_c_id" --label sort=c --json + assert_success + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$rr_a_id" --arg b "$rr_b_id" --arg c "$rr_c_id" '[.resources[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$rr_a_id,$rr_b_id,$rr_c_id" + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.resources[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$rr_c_id,$rr_b_id,$rr_a_id" + + run_otdfctl_reg_res list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$rr_a_id" --arg b "$rr_b_id" --arg c "$rr_c_id" '[.resources[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$rr_a_id,$rr_b_id,$rr_c_id" + + run_otdfctl_reg_res delete --id "$rr_a_id" --force + run_otdfctl_reg_res delete --id "$rr_b_id" --force + run_otdfctl_reg_res delete --id "$rr_c_id" --force +} + +@test "Update registered resource" { + # setup a resource to update + run_otdfctl_reg_res create --name test_update_rr --namespace "$NS_ID" + assert_success + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # force replace labels + run_otdfctl_reg_res update --id "$created_id" -l key=other --force-replace-labels + assert_success + assert_line --regexp "Id.*$created_id" + assert_line --regexp "Name.*test_update_rr" + assert_line --regexp "Labels.*key: other" + refute_output --regexp "Labels.*key: value" + refute_output --regexp "Labels.*test: true" + refute_output --regexp "Labels.*test: true" + + # renamed + run_otdfctl_reg_res update --id "$created_id" --name test_renamed_rr + assert_success + assert_line --regexp "Id.*$created_id" + assert_line --regexp "Name.*test_renamed_rr" + refute_output --regexp "Name.*test_update_rr" + + # cleanup + run_otdfctl_reg_res delete --id $created_id --force +} + +@test "Delete registered resource - Good" { + # setup a resource to delete + run_otdfctl_reg_res create --name test_delete_rr --namespace "$NS_ID" + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + run_otdfctl_reg_res delete --id "$created_id" --force + assert_success +} + +@test "Delete registered resource - Bad" { + # no id + run_otdfctl_reg_res delete + assert_failure + assert_output --partial "Flag '--id' is required" + + # invalid id + run_otdfctl_reg_res delete --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" +} + +# Tests for registered resource values + +@test "Create a registered resource value - Good" { + # simple by resource ID + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val + assert_output --partial "SUCCESS" + assert_line --regexp "Value.*test_create_rr_val" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + created_id_simple=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # simple by resource name + run_otdfctl_reg_res_values create --resource "$RR_NAME" --value test_create_rr_val_by_res_name + assert_output --partial "SUCCESS" + assert_line --regexp "Value.*test_create_rr_val" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + created_id_simple_by_res_name=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # with action attribute values + # TODO(namespaced-actions): switch custom action identifier back to name when RR action resolution is namespaced. + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val_with_action_attr_vals --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_FQN\"" --action-attribute-value "\"$CUSTOM_ACTION_ID;$ATTR_VAL_2_ID\"" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" != "" ] + [ "$(echo "$output" | jq -r '.value')" = "test_create_rr_val_with_action_attr_vals" ] + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$READ_ACTION_ID"'" and .action.name == "'"$READ_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_1_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_1_FQN"'")')" = "true" ] + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$CUSTOM_ACTION_ID"'" and .action.name == "'"$CUSTOM_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_2_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_2_FQN"'")')" = "true" ] + created_id_with_action_attr_vals=$(echo "$output" | jq -r '.id') + + # cleanup + run_otdfctl_reg_res_values delete --id $created_id_simple --force + run_otdfctl_reg_res_values delete --id $created_id_simple_by_res_name --force + run_otdfctl_reg_res_values delete --id $created_id_with_action_attr_vals --force +} + +@test "Create a registered resource value includes FQN in JSON output" { + value="test_create_rr_val_fqn" + expected_fqn="https://$NS_NAME/reg_res/$RR_NAME/value/$value" + + run_otdfctl_reg_res_values create --resource "$RR_ID" --value "$value" --json + assert_success + created_id=$(echo "$output" | jq -r '.id') + assert_equal "$(echo "$output" | jq -r '.value')" "$value" + assert_equal "$(echo "$output" | jq -r '.fqn')" "$expected_fqn" + + run_otdfctl_reg_res_values get --id "$created_id" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.id')" "$created_id" + assert_equal "$(echo "$output" | jq -r '.fqn')" "$expected_fqn" + + run_otdfctl_reg_res_values delete --id "$created_id" --force +} + +@test "Create a registered resource value - Bad" { + # bad resource value names + run_otdfctl_reg_res_values create --resource "$RR_ID" --value ends_underscored_ + assert_failure + run_otdfctl_reg_res_values create --resource "$RR_ID" --value -first-char-hyphen + assert_failure + run_otdfctl_reg_res_values create --resource "$RR_ID" --value inval!d.chars + assert_failure + + # missing flag + run_otdfctl_reg_res_values create + assert_failure + assert_output --partial "Flag '--resource' is required" + run_otdfctl_reg_res_values create --resource "$RR_ID" + assert_failure + assert_output --partial "Flag '--value' is required" + + # bad action attribute value arg separator (not a semicolon) + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val_bad_aav --action-attribute-value "\"$READ_ACTION_ID:$ATTR_VAL_1_ID\"" + assert_failure + assert_output --partial "Invalid action attribute value arg format" + + # non-existent resource name + run_otdfctl_reg_res_values create --resource invalid_rr --value test_create_rr_val_bad_aav_action_name + assert_failure + assert_output --partial "Failed to find registered resource (name: invalid_rr)" + + # conflict + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val_conflict + assert_output --partial "SUCCESS" + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val_conflict + assert_failure + assert_output --partial "already_exists" + + # cleanup + run_otdfctl_reg_res_values delete --id $created_id --force +} + +@test "Get a registered resource value - Good" { + # setup a resource value to get + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_get_rr_val --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_ID\"" + assert_success + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # get by id + run_otdfctl_reg_res_values get --id "$created_id" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.value')" = "test_get_rr_val" ] + [ "$(echo "$output" | jq -r '.fqn')" = "https://$NS_NAME/reg_res/$RR_NAME/value/test_get_rr_val" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].action.id')" = "$READ_ACTION_ID" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].action.name')" = "$READ_ACTION_NAME" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].attribute_value.id')" = "$ATTR_VAL_1_ID" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].attribute_value.fqn')" = "$ATTR_VAL_1_FQN" ] + + # get by fqn + run_otdfctl_reg_res_values get --fqn "https://$NS_NAME/reg_res/$RR_NAME/value/test_get_rr_val" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.value')" = "test_get_rr_val" ] + [ "$(echo "$output" | jq -r '.fqn')" = "https://$NS_NAME/reg_res/$RR_NAME/value/test_get_rr_val" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].action.id')" = "$READ_ACTION_ID" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].action.name')" = "$READ_ACTION_NAME" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].attribute_value.id')" = "$ATTR_VAL_1_ID" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].attribute_value.fqn')" = "$ATTR_VAL_1_FQN" ] + + # cleanup + run_otdfctl_reg_res_values delete --id $created_id --force +} + +@test "Get a registered resource value - Bad" { + run_otdfctl_reg_res_values get + assert_failure + assert_output --partial "Either 'id' or 'fqn' must be provided" + + # invalud id + run_otdfctl_reg_res_values get --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" + + # invalid fqn + run_otdfctl_reg_res_values get --fqn 'not_a_fqn' + assert_failure + assert_output --partial "must be a valid URI" +} + +@test "List registered resource values - Good" { + # setup values to list + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_list_rr_val_1 --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_ID\"" + reg_res_val1_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_list_rr_val_2 + reg_res_val2_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # by resource ID + run_otdfctl_reg_res_values list --resource "$RR_ID" + assert_success + assert_output --partial "$reg_res_val1_id" + assert_output --partial "test_list_rr_val_1" + # check for partial FQN due to possible trimmed output + assert_output --partial "$READ_ACTION_NAME -> https://$NS_NAME/attr/$ATTR_NAME" + assert_output --partial "$reg_res_val2_id" + assert_output --partial "test_list_rr_val_2" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + # by resource name + run_otdfctl_reg_res_values list --resource "$RR_NAME" + assert_success + assert_output --partial "$reg_res_val1_id" + assert_output --partial "test_list_rr_val_1" + assert_output --partial "$READ_ACTION_NAME -> https://$NS_NAME/attr/$ATTR_NAME" + assert_output --partial "$reg_res_val2_id" + assert_output --partial "test_list_rr_val_2" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + run_otdfctl_reg_res_values list --resource "$RR_NAME" --json + assert_success + assert_output --partial "$reg_res_val1_id" + assert_output --partial "test_list_rr_val_1" + assert_output --partial "https://$NS_NAME/attr/$ATTR_NAME/value/test_reg_res_attr__val_1" + assert_output --partial "$reg_res_val2_id" + assert_output --partial "test_list_rr_val_2" + [[ $(echo "$output" | jq -r '.pagination.total') -ge 1 ]] + + # cleanup + run_otdfctl_reg_res_values delete --id $reg_res_val1_id --force + run_otdfctl_reg_res_values delete --id $reg_res_val2_id --force +} + +@test "List registered resource values - Bad" { + # non-existent resource name + run_otdfctl_reg_res_values list --resource 'invalid_rr' + assert_failure + assert_output --partial "Failed to find registered resource (name: invalid_rr)" +} + +@test "Update registered resource values" { + # setup a resource value to update + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_update_rr_val --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_ID\"" + assert_success + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # force replace labels + run_otdfctl_reg_res_values update --id "$created_id" -l key=other --force-replace-labels + assert_success + assert_line --regexp "Id.*$created_id" + assert_line --regexp "Value.*test_update_rr_val" + assert_line --regexp "Labels.*key: other" + refute_output --regexp "Labels.*key: value" + refute_output --regexp "Labels.*test: true" + refute_output --regexp "Labels.*test: true" + + # renamed + run_otdfctl_reg_res_values update --id "$created_id" --value test_renamed_rr_val + assert_success + assert_line --regexp "Id.*$created_id" + assert_line --regexp "Value.*test_renamed_rr_val" + refute_output --regexp "Value.*test_update_rr_val" + + # ensure previous updates without action attribute value args did not clear action attribute values + run_otdfctl_reg_res_values get --id "$created_id" --json + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$READ_ACTION_ID"'" and .action.name == "'"$READ_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_1_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_1_FQN"'")')" = "true" ] + + # update action attribute values + # TODO(namespaced-actions): switch action identifiers back to names when RR action resolution is namespaced. + run_otdfctl_reg_res_values update --id "$created_id" --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_FQN\"" --action-attribute-value "\"$CUSTOM_ACTION_ID;$ATTR_VAL_2_ID\"" --force --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$READ_ACTION_ID"'" and .action.name == "'"$READ_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_1_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_1_FQN"'")')" = "true" ] + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$CUSTOM_ACTION_ID"'" and .action.name == "'"$CUSTOM_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_2_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_2_FQN"'")')" = "true" ] + + # cleanup + run_otdfctl_reg_res_values delete --id $created_id --force +} + +@test "Delete registered resource value - Good" { + # setup a value to delete + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_delete_rr_val + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + run_otdfctl_reg_res_values delete --id "$created_id" --force + assert_success +} + +@test "Delete registered resource value - Bad" { + # no id + run_otdfctl_reg_res_values delete + assert_failure + assert_output --partial "Flag '--id' is required" + + # invalid id + run_otdfctl_reg_res_values delete --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" +} diff --git a/otdfctl/e2e/resize_terminal.sh b/otdfctl/e2e/resize_terminal.sh new file mode 100755 index 0000000000..a2a65680ff --- /dev/null +++ b/otdfctl/e2e/resize_terminal.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +#### +# Make sure we have a terminal size large enough to test table output +#### + +## Accepts two arguments: rows and columns (both integers) + +# Default terminal size +DEFAULT_ROWS=40 +DEFAULT_COLUMNS=200 + +# Set rows and columns to the defaults or use the provided arguments +ROWS=${1:-$DEFAULT_ROWS} +COLUMNS=${2:-$DEFAULT_COLUMNS} + +set_terminal_size_linux() { + if command -v resize &> /dev/null; then + resize -s "$ROWS" "$COLUMNS" + else + export COLUMNS="$COLUMNS" + export LINES="$ROWS" + fi +} + +set_terminal_size_mac() { + printf '\e[8;%d;%dt' "$ROWS" "$COLUMNS" +} + +set_terminal_size_windows() { + if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + printf '\e[8;%d;%dt' "$ROWS" "$COLUMNS" + else + cmd.exe /c "mode con: cols=$COLUMNS lines=$ROWS" + fi +} + +# Detect the OS and set the terminal size appropriately +case "$OSTYPE" in + linux*) + set_terminal_size_linux + ;; + darwin*) + set_terminal_size_mac + ;; + msys* | cygwin* | win*) + set_terminal_size_windows + ;; + *) + echo "Unsupported OS: $OSTYPE" + ;; +esac \ No newline at end of file diff --git a/otdfctl/e2e/resource-mapping-groups.bats b/otdfctl/e2e/resource-mapping-groups.bats new file mode 100644 index 0000000000..0b52c27328 --- /dev/null +++ b/otdfctl/e2e/resource-mapping-groups.bats @@ -0,0 +1,152 @@ +#!/usr/bin/env bats + +# Tests for resource mapping groups + +setup_file() { + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # Create two namespaced values to be used in other tests + export NS_NAME="resource-mapping-groups.io" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$NS_NAME" --json | jq -r '.id') + export NS_NAME2="resource-mapping-groups-2.io" + export NS2_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$NS_NAME2" --json | jq -r '.id') + ATTR_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name attr1 --rule ANY_OF --json | jq -r '.id') + # Name is prefixed with RMG to avoid conflicts across tests when running in parallel + export RMG_VAL1_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$ATTR_ID" --value val1 --json | jq -r '.id') + + # Create a resource mapping group + export RMG1_NAME="rmgrp-test" + export RMG1_FQN="https://${NS_NAME}/resm/${RMG1_NAME}" + export RMG1_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mapping-groups create --namespace-id "$NS_ID" --name "$RMG1_NAME" --json | jq -r '.id') + + # Create a couple resource mappings to val1 - comma separated + export RM1_TERMS="valueone,valuefirst,first,one" + export RM1_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mappings create --attribute-value-id "$RMG_VAL1_ID" --terms "$RM1_TERMS" --group-id "$RMG1_ID" --json | jq -r '.id') + export RM1_OTHER_TERMS="otherone,othervaluefirst,otherfirst,otherone" + export RM1_OTHER_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mappings create --attribute-value-id "$RMG_VAL1_ID" --terms "$RM1_OTHER_TERMS" --group-id "$RMG1_ID" --json | jq -r '.id') +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_rmg () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy resource-mapping-groups $*" + } + +} + +teardown_file() { + # remove the created namespace with all underneath upon test suite completion + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --force --id "$NS_ID" + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --force --id "$NS2_ID" + + unset HOST WITH_CREDS RMG_VAL1_ID NS_NAME NS_ID NS_NAME2 NS2_ID RM1_TERMS RM1_ID RM1_OTHER_TERMS RM1_OTHER_ID RMG1_NAME RMG1_FQN RMG1_ID +} + +@test "Create resource mapping group" { + # create with multiple terms flags instead of comma-separated + run_otdfctl_rmg create --namespace-id "$NS_ID" --name rmgrp1 + assert_success + assert_output --partial "rmgrp1" + assert_line --regexp "Namespace Id.*$NS_ID" + + run_otdfctl_rmg create --namespace-id "$NS_ID" --name rmgrp1-json --json + assert_success + assert_equal "$(echo "$output" | jq -r '.fqn')" "https://${NS_NAME}/resm/rmgrp1-json" + + # ns id flag must be uuid + run_otdfctl_rmg create --namespace-id "something" --name testing + assert_failure + assert_output --partial "must be a valid UUID" + + # name is required + run_otdfctl_rmg create --namespace-id "$NS_ID" + assert_failure + assert_output --partial "Flag '--name' is required" +} + +@test "Get resource mapping group" { + # table + run_otdfctl_rmg get --id "$RMG1_ID" + assert_success + assert_line --regexp "Id.*$RMG1_ID" + assert_line --regexp "Namespace Id.*$NS_ID" + assert_line --regexp "Name.*$RMG1_NAME" + + # json + run_otdfctl_rmg get --id "$RMG1_ID" --json + assert_success + [ $(echo $output | jq -r '.id') = "$RMG1_ID" ] + [ $(echo $output | jq -r '.namespace_id') = "$NS_ID" ] + [ $(echo $output | jq -r '.name') = "$RMG1_NAME" ] + [ $(echo $output | jq -r '.fqn') = "$RMG1_FQN" ] + + # id required + run_otdfctl_rmg get + assert_failure + assert_output --partial "is required" + run_otdfctl_rmg get --id "test" + assert_failure + assert_output --partial "must be a valid UUID" +} + +@test "Update a resource mapping group" { + NEW_RMG_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mapping-groups create --namespace-id "$NS_ID" --name test-rsmg --json | jq -r '.id') + + # replace the terms + run_otdfctl_rmg update --id "$NEW_RMG_ID" --name "new-rsmg-name" + assert_success + refute_output --partial "test-rsmg" + assert_output --partial "new-rsmg-name" + assert_output --partial "$NS_ID" + + # reassign the namespace being mapped + run_otdfctl_rmg update --id "$NEW_RMG_ID" --namespace-id "$NS2_ID" + assert_success + refute_output --partial "test-rsmg" + assert_output --partial "new-rsmg-name" + refute_output --partial "$NS_ID" + assert_output --partial "$NS2_ID" + + run_otdfctl_rmg update --id "$NEW_RMG_ID" --name "new-rsmg-name-json" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.fqn')" "https://${NS_NAME2}/resm/new-rsmg-name-json" +} + +@test "List resource mapping groups" { + run_otdfctl_rmg list + assert_success + assert_output --partial "$RMG1_ID" + assert_output --partial "$NS_ID" + assert_output --partial "$RMG1_NAME" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + run_otdfctl_rmg list --json + assert_success + [[ "$(echo "$output" | jq -r '.resource_mapping_groups | length')" -ge 1 ]] + found_rmg=$(echo "$output" | jq -c --arg id "$RMG1_ID" '.resource_mapping_groups as $a | ($a | map(.id) | index($id)) as $i | $a[$i]') + assert_equal "$(echo "$found_rmg" | jq -r '.id')" "$RMG1_ID" + assert_equal "$(echo "$found_rmg" | jq -r '.name')" "$RMG1_NAME" + assert_equal "$(echo "$found_rmg" | jq -r '.fqn')" "$RMG1_FQN" + [[ "$(echo "$output" | jq -r '.pagination.total')" -ge 1 ]] + assert_equal "$(echo "$output" | jq -r '.pagination.current_offset')" "null" + assert_equal "$(echo "$output" | jq -r '.pagination.next_offset')" "null" +} + +@test "Delete resource mapping group" { + # --force to avoid indefinite hang waiting for confirmation + run_otdfctl_rmg delete --id "$RMG1_ID" --force + assert_success + assert_line --regexp "Id.*$RMG1_ID" + assert_line --regexp "Namespace Id.*$NS_ID" + assert_line --regexp "Name.*$RMG1_NAME" + + NEW_RMG_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mapping-groups create --namespace-id "$NS_ID" --name rmgrp-delete-json --json | jq -r '.id') + run_otdfctl_rmg delete --id "$NEW_RMG_ID" --force --json + assert_success + assert_equal "$(echo "$output" | jq -r '.fqn')" "https://${NS_NAME}/resm/rmgrp-delete-json" +} diff --git a/otdfctl/e2e/resource-mapping.bats b/otdfctl/e2e/resource-mapping.bats new file mode 100755 index 0000000000..c76208dd37 --- /dev/null +++ b/otdfctl/e2e/resource-mapping.bats @@ -0,0 +1,154 @@ +#!/usr/bin/env bats + +# Tests for resource mappings + +setup_file() { + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # Create two namespaced values to be used in other tests + NS_NAME="resource-mappings.io" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$NS_NAME" --json | jq -r '.id') + ATTR_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name attr1 --rule ANY_OF --json | jq -r '.id') + # Names prefixed with RM to avoid conflicts across tests when running in parallel + export RM_VAL1_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$ATTR_ID" --value val1 --json | jq -r '.id') + export RM_VAL2_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$ATTR_ID" --value val2 --json | jq -r '.id') + + # Create a single resource mapping to val1 - comma separated + export RM1_TERMS="valueone,valuefirst,first,one" + export RM1_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mappings create --attribute-value-id "$RM_VAL1_ID" --terms "$RM1_TERMS" --json | jq -r '.id') + + # Create a resource mapping group + export RMG1_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mapping-groups create --namespace-id "$NS_ID" --name rmgrp-test --json | jq -r '.id') + export RMG2_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mapping-groups create --namespace-id "$NS_ID" --name rmgrp-test-2 --json | jq -r '.id') +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_rm () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy resource-mappings $*" + } + +} + +teardown_file() { + # remove the created namespace with all underneath upon test suite completion + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --force --id "$NS_ID" + + unset HOST WITH_CREDS RM_VAL1_ID RM_VAL2_ID NS_ID RM1_TERMS RM1_ID RMG1_ID RMG2_ID +} + +@test "Create resource mapping" { + # create with multiple terms flags instead of comma-separated + run_otdfctl_rm create --attribute-value-id "$RM_VAL2_ID" --terms "second" --terms "TWO" + assert_success + assert_output --partial "second" + assert_output --partial "TWO" + assert_line --regexp "Attribute Value Id.*$RM_VAL2_ID" + + # value id flag must be uuid + run_otdfctl_rm create --attribute-value-id "val2" --terms "testing" + assert_failure + assert_output --partial "must be a valid UUID" + + # terms are required + run_otdfctl_rm create --attribute-value-id $RM_VAL2_ID + assert_failure + assert_output --partial "must have at least 1 non-empty values" +} + +@test "Create resource mapping in a group" { + # create with multiple terms flags instead of comma-separated + run_otdfctl_rm create --attribute-value-id "$RM_VAL2_ID" --terms "second,TWO" --group-id "$RMG1_ID" + assert_success + assert_output --partial "second" + assert_output --partial "TWO" + assert_line --regexp "Attribute Value Id.*$RM_VAL2_ID" + assert_line --regexp "Group Id.*$RMG1_ID" + + # group id flag must be uuid + run_otdfctl_rm create --attribute-value-id "$RM_VAL2_ID" --terms "testing" --group-id "grp1" + assert_failure + assert_output --partial "must be a valid UUID" +} + +@test "Get resource mapping" { + spaced_terms=$(echo $RM1_TERMS | sed 's/,/, /g') + # table + run_otdfctl_rm get --id "$RM1_ID" + assert_success + assert_line --regexp "Id.*$RM1_ID" + assert_line --regexp "Attribute Value Id.*$RM_VAL1_ID" + assert_line --regexp "Terms.*$spaced_terms" + + # json + run_otdfctl_rm get --id "$RM1_ID" --json + assert_success + [ $(echo $output | jq -r '.id') = "$RM1_ID" ] + [ $(echo $output | jq -r '.attribute_value.id') = "$RM_VAL1_ID" ] + [ $(echo $output | jq -r '.terms | join (",")') = "$RM1_TERMS" ] + + # id required + run_otdfctl_rm get + assert_failure + assert_output --partial "is required" + run_otdfctl_rm get --id "test" + assert_failure + assert_output --partial "must be a valid UUID" +} + +@test "Update a resource mapping" { + NEW_RM_ID=$(./otdfctl $HOST $WITH_CREDS policy resource-mappings create --attribute-value-id "$RM_VAL2_ID" --terms test --terms found --group-id "$RMG1_ID" --json | jq -r '.id') + + # replace the terms + run_otdfctl_rm update --id "$NEW_RM_ID" --terms replaced,new + assert_success + refute_line --regexp "Terms.*test" + refute_line --regexp "Terms.*found" + assert_output --partial "replaced" + assert_output --partial "new" + assert_output --partial "$RM_VAL2_ID" + + # reassign the attribute value being mapped + run_otdfctl_rm update --id "$NEW_RM_ID" --attribute-value-id "$RM_VAL1_ID" + assert_success + refute_line --regexp "Terms.*test" + refute_line --regexp "Terms.*found" + assert_output --partial "replaced" + assert_output --partial "new" + refute_output --partial "$RM_VAL2_ID" + assert_output --partial "$RM_VAL1_ID" +} + +@test "List resource mappings" { + run_otdfctl_rm list + assert_success + assert_output --partial "$RM1_ID" + assert_output --partial "$RM_VAL1_ID" + assert_output --partial "valueone, valuefirst, first" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + run_otdfctl_rm list --json + assert_success + [[ "$(echo "$output" | jq -r '.resource_mappings | length')" -ge 1 ]] + found_rm=$(echo "$output" | jq -c --arg id "$RM1_ID" '.resource_mappings as $a | ($a | map(.id) | index($id)) as $i | $a[$i]') + assert_equal "$(echo "$found_rm" | jq -r '.id')" "$RM1_ID" + assert_equal "$(echo "$found_rm" | jq -r '.attribute_value.id')" "$RM_VAL1_ID" + [[ "$(echo "$output" | jq -r '.pagination.total')" -ge 1 ]] + assert_equal "$(echo "$output" | jq -r '.pagination.current_offset')" "null" + assert_equal "$(echo "$output" | jq -r '.pagination.next_offset')" "null" +} + +@test "Delete resource mapping" { + spaced_terms=$(echo $RM1_TERMS | sed 's/,/, /g') + # --force to avoid indefinite hang waiting for confirmation + run_otdfctl_rm delete --id "$RM1_ID" --force + assert_success + assert_line --regexp "Id.*$RM1_ID" + assert_line --regexp "Attribute Value Id.*$RM_VAL1_ID" + assert_line --regexp "Terms.*$spaced_terms" +} diff --git a/otdfctl/e2e/setup_suite.bash b/otdfctl/e2e/setup_suite.bash new file mode 100755 index 0000000000..f6754fc12c --- /dev/null +++ b/otdfctl/e2e/setup_suite.bash @@ -0,0 +1,31 @@ +#!/bin/bash + +#### +# Make sure we can load BATS dependencies +#### + +setup_suite(){ + + bats_require_minimum_version 1.7.0 + + if [[ "$(which bats)" == *"homebrew"* ]]; then + BATS_LIB_PATH=$(brew --prefix)/lib + fi + + # Check if BATS_LIB_PATH environment variable exists + if [ -z "${BATS_LIB_PATH}" ]; then + # Check if bats bin has homebrew in path name + if [[ "$(which bats)" == *"homebrew"* ]]; then + BATS_LIB_PATH=$(dirname "$(which bats)")/../lib + elif [ -d "/usr/lib/bats-support" ]; then + BATS_LIB_PATH="/usr/lib" + elif [ -d "/usr/local/lib/bats-support" ]; then + # Check if bats-support exists in /usr/local/lib + BATS_LIB_PATH="/usr/local/lib" + fi + fi + echo "BATS_LIB_PATH: $BATS_LIB_PATH" + export BATS_LIB_PATH=$BATS_LIB_PATH + + echo -n '{"clientId":"opentdf","clientSecret":"secret"}' > creds.json +} \ No newline at end of file diff --git a/otdfctl/e2e/subject-condition-sets.bats b/otdfctl/e2e/subject-condition-sets.bats new file mode 100755 index 0000000000..1c7b9aef20 --- /dev/null +++ b/otdfctl/e2e/subject-condition-sets.bats @@ -0,0 +1,237 @@ +#!/usr/bin/env bats + +# Tests for subject condition sets + +setup_file() { + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + export NS_NAME="subject-condition-sets.net" + export NS_FQN="https://$NS_NAME" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$NS_NAME" --json | jq -r '.id') + + export SCS_1='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["marketing"],"subject_external_selector_value":".org.name"},{"operator":1,"subject_external_values":["ShinyThing"],"subject_external_selector_value":".team.name"}],"boolean_operator":1}]}]' + export SCS_2='[{"condition_groups":[{"conditions":[{"operator":3,"subject_external_values":["piedpiper.com","hooli.com"],"subject_external_selector_value":".emailAddress"},{"operator":1,"subject_external_values":["sales"],"subject_external_selector_value":".department"}],"boolean_operator":2}]}]' + export SCS_3='[{"condition_groups":[{"conditions":[{"operator":2,"subject_external_values":["CoolTool","RadService"],"subject_external_selector_value":".team.name"}],"boolean_operator":2}]}]' +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_scs () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy subject-condition-sets $*" + } + + run_delete_scs () { + # Capture the first argument as the ID + local id="$1" + + run sh -c "./otdfctl $HOST $WITH_CREDS policy scs delete --id $id --force" + } +} + +teardown_file() { + # remove the created namespace with all underneath upon test suite completion + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --force --id "$NS_ID" + + # clear out all test env vars + unset HOST WITH_CREDS NS_NAME NS_FQN NS_ID SCS_1 SCS_2 SCS_3 + + rm scs.json +} + +@test "Create a Subject Condition Set (SCS) - from file" { + echo -n "$SCS_1" > scs.json + + run_otdfctl_scs create --subject-sets-file-json scs.json -l fromfile=true + assert_success + assert_output --partial "Id" + assert_output --partial "Namespace" + assert_output --partial "SubjectSets" + assert_output --partial ".org.name" + assert_output --partial "SUBJECT_MAPPING_OPERATOR_ENUM_IN" + assert_line --regexp "fromfile: true" +} + +@test "Create a Subject Condition Set (SCS) - from flag value JSON" { + run ./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" + assert_success + assert_output --partial "Id" + assert_output --partial "Namespace" + assert_output --partial "SubjectSets" + assert_output --partial ".emailAddress" + assert_output --partial "SUBJECT_MAPPING_OPERATOR_ENUM_IN" +} + +@test "Get a SCS" { + CREATED_ID=$(./otdfctl $HOST $WITH_CREDS policy scs add -s "$SCS_3" -l hello=world --json | jq -r '.id') + run_otdfctl_scs get --id "$CREATED_ID" + assert_success + assert_line --regexp "Id.*$CREATED_ID" + assert_output --partial "Namespace" + assert_output --partial "Labels" + assert_output --partial "hello: world" + assert_output --partial "Created At" + assert_output --partial "Updated At" + assert_output --partial ".team.name" + assert_output --partial "SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN" + + run_delete_scs "$CREATED_ID" +} + +@test "Update a SCS - from flag value JSON" { + echo -n "$SCS_1" > scs.json + CREATED_ID=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets-file-json scs.json -l fromfile=true --json | jq -r '.id') + + run ./otdfctl $HOST $WITH_CREDS policy scs update --subject-sets "$SCS_2" --id "$CREATED_ID" + assert_success + assert_output --partial ".emailAddress" + assert_output --partial "SUBJECT_MAPPING_OPERATOR_ENUM_IN" + assert_output --partial "fromfile: true" + refute_output --partial ".org.name" + + run_delete_scs "$CREATED_ID" +} + +@test "Update a SCS - from file" { + CREATED_ID=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" -l fromfile=false --json | jq -r '.id') + + echo -n "$SCS_3" > scs.json + + run ./otdfctl $HOST $WITH_CREDS policy scs update --subject-sets-file-json scs.json --id "$CREATED_ID" -l fromfile=true + assert_success + refute_output --partial ".emailAddress" + refute_output --partial "SUBJECT_MAPPING_OPERATOR_ENUM_IN" + assert_output --partial ".team.name" + assert_output --partial "fromfile: true" + assert_output --partial "SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN" + + run_delete_scs "$CREATED_ID" +} + +@test "List SCS" { + CREATED_ID=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" -l fromfile=false --json | jq -r '.id') + + run_otdfctl_scs list + assert_success + assert_output --partial "$CREATED_ID" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + run_otdfctl_scs list --json + assert_success + assert_output --partial ".department" + assert_output --partial ".emailAddress" + assert_output --partial ".team.name" + assert_output --partial ".org.name" + matched_object=$(echo "$output" | jq -r --arg id "$CREATED_ID" '.subject_condition_sets[] | select(.id == $id)') + [ $(echo "$matched_object" | jq -r '.subject_sets[0].condition_groups[0].conditions[0].subject_external_values | contains(["piedpiper.com"])') = "true" ] + [ $(echo "$matched_object" | jq -r '.metadata.labels.fromfile') = "false" ] + [[ $(echo "$output" | jq -r ".pagination.total") -ge 1 ]] + + + # validate deletion + run_delete_scs "$CREATED_ID" + assert_success + assert_output --partial "$CREATED_ID" +} + +@test "List SCS supports sort and order flags" { + scs_a_id=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_1" --namespace "$NS_ID" --json | jq -r '.id') + scs_b_id=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" --namespace "$NS_ID" --json | jq -r '.id') + scs_c_id=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_3" --namespace "$NS_ID" --json | jq -r '.id') + + run_otdfctl_scs list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_a_id,$scs_b_id,$scs_c_id" + + run_otdfctl_scs list --namespace "$NS_ID" --sort created_at --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_c_id,$scs_b_id,$scs_a_id" + + run ./otdfctl $HOST $WITH_CREDS policy scs update --id "$scs_a_id" --subject-sets "$SCS_1" --label sort=a --json + assert_success + run ./otdfctl $HOST $WITH_CREDS policy scs update --id "$scs_b_id" --subject-sets "$SCS_2" --label sort=b --json + assert_success + run ./otdfctl $HOST $WITH_CREDS policy scs update --id "$scs_c_id" --subject-sets "$SCS_3" --label sort=c --json + assert_success + + run_otdfctl_scs list --namespace "$NS_ID" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_a_id,$scs_b_id,$scs_c_id" + + run_otdfctl_scs list --namespace "$NS_ID" --sort created_at --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_c_id,$scs_b_id,$scs_a_id" + + run_otdfctl_scs list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_a_id,$scs_b_id,$scs_c_id" + + run_delete_scs "$scs_a_id" + run_delete_scs "$scs_b_id" + run_delete_scs "$scs_c_id" +} + +@test "Create a SCS with namespace id" { + run ./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" --namespace "$NS_ID" + assert_output --partial "Id" + assert_output --partial "Namespace" + assert_output --partial "SubjectSets" +} + +@test "Create a SCS with namespace FQN" { + run ./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" --namespace "$NS_FQN" + assert_output --partial "Id" + assert_output --partial "Namespace" + assert_output --partial "SubjectSets" +} + +@test "List SCS with namespace filter" { + test_ns_name="scs-list-$BATS_TEST_NUMBER.net" + test_ns_fqn="https://$test_ns_name" + test_ns_id=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$test_ns_name" --json | jq -r '.id') + CREATED_ID=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" --namespace "$test_ns_id" --json | jq -r '.id') + + run_otdfctl_scs list --namespace "$test_ns_id" + assert_success + assert_output --partial "$CREATED_ID" + assert_output --partial "Total" + + run_otdfctl_scs list --namespace "$test_ns_id" --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg id "$CREATED_ID" '.subject_condition_sets[] | select(.id == $id) | .id')" "$CREATED_ID" + # Ensure only SCS from the filtered namespace are returned + assert_equal "$(echo "$output" | jq -r --arg ns "$test_ns_id" '[.subject_condition_sets[] | select(.namespace.id != $ns)] | length')" "0" + + # Filter by namespace FQN + run_otdfctl_scs list --namespace "$test_ns_fqn" --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg id "$CREATED_ID" '.subject_condition_sets[] | select(.id == $id) | .id')" "$CREATED_ID" + # Ensure only SCS from the filtered namespace are returned + assert_equal "$(echo "$output" | jq -r --arg ns "$test_ns_id" '[.subject_condition_sets[] | select(.namespace.id != $ns)] | length')" "0" + + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --force --id "$test_ns_id" +} + +@test "Prune SCS - deletes unmapped SCS alone" { + echo -n "$SCS_1" > scs.json + + UNMAPPED_ID=$(./otdfctl policy scs create --subject-sets-file-json scs.json $HOST $WITH_CREDS --json | jq -r '.id') + MAPPED_ID=$(./otdfctl policy scs create --subject-sets "$SCS_2" $HOST $WITH_CREDS --json | jq -r '.id') + + # create a namespace, definition, value, sm to the value with the MAPPED_ID SCS + NS_ID=$(./otdfctl policy attributes namespaces create -n 'scs.net' $HOST $WITH_CREDS --json | jq -r '.id') + ATTR_ID=$(./otdfctl policy attributes create -n 'my_attr' --namespace "$NS_ID" -r "ANY_OF" $HOST $WITH_CREDS --json | jq -r '.id') + VAL_ID=$(./otdfctl policy attributes values create -v 'my_value' -a "$ATTR_ID" $HOST $WITH_CREDS --json | jq -r '.id') + + run ./otdfctl policy sm create --action 'delete' -a "$VAL_ID" --subject-condition-set-id "$MAPPED_ID" $HOST $WITH_CREDS + assert_success + + run_otdfctl_scs prune --force + assert_success + assert_output --partial "$UNMAPPED_ID" + refute_output --partial "$MAPPED_ID" +} diff --git a/otdfctl/e2e/subject-mapping.bats b/otdfctl/e2e/subject-mapping.bats new file mode 100755 index 0000000000..a92107fcfe --- /dev/null +++ b/otdfctl/e2e/subject-mapping.bats @@ -0,0 +1,275 @@ +#!/usr/bin/env bats + +# Tests for subject mappings + +setup_file() { + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # Create two namespaced values to be used in other tests + export NS_NAME="subject-mappings-test.net" + export NS_FQN="https://$NS_NAME" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$NS_NAME" --json | jq -r '.id') + ATTR_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name attr1 --rule ANY_OF --json | jq -r '.id') + # Names prefixed with SM to avoid conflicts across tests when running in parallel + export SM_VAL1_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$ATTR_ID" --value val1 --json | jq -r '.id') + export SM_VAL2_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$ATTR_ID" --value value2 --json | jq -r '.id') + + export SCS_1='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["ShinyThing"],"subject_external_selector_value":".team.name"},{"operator":2,"subject_external_values":["marketing"],"subject_external_selector_value":".org.name"}],"boolean_operator":1}]}]' + export SCS_2='[{"condition_groups":[{"conditions":[{"operator":2,"subject_external_values":["CoolTool","RadService"],"subject_external_selector_value":".team.name"},{"operator":1,"subject_external_values":["sales"],"subject_external_selector_value":".org.name"}],"boolean_operator":2}]}]' + + export ACTION_READ_NAME='read' + export ACTION_READ_ID=$(./otdfctl $HOST $WITH_CREDS policy actions get --name "$ACTION_READ_NAME" --json | jq -r '.id') + export ACTION_CREATE_NAME='create' + export ACTION_CREATE_ID=$(./otdfctl $HOST $WITH_CREDS policy actions get --name "$ACTION_CREATE_NAME" --json | jq -r '.id') +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + # invoke binary with credentials + run_otdfctl_sm () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy subject-mappings $*" + } + +} + +teardown_file() { + # remove the created namespace with all underneath upon test suite completion + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --force --id "$NS_ID" + + unset HOST WITH_CREDS SM_VAL1_ID SM_VAL2_ID NS_ID NS_FQN NS_NAME SCS_1 SCS_2 +} + +@test "Create subject mapping" { + # create with simultaneous new SCS + run ./otdfctl $HOST $WITH_CREDS policy subject-mappings create -a "$SM_VAL1_ID" --action "$ACTION_CREATE_NAME" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_2" + assert_success + assert_output --partial "Namespace" + assert_output --partial "Subject Condition Set: Id" + assert_output --partial ".team.name" + assert_line --regexp "Attribute Value Id.*$SM_VAL1_ID" + + # scs is required + run_otdfctl_sm create --attribute-value-id "$SM_VAL2_ID" --action "$ACTION_CREATE_NAME" + assert_failure + assert_output --partial "At least one Subject Condition Set flag [--subject-condition-set-id, --subject-condition-set-new] must be provided" + + # action is required + run_otdfctl_sm create -a "$SM_VAL1_ID" --subject-condition-set-new "$SCS_2" + assert_failure + assert_output --partial "At least one Action [--action] is required" +} + +@test "Match subject mapping" { + # create with simultaneous new SCS + NEW_SCS='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["sales"],"subject_external_selector_value":".department"}],"boolean_operator":2}]}]' + NEW_SM_ID=$(./otdfctl $HOST $WITH_CREDS policy subject-mappings create -a "$SM_VAL2_ID" --action "$ACTION_READ_NAME" --subject-condition-set-new "$NEW_SCS" --json | jq -r '.id') + + run_otdfctl_sm match -x '.department' + assert_success + assert_output --partial "$NEW_SM_ID" + + matched_subject='{"department":"any_department"}' + run ./otdfctl policy sm match --subject "$matched_subject" $HOST $WITH_CREDS + assert_success + assert_output --partial "$NEW_SM_ID" + + # JWT includes 'department' in token claims + run_otdfctl_sm match -s 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50Ijoibm93aGVyZV9zcGVjaWFsIn0.784uXYtfOv4tdM6JRgBMua4bBNDjUGbcr89QQKzCXfU' + assert_success + assert_output --partial "$NEW_SM_ID" + + run_otdfctl_sm match --selector '.not_found' + assert_success + refute_output --partial "$NEW_SM_ID" + + unmatched_subject='{"dept":"nope"}' + run ./otdfctl policy sm match -s "$unmatched_subject" $HOST $WITH_CREDS + assert_success + refute_output --partial "$NEW_SM_ID" + + # JWT lacks 'department' in token claims + run_otdfctl_sm match -s 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhYmMiOiJub3doZXJlX3NwZWNpYWwifQ.H39TXi1gYWRhXIRkfxFJwrZz42eE4y8V5BQX-mg8JAo' + assert_success + refute_output --partial "$NEW_SM_ID" +} + +@test "Get subject mapping" { + run ./otdfctl $HOST $WITH_CREDS policy sm create -a "$SM_VAL2_ID" --action "custom_sm_action_test" --subject-condition-set-new "$SCS_1" --json + assert_success + created=$(echo "$output" | jq -r '.id') + scs_1_id=$(echo "$output" | jq -r '.subject_condition_set.id') + assert_not_equal "$created" "null" + assert_not_equal "$created" "" + assert_not_equal "$scs_1_id" "null" + assert_not_equal "$scs_1_id" "" + + # table + run_otdfctl_sm get --id "$created" + assert_success + assert_line --regexp "Id.*$created" + assert_output --partial "Namespace" + assert_line --regexp "Attribute Value: Id.*$SM_VAL2_ID" + assert_line --regexp "Attribute Value: Value.*value2" + assert_line --regexp "Subject Condition Set: Id.*$scs_1_id" + + # json + run_otdfctl_sm get --id "$created" --json + assert_success + [ "$(echo $output | jq -r '.id')" = "$created" ] + [ "$(echo $output | jq -r '.attribute_value.id')" = "$SM_VAL2_ID" ] + [ "$(echo $output | jq -r '.subject_condition_set.id')" = "$scs_1_id" ] + [ "$(echo $output | jq -r '.actions[0].name')" = "custom_sm_action_test" ] +} + +@test "Update a subject mapping" { + skip "Temporarily disabled [namespaced-actions]: expected action ID assertion is failing in CI" + run ./otdfctl $HOST $WITH_CREDS policy sm create -a "$SM_VAL1_ID" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_2" --json + assert_success + scs_to_update_with_id=$(echo "$output" | jq -r '.subject_condition_set.id') + assert_not_equal "$scs_to_update_with_id" "null" + assert_not_equal "$scs_to_update_with_id" "" + + run ./otdfctl $HOST $WITH_CREDS policy sm create -a "$SM_VAL1_ID" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_1" --json + assert_success + created=$(echo "$output" | jq -r '.id') + assert_not_equal "$created" "null" + assert_not_equal "$created" "" + + # replace the action (always destructive replacement) + run_otdfctl_sm update --id "$created" --action "$ACTION_CREATE_NAME" --json + assert_success + [ "$(echo $output | jq -r '.id')" = "$created" ] + [ "$(echo $output | jq -r '.actions[0].name')" = "$ACTION_CREATE_NAME" ] + [ "$(echo $output | jq -r '.actions[0].id')" = "$ACTION_CREATE_ID" ] + + # reassign the SCS being mapped to + run_otdfctl_sm update --id "$created" --subject-condition-set-id "$scs_to_update_with_id" --json + assert_success + assert_equal "$(echo $output | jq -r '.id')" "$created" + assert_equal "$(echo $output | jq -r '.subject_condition_set.id')" "$scs_to_update_with_id" +} + +@test "List subject mappings" { + created=$(./otdfctl $HOST $WITH_CREDS policy sm create -a "$SM_VAL1_ID" --action "$ACTION_CREATE_NAME" --subject-condition-set-new "$SCS_2" --json | jq -r '.id') + + run_otdfctl_sm list + assert_success + assert_output --partial "$created" + assert_output --partial "Namespace" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + run_otdfctl_sm list --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg id "$created" '.subject_mappings[] | select(.id == $id) | .attribute_value.fqn')" "$NS_FQN/attr/attr1/value/val1" + assert_not_equal $(echo "$output" | jq -r 'pagination') "null" + total=$(echo "$output" | jq -r '.pagination.total') + [[ "$total" -ge 1 ]] +} + +@test "List subject mappings supports sort and order flags" { + sort_attr=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name "sort_sm_${BATS_TEST_NUMBER}_$RANDOM" --rule ANY_OF -v "sort_sm_a" -v "sort_sm_b" -v "sort_sm_c" --json) + sort_val_a_id=$(echo "$sort_attr" | jq -r '.values[0].id') + sort_val_b_id=$(echo "$sort_attr" | jq -r '.values[1].id') + sort_val_c_id=$(echo "$sort_attr" | jq -r '.values[2].id') + sm_a_id=$(./otdfctl $HOST $WITH_CREDS policy sm create --namespace "$NS_ID" -a "$sort_val_a_id" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_1" --json | jq -r '.id') + sm_b_id=$(./otdfctl $HOST $WITH_CREDS policy sm create --namespace "$NS_ID" -a "$sort_val_b_id" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_1" --json | jq -r '.id') + sm_c_id=$(./otdfctl $HOST $WITH_CREDS policy sm create --namespace "$NS_ID" -a "$sort_val_c_id" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_1" --json | jq -r '.id') + + run_otdfctl_sm list --namespace "$NS_ID" --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_a_id,$sm_b_id,$sm_c_id" + + run_otdfctl_sm list --namespace "$NS_ID" --sort created_at --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_c_id,$sm_b_id,$sm_a_id" + + run_otdfctl_sm update --id "$sm_a_id" --label sort=a --json + assert_success + run_otdfctl_sm update --id "$sm_b_id" --label sort=b --json + assert_success + run_otdfctl_sm update --id "$sm_c_id" --label sort=c --json + assert_success + + run_otdfctl_sm list --namespace "$NS_ID" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_a_id,$sm_b_id,$sm_c_id" + + run_otdfctl_sm list --namespace "$NS_ID" --sort created_at --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_c_id,$sm_b_id,$sm_a_id" + + run_otdfctl_sm list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_a_id,$sm_b_id,$sm_c_id" + + run_otdfctl_sm delete --id "$sm_a_id" --force + run_otdfctl_sm delete --id "$sm_b_id" --force + run_otdfctl_sm delete --id "$sm_c_id" --force +} + +@test "Create subject mapping with namespace ID" { + run ./otdfctl $HOST $WITH_CREDS policy subject-mappings create -a "$SM_VAL2_ID" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_2" --namespace "$NS_ID" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.namespace.id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.attribute_value.id')" "$SM_VAL2_ID" + assert_not_equal "$(echo "$output" | jq -r '.subject_condition_set.id')" "null" + assert_equal "$(echo "$output" | jq -r '.. | .subject_external_selector_value? // empty' | head -n 1)" ".team.name" + created=$(echo "$output" | jq -r '.id') + run_otdfctl_sm delete --id "$created" --force + assert_success +} + +@test "Create subject mapping with namespace FQN" { + run ./otdfctl $HOST $WITH_CREDS policy subject-mappings create -a "$SM_VAL2_ID" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_2" --namespace "$NS_FQN" --json + assert_success + assert_equal "$(echo "$output" | jq -r '.namespace.id')" "$NS_ID" + assert_equal "$(echo "$output" | jq -r '.attribute_value.id')" "$SM_VAL2_ID" + assert_not_equal "$(echo "$output" | jq -r '.subject_condition_set.id')" "null" + assert_output --partial ".team.name" + created=$(echo "$output" | jq -r '.id') + + run_otdfctl_sm delete --id "$created" --force + assert_success +} + +@test "List subject mappings with namespace" { + test_ns_name="subject-mappings-list-$BATS_TEST_NUMBER.net" + test_ns_fqn="https://$test_ns_name" + test_ns_id=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$test_ns_name" --json | jq -r '.id') + test_attr_id=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$test_ns_id" --name attr-list --rule ANY_OF --json | jq -r '.id') + test_val_id=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$test_attr_id" --value val-list --json | jq -r '.id') + created=$(./otdfctl $HOST $WITH_CREDS policy sm create -a "$test_val_id" --action "$ACTION_CREATE_NAME" --subject-condition-set-new "$SCS_2" --namespace "$test_ns_id" --json | jq -r '.id') + + run_otdfctl_sm list --namespace "$test_ns_id" + assert_success + assert_output --partial "$created" + assert_output --partial "Total" + + run_otdfctl_sm list --namespace "$test_ns_id" --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg id "$created" '.subject_mappings[] | select(.id == $id) | .id')" "$created" + # Ensure only subject mappings from the filtered namespace are returned + assert_equal "$(echo "$output" | jq -r --arg ns "$test_ns_id" '[.subject_mappings[] | select(.namespace.id != $ns)] | length')" "0" + + # Filter by namespace fqn + run_otdfctl_sm list --namespace "$test_ns_fqn" --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg id "$created" '.subject_mappings[] | select(.id == $id) | .id')" "$created" + # Ensure only subject mappings from the filtered namespace are returned + assert_equal "$(echo "$output" | jq -r --arg ns "$test_ns_id" '[.subject_mappings[] | select(.namespace.id != $ns)] | length')" "0" + + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --force --id "$test_ns_id" +} + +@test "Delete subject mapping" { + # Create a subject mapping specifically for deletion to avoid race conditions in parallel test execution + to_delete=$(./otdfctl $HOST $WITH_CREDS policy sm create -a "$SM_VAL1_ID" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_1" --json | jq -r '.id') + # --force to avoid indefinite hang waiting for confirmation + run_otdfctl_sm delete --id "$to_delete" --force + assert_success + assert_line --regexp "Id.*$to_delete" +} diff --git a/otdfctl/e2e/teardown_suite.bash b/otdfctl/e2e/teardown_suite.bash new file mode 100755 index 0000000000..2cae5e7e19 --- /dev/null +++ b/otdfctl/e2e/teardown_suite.bash @@ -0,0 +1,9 @@ +#!/bin/bash + +#### +# Remove the creds file if it exists +#### + +setup_suite(){ + rm -f ./creds.json +} \ No newline at end of file diff --git a/otdfctl/e2e/testrail-integration/samples-for-virtru-instance/testname-to-testrail-id.virtru.json b/otdfctl/e2e/testrail-integration/samples-for-virtru-instance/testname-to-testrail-id.virtru.json new file mode 100644 index 0000000000..ecc0cc939c --- /dev/null +++ b/otdfctl/e2e/testrail-integration/samples-for-virtru-instance/testname-to-testrail-id.virtru.json @@ -0,0 +1,316 @@ +{ + "Actions": { + "Create a new custom action - Good": "C871535", + "Create a new action - Bad": "C871536", + "Get an action - Good": "C871537", + "Get an action - Bad": "C871538", + "List actions": "C871539", + "Update action": "C871540", + "Delete action - bad": "C871541", + "Delete action - good": "C871542" + }, + "Attributes": { + "Create an attribute - with values": "C793409", + "Create an attribute - Allow Traversal": "C875790", + "Create an attribute - bad": "C793410", + "Get an attribute definition - good": "C793412", + "Get an attribute definition - bad": "C793413", + "Update an attribute definition (safe) - good": "C793414", + "Update an attribute definition (safe) - bad": "C793415", + "List attribute definitions": "C793416", + "List - comprehensive pagination tests": "C871543", + "Deactivate then unsafe reactivate an attribute definition": "C793417", + "Unsafe update an attribute definition": "C793418", + "Unsafe Update preserves allow traversal when unchanged": "C875791", + "Unsafe Update can disallow traversal": "C875792", + "Assign/Remove KAS key from attribute definition - With Attribute Id": "C874449", + "Assign/Remove KAS key from attribute definition - With Attribute FQN": "C874450", + "Assign/Remove KAS key from attribute value - With Value Id": "C874451", + "Assign/Remove KAS key from attribute value - With Value FQN": "C874452", + "KAS key assignment error handling - attribute": "C874453", + "KAS key assignment error handling - attribute value": "C874454", + "List attribute values - Good": "C875684", + "List attribute values - Bad": "C875685" + }, + "Auth": { + "helpful error if wrong platform endpoint host": "C793419", + "helpful error if bad credentials": "C793420", + "helpful error if missing client credentials": "C793421", + "helpful error if missing host": "C793422" + }, + "Encrypt/Decrypt": { + "roundtrip TDF3, no attributes, file": "C793423", + "roundtrip TDF3, no attributes, ec-wrapping, file": "C797126", + "roundtrip TDF3, one attribute, stdin": "C793424", + "roundtrip TDF3, one attribute, mixed case FQN, stdin": "C793425", + "allow traversal with mapped key uses definition when value missing": "C875793", + "allow traversal uses attribute value mapping when value present": "C875794", + "allow traversal with inactive attribute value fails": "C875795", + "roundtrip TDF3, assertions, stdin": "C797127", + "roundtrip TDF3, assertions with HS256 keys and verification, file": "C797128", + "roundtrip TDF3, assertions with RS256 keys and verification, file": "C797129", + "roundtrip TDF3, with target version < 4.3.0": "C871544", + "roundtrip TDF3, with target version >= 4.3.0": "C871545", + "roundtrip TDF3, with allowlist containing platform kas": "C871546", + "roundtrip TDF3, with allowlist containing non existent kas (should fail)": "C871547", + "roundtrip TDF3, ignoring allowlist": "C871548", + "roundtrip TDF3, not entitled to data, no required obligations returned": "C875374", + "roundtrip TDF3, entitled to data, required obligations returned": "C875375" + }, + "KAS Grants": { + "Unassign rejects more than one type of grant at once": "C793434", + "Assign grant prints warning": "C874455", + "Optional ID flag string error message": "C793435" + }, + "KAS Keys Mappings": { + "kas-keys-mappings: list key mappings for a specific key by kas id": "C874696", + "kas-keys-mappings: list key mappings for a specific key by kas name": "C874697", + "kas-keys-mappings: list key mappings for a specific key by kas uri": "C874698", + "kas-keys-mappings: list key mappings with pagination": "C874699", + "kas-keys-mappings: list key mappings - required together are missing": "C874700", + "kas-keys-mappings: list key mappings - mutually exclusive flags": "C874701" + }, + "KAS Keys": { + "kas-keys: create key (local mode, rsa:2048)": "C874456", + "kas-keys: create key (local mode, ec:secp256r1)": "C874457", + "kas-keys: create key (public_key mode)": "C874458", + "kas-keys: create key (remote mode)": "C874459", + "kas-keys: create key (provider mode)": "C874460", + "kas-keys: create key with labels": "C874461", + "kas-keys: create key (missing key-id)": "C874462", + "kas-keys: create key (missing algorithm)": "C874463", + "kas-keys: create key (missing mode)": "C874464", + "kas-keys: create key (local mode, missing wrapping-key-id)": "C874465", + "kas-keys: create key (local mode, missing wrapping-key)": "C874466", + "kas-keys: create key (public_key mode, missing pem)": "C874467", + "kas-keys: create key (remote mode, missing pem)": "C874468", + "kas-keys: create key (remote mode, missing provider-config-id)": "C874469", + "kas-keys: create key (remote mode, missing wrapping-key-id)": "C874470", + "kas-keys: create key (provider mode, missing wrapping-key-id)": "C874471", + "kas-keys: create key (provider mode, missing provider-config-id)": "C874472", + "kas-keys: create key (remote mode, pem not base64)": "C874473", + "kas-keys: create key (public_key mode, pem not base64)": "C874474", + "kas-keys: create key (public_key mode, invalid PEM content)": "C875376", + "kas-keys: create key (public_key mode, EC key with RSA algorithm)": "C875377", + "kas-keys: create key (missing kas identifier)": "C874475", + "kas-keys: create key (using kasName)": "C874476", + "kas-keys: create key (using kasUri)": "C874477", + "kas-keys: create key (invalid algorithm value)": "C874478", + "kas-keys: create key (invalid mode value)": "C874479", + "kas-keys: create key (duplicate key-id)": "C874480", + "kas-keys: create key (invalid kas identifier)": "C874481", + "kas-keys: create key (invalid hex encoded wrapping-key)": "C874482", + "kas-keys: get key by system ID": "C874483", + "kas-keys: get key by user key-id and kasId": "C874484", + "kas-keys: get key by user key-id and kasName": "C874485", + "kas-keys: get key by user key-id and kasUri": "C874486", + "kas-keys: get key (failure: only key-id, missing KAS identifier)": "C874487", + "kas-keys: get key (failure: only kas, missing key-id or system id)": "C874488", + "kas-keys: get key (not found by system ID)": "C874489", + "kas-keys: get key (not found by user key-id and kas)": "C874490", + "kas-keys: update key labels (add)": "C874491", + "kas-keys: update key labels (replace)": "C874492", + "kas-keys: update key (not found)": "C874493", + "kas-keys: update key (missing id)": "C874494", + "kas-keys: list keys (default limit and offset)": "C874495", + "kas-keys: list keys (pagination with limit and offset)": "C874496", + "kas-keys: list keys (filter by algorithm rsa:2048)": "C874497", + "kas-keys: list keys (filter by kas)": "C874498", + "kas-keys: list keys (filter by kasName)": "C874499", + "kas-keys: list keys (filter by kasUri)": "C874500", + "kas-keys: list legacy keys": "C875225", + "kas-keys: list keys (invalid algorithm)": "C874501", + "kas-keys: list keys (legacy=invalid)": "C875226", + "kas-keys: rotate key": "C874502", + "kas-keys: rotate key (missing key)": "C874503", + "kas-keys: rotate key (missing key-id)": "C874504", + "kas-keys: rotate key (missing algorithm)": "C874505", + "kas-keys: rotate key (missing mode)": "C874506", + "kas-keys: rotate key (local mode, missing wrapping-key-id)": "C874507", + "kas-keys: rotate key (local mode, missing wrapping-key)": "C874508", + "kas-keys: rotate key (public_key mode, missing public-key-pem)": "C874509", + "kas-keys: rotate key (remote mode, missing provider-config-id)": "C874510", + "kas-keys: rotate key (invalid algorithm)": "C874511", + "kas-keys: rotate key (invalid mode)": "C874512", + "kas-keys: rotate key (invalid hex encoded wrapping-key)": "C874513", + "kas-keys: import key successful": "C874682", + "kas-keys: import key successful (legacy=true)": "C875227", + "kas-keys: import key successful (legacy=false)": "C875228", + "kas-keys: import key failure (legacy=invalid)": "C875229", + "kas-keys: import key failure - missing required private key": "C874683", + "kas-keys: import key failure - invalid wrapping key": "C874684", + "kas-keys: import key failure - invalid public key PEM": "C874685", + "kas-keys: import key failure - invalid private key PEM": "C874687", + "kas-keys: import key failure - invalid algorithm": "C874688", + "kas-keys: import key failure - missing wrapping key ID": "C874689", + "kas-keys: import key failure - missing wrapping key": "C874690", + "kas-keys: delete key": "C874691", + "kas-keys: delete key failure - (missing id)": "C874692", + "kas-keys: delete key failure - (missing key-id)": "C874693", + "kas-keys: delete key failure - (missing kas-uri)": "C874694" + }, + "KAS Registry": { + "create KAS registration with invalid URI - fails": "C797131", + "create KAS registration with duplicate URI - fails": "C797132", + "create KAS registration with duplicate name - fails": "C797133", + "create KAS registration with invalid name - fails": "C797134", + "update registered KAS": "C793438", + "update registered KAS with invalid URI - fails": "C797135", + "update registered KAS with invalid name - fails": "C797136", + "list registered KASes": "C793439" + }, + "Base Key": { + "base-key: get (initially no base key should be set for a new KAS)": "C874514", + "base-key: set by --key (uuid)": "C874515", + "base-key: set by --key(id) and --kas(id)": "C874516", + "base-key: get (after setting a base key)": "C874517", + "base-key: set by --key(id) and --kas(name)": "C874518", + "base-key: set by --key(id) and --kas(uri)": "C874519", + "base-key: set, get, and verify previous base key": "C874520", + "base-key: set (missing kas identifier)": "C874521", + "base-key: set (missing key identifier: id or keyId)": "C874522", + "base-key: set (using non-existent keyId)": "C874523", + "base-key: set (using non-existent kasId)": "C874524" + }, + "Logging": { + "version is logged to stderr when debug logging enabled": "C875686", + "version is logged to stderr when debug enabled": "C875687" + }, + "Namespaces": { + "Create a namespace - Good": "C793440", + "Create a namespace - Bad": "C793441", + "Get a namespace - Good": "C793442", + "Get a namespace - Bad": "C793443", + "List namespaces - when active": "C793444", + "Update namespace - Safe": "C793445", + "Update namespace - Unsafe": "C793446", + "Assign/Remove KAS key from namespace - With Namespace ID": "C874527", + "Assign/Remove KAS key from namespace - With Namespace FQN": "C874528", + "KAS key assignment error handling - namespace": "C874529", + "Deactivate namespace": "C793447", + "List namespaces - when inactive": "C793448", + "Unsafe reactivate namespace": "C793449", + "List namespaces - when reactivated": "C793450", + "Unsafe delete namespace": "C793451", + "List namespaces - when deleted": "C793452", + "Direct path: policy namespaces commands are accessible": "C10294193" + }, + "Obligations": { + "Create a obligation - Good": "C875288", + "Create a obligation - Bad": "C875289", + "Get an obligation - Good": "C875290", + "Get an obligation - Bad": "C875291", + "List obligations": "C875292", + "Update obligation": "C875293", + "Delete obligation - Good": "C875294", + "Delete obligation - Bad": "C875295", + "Create an obligation value - Good": "C875296", + "Create an obligation value - Bad": "C875297", + "Create an obligation value with triggers - JSON Array - Success": "C875298", + "Create an obligation value with triggers - JSON File - Success": "C875299", + "Create an obligation value with triggers - Bad": "C875300", + "Get an obligation value - Good": "C875301", + "Get an obligation value - Bad": "C875302", + "Update obligation values": "C875303", + "Update obligation values with triggers - Success": "C875304", + "Update obligation values with triggers - Bad": "C875305", + "Delete obligation value - Good": "C875306", + "Delete obligation value - Bad": "C875307", + "Create an obligation trigger - Required Only - IDs - Success": "C875308", + "Create an obligation trigger - Required Only - FQNs - Success": "C875309", + "Create an obligation trigger - Optional Fields - Success": "C875310", + "Create an obligation trigger - Same tuple different client IDs - Success": "C10294194", + "Create an obligation trigger - Bad": "C875311", + "Delete an obligation trigger - Good": "C875312", + "List obligation triggers - No filters": "C875378", + "List obligation triggers - Limit and Offset": "C875379", + "List obligation triggers - Filter by Namespace ID": "C875380", + "List obligation triggers - Filter by Namespace FQN": "C875381" + }, + "Profile": { + "profile create": "C793453", + "profile list shows profiles and default": "C793454", + "profile get shows profile details": "C793455", + "profile delete removes profile": "C793456", + "profile set-default updates default profile": "C793457", + "profile set-endpoint updates endpoint": "C793458", + "profile delete-all deletes all profiles": "C875688", + "profile migrate moves keyring profiles to filesystem": "C875689", + "profile keyring cleanup removes all keyring profiles": "C875690" + }, + "Provider Configuration": { + "fail to create provider configuration without config": "C874530", + "fail to create provider configuration without name": "C874531", + "fail to create provider configuration with invalid config": "C874532", + "create provider configuration": "C874533", + "get provider configuration by id": "C874534", + "get provider configuration by name": "C874535", + "fail to get provider configuration - no required flags": "C874536", + "fail to get provider configuration with non-existent name": "C874537", + "list provider configurations": "C874538", + "update provider configuration - success": "C874539", + "fail to update provider configuration - missing id": "C874540", + "fail to update provider configuration - no optional flags": "C874541", + "fail to update provider configuration - invalid config format": "C874542", + "delete provider configuration -- success": "C874543", + "delete provider configuration fail -- no id": "C874544", + "delete provider configuration fail -- no force": "C875223" + }, + "Registered Resources": { + "Create a registered resource - Good": "C874545", + "Create a registered resource - Bad": "C874546", + "Get a registered resource - Good": "C874547", + "Get a registered resource - Bad": "C874548", + "List registered resources": "C874549", + "Update registered resource": "C874550", + "Delete registered resource - Good": "C874551", + "Delete registered resource - Bad": "C874552", + "Create a registered resource value - Good": "C874553", + "Create a registered resource value - Bad": "C874554", + "Get a registered resource value - Good": "C874555", + "Get a registered resource value - Bad": "C874556", + "List registered resource values - Good": "C874557", + "List registered resource values - Bad": "C874558", + "Update registered resource values": "C874559", + "Delete registered resource value - Good": "C874560", + "Delete registered resource value - Bad": "C874561" + }, + "Resource Mapping Groups": { + "Create resource mapping group": "C874562", + "Get resource mapping group": "C874563", + "Update a resource mapping group": "C874564", + "List resource mapping groups": "C874565", + "Delete resource mapping group": "C874566" + }, + "Resource Mapping": { + "Create resource mapping": "C793459", + "Create resource mapping in a group": "C874567", + "Get resource mapping": "C793460", + "Update a resource mapping": "C793461", + "List resource mappings": "C793462", + "Delete resource mapping": "C793463" + }, + "Subject Condition Sets": { + "Create a Subject Condition Set (SCS) - from file": "C794011", + "Create a Subject Condition Set (SCS) - from flag value JSON": "C794012", + "Get a SCS": "C794013", + "Update a SCS - from flag value JSON": "C794014", + "Update a SCS - from file": "C797138", + "List SCS": "C794015", + "Create a SCS with namespace id": "C10322092", + "Create a SCS with namespace FQN": "C10322093", + "List SCS with namespace filter": "C10322094", + "Prune SCS - deletes unmapped SCS alone": "C794016" + }, + "Subject Mapping": { + "Create subject mapping": "C793464", + "Match subject mapping": "C797137", + "Get subject mapping": "C793465", + "Update a subject mapping": "C793466", + "List subject mappings": "C793467", + "Create subject mapping with namespace ID": "C10322095", + "Create subject mapping with namespace FQN": "C10322096", + "List subject mappings with namespace": "C10322097", + "Delete subject mapping": "C793468" + } +} diff --git a/otdfctl/e2e/testrail-integration/samples-for-virtru-instance/testrail-virtru.config.json b/otdfctl/e2e/testrail-integration/samples-for-virtru-instance/testrail-virtru.config.json new file mode 100644 index 0000000000..0617cec05f --- /dev/null +++ b/otdfctl/e2e/testrail-integration/samples-for-virtru-instance/testrail-virtru.config.json @@ -0,0 +1,7 @@ +{ + "url": "https://virtru.testrail.net", + "projectId": 63, + "tapFile": "./bats-results.tap", + "suiteId": 1, + "milestoneId": 1 +} diff --git a/otdfctl/e2e/testrail-integration/testname-to-testrail-id.example.json b/otdfctl/e2e/testrail-integration/testname-to-testrail-id.example.json new file mode 100644 index 0000000000..08373aad8c --- /dev/null +++ b/otdfctl/e2e/testrail-integration/testname-to-testrail-id.example.json @@ -0,0 +1,9 @@ +{ + "section_1": { + "test_name_1": "C12345", + "test_name_2": "C67890" + }, + "section_2": { + "test_name_3": "C54321" + } +} \ No newline at end of file diff --git a/otdfctl/e2e/testrail-integration/testrail.config.example.json b/otdfctl/e2e/testrail-integration/testrail.config.example.json new file mode 100644 index 0000000000..ce7d6a9c95 --- /dev/null +++ b/otdfctl/e2e/testrail-integration/testrail.config.example.json @@ -0,0 +1,5 @@ +{ + "url": "https://yourcompany.testrail.net", + "projectId": 123, + "tapFile": "./bats-results.tap", +} diff --git a/otdfctl/e2e/testrail-integration/upload-bats-test-results-to-testrail.sh b/otdfctl/e2e/testrail-integration/upload-bats-test-results-to-testrail.sh new file mode 100755 index 0000000000..d652b26a18 --- /dev/null +++ b/otdfctl/e2e/testrail-integration/upload-bats-test-results-to-testrail.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ================================================================ +# TestRail Integration Script for BATS TAP results +# +# This script: +# 1. Reads TestRail config from `testrail.config.json` +# 2. Reads mapping file `testname-to-testrail-id.json` (test name → case ID) +# 3. Parses BATS TAP results from `bats-results.tap` +# 4. Creates or finds a TestRail run by name +# 5. Uploads results for matched cases +# 6. Writes a local mapping report file (mapping-report.json) +# +# Dependencies: jq, curl +# ================================================================ + +# ----------------------------- +# Colors +# ----------------------------- +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +# ----------------------------- +# Load TestRail config +# ----------------------------- + +CONFIG_FILE="$(dirname "$0")/testrail.config.json" + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "❌ Missing $CONFIG_FILE. Copy testrail.config.example.json and update values." + exit 1 +fi + +TESTRAIL_URL=$(jq -r '.url' "$CONFIG_FILE") +PROJECT_ID=$(jq -r '.projectId' "$CONFIG_FILE") +TAP_FILE=$(jq -r '.tapFile' "$CONFIG_FILE") + +# ----------------------------- +# Mapping config and report file +# ----------------------------- +MAPPING_FILE="$(dirname "$0")/testname-to-testrail-id.json" +REPORT_FILE="mapping-report.txt" + +if [ ! -f "$MAPPING_FILE" ]; then + echo "❌ Missing $MAPPING_FILE. Copy testname-to-testrail-id.example.json and update with your case IDs." + exit 1 +fi + +# ----------------------------- +# Load TestRails credentials from env +# ----------------------------- +TESTRAIL_USER=${TESTRAIL_USER:-""} +TESTRAIL_PASS=${TESTRAIL_PASS:-""} + +if [[ -z "$TESTRAIL_USER" || -z "$TESTRAIL_PASS" ]]; then + echo "❌ Missing TestRail credentials. Please set TESTRAIL_USER and TESTRAIL_PASS env vars." + exit 1 +fi + +# ----------------------------- +# Run name (env override or auto-generate) +# ----------------------------- +RUN_NAME=${TESTRAIL_CLI_RUN_NAME:-"Otdfctl CLI auto tests - $(date -Iseconds)"} + +# ----------------------------- +# Functions +# ----------------------------- + +# ---- Lookup TestRail case ID in JSON mapping file by test name (case-insensitive names comparison to avoid accident failures) ---- +lookup_case_id() { + local name="$1" + local lowercasename + lowercasename=$(echo "$name" | tr '[:upper:]' '[:lower:]') + + # Detect whether JSON is nested (values are objects) or flat + if jq -e 'map_values(type) | .[] | select(.=="object")' "$MAPPING_FILE" >/dev/null 2>&1; then + # Nested JSON: preserve spaces in section names (allow multi words) + while IFS= read -r section; do + id=$(jq -r --arg n "$lowercasename" --arg s "$section" ' + reduce ( .[$s] | to_entries[] ) as $item (null; + if ($item.key | ascii_downcase | ltrimstr("[auto] ") | ltrimstr("(auto) ")) == $n then $item.value else . end + ) + ' "$MAPPING_FILE") + + if [[ -n "$id" && "$id" != "null" ]]; then + echo "$id|$section" + return 0 + fi + done < <(jq -r 'keys[]' "$MAPPING_FILE") + else + # Flat JSON + id=$(jq -r --arg n "$lowercasename" ' + reduce to_entries[] as $item (null; + if ($item.key | ascii_downcase | ltrimstr("[auto] ") | ltrimstr("(auto) ")) == $n then $item.value else . end + ) + ' "$MAPPING_FILE") + + if [[ -n "$id" && "$id" != "null" ]]; then + echo "$id|" + return 0 + fi + fi + + echo "" + return 1 +} + +# Parse TAP report and build results to push + generate mapping report to identify gaps more easily +parse_tap() { + # Check if TAP file exists + if [[ ! -f "$TAP_FILE" ]]; then + echo "❌ TAP file not found: $TAP_FILE" + exit 1 + fi + + : > "$REPORT_FILE" # truncate/clear old report + + while IFS= read -r line; do + if [[ "$line" =~ ^(ok|not\ ok)\ ([0-9]+)\ (.*) ]]; then + status="${BASH_REMATCH[1]}" + name="${BASH_REMATCH[3]}" + + # Detect and handle skip + if [[ "$line" =~ \#\ skip ]]; then + status_id=2 # Skipped + name=$(echo "$name" | sed -E 's/ +# skip.*//') # remove trailing " # skip ..." + elif [[ "$status" == "ok" ]]; then + status_id=1 # Passed + else + status_id=5 # Failed + fi + + mapping=$(lookup_case_id "$name" || true) + if [[ -n "$mapping" ]]; then + case_id="${mapping%%|*}" + section="${mapping##*|}" + printf "\"%s\" ${GREEN}YES_MAPPING_FOUND${NC} %s (Section: %s)\n" "$name" "$case_id" "$section" + printf "\"%s\" YES_MAPPING_FOUND %s\n" "$name" "$case_id" >> "$REPORT_FILE" + results+=("$(jq -n -c --arg cid "${case_id#C}" --arg sid "$status_id" --arg comment "$name" '{case_id: ($cid|tonumber), status_id: ($sid|tonumber), comment: $comment}')") + else + printf "\"%s\" ${RED}MAPPING_NOT_FOUND${NC}\n" "$name" + printf "\"%s\" MAPPING_NOT_FOUND\n" "$name" >> "$REPORT_FILE" + fi + fi + done < "$TAP_FILE" +} + +find_existing_run() { + curl -s -u "$TESTRAIL_USER:$TESTRAIL_PASS" \ + "$TESTRAIL_URL/index.php?/api/v2/get_runs/$PROJECT_ID" | + jq --arg run_name "$RUN_NAME" '.runs[] | select(.name == $run_name) | .id' | head -n1 +} + +create_run() { + local case_ids_json + local payload + case_ids_json=$(printf '%s\n' "${results[@]}" | jq -s '.[].case_id' | jq -s .) + payload=$(jq -n \ + --arg name "$RUN_NAME" \ + --argjson case_ids "$case_ids_json" \ + '{name: $name, include_all: false, case_ids: $case_ids}') + + + curl -s -u "$TESTRAIL_USER:$TESTRAIL_PASS" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "$TESTRAIL_URL/index.php?/api/v2/add_run/$PROJECT_ID" | jq .id +} + +push_results() { + local run_id="$1" + local results_json + results_json=$(printf '%s\n' "${results[@]}" | jq -s .) + + curl -s -u "$TESTRAIL_USER:$TESTRAIL_PASS" \ + -H "Content-Type: application/json" \ + -d "{\"results\": $results_json}" \ + "$TESTRAIL_URL/index.php?/api/v2/add_results_for_cases/$run_id" > /dev/null +} + +# ----------------------------- +# Main +# ----------------------------- +declare -a results=() + +parse_tap + +run_id=$(find_existing_run) +if [[ -z "$run_id" ]]; then + echo "ℹ️ No existing run found, creating new one..." + run_id=$(create_run) + echo "✅ Created new run ID: $run_id" +else + echo "ℹ️ Found existing run ID: $run_id" +fi + +push_results "$run_id" +echo "✅ Results uploaded to TestRail run $run_id" +echo "📄 Mapping report written to $REPORT_FILE" diff --git a/otdfctl/go.mod b/otdfctl/go.mod new file mode 100644 index 0000000000..4dbfa4776c --- /dev/null +++ b/otdfctl/go.mod @@ -0,0 +1,111 @@ +module github.com/opentdf/platform/otdfctl + +go 1.25.0 + +require ( + github.com/adrg/frontmatter v0.2.0 + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/glamour v0.10.0 + github.com/charmbracelet/huh v0.8.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 + github.com/evertras/bubble-table v0.19.2 + github.com/gabriel-vasile/mimetype v1.4.13 + github.com/go-jose/go-jose/v3 v3.0.5 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.6.0 + github.com/jrschumacher/go-osprofiles v0.0.0-20251201220924-3d077c5481e5 + github.com/opentdf/platform/lib/flattening v0.1.3 + github.com/opentdf/platform/lib/identifier v0.4.0 + github.com/opentdf/platform/lib/ocrypto v0.12.0 + github.com/opentdf/platform/protocol/go v0.32.0 + github.com/opentdf/platform/sdk v0.21.0 + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 + github.com/zitadel/oidc/v3 v3.45.1 + golang.org/x/oauth2 v0.36.0 + golang.org/x/term v0.43.0 + google.golang.org/grpc v1.81.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1 // indirect + connectrpc.com/connect v1.19.2 // indirect + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gowebpki/jcs v1.0.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect + github.com/zalando/go-keyring v0.2.6 // indirect + github.com/zitadel/logging v0.6.2 // indirect + github.com/zitadel/schema v1.3.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/otdfctl/go.sum b/otdfctl/go.sum new file mode 100644 index 0000000000..75d62b9678 --- /dev/null +++ b/otdfctl/go.sum @@ -0,0 +1,326 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1 h1:AUL6VF5YWL01j/1H/DQbPUSDkEwYqwVCNw7yhbpOxSQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= +connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= +connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/grpchealth v1.4.0 h1:MJC96JLelARPgZTiRF9KRfY/2N9OcoQvF2EWX07v2IE= +connectrpc.com/grpchealth v1.4.0/go.mod h1:WhW6m1EzTmq3Ky1FE8EfkIpSDc6TfUx2M2KqZO3ts/Q= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= +github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/evertras/bubble-table v0.19.2 h1:u77oiM6JlRR+CvS5FZc3Hz+J6iEsvEDcR5kO8OFb1Yw= +github.com/evertras/bubble-table v0.19.2/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= +github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= +github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= +github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/jrschumacher/go-osprofiles v0.0.0-20251201220924-3d077c5481e5 h1:NRqBTDlgz/9hwlLPQlkfoIOJE0leWwCO9PwMrSAiY34= +github.com/jrschumacher/go-osprofiles v0.0.0-20251201220924-3d077c5481e5/go.mod h1:xQeFVn7ra/QR1KbTZ3KApzUeicH1IFkJx1kr9dG8uOI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= +github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= +github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/opentdf/platform/lib/flattening v0.1.3 h1:IuOm/wJVXNrzOV676Ticgr0wyBkL+lVjsoSfh+WSkNo= +github.com/opentdf/platform/lib/flattening v0.1.3/go.mod h1:Gs/T+6FGZKk9OAdz2Jf1R8CTGeNRYrq1lZGDeYT3hrY= +github.com/opentdf/platform/lib/identifier v0.4.0 h1:gJf4FqHxqpMdMdMwhI9QmvfHEfMLW4KvEr/qjk7hnio= +github.com/opentdf/platform/lib/identifier v0.4.0/go.mod h1:+gONr5mVf1YlLorZUeRefxiudYfC6JeQN7EwrKMk4g8= +github.com/opentdf/platform/lib/ocrypto v0.12.0 h1:N449KWy7VdMO0JwfsrG0kM6Uy8VrEnVvBciwzRHwnlg= +github.com/opentdf/platform/lib/ocrypto v0.12.0/go.mod h1:51UTmAWO6C8ghuMXiktpn63N+fLUQxY6zo8D65Ly0wQ= +github.com/opentdf/platform/protocol/go v0.32.0 h1:XdH/MscjqpESzmfNHSlC3/b84KDRJWrKSoRjbTTfKh4= +github.com/opentdf/platform/protocol/go v0.32.0/go.mod h1:GCiAAv0I8tkQDA2j9FuWzmK78OtIZSl+eAxAf2WHG+4= +github.com/opentdf/platform/sdk v0.21.0 h1:Q+oz/SU4L+ssqeIxkFISFnS4x2GAT03jI1LcLn4eO8k= +github.com/opentdf/platform/sdk v0.21.0/go.mod h1:1LcAnUbgVwJkX+T8hj24bcAQm91pYEbL2EiOdV+fLJ4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= +github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= +github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= +github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= +github.com/zitadel/oidc/v3 v3.45.1 h1:x7J8NywTUtLR9T5uu2dufae3gJrl6VVpIfvGZy+kzJg= +github.com/zitadel/oidc/v3 v3.45.1/go.mod h1:oFArtAPTXEA4ajkIe/JfBjv7hhlD0kr///UqaO3Uzd0= +github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= +github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/otdfctl/main.go b/otdfctl/main.go new file mode 100644 index 0000000000..36cd86510f --- /dev/null +++ b/otdfctl/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "log/slog" + "os" + + "github.com/opentdf/platform/otdfctl/cmd" + "github.com/spf13/cobra" +) + +func main() { + // f, err := os.Create("cpu.pprof") + // if err != nil { + // panic(err) + // } + // pprof.StartCPUProfile(f) + // defer pprof.StopCPUProfile() + + l := new(slog.LevelVar) + l.Set(slog.LevelInfo) + l.UnmarshalText([]byte(os.Getenv("LOG_LEVEL"))) //nolint:errcheck // ignore error, just use default level + logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + Level: l, + })) + + slog.SetDefault(logger) + + cobra.EnableTraverseRunHooks = true + cmd.Execute() +} diff --git a/otdfctl/migrations/namespacedpolicy/actions_execute.go b/otdfctl/migrations/namespacedpolicy/actions_execute.go new file mode 100644 index 0000000000..cae9099756 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/actions_execute.go @@ -0,0 +1,131 @@ +package namespacedpolicy + +import ( + "context" + "fmt" + + "github.com/opentdf/platform/protocol/go/policy" +) + +func (e *MigrationExecutor) rememberActionTarget(sourceID string, target *ActionTargetPlan) { + if e == nil || sourceID == "" || target == nil { + return + } + + namespaceKey := namespaceRefKey(target.Namespace) + if namespaceKey == "" { + return + } + + if e.actionTargets == nil { + e.actionTargets = make(map[string]map[string]*ActionTargetPlan) + } + if e.actionTargets[sourceID] == nil { + e.actionTargets[sourceID] = make(map[string]*ActionTargetPlan) + } + + e.actionTargets[sourceID][namespaceKey] = target +} + +func (e *MigrationExecutor) cachedActionTargetID(sourceID string, namespace *policy.Namespace) string { + if e == nil || sourceID == "" { + return "" + } + + namespaceKey := namespaceRefKey(namespace) + if namespaceKey == "" { + return "" + } + + targets := e.actionTargets[sourceID] + if targets == nil { + return "" + } + + target := targets[namespaceKey] + if target == nil { + return "" + } + + return target.TargetID() +} + +func (e *MigrationExecutor) executeActions(ctx context.Context, actionPlans []*ActionPlan) error { + for _, actionPlan := range actionPlans { + if actionPlan == nil || actionPlan.Source == nil { + continue + } + + for _, target := range actionPlan.Targets { + if target == nil { + continue + } + + if err := e.executeActionTarget(ctx, actionPlan, target); err != nil { + return err + } + } + } + + return nil +} + +func (e *MigrationExecutor) executeActionTarget(ctx context.Context, actionPlan *ActionPlan, target *ActionTargetPlan) error { + switch target.Status { + case TargetStatusExistingStandard, TargetStatusAlreadyMigrated: + if target.TargetID() == "" { + errKind := ErrMissingExistingTarget + if target.Status == TargetStatusAlreadyMigrated { + errKind = ErrMissingMigratedTarget + } + return fmt.Errorf("%w: action %q target %q", errKind, actionPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + e.rememberActionTarget(actionPlan.Source.GetId(), target) + return nil + case TargetStatusSkipped: + return nil + case TargetStatusCreate: + return e.createActionTarget(ctx, actionPlan, target) + case TargetStatusUnresolved: + return nil + default: + return fmt.Errorf("%w: action %q target %q has unsupported status %q", ErrUnsupportedStatus, actionPlan.Source.GetId(), namespaceLabel(target.Namespace), target.Status) + } +} + +func (e *MigrationExecutor) createActionTarget(ctx context.Context, actionPlan *ActionPlan, target *ActionTargetPlan) error { + namespace := namespaceIdentifier(target.Namespace) + if namespace == "" { + return fmt.Errorf("%w: action %q", ErrTargetNamespaceRequired, actionPlan.Source.GetId()) + } + + created, err := e.handler.CreateAction( + ctx, + actionPlan.Source.GetName(), + namespace, + metadataForCreate( + actionPlan.Source.GetId(), + metadataLabels(actionPlan.Source.GetMetadata()), + ), + ) + if err != nil { + target.Execution = &ExecutionResult{ + Failure: err.Error(), + } + return fmt.Errorf("create action %q in namespace %q: %w", actionPlan.Source.GetId(), namespaceLabel(target.Namespace), err) + } + if created.GetId() == "" { + target.Execution = &ExecutionResult{ + Failure: ErrMissingCreatedTargetID.Error(), + } + return fmt.Errorf("%w: action %q target %q", ErrMissingCreatedTargetID, actionPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + target.Execution = &ExecutionResult{ + Applied: true, + CreatedTargetID: created.GetId(), + } + e.rememberActionTarget(actionPlan.Source.GetId(), target) + + return nil +} diff --git a/otdfctl/migrations/namespacedpolicy/actions_execute_test.go b/otdfctl/migrations/namespacedpolicy/actions_execute_test.go new file mode 100644 index 0000000000..c6a3e25489 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/actions_execute_test.go @@ -0,0 +1,295 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExecuteActions(t *testing.T) { + t.Parallel() + + namespace1 := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + namespace2 := &policy.Namespace{Id: "ns-2", Fqn: "https://example.net"} + namespace3 := &policy.Namespace{Id: "ns-3", Fqn: "https://example.org"} + + tests := []struct { + name string + plan *MigrationPlan + handler *mockExecutorHandler + wantErr *expectedError + assert func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) + }{ + { + name: "handles created, existing, and already migrated action targets", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{ + Id: "action-1", + Name: "decrypt", + Metadata: &common.Metadata{ + Labels: map[string]string{ + "owner": "policy-team", + "env": "dev", + }, + }, + }, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + { + Namespace: namespace2, + Status: TargetStatusExistingStandard, + ExistingID: "standard-action", + }, + { + Namespace: namespace3, + Status: TargetStatusAlreadyMigrated, + ExistingID: "migrated-action", + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + results: map[string]map[string]*policy.Action{ + "decrypt": { + "ns-1": {Id: "created-action-1", Name: "decrypt"}, + }, + }, + }, + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + require.Contains(t, handler.created, "decrypt") + require.Contains(t, handler.created["decrypt"], "ns-1") + assert.Len(t, handler.created["decrypt"], 1) + assert.Equal(t, "decrypt", handler.created["decrypt"]["ns-1"].Name) + assert.Equal(t, "ns-1", handler.created["decrypt"]["ns-1"].Namespace) + assert.Equal(t, map[string]string{ + "owner": "policy-team", + "env": "dev", + migrationLabelMigratedFrom: "action-1", + }, handler.created["decrypt"]["ns-1"].Metadata.GetLabels()) + + createdTarget := plan.Actions[0].Targets[0] + assert.Equal(t, TargetStatusCreate, createdTarget.Status) + assert.Empty(t, createdTarget.ExistingID) + require.NotNil(t, createdTarget.Execution) + assert.True(t, createdTarget.Execution.Applied) + assert.Equal(t, "created-action-1", createdTarget.Execution.CreatedTargetID) + assert.Equal(t, "created-action-1", createdTarget.TargetID()) + + existingTarget := plan.Actions[0].Targets[1] + assert.Equal(t, "standard-action", existingTarget.TargetID()) + + migratedTarget := plan.Actions[0].Targets[2] + assert.Equal(t, "migrated-action", migratedTarget.TargetID()) + + assert.Equal(t, "created-action-1", executor.cachedActionTargetID("action-1", namespace1)) + assert.Equal(t, "standard-action", executor.cachedActionTargetID("action-1", namespace2)) + assert.Equal(t, "migrated-action", executor.cachedActionTargetID("action-1", namespace3)) + assert.Empty(t, executor.cachedActionTargetID("action-2", namespace1)) + }, + }, + { + name: "ignores unresolved target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusUnresolved, + Reason: "missing target namespace mapping", + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Empty(t, handler.created) + assert.Empty(t, executor.cachedActionTargetID("action-1", namespace1)) + }, + }, + { + name: "returns error for missing existing standard target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusExistingStandard, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrMissingExistingTarget, `action %q target %q`, "action-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.created) + assert.Empty(t, executor.cachedActionTargetID("action-1", namespace1)) + }, + }, + { + name: "returns error for missing already migrated target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrMissingMigratedTarget, `action %q target %q`, "action-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.created) + assert.Empty(t, executor.cachedActionTargetID("action-1", namespace1)) + }, + }, + { + name: "returns error for missing target namespace", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Status: TargetStatusCreate, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrTargetNamespaceRequired, `action %q`, "action-1"), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.created) + assert.Empty(t, executor.cachedActionTargetID("action-1", nil)) + }, + }, + { + name: "returns error for missing created target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + results: map[string]map[string]*policy.Action{ + "decrypt": { + "ns-1": {}, + }, + }, + }, + wantErr: wantError(ErrMissingCreatedTargetID, `action %q target %q`, "action-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.created, "decrypt") + require.NotNil(t, plan.Actions[0].Targets[0].Execution) + assert.Equal(t, ErrMissingCreatedTargetID.Error(), plan.Actions[0].Targets[0].Execution.Failure) + assert.Empty(t, executor.cachedActionTargetID("action-1", namespace1)) + }, + }, + { + name: "returns error for unsupported target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatus("bogus"), + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError( + ErrUnsupportedStatus, + `action %q target %q has unsupported status %q`, + "action-1", + namespace1.GetFqn(), + TargetStatus("bogus"), + ), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.created) + assert.Empty(t, executor.cachedActionTargetID("action-1", namespace1)) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + executor, err := NewMigrationExecutor(tt.handler) + require.NoError(t, err) + + err = executor.ExecuteMigration(t.Context(), tt.plan) + switch { + case tt.wantErr != nil: + require.Error(t, err) + require.ErrorIs(t, err, tt.wantErr.is) + require.EqualError(t, err, tt.wantErr.message) + default: + require.NoError(t, err) + } + + tt.assert(t, err, executor, tt.handler, tt.plan) + }) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/backup_confirmation.go b/otdfctl/migrations/namespacedpolicy/backup_confirmation.go new file mode 100644 index 0000000000..499f2f0c88 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/backup_confirmation.go @@ -0,0 +1,91 @@ +//nolint:forbidigo // interactive migration review requires terminal prompts +package namespacedpolicy + +import ( + "context" + "errors" + "fmt" + + "github.com/opentdf/platform/otdfctl/migrations" +) + +const ( + namespacedPolicyCommitConfirm = "confirm" + namespacedPolicyCommitSkip = "skip" + namespacedPolicyCommitAbort = "abort" + noneLabel = "(none)" + skippedByUserReason = "skipped by user" + + //nolint:gosec // user-facing backup prompt text, not credentials + backupWarningTitle = "WARNING: This operation will migrate namespaced policy objects and may create new policy objects." + backupWarningBody = "It is STRONGLY recommended to take a complete backup of your system before proceeding.\n" + backupConfirmTitle = "Have you taken a complete backup?" + backupConfirmDetail = "Commit mode will apply namespaced policy changes to the target system." + backupAbortDetail = "Choose abort if you have not created a backup yet." + backupConfirmLabel = "Yes, continue" + backupCancelLabel = "Abort" + + //nolint:gosec // user-facing backup prompt text, not credentials + pruneBackupWarningTitle = "WARNING: This operation will prune migrated namespaced policy and permanently delete legacy policy objects." + pruneBackupConfirmDetail = "Commit mode will delete legacy/global policy objects from the target system." + skipObjectLabel = "Skip this object" + skipObjectDescription = "leave this object untouched" +) + +var ( + ErrNamespacedPolicyBackupNotConfirmed = errors.New("user did not confirm backup") + errInteractiveSkipSelected = errors.New("interactive commit target skipped by user") +) + +func ConfirmNamespacedPolicyBackup(ctx context.Context, prompter InteractivePrompter) error { + return confirmNamespacedPolicyBackup(ctx, prompter, backupWarningTitle, backupConfirmDetail) +} + +func ConfirmNamespacedPolicyPruneBackup(ctx context.Context, prompter InteractivePrompter) error { + return confirmNamespacedPolicyBackup(ctx, prompter, pruneBackupWarningTitle, pruneBackupConfirmDetail) +} + +func confirmNamespacedPolicyBackup(ctx context.Context, prompter InteractivePrompter, warningTitle, confirmDetail string) error { + if prompter == nil { + prompter = &HuhPrompter{} + } + + styles := migrations.NewDisplayStyles() + fmt.Println(styles.Warning().Render(warningTitle)) + fmt.Println(styles.Warning().Render(backupWarningBody)) + + err := prompter.Confirm(ctx, ConfirmPrompt{ + Title: backupConfirmTitle, + Description: []string{ + confirmDetail, + backupAbortDetail, + }, + ConfirmLabel: backupConfirmLabel, + CancelLabel: backupCancelLabel, + }) + if err == nil { + return nil + } + if errors.Is(err, ErrInteractiveReviewAborted) { + return ErrNamespacedPolicyBackupNotConfirmed + } + return err +} + +func applyInteractiveDecision(ctx context.Context, prompter InteractivePrompter, prompt SelectPrompt) error { + choice, err := prompter.Select(ctx, prompt) + if err != nil { + return err + } + + switch choice { + case namespacedPolicyCommitConfirm: + return nil + case namespacedPolicyCommitSkip: + return errInteractiveSkipSelected + case namespacedPolicyCommitAbort: + return ErrInteractiveReviewAborted + default: + return fmt.Errorf("invalid interactive commit selection %q", choice) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/canonical.go b/otdfctl/migrations/namespacedpolicy/canonical.go new file mode 100644 index 0000000000..38b6711210 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/canonical.go @@ -0,0 +1,311 @@ +package namespacedpolicy + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" +) + +type registeredResourceValueCanonical struct { + Value string `json:"value"` + ActionAttributeValues []string `json:"action_attribute_values"` +} +type canonicalSubjectSetEntry struct { + ConditionGroups []canonicalConditionGroupEntry `json:"condition_groups"` +} + +type canonicalConditionGroupEntry struct { + Conditions []canonicalConditionEntry `json:"conditions"` + BooleanOperator int32 `json:"boolean_operator"` +} + +type canonicalConditionEntry struct { + Selector string `json:"selector"` + Operator int32 `json:"operator"` + Values []string `json:"values"` +} + +type canonicalRequestContextEntry struct { + ClientID string `json:"client_id"` +} + +func actionCanonicalEqual(source, target *policy.Action) bool { + s := canonicalActionName(source) + return s != "" && s == canonicalActionName(target) +} + +func subjectConditionSetCanonicalEqual(source, target *policy.SubjectConditionSet) bool { + s := canonicalSubjectConditionSet(source) + return s != "" && s == canonicalSubjectConditionSet(target) +} + +func subjectMappingCanonicalEqual(source, target *policy.SubjectMapping) bool { + s := canonicalSubjectMapping(source) + return s != "" && s == canonicalSubjectMapping(target) +} + +func obligationTriggerCanonicalEqual(source, target *policy.ObligationTrigger) bool { + s := canonicalObligationTrigger(source) + return s != "" && s == canonicalObligationTrigger(target) +} + +func registeredResourceCanonicalEqual(source, target *policy.RegisteredResource) bool { + s := canonicalRegisteredResource(source) + return s != "" && s == canonicalRegisteredResource(target) +} + +func canonicalActionName(action *policy.Action) string { + if action == nil { + return "" + } + return strings.ToLower(strings.TrimSpace(action.GetName())) +} + +// canonicalSubjectConditionSet produces a deterministic string key from the +// semantically meaningful fields of a SubjectConditionSet. We extract into +// plain Go types and sort at every level rather than relying on protobuf +// serialization (protojson.Marshal, proto.Marshal with Deterministic: true), +// because neither guarantees stable output across library versions or builds. +// Canonical comparison is only performed within a single planning run, but +// explicit field extraction makes the stability guarantee self-evident. +func canonicalSubjectConditionSet(scs *policy.SubjectConditionSet) string { + if scs == nil || len(scs.GetSubjectSets()) == 0 { + return "" + } + + sets := make([]canonicalSubjectSetEntry, 0, len(scs.GetSubjectSets())) + for _, ss := range scs.GetSubjectSets() { + if ss == nil { + continue + } + sets = append(sets, normalizeSubjectSet(ss)) + } + if len(sets) == 0 { + return "" + } + sortByJSON(sets) + + encoded, err := json.Marshal(sets) + if err != nil { + return "" + } + return string(encoded) +} + +func canonicalSubjectMapping(mapping *policy.SubjectMapping) string { + if mapping == nil { + return "" + } + + payload := struct { + AttributeValueFQN string `json:"attribute_value_fqn"` + ActionNames []string `json:"action_names"` + SubjectSetKey string `json:"subject_condition_set"` + }{ + AttributeValueFQN: strings.TrimSpace(mapping.GetAttributeValue().GetFqn()), + ActionNames: canonicalActionNames(mapping.GetActions()), + SubjectSetKey: canonicalSubjectConditionSet(mapping.GetSubjectConditionSet()), + } + if payload.AttributeValueFQN == "" || payload.SubjectSetKey == "" { + return "" + } + + encoded, err := json.Marshal(payload) + if err != nil { + return "" + } + return string(encoded) +} + +func canonicalObligationTrigger(trigger *policy.ObligationTrigger) string { + if trigger == nil { + return "" + } + + payload := struct { + AttributeValueFQN string `json:"attribute_value_fqn"` + ActionName string `json:"action_name"` + ObligationValueFQN string `json:"obligation_value_fqn"` + Context string `json:"context"` + }{ + AttributeValueFQN: strings.TrimSpace(trigger.GetAttributeValue().GetFqn()), + ActionName: canonicalActionName(trigger.GetAction()), + ObligationValueFQN: strings.TrimSpace(trigger.GetObligationValue().GetFqn()), + Context: canonicalObligationTriggerContext(trigger.GetContext()), + } + if payload.AttributeValueFQN == "" || payload.ActionName == "" || payload.ObligationValueFQN == "" { + return "" + } + + encoded, err := json.Marshal(payload) + if err != nil { + return "" + } + return string(encoded) +} + +// canonicalObligationTriggerContext produces a deterministic string key from +// RequestContext fields. See canonicalSubjectConditionSet for rationale on +// avoiding protobuf serialization. +func canonicalObligationTriggerContext(contexts []*policy.RequestContext) string { + if len(contexts) == 0 { + return "" + } + + entries := make([]canonicalRequestContextEntry, 0, len(contexts)) + for _, rc := range contexts { + if rc == nil || rc.GetPep() == nil { + continue + } + entries = append(entries, canonicalRequestContextEntry{ + ClientID: strings.TrimSpace(rc.GetPep().GetClientId()), + }) + } + + if len(entries) == 0 { + return "" + } + + sort.SliceStable(entries, func(i, j int) bool { + return entries[i].ClientID < entries[j].ClientID + }) + + encoded, err := json.Marshal(entries) + if err != nil { + return "" + } + return string(encoded) +} + +func normalizeSubjectSet(ss *policy.SubjectSet) canonicalSubjectSetEntry { + groups := make([]canonicalConditionGroupEntry, 0, len(ss.GetConditionGroups())) + for _, cg := range ss.GetConditionGroups() { + if cg == nil { + continue + } + groups = append(groups, normalizeConditionGroup(cg)) + } + sortByJSON(groups) + return canonicalSubjectSetEntry{ConditionGroups: groups} +} + +func normalizeConditionGroup(cg *policy.ConditionGroup) canonicalConditionGroupEntry { + conditions := make([]canonicalConditionEntry, 0, len(cg.GetConditions())) + for _, c := range cg.GetConditions() { + if c == nil { + continue + } + values := append([]string(nil), c.GetSubjectExternalValues()...) + sort.Strings(values) + conditions = append(conditions, canonicalConditionEntry{ + Selector: strings.TrimSpace(c.GetSubjectExternalSelectorValue()), + Operator: int32(c.GetOperator()), + Values: values, + }) + } + sort.SliceStable(conditions, func(i, j int) bool { + if conditions[i].Selector != conditions[j].Selector { + return conditions[i].Selector < conditions[j].Selector + } + if conditions[i].Operator != conditions[j].Operator { + return conditions[i].Operator < conditions[j].Operator + } + return strings.Join(conditions[i].Values, ",") < strings.Join(conditions[j].Values, ",") + }) + return canonicalConditionGroupEntry{ + Conditions: conditions, + BooleanOperator: int32(cg.GetBooleanOperator()), + } +} + +func sortByJSON[T any](items []T) { + type keyedItem struct { + value T + key string + } + + keyed := make([]keyedItem, 0, len(items)) + for _, item := range items { + k, _ := json.Marshal(item) + keyed = append(keyed, keyedItem{ + value: item, + key: string(k), + }) + } + + sort.SliceStable(keyed, func(i, j int) bool { + return keyed[i].key < keyed[j].key + }) + for i := range keyed { + items[i] = keyed[i].value + } +} + +// TODO: Revisit this. Probably can be simpler. +func canonicalRegisteredResource(resource *policy.RegisteredResource) string { + if resource == nil { + return "" + } + + values := make([]registeredResourceValueCanonical, 0, len(resource.GetValues())) + for _, value := range resource.GetValues() { + if value == nil { + continue + } + + aavs := make([]string, 0, len(value.GetActionAttributeValues())) + for _, aav := range value.GetActionAttributeValues() { + if aav == nil { + continue + } + key := fmt.Sprintf("%s|%s", canonicalActionName(aav.GetAction()), strings.TrimSpace(aav.GetAttributeValue().GetFqn())) + if key == "|" { + continue + } + aavs = append(aavs, key) + } + sort.Strings(aavs) + + values = append(values, registeredResourceValueCanonical{ + Value: strings.ToLower(strings.TrimSpace(value.GetValue())), + ActionAttributeValues: aavs, + }) + } + sort.Slice(values, func(i, j int) bool { + if values[i].Value == values[j].Value { + return strings.Join(values[i].ActionAttributeValues, ",") < strings.Join(values[j].ActionAttributeValues, ",") + } + return values[i].Value < values[j].Value + }) + + payload := struct { + Name string `json:"name"` + Values []registeredResourceValueCanonical `json:"values"` + }{ + Name: strings.ToLower(strings.TrimSpace(resource.GetName())), + Values: values, + } + if payload.Name == "" { + return "" + } + + encoded, err := json.Marshal(payload) + if err != nil { + return "" + } + return string(encoded) +} + +func canonicalActionNames(actions []*policy.Action) []string { + names := make([]string, 0, len(actions)) + for _, action := range actions { + if name := canonicalActionName(action); name != "" { + names = append(names, name) + } + } + sort.Strings(names) + return names +} diff --git a/otdfctl/migrations/namespacedpolicy/canonical_test.go b/otdfctl/migrations/namespacedpolicy/canonical_test.go new file mode 100644 index 0000000000..13ffaccf3f --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/canonical_test.go @@ -0,0 +1,632 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" +) + +func TestSortByJSONOrdersItemsByEncodedKey(t *testing.T) { + t.Parallel() + + items := []struct { + Name string `json:"name"` + }{ + {Name: "b"}, + {Name: "c"}, + {Name: "a"}, + } + + sortByJSON(items) + + assert.Equal(t, []struct { + Name string `json:"name"` + }{ + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, + }, items) +} + +func TestCanonicalRegisteredResourceIgnoresValueAndBindingOrder(t *testing.T) { + t.Parallel() + + left := testRegisteredResource( + "resource-left", + " Documents ", + testRegisteredResourceValue( + "Prod", + testActionAttributeValue( + "action-read", + "Read", + testAttributeValue("https://example.com/attr/classification/value/public", nil), + ), + testActionAttributeValue( + "action-write", + "Write", + testAttributeValue("https://example.com/attr/classification/value/internal", nil), + ), + ), + testRegisteredResourceValue( + "Dev", + testActionAttributeValue( + "action-read", + "Read", + testAttributeValue("https://example.com/attr/classification/value/public", nil), + ), + ), + ) + right := testRegisteredResource( + "resource-right", + "documents", + testRegisteredResourceValue( + "dev", + testActionAttributeValue( + "action-read", + "read", + testAttributeValue("https://example.com/attr/classification/value/public", nil), + ), + ), + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-write", + "write", + testAttributeValue("https://example.com/attr/classification/value/internal", nil), + ), + testActionAttributeValue( + "action-read", + "read", + testAttributeValue("https://example.com/attr/classification/value/public", nil), + ), + ), + ) + + assert.Equal(t, canonicalRegisteredResource(left), canonicalRegisteredResource(right)) + assert.True(t, registeredResourceCanonicalEqual(left, right)) +} + +func TestCanonicalSubjectConditionSetIgnoresOrderAtEveryLevel(t *testing.T) { + t.Parallel() + + condA := &policy.Condition{ + SubjectExternalSelectorValue: ".department", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"engineering", "security"}, + } + condB := &policy.Condition{ + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"admin"}, + } + groupAB := &policy.ConditionGroup{ + Conditions: []*policy.Condition{condA, condB}, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + } + groupBA := &policy.ConditionGroup{ + Conditions: []*policy.Condition{condB, condA}, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + } + + left := &policy.SubjectConditionSet{ + Id: "scs-left", + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{groupAB}}, + {ConditionGroups: []*policy.ConditionGroup{groupBA}}, + }, + } + right := &policy.SubjectConditionSet{ + Id: "scs-right", + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{groupBA}}, + {ConditionGroups: []*policy.ConditionGroup{groupAB}}, + }, + } + + assert.Equal(t, canonicalSubjectConditionSet(left), canonicalSubjectConditionSet(right)) + assert.True(t, subjectConditionSetCanonicalEqual(left, right)) +} + +func TestCanonicalSubjectConditionSetSortsValuesWithinConditions(t *testing.T) { + t.Parallel() + + left := &policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"admin", "editor", "viewer"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + }, + }}, + }, + } + right := &policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"viewer", "admin", "editor"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + }, + }}, + }, + } + + assert.True(t, subjectConditionSetCanonicalEqual(left, right)) +} + +func TestCanonicalSubjectConditionSetDistinguishesDifferentConditions(t *testing.T) { + t.Parallel() + + left := &policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"admin"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + }, + }}, + }, + } + right := &policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"editor"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + }, + }}, + }, + } + + assert.False(t, subjectConditionSetCanonicalEqual(left, right)) +} + +func TestCanonicalSubjectConditionSetReturnsEmptyForNilOrEmpty(t *testing.T) { + t.Parallel() + + assert.Empty(t, canonicalSubjectConditionSet(nil)) + assert.Empty(t, canonicalSubjectConditionSet(&policy.SubjectConditionSet{})) + assert.Empty(t, canonicalSubjectConditionSet(&policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{nil}, + })) +} + +func TestCanonicalObligationTriggerContextIgnoresOrder(t *testing.T) { + t.Parallel() + + left := []*policy.RequestContext{ + {Pep: &policy.PolicyEnforcementPoint{ClientId: "ingress-client"}}, + {Pep: &policy.PolicyEnforcementPoint{ClientId: "egress-client"}}, + } + right := []*policy.RequestContext{ + {Pep: &policy.PolicyEnforcementPoint{ClientId: "egress-client"}}, + {Pep: &policy.PolicyEnforcementPoint{ClientId: "ingress-client"}}, + } + + assert.Equal(t, canonicalObligationTriggerContext(left), canonicalObligationTriggerContext(right)) +} + +func TestCanonicalObligationTriggerContextSkipsNilEntries(t *testing.T) { + t.Parallel() + + withNils := []*policy.RequestContext{ + nil, + {Pep: &policy.PolicyEnforcementPoint{ClientId: "client-a"}}, + {Pep: nil}, + } + clean := []*policy.RequestContext{ + {Pep: &policy.PolicyEnforcementPoint{ClientId: "client-a"}}, + } + + assert.Equal(t, canonicalObligationTriggerContext(withNils), canonicalObligationTriggerContext(clean)) +} + +func TestCanonicalObligationTriggerContextReturnsEmptyForNilOrEmpty(t *testing.T) { + t.Parallel() + + assert.Empty(t, canonicalObligationTriggerContext(nil)) + assert.Empty(t, canonicalObligationTriggerContext([]*policy.RequestContext{})) + assert.Empty(t, canonicalObligationTriggerContext([]*policy.RequestContext{nil})) +} + +func TestCanonicalObligationTriggerIncludesContext(t *testing.T) { + t.Parallel() + + base := &policy.ObligationTrigger{ + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + AttributeValue: &policy.Value{Id: "value-1", Fqn: "https://attr.example.com/value/secret"}, + ObligationValue: &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://obligation.example.com/value/notify", + }, + } + + left := protoCloneTrigger(base) + left.Context = []*policy.RequestContext{ + {Pep: &policy.PolicyEnforcementPoint{ClientId: "ingress-client"}}, + } + + right := protoCloneTrigger(base) + right.Context = []*policy.RequestContext{ + {Pep: &policy.PolicyEnforcementPoint{ClientId: "egress-client"}}, + } + + assert.NotEqual(t, canonicalObligationTrigger(left), canonicalObligationTrigger(right)) +} + +func protoCloneTrigger(trigger *policy.ObligationTrigger) *policy.ObligationTrigger { + if trigger == nil { + return nil + } + + return &policy.ObligationTrigger{ + Id: trigger.GetId(), + ObligationValue: trigger.GetObligationValue(), + Action: trigger.GetAction(), + AttributeValue: trigger.GetAttributeValue(), + Metadata: trigger.GetMetadata(), + } +} + +func TestActionCanonicalEqual(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + left *policy.Action + right *policy.Action + want bool + }{ + {name: "both nil", left: nil, right: nil, want: false}, + {name: "nil left, populated right", left: nil, right: &policy.Action{Name: "decrypt"}, want: false}, + {name: "both blank names", left: &policy.Action{Name: " "}, right: &policy.Action{Name: ""}, want: false}, + {name: "equal ignoring case and whitespace", left: &policy.Action{Name: " Decrypt "}, right: &policy.Action{Name: "DECRYPT"}, want: true}, + {name: "different names", left: &policy.Action{Name: "decrypt"}, right: &policy.Action{Name: "read"}, want: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, actionCanonicalEqual(tc.left, tc.right)) + }) + } +} + +func canonicalTestSCS(values ...string) *policy.SubjectConditionSet { + return &policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: values, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + }, + }}, + }, + } +} + +func TestSubjectConditionSetCanonicalEqualSkipsNilChildren(t *testing.T) { + t.Parallel() + + clean := canonicalTestSCS("admin") + cleanGroup := clean.GetSubjectSets()[0].GetConditionGroups()[0] + + noisyGroup := &policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{nil, cleanGroup}}, + }, + } + noisyCondition := &policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + nil, + { + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"admin"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + }, + }}, + }, + } + + tests := []struct { + name string + left *policy.SubjectConditionSet + right *policy.SubjectConditionSet + want bool + }{ + {name: "both nil", left: nil, right: nil, want: false}, + {name: "nil condition group is skipped", left: noisyGroup, right: clean, want: true}, + {name: "nil condition within a group is skipped", left: noisyCondition, right: clean, want: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, subjectConditionSetCanonicalEqual(tc.left, tc.right)) + }) + } +} + +func TestSubjectMappingCanonicalEqual(t *testing.T) { + t.Parallel() + + scs := canonicalTestSCS("admin") + fqn := "https://example.com/attr/c/value/s" + + equivalentLeft := &policy.SubjectMapping{ + Id: "sm-left", + AttributeValue: &policy.Value{Fqn: fqn}, + Actions: []*policy.Action{{Name: "Read"}, {Name: "WRITE"}}, + SubjectConditionSet: scs, + } + equivalentRight := &policy.SubjectMapping{ + Id: "sm-right", + AttributeValue: &policy.Value{Fqn: " " + fqn + " "}, + Actions: []*policy.Action{{Name: "write"}, {Name: "read"}}, + SubjectConditionSet: scs, + } + + differentActionsLeft := &policy.SubjectMapping{ + AttributeValue: &policy.Value{Fqn: fqn}, + Actions: []*policy.Action{{Name: "read"}}, + SubjectConditionSet: scs, + } + differentActionsRight := &policy.SubjectMapping{ + AttributeValue: &policy.Value{Fqn: fqn}, + Actions: []*policy.Action{{Name: "write"}}, + SubjectConditionSet: scs, + } + + missingFQN := &policy.SubjectMapping{ + SubjectConditionSet: scs, + Actions: []*policy.Action{{Name: "read"}}, + } + missingSCS := &policy.SubjectMapping{ + AttributeValue: &policy.Value{Fqn: fqn}, + Actions: []*policy.Action{{Name: "read"}}, + } + complete := &policy.SubjectMapping{ + AttributeValue: &policy.Value{Fqn: fqn}, + Actions: []*policy.Action{{Name: "read"}}, + SubjectConditionSet: scs, + } + + tests := []struct { + name string + left *policy.SubjectMapping + right *policy.SubjectMapping + want bool + }{ + {name: "both nil", left: nil, right: nil, want: false}, + {name: "equal ignoring id, case, whitespace, action order", left: equivalentLeft, right: equivalentRight, want: true}, + {name: "different actions are not equal", left: differentActionsLeft, right: differentActionsRight, want: false}, + {name: "missing attribute value fqn never equals", left: missingFQN, right: complete, want: false}, + {name: "missing subject condition set never equals", left: missingSCS, right: complete, want: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, subjectMappingCanonicalEqual(tc.left, tc.right)) + }) + } +} + +func TestObligationTriggerCanonicalEqual(t *testing.T) { + t.Parallel() + + base := &policy.ObligationTrigger{ + AttributeValue: &policy.Value{Fqn: "https://example.com/attr/c/value/s"}, + ObligationValue: &policy.ObligationValue{Fqn: "https://example.com/obligation/o/value/notify"}, + Action: &policy.Action{Name: "decrypt"}, + } + + equivalentLeft := protoCloneTrigger(base) + equivalentLeft.Id = "ot-left" + equivalentLeft.Action = &policy.Action{Name: "Decrypt"} + equivalentLeft.AttributeValue = &policy.Value{Fqn: " " + base.GetAttributeValue().GetFqn() + " "} + + equivalentRight := protoCloneTrigger(base) + equivalentRight.Id = "ot-right" + equivalentRight.Action = &policy.Action{Name: "DECRYPT"} + equivalentRight.ObligationValue = &policy.ObligationValue{Fqn: " " + base.GetObligationValue().GetFqn() + " "} + + diffActionLeft := protoCloneTrigger(base) + diffActionLeft.Action = &policy.Action{Name: "read"} + diffActionRight := protoCloneTrigger(base) + diffActionRight.Action = &policy.Action{Name: "write"} + + missingAttr := protoCloneTrigger(base) + missingAttr.AttributeValue = &policy.Value{} + + missingAction := protoCloneTrigger(base) + missingAction.Action = &policy.Action{} + + missingObligation := protoCloneTrigger(base) + missingObligation.ObligationValue = &policy.ObligationValue{} + + tests := []struct { + name string + left *policy.ObligationTrigger + right *policy.ObligationTrigger + want bool + }{ + {name: "both nil", left: nil, right: nil, want: false}, + {name: "equal ignoring id, case, whitespace", left: equivalentLeft, right: equivalentRight, want: true}, + {name: "different action names are not equal", left: diffActionLeft, right: diffActionRight, want: false}, + {name: "missing attribute value fqn never equals", left: missingAttr, right: base, want: false}, + {name: "missing action name never equals", left: missingAction, right: base, want: false}, + {name: "missing obligation value fqn never equals", left: missingObligation, right: base, want: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, obligationTriggerCanonicalEqual(tc.left, tc.right)) + }) + } +} + +func TestRegisteredResourceCanonicalEqual(t *testing.T) { + t.Parallel() + + clean := testRegisteredResource( + "resource-clean", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "", + "read", + testAttributeValue("https://example.com/attr/c/value/p", nil), + ), + ), + ) + + withNilValuesAndAAVs := &policy.RegisteredResource{ + Name: "documents", + Values: []*policy.RegisteredResourceValue{ + nil, + { + Value: "prod", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + nil, + { + Action: &policy.Action{Name: "read"}, + AttributeValue: &policy.Value{Fqn: "https://example.com/attr/c/value/p"}, + }, + }, + }, + }, + } + + withSentinelAAV := &policy.RegisteredResource{ + Name: "documents", + Values: []*policy.RegisteredResourceValue{ + { + Value: "prod", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + {Action: &policy.Action{}, AttributeValue: &policy.Value{}}, + { + Action: &policy.Action{Name: "read"}, + AttributeValue: &policy.Value{Fqn: "https://example.com/attr/c/value/p"}, + }, + }, + }, + }, + } + + differentValue := testRegisteredResource( + "resource-other", + "documents", + testRegisteredResourceValue( + "dev", + testActionAttributeValue( + "", + "read", + testAttributeValue("https://example.com/attr/c/value/p", nil), + ), + ), + ) + + differentName := testRegisteredResource( + "resource-renamed", + "reports", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "", + "read", + testAttributeValue("https://example.com/attr/c/value/p", nil), + ), + ), + ) + + differentAttributeValue := testRegisteredResource( + "resource-other", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "", + "read", + testAttributeValue("https://example.com/attr/c/value/other", nil), + ), + ), + ) + + differentAction := testRegisteredResource( + "resource-other", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "", + "write", + testAttributeValue("https://example.com/attr/c/value/p", nil), + ), + ), + ) + + emptyName := &policy.RegisteredResource{Name: " "} + + tests := []struct { + name string + left *policy.RegisteredResource + right *policy.RegisteredResource + want bool + }{ + {name: "both nil", left: nil, right: nil, want: false}, + {name: "nil value and nil AAV are skipped", left: withNilValuesAndAAVs, right: clean, want: true}, + {name: "AAV with empty action and attribute is skipped", left: withSentinelAAV, right: clean, want: true}, + {name: "different values are not equal", left: clean, right: differentValue, want: false}, + {name: "different resource names are not equal", left: clean, right: differentName, want: false}, + {name: "different AAV attribute value FQNs are not equal", left: clean, right: differentAttributeValue, want: false}, + {name: "different AAV action names are not equal", left: clean, right: differentAction, want: false}, + {name: "empty name never equals", left: emptyName, right: clean, want: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, registeredResourceCanonicalEqual(tc.left, tc.right)) + }) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/derived.go b/otdfctl/migrations/namespacedpolicy/derived.go new file mode 100644 index 0000000000..72f8ed70d9 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/derived.go @@ -0,0 +1,441 @@ +package namespacedpolicy + +import ( + "errors" + "fmt" + + "github.com/opentdf/platform/protocol/go/policy" +) + +type DerivedTargets struct { + Scopes []Scope + Actions []*DerivedAction + SubjectConditionSets []*DerivedSubjectConditionSet + SubjectMappings []*DerivedSubjectMapping + RegisteredResources []*DerivedRegisteredResource + ObligationTriggers []*DerivedObligationTrigger +} + +type DerivedAction struct { + Source *policy.Action + Targets []*policy.Namespace +} + +type DerivedSubjectConditionSet struct { + Source *policy.SubjectConditionSet + Targets []*policy.Namespace +} + +type DerivedSubjectMapping struct { + Source *policy.SubjectMapping + Target *policy.Namespace +} + +type DerivedRegisteredResource struct { + Source *policy.RegisteredResource + Target *policy.Namespace + Unresolved *Unresolved +} + +type DerivedObligationTrigger struct { + Source *policy.ObligationTrigger + Target *policy.Namespace +} + +type targetDeriver struct { + namespaces []*policy.Namespace + namespaceByID map[string]*policy.Namespace + namespaceByFQN map[string]*policy.Namespace + actionTargetsByID map[string]*namespaceAccumulator + scsTargetsByID map[string]*namespaceAccumulator +} + +var errSkipRegisteredResource = errors.New("skip registered resource") + +func deriveTargets(retrieved *Retrieved, namespaces []*policy.Namespace) (*DerivedTargets, error) { + if retrieved == nil { + return nil, ErrNilRetrieved + } + + deriver := newTargetDeriver(namespaces) + derived := &DerivedTargets{ + Scopes: append([]Scope(nil), retrieved.Scopes...), + Actions: make([]*DerivedAction, 0, len(retrieved.Candidates.Actions)), + SubjectConditionSets: make([]*DerivedSubjectConditionSet, 0, len(retrieved.Candidates.SubjectConditionSets)), + SubjectMappings: make([]*DerivedSubjectMapping, 0, len(retrieved.Candidates.SubjectMappings)), + RegisteredResources: make([]*DerivedRegisteredResource, 0, len(retrieved.Candidates.RegisteredResources)), + ObligationTriggers: make([]*DerivedObligationTrigger, 0, len(retrieved.Candidates.ObligationTriggers)), + } + + for _, mapping := range retrieved.Candidates.SubjectMappings { + if mapping == nil { + continue + } + item, err := deriver.deriveSubjectMapping(mapping) + if err != nil { + return nil, err + } + derived.SubjectMappings = append(derived.SubjectMappings, item) + deriver.observeSubjectMapping(item) + } + + for _, resource := range retrieved.Candidates.RegisteredResources { + if resource == nil { + continue + } + item, err := deriver.deriveRegisteredResource(resource) + if err != nil { + if errors.Is(err, errSkipRegisteredResource) { + continue + } + return nil, err + } + derived.RegisteredResources = append(derived.RegisteredResources, item) + deriver.observeRegisteredResource(item) + } + + for _, trigger := range retrieved.Candidates.ObligationTriggers { + if trigger == nil { + continue + } + item, err := deriver.deriveObligationTrigger(trigger) + if err != nil { + return nil, err + } + derived.ObligationTriggers = append(derived.ObligationTriggers, item) + deriver.observeObligationTrigger(item) + } + + for _, action := range retrieved.Candidates.Actions { + if action == nil { + continue + } + item := deriver.deriveAction(action) + if item == nil { + continue + } + derived.Actions = append(derived.Actions, item) + } + + for _, scs := range retrieved.Candidates.SubjectConditionSets { + if scs == nil { + continue + } + item := deriver.deriveSubjectConditionSet(scs) + if item == nil { + continue + } + derived.SubjectConditionSets = append(derived.SubjectConditionSets, item) + } + + return derived, nil +} + +func newTargetDeriver(namespaces []*policy.Namespace) *targetDeriver { + namespaceByID := make(map[string]*policy.Namespace, len(namespaces)) + namespaceByFQN := make(map[string]*policy.Namespace, len(namespaces)) + for _, namespace := range namespaces { + if namespace == nil { + continue + } + if id := namespace.GetId(); id != "" { + namespaceByID[id] = namespace + } + if fqn := namespace.GetFqn(); fqn != "" { + namespaceByFQN[fqn] = namespace + } + } + + return &targetDeriver{ + namespaces: namespaces, + namespaceByID: namespaceByID, + namespaceByFQN: namespaceByFQN, + actionTargetsByID: make(map[string]*namespaceAccumulator), + scsTargetsByID: make(map[string]*namespaceAccumulator), + } +} + +func (d *targetDeriver) deriveSubjectMapping(mapping *policy.SubjectMapping) (*DerivedSubjectMapping, error) { + item := &DerivedSubjectMapping{Source: mapping} + namespace, err := d.resolveNamespace(namespaceFromAttributeValue(mapping.GetAttributeValue())) + if err != nil { + return nil, fmt.Errorf("subject mapping %q: %w", mapping.GetId(), err) + } + + item.Target = namespace + return item, nil +} + +func (d *targetDeriver) deriveRegisteredResource(resource *policy.RegisteredResource) (*DerivedRegisteredResource, error) { + item := &DerivedRegisteredResource{Source: resource} + + namespaceRef, ok := registeredResourceNamespaceRef(resource) + if !ok { + // Registered resources only resolve when their action-attribute values + // imply exactly one target namespace. No AAV-derived namespace, or AAVs + // spanning multiple namespaces, leaves the RR unresolved here. + if hasRegisteredResourceActionAttributeValues(resource) { + item.Unresolved = &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + Message: fmt.Errorf("%w: registered resource spans multiple target namespaces", ErrUndeterminedTargetMapping).Error(), + } + return item, nil + } + // Skip registered resources that have no action-attribute values because they do not provide a derivable namespace target. + return nil, errSkipRegisteredResource + } + + namespace, err := d.resolveNamespace(namespaceRef) + if err != nil { + return nil, fmt.Errorf("registered resource %q: %w", resource.GetId(), err) + } + + item.Target = namespace + return item, nil +} + +func (d *targetDeriver) deriveObligationTrigger(trigger *policy.ObligationTrigger) (*DerivedObligationTrigger, error) { + item := &DerivedObligationTrigger{Source: trigger} + namespace, err := d.resolveNamespace(namespaceFromObligationValue(trigger.GetObligationValue())) + if err != nil { + return nil, fmt.Errorf("obligation trigger %q: %w", trigger.GetId(), err) + } + + item.Target = namespace + return item, nil +} + +// deriveAction returns nil when the action has no observed referencing +// subject mapping, registered resource, or obligation trigger in scope — an +// orphan action has no target namespace to migrate to and is silently skipped +// rather than carried through as an empty-targets ResolvedAction. +func (d *targetDeriver) deriveAction(action *policy.Action) *DerivedAction { + targets := d.targets(d.actionTargetsByID[action.GetId()]) + if len(targets) == 0 { + return nil + } + + return &DerivedAction{ + Source: action, + Targets: targets, + } +} + +// deriveSubjectConditionSet returns nil when the SCS has no referencing +// subject mapping in scope — a legacy SCS that isn't being migrated is silently +// skipped rather than treated as a retrieval invariant violation. +func (d *targetDeriver) deriveSubjectConditionSet(scs *policy.SubjectConditionSet) *DerivedSubjectConditionSet { + targets := d.targets(d.scsTargetsByID[scs.GetId()]) + if len(targets) == 0 { + return nil + } + + return &DerivedSubjectConditionSet{ + Source: scs, + Targets: targets, + } +} + +func (d *targetDeriver) observeSubjectMapping(item *DerivedSubjectMapping) { + if item == nil || item.Source == nil || item.Target == nil { + return + } + + for _, action := range item.Source.GetActions() { + d.addActionTarget(action.GetId(), item.Target) + } + + if scsID := item.Source.GetSubjectConditionSet().GetId(); scsID != "" { + d.addSubjectConditionSetTarget(scsID, item.Target) + } +} + +func (d *targetDeriver) observeRegisteredResource(item *DerivedRegisteredResource) { + if item == nil || item.Source == nil || item.Target == nil { + return + } + + for _, value := range item.Source.GetValues() { + for _, aav := range value.GetActionAttributeValues() { + d.addActionTarget(aav.GetAction().GetId(), item.Target) + } + } +} + +func (d *targetDeriver) observeObligationTrigger(item *DerivedObligationTrigger) { + if item == nil || item.Source == nil || item.Target == nil { + return + } + + d.addActionTarget(item.Source.GetAction().GetId(), item.Target) +} + +func (d *targetDeriver) addActionTarget(actionID string, namespace *policy.Namespace) { + if actionID == "" || namespace == nil { + return + } + + targets := d.actionTargetsByID[actionID] + if targets == nil { + targets = newNamespaceAccumulator() + d.actionTargetsByID[actionID] = targets + } + targets.add(namespace) +} + +func (d *targetDeriver) addSubjectConditionSetTarget(scsID string, namespace *policy.Namespace) { + if scsID == "" || namespace == nil { + return + } + + targets := d.scsTargetsByID[scsID] + if targets == nil { + targets = newNamespaceAccumulator() + d.scsTargetsByID[scsID] = targets + } + targets.add(namespace) +} + +func (d *targetDeriver) targets(targets *namespaceAccumulator) []*policy.Namespace { + if targets == nil { + return nil + } + + return targets.slice() +} + +func (d *targetDeriver) resolveNamespace(namespace *policy.Namespace) (*policy.Namespace, error) { + if namespace == nil { + return nil, fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + if id := namespace.GetId(); id != "" { + if resolved, ok := d.namespaceByID[id]; ok { + return resolved, nil + } + } + if fqn := namespace.GetFqn(); fqn != "" { + if resolved, ok := d.namespaceByFQN[fqn]; ok { + return resolved, nil + } + } + + return nil, fmt.Errorf("%w: id=%q fqn=%q", ErrMissingTargetNamespace, namespace.GetId(), namespace.GetFqn()) +} + +func derivedActionNamespaces(derived *DerivedTargets) []*policy.Namespace { + if derived == nil { + return nil + } + + ordered := newNamespaceAccumulator() + for _, action := range derived.Actions { + if action == nil { + continue + } + for _, namespace := range action.Targets { + ordered.add(namespace) + } + } + + return ordered.slice() +} + +func derivedSubjectConditionSetNamespaces(derived *DerivedTargets) []*policy.Namespace { + if derived == nil { + return nil + } + + ordered := newNamespaceAccumulator() + for _, scs := range derived.SubjectConditionSets { + if scs == nil { + continue + } + for _, namespace := range scs.Targets { + ordered.add(namespace) + } + } + + return ordered.slice() +} + +func derivedSubjectMappingNamespaces(derived *DerivedTargets) []*policy.Namespace { + if derived == nil { + return nil + } + + ordered := newNamespaceAccumulator() + for _, mapping := range derived.SubjectMappings { + if mapping == nil { + continue + } + ordered.add(mapping.Target) + } + + return ordered.slice() +} + +func derivedRegisteredResourceNamespaces(derived *DerivedTargets) []*policy.Namespace { + if derived == nil { + return nil + } + + ordered := newNamespaceAccumulator() + for _, resource := range derived.RegisteredResources { + if resource == nil { + continue + } + ordered.add(resource.Target) + } + + return ordered.slice() +} + +func derivedObligationTriggerNamespaces(derived *DerivedTargets) []*policy.Namespace { + if derived == nil { + return nil + } + + ordered := newNamespaceAccumulator() + for _, trigger := range derived.ObligationTriggers { + if trigger == nil { + continue + } + ordered.add(trigger.Target) + } + + return ordered.slice() +} + +type namespaceAccumulator struct { + items []*policy.Namespace + seen map[string]struct{} +} + +func newNamespaceAccumulator() *namespaceAccumulator { + return &namespaceAccumulator{ + seen: make(map[string]struct{}), + } +} + +func (a *namespaceAccumulator) add(namespace *policy.Namespace) { + if a == nil || namespace == nil { + return + } + key := namespaceRefKey(namespace) + if key == "" { + return + } + if _, ok := a.seen[key]; ok { + return + } + a.seen[key] = struct{}{} + a.items = append(a.items, namespace) +} + +func (a *namespaceAccumulator) slice() []*policy.Namespace { + if a == nil { + return nil + } + + return append([]*policy.Namespace(nil), a.items...) +} diff --git a/otdfctl/migrations/namespacedpolicy/derived_test.go b/otdfctl/migrations/namespacedpolicy/derived_test.go new file mode 100644 index 0000000000..9f61e4f7a9 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/derived_test.go @@ -0,0 +1,458 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDeriveTargetsCollectsTargetsAndReferencesFromDependencies(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + retrieved := &Retrieved{ + Scopes: []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeRegisteredResources, + ScopeObligationTriggers, + }, + Candidates: Candidates{ + Actions: []*policy.Action{ + {Id: "action-1", Name: "decrypt"}, + }, + SubjectConditionSets: []*policy.SubjectConditionSet{ + {Id: "scs-1"}, + }, + SubjectMappings: []*policy.SubjectMapping{ + { + Id: "mapping-1", + AttributeValue: testAttributeValue( + "https://example.com/attr/classification/value/secret", + namespace, + ), + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + Actions: []*policy.Action{ + {Id: "action-1", Name: "decrypt"}, + }, + }, + }, + RegisteredResources: []*policy.RegisteredResource{ + testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", namespace), + ), + ), + ), + }, + ObligationTriggers: []*policy.ObligationTrigger{ + { + Id: "trigger-1", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + ObligationValue: &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{Namespace: namespace}, + }, + }, + }, + }, + } + + derived, err := deriveTargets(retrieved, []*policy.Namespace{namespace}) + require.NoError(t, err) + + require.Len(t, derived.Actions, 1) + require.Len(t, derived.Actions[0].Targets, 1) + assert.Equal(t, namespace.GetId(), derived.Actions[0].Targets[0].GetId()) + + require.Len(t, derived.SubjectConditionSets, 1) + require.Len(t, derived.SubjectConditionSets[0].Targets, 1) + assert.Equal(t, namespace.GetId(), derived.SubjectConditionSets[0].Targets[0].GetId()) + require.Len(t, derived.SubjectMappings, 1) + assert.Equal(t, namespace.GetId(), derived.SubjectMappings[0].Target.GetId()) + require.Len(t, derived.RegisteredResources, 1) + assert.Equal(t, namespace.GetId(), derived.RegisteredResources[0].Target.GetId()) + require.Len(t, derived.ObligationTriggers, 1) + assert.Equal(t, namespace.GetId(), derived.ObligationTriggers[0].Target.GetId()) +} + +func TestDeriveTargetsFailsWhenSubjectMappingNamespaceCannotBeDerived(t *testing.T) { + t.Parallel() + + retrieved := &Retrieved{ + Scopes: []Scope{ScopeSubjectMappings}, + Candidates: Candidates{ + SubjectMappings: []*policy.SubjectMapping{ + { + Id: "mapping-1", + AttributeValue: &policy.Value{}, + }, + }, + }, + } + + derived, err := deriveTargets(retrieved, nil) + require.Error(t, err) + assert.Nil(t, derived) + assert.EqualError(t, err, `subject mapping "mapping-1": could not determine target namespace: empty namespace reference`) +} + +func TestDeriveTargetsKeepsRegisteredResourceNamespaceConflictUnresolved(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://other.example.com", + } + retrieved := &Retrieved{ + Scopes: []Scope{ScopeRegisteredResources}, + Candidates: Candidates{ + RegisteredResources: []*policy.RegisteredResource{ + testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "encrypt", + testAttributeValue("https://other.example.com/attr/classification/value/secret", rightNamespace), + ), + ), + ), + }, + }, + } + + derived, err := deriveTargets(retrieved, []*policy.Namespace{leftNamespace, rightNamespace}) + require.NoError(t, err) + require.Len(t, derived.RegisteredResources, 1) + require.NotNil(t, derived.RegisteredResources[0].Unresolved) + assert.Equal(t, UnresolvedReasonRegisteredResourceConflictingNamespaces, derived.RegisteredResources[0].Unresolved.Reason) + assert.Equal( + t, + "could not determine target namespace: registered resource spans multiple target namespaces", + derived.RegisteredResources[0].Unresolved.Message, + ) + assert.Nil(t, derived.RegisteredResources[0].Target) +} + +func TestDeriveTargetsSkipsRegisteredResourceWithoutActionAttributeValues(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + retrieved := &Retrieved{ + Scopes: []Scope{ScopeRegisteredResources}, + Candidates: Candidates{ + RegisteredResources: []*policy.RegisteredResource{ + testRegisteredResource( + "resource-1", + "documents", + &policy.RegisteredResourceValue{ + Value: "prod", + }, + ), + }, + }, + } + + derived, err := deriveTargets(retrieved, []*policy.Namespace{namespace}) + require.NoError(t, err) + assert.Empty(t, derived.RegisteredResources) +} + +func TestDeriveTargetsSkipsNilSubjectMappingCandidate(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + // A nil entry in Candidates.SubjectMappings is a retrieval artifact and + // must be silently dropped, not halt the whole loop. + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeSubjectMappings}, + Candidates: Candidates{ + SubjectMappings: []*policy.SubjectMapping{ + nil, + { + Id: "mapping-1", + AttributeValue: testAttributeValue( + "https://example.com/attr/classification/value/secret", + namespace, + ), + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + Actions: []*policy.Action{{Id: "action-1", Name: "decrypt"}}, + }, + }, + }, + }, + []*policy.Namespace{namespace}, + ) + require.NoError(t, err) + require.Len(t, derived.SubjectMappings, 1) + assert.Equal(t, "mapping-1", derived.SubjectMappings[0].Source.GetId()) +} + +func TestDeriveTargetsSkipsNilObligationTriggerCandidate(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + // A nil entry in Candidates.ObligationTriggers is a retrieval artifact + // and must be silently dropped, not halt the whole loop. + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeObligationTriggers}, + Candidates: Candidates{ + ObligationTriggers: []*policy.ObligationTrigger{ + nil, + { + Id: "trigger-1", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + ObligationValue: &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{Namespace: namespace}, + }, + }, + }, + }, + }, + []*policy.Namespace{namespace}, + ) + require.NoError(t, err) + require.Len(t, derived.ObligationTriggers, 1) + assert.Equal(t, "trigger-1", derived.ObligationTriggers[0].Source.GetId()) +} + +func TestDeriveTargetsSkipsNilActionCandidate(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + // A nil entry in Candidates.Actions is treated as a retrieval artifact and + // silently dropped rather than aborting the loop. The surviving action is + // referenced by a subject mapping so it has an observable target namespace + // (otherwise deriveAction would also drop it as an orphan). + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeActions, ScopeSubjectMappings}, + Candidates: Candidates{ + Actions: []*policy.Action{ + nil, + {Id: "action-1", Name: "decrypt"}, + }, + SubjectMappings: []*policy.SubjectMapping{ + { + Id: "mapping-1", + AttributeValue: testAttributeValue( + "https://example.com/attr/classification/value/secret", + namespace, + ), + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + Actions: []*policy.Action{{Id: "action-1", Name: "decrypt"}}, + }, + }, + }, + }, + []*policy.Namespace{namespace}, + ) + require.NoError(t, err) + require.Len(t, derived.Actions, 1) + assert.Equal(t, "action-1", derived.Actions[0].Source.GetId()) +} + +func TestDeriveTargetsSkipsActionsWithNoReferencingDependency(t *testing.T) { + t.Parallel() + + // An orphan action — one no in-scope subject mapping, registered resource, + // or obligation trigger refers to — has no derivable target namespace. + // Treat it as "nothing to migrate" and drop it at derive time rather than + // carrying it through the resolver as a zero-Results ResolvedAction. + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeActions}, + Candidates: Candidates{ + Actions: []*policy.Action{ + {Id: "action-orphan", Name: "decrypt"}, + }, + }, + }, + nil, + ) + require.NoError(t, err) + assert.Empty(t, derived.Actions) +} + +func TestDeriveTargetsSkipsNilSubjectConditionSetCandidate(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + // Include a surviving SCS referenced by a subject mapping so the slice + // isn't empty — the nil must be dropped in place, not abort the loop. + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeSubjectConditionSets, ScopeSubjectMappings}, + Candidates: Candidates{ + SubjectConditionSets: []*policy.SubjectConditionSet{ + nil, + {Id: "scs-1"}, + }, + SubjectMappings: []*policy.SubjectMapping{ + { + Id: "mapping-1", + AttributeValue: testAttributeValue( + "https://example.com/attr/classification/value/secret", + namespace, + ), + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + Actions: []*policy.Action{{Id: "action-1", Name: "decrypt"}}, + }, + }, + }, + }, + []*policy.Namespace{namespace}, + ) + require.NoError(t, err) + require.Len(t, derived.SubjectConditionSets, 1) + assert.Equal(t, "scs-1", derived.SubjectConditionSets[0].Source.GetId()) +} + +func TestDeriveTargetsSkipsSubjectConditionSetWithNoReferencingDependency(t *testing.T) { + t.Parallel() + + // A legacy SCS that no in-scope subject mapping points at has no + // derivable target. Treat it as "nothing to migrate" and drop it, rather + // than erroring and halting the whole plan. + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeSubjectConditionSets}, + Candidates: Candidates{ + SubjectConditionSets: []*policy.SubjectConditionSet{ + {Id: "scs-1"}, + }, + }, + }, + nil, + ) + require.NoError(t, err) + assert.Empty(t, derived.SubjectConditionSets) +} + +func TestDeriveTargetsSkipsNilRegisteredResourceCandidate(t *testing.T) { + t.Parallel() + + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeRegisteredResources}, + Candidates: Candidates{ + RegisteredResources: []*policy.RegisteredResource{nil}, + }, + }, + nil, + ) + require.NoError(t, err) + assert.Empty(t, derived.RegisteredResources) +} + +func TestDeriveTargetsResolvesNamespaceByFQNWhenIDMissing(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + // AttributeValue has no nested Attribute.Namespace, forcing + // namespaceFromAttributeValue to produce an {Fqn-only} reference from + // the parsed FQN. resolveNamespace must fall back from the empty ID + // lookup to the FQN lookup and return the full namespace record. + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeSubjectMappings}, + Candidates: Candidates{ + SubjectMappings: []*policy.SubjectMapping{ + { + Id: "mapping-1", + AttributeValue: &policy.Value{ + Fqn: "https://example.com/attr/classification/value/secret", + }, + }, + }, + }, + }, + []*policy.Namespace{targetNamespace}, + ) + require.NoError(t, err) + require.Len(t, derived.SubjectMappings, 1) + assert.Equal(t, targetNamespace.GetId(), derived.SubjectMappings[0].Target.GetId()) + assert.Equal(t, targetNamespace.GetFqn(), derived.SubjectMappings[0].Target.GetFqn()) +} + +func TestDeriveTargetsFailsWhenNamespaceRefNotFound(t *testing.T) { + t.Parallel() + + derived, err := deriveTargets( + &Retrieved{ + Scopes: []Scope{ScopeSubjectMappings}, + Candidates: Candidates{ + SubjectMappings: []*policy.SubjectMapping{ + { + Id: "mapping-1", + AttributeValue: &policy.Value{ + Fqn: "https://missing.example.com/attr/foo/value/bar", + }, + }, + }, + }, + }, + []*policy.Namespace{ + {Id: "ns-1", Fqn: "https://example.com"}, + }, + ) + require.Error(t, err) + assert.Nil(t, derived) + require.ErrorIs(t, err, ErrMissingTargetNamespace) + assert.Contains(t, err.Error(), `subject mapping "mapping-1"`) + assert.Contains(t, err.Error(), "missing.example.com") +} + +func TestNamespaceAccumulatorDeduplicatesByRef(t *testing.T) { + t.Parallel() + + // Dedup is load-bearing: an action referenced from multiple observers + // targeting the same namespace must resolve to a single target, not N — + // this drives single-vs-multi-namespace branching downstream. + acc := newNamespaceAccumulator() + acc.add(&policy.Namespace{Id: "ns-1", Fqn: "https://example.com"}) + acc.add(&policy.Namespace{Id: "ns-1", Fqn: "https://example.com"}) // same identifier, different struct + acc.add(&policy.Namespace{Id: "ns-2", Fqn: "https://other.example.com"}) + acc.add(nil) + acc.add(&policy.Namespace{}) // empty key — must be skipped + + got := acc.slice() + require.Len(t, got, 2) + assert.Equal(t, "ns-1", got[0].GetId()) + assert.Equal(t, "ns-2", got[1].GetId()) +} diff --git a/otdfctl/migrations/namespacedpolicy/execute_test_helpers_test.go b/otdfctl/migrations/namespacedpolicy/execute_test_helpers_test.go new file mode 100644 index 0000000000..2ee1840c3c --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/execute_test_helpers_test.go @@ -0,0 +1,369 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "fmt" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" +) + +var ( + errMissingMockActionResult = errors.New("missing mock action result") + errMissingMockSubjectConditionSetResult = errors.New("missing mock subject condition set result") + errMissingMockSubjectMappingResult = errors.New("missing mock subject mapping result") + errMissingMockObligationTriggerResult = errors.New("missing mock obligation trigger result") + errMissingMockRegisteredResourceResult = errors.New("missing mock registered resource result") + errMissingMockRegisteredResourceValue = errors.New("missing mock registered resource value result") +) + +type expectedError struct { + is error + message string +} + +func wantError(is error, format string, args ...any) *expectedError { + return &expectedError{ + is: is, + message: fmt.Sprintf("%s: %s", is, fmt.Sprintf(format, args...)), + } +} + +type mockExecutorHandler struct { + created map[string]map[string]*createdActionCall + results map[string]map[string]*policy.Action // ! Should be renamed to actionResults + errs map[string]map[string]error + createdSubjectConditions map[string]map[string]*createdSubjectConditionSetCall + subjectConditionSetResult map[string]map[string]*policy.SubjectConditionSet + subjectConditionSetErrs map[string]map[string]error + createdSubjectMappings map[string]map[string]*createdSubjectMappingCall + subjectMappingResults map[string]map[string]*policy.SubjectMapping + subjectMappingErrs map[string]map[string]error + createdObligationTriggers map[string]map[string]*createdObligationTriggerCall + obligationTriggerResult map[string]map[string]*policy.ObligationTrigger + obligationTriggerErrs map[string]map[string]error + createdRegisteredResources map[string]map[string]*createdRegisteredResourceCall + registeredResourceResult map[string]map[string]*policy.RegisteredResource + registeredResourcesByID map[string]*policy.RegisteredResource + registeredResourceErrs map[string]map[string]error + createdRegisteredResourceValues map[string]map[string]*createdRegisteredResourceValueCall + registeredResourceValueResult map[string]map[string]*policy.RegisteredResourceValue + registeredResourceValueErrs map[string]map[string]error + deleteCalls []string + deletedActions []string + deleteActionErrs map[string]error + deletedSubjectConditionSets []string + deleteSubjectConditionSetErrs map[string]error + deletedSubjectMappings []string + deleteSubjectMappingErrs map[string]error + deletedRegisteredResources []string + deleteRegisteredResourceErrs map[string]error + deletedRegisteredResourceValues []string + deleteRegisteredResourceValueErrs map[string]error + deletedObligationTriggers []string + deleteObligationTriggerErrs map[string]error +} + +type createdActionCall struct { + Name string + Namespace string + Metadata *common.MetadataMutable +} + +type createdSubjectConditionSetCall struct { + SubjectSets []*policy.SubjectSet + Namespace string + Metadata *common.MetadataMutable +} + +type createdSubjectMappingCall struct { + AttributeValueID string + Actions []*policy.Action + ExistingSubjectConditionSet string + NewSubjectConditionSet *subjectmapping.SubjectConditionSetCreate + Namespace string + Metadata *common.MetadataMutable +} +type createdObligationTriggerCall struct { + AttributeValue string + Action string + ObligationValue string + ClientID string + Metadata *common.MetadataMutable +} + +type createdRegisteredResourceCall struct { + Name string + Namespace string + Values []string + Metadata *common.MetadataMutable +} + +type createdRegisteredResourceValueCall struct { + ResourceID string + Value string + ActionAttributeValues []*registeredresources.ActionAttributeValue + Metadata *common.MetadataMutable +} + +func (m *mockExecutorHandler) CreateAction(_ context.Context, name string, namespace string, metadata *common.MetadataMutable) (*policy.Action, error) { + if m.created == nil { + m.created = make(map[string]map[string]*createdActionCall) + } + if m.created[name] == nil { + m.created[name] = make(map[string]*createdActionCall) + } + + m.created[name][namespace] = &createdActionCall{ + Name: name, + Namespace: namespace, + Metadata: metadata, + } + + if m.errs != nil && m.errs[name] != nil { + if err := m.errs[name][namespace]; err != nil { + return nil, err + } + } + if m.results != nil && m.results[name] != nil { + if result := m.results[name][namespace]; result != nil { + return result, nil + } + } + + return nil, errMissingMockActionResult +} + +func (m *mockExecutorHandler) CreateSubjectConditionSet(_ context.Context, ss []*policy.SubjectSet, metadata *common.MetadataMutable, namespace string) (*policy.SubjectConditionSet, error) { + sourceID := metadata.GetLabels()[migrationLabelMigratedFrom] + + if m.createdSubjectConditions == nil { + m.createdSubjectConditions = make(map[string]map[string]*createdSubjectConditionSetCall) + } + if m.createdSubjectConditions[sourceID] == nil { + m.createdSubjectConditions[sourceID] = make(map[string]*createdSubjectConditionSetCall) + } + + m.createdSubjectConditions[sourceID][namespace] = &createdSubjectConditionSetCall{ + SubjectSets: ss, + Namespace: namespace, + Metadata: metadata, + } + + if m.subjectConditionSetErrs != nil && m.subjectConditionSetErrs[sourceID] != nil { + if err := m.subjectConditionSetErrs[sourceID][namespace]; err != nil { + return nil, err + } + } + if m.subjectConditionSetResult != nil && m.subjectConditionSetResult[sourceID] != nil { + if result := m.subjectConditionSetResult[sourceID][namespace]; result != nil { + return result, nil + } + } + + return nil, errMissingMockSubjectConditionSetResult +} + +func (m *mockExecutorHandler) CreateNewSubjectMapping(_ context.Context, attrValID string, actions []*policy.Action, existingSCSId string, newScs *subjectmapping.SubjectConditionSetCreate, metadata *common.MetadataMutable, namespace string) (*policy.SubjectMapping, error) { + sourceID := metadata.GetLabels()[migrationLabelMigratedFrom] + + if m.createdSubjectMappings == nil { + m.createdSubjectMappings = make(map[string]map[string]*createdSubjectMappingCall) + } + if m.createdSubjectMappings[sourceID] == nil { + m.createdSubjectMappings[sourceID] = make(map[string]*createdSubjectMappingCall) + } + + m.createdSubjectMappings[sourceID][namespace] = &createdSubjectMappingCall{ + AttributeValueID: attrValID, + Actions: actions, + ExistingSubjectConditionSet: existingSCSId, + NewSubjectConditionSet: newScs, + Namespace: namespace, + Metadata: metadata, + } + + if m.subjectMappingErrs != nil && m.subjectMappingErrs[sourceID] != nil { + if err := m.subjectMappingErrs[sourceID][namespace]; err != nil { + return nil, err + } + } + if m.subjectMappingResults != nil && m.subjectMappingResults[sourceID] != nil { + if result := m.subjectMappingResults[sourceID][namespace]; result != nil { + return result, nil + } + } + + return nil, errMissingMockSubjectMappingResult +} + +func (m *mockExecutorHandler) CreateObligationTrigger(_ context.Context, attributeValue, action, obligationValue, clientID string, metadata *common.MetadataMutable) (*policy.ObligationTrigger, error) { + sourceID := metadata.GetLabels()[migrationLabelMigratedFrom] + + if m.createdObligationTriggers == nil { + m.createdObligationTriggers = make(map[string]map[string]*createdObligationTriggerCall) + } + if m.createdObligationTriggers[sourceID] == nil { + m.createdObligationTriggers[sourceID] = make(map[string]*createdObligationTriggerCall) + } + + m.createdObligationTriggers[sourceID][action] = &createdObligationTriggerCall{ + AttributeValue: attributeValue, + Action: action, + ObligationValue: obligationValue, + ClientID: clientID, + Metadata: metadata, + } + + if m.obligationTriggerErrs != nil && m.obligationTriggerErrs[sourceID] != nil { + if err := m.obligationTriggerErrs[sourceID][action]; err != nil { + return nil, err + } + } + if m.obligationTriggerResult != nil && m.obligationTriggerResult[sourceID] != nil { + if result := m.obligationTriggerResult[sourceID][action]; result != nil { + return result, nil + } + } + + return nil, errMissingMockObligationTriggerResult +} + +func (m *mockExecutorHandler) CreateRegisteredResource(_ context.Context, namespace string, name string, values []string, metadata *common.MetadataMutable) (*policy.RegisteredResource, error) { + sourceID := metadata.GetLabels()[migrationLabelMigratedFrom] + + if m.createdRegisteredResources == nil { + m.createdRegisteredResources = make(map[string]map[string]*createdRegisteredResourceCall) + } + if m.createdRegisteredResources[sourceID] == nil { + m.createdRegisteredResources[sourceID] = make(map[string]*createdRegisteredResourceCall) + } + + m.createdRegisteredResources[sourceID][namespace] = &createdRegisteredResourceCall{ + Name: name, + Namespace: namespace, + Values: values, + Metadata: metadata, + } + + if m.registeredResourceErrs != nil && m.registeredResourceErrs[sourceID] != nil { + if err := m.registeredResourceErrs[sourceID][namespace]; err != nil { + return nil, err + } + } + if m.registeredResourceResult != nil && m.registeredResourceResult[sourceID] != nil { + if result := m.registeredResourceResult[sourceID][namespace]; result != nil { + if m.registeredResourcesByID == nil { + m.registeredResourcesByID = make(map[string]*policy.RegisteredResource) + } + m.registeredResourcesByID[result.GetId()] = result + return result, nil + } + } + + return nil, errMissingMockRegisteredResourceResult +} + +func (m *mockExecutorHandler) GetRegisteredResource(_ context.Context, id, _, _ string) (*policy.RegisteredResource, error) { + if id == "" { + return nil, errMissingMockRegisteredResourceResult + } + if m.registeredResourcesByID != nil { + if result := m.registeredResourcesByID[id]; result != nil { + return result, nil + } + } + return nil, errMissingMockRegisteredResourceResult +} + +func (m *mockExecutorHandler) CreateRegisteredResourceValue(_ context.Context, resourceID string, value string, actionAttributeValues []*registeredresources.ActionAttributeValue, metadata *common.MetadataMutable) (*policy.RegisteredResourceValue, error) { + sourceID := metadata.GetLabels()[migrationLabelMigratedFrom] + + if m.createdRegisteredResourceValues == nil { + m.createdRegisteredResourceValues = make(map[string]map[string]*createdRegisteredResourceValueCall) + } + if m.createdRegisteredResourceValues[sourceID] == nil { + m.createdRegisteredResourceValues[sourceID] = make(map[string]*createdRegisteredResourceValueCall) + } + + m.createdRegisteredResourceValues[sourceID][resourceID] = &createdRegisteredResourceValueCall{ + ResourceID: resourceID, + Value: value, + ActionAttributeValues: actionAttributeValues, + Metadata: metadata, + } + + if m.registeredResourceValueErrs != nil && m.registeredResourceValueErrs[sourceID] != nil { + if err := m.registeredResourceValueErrs[sourceID][resourceID]; err != nil { + return nil, err + } + } + if m.registeredResourceValueResult != nil && m.registeredResourceValueResult[sourceID] != nil { + if result := m.registeredResourceValueResult[sourceID][resourceID]; result != nil { + return result, nil + } + } + + return nil, errMissingMockRegisteredResourceValue +} + +func (m *mockExecutorHandler) DeleteAction(_ context.Context, id string) error { + m.deleteCalls = append(m.deleteCalls, "action:"+id) + m.deletedActions = append(m.deletedActions, id) + if m.deleteActionErrs == nil { + return nil + } + return m.deleteActionErrs[id] +} + +func (m *mockExecutorHandler) DeleteSubjectConditionSet(_ context.Context, id string) error { + m.deleteCalls = append(m.deleteCalls, "subject-condition-set:"+id) + m.deletedSubjectConditionSets = append(m.deletedSubjectConditionSets, id) + if m.deleteSubjectConditionSetErrs == nil { + return nil + } + return m.deleteSubjectConditionSetErrs[id] +} + +func (m *mockExecutorHandler) DeleteSubjectMapping(_ context.Context, id string) (*policy.SubjectMapping, error) { + m.deleteCalls = append(m.deleteCalls, "subject-mapping:"+id) + m.deletedSubjectMappings = append(m.deletedSubjectMappings, id) + if m.deleteSubjectMappingErrs != nil { + if err := m.deleteSubjectMappingErrs[id]; err != nil { + return nil, err + } + } + return &policy.SubjectMapping{Id: id}, nil +} + +func (m *mockExecutorHandler) DeleteRegisteredResource(_ context.Context, id string) error { + m.deleteCalls = append(m.deleteCalls, "registered-resource:"+id) + m.deletedRegisteredResources = append(m.deletedRegisteredResources, id) + if m.deleteRegisteredResourceErrs == nil { + return nil + } + return m.deleteRegisteredResourceErrs[id] +} + +func (m *mockExecutorHandler) DeleteRegisteredResourceValue(_ context.Context, id string) error { + m.deleteCalls = append(m.deleteCalls, "registered-resource-value:"+id) + m.deletedRegisteredResourceValues = append(m.deletedRegisteredResourceValues, id) + if m.deleteRegisteredResourceValueErrs == nil { + return nil + } + return m.deleteRegisteredResourceValueErrs[id] +} + +func (m *mockExecutorHandler) DeleteObligationTrigger(_ context.Context, id string) (*policy.ObligationTrigger, error) { + m.deleteCalls = append(m.deleteCalls, "obligation-trigger:"+id) + m.deletedObligationTriggers = append(m.deletedObligationTriggers, id) + if m.deleteObligationTriggerErrs != nil { + if err := m.deleteObligationTriggerErrs[id]; err != nil { + return nil, err + } + } + return &policy.ObligationTrigger{Id: id}, nil +} diff --git a/otdfctl/migrations/namespacedpolicy/interactive_prompt.go b/otdfctl/migrations/namespacedpolicy/interactive_prompt.go new file mode 100644 index 0000000000..7d89e61d5f --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/interactive_prompt.go @@ -0,0 +1,124 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "strings" + + "github.com/charmbracelet/huh" +) + +var ErrInteractiveReviewAborted = errors.New("interactive review aborted by user") + +// ConfirmPrompt is a generic confirmation prompt for planner-owned review flows. +type ConfirmPrompt struct { + Title string + Description []string + ConfirmLabel string + CancelLabel string +} + +// PromptOption is one selectable value in a generic interactive prompt. +type PromptOption struct { + Label string + Value string + Description string +} + +// SelectPrompt is a generic single-select prompt for planner-owned review flows. +type SelectPrompt struct { + Title string + Description []string + Options []PromptOption +} + +// InteractivePrompter abstracts the concrete prompt implementation so review +// orchestration stays planner-owned and testable. +type InteractivePrompter interface { + Confirm(context.Context, ConfirmPrompt) error + Select(context.Context, SelectPrompt) (string, error) +} + +// HuhPrompter implements InteractivePrompter using charmbracelet/huh forms. +type HuhPrompter struct{} + +func (p *HuhPrompter) Confirm(ctx context.Context, prompt ConfirmPrompt) error { + confirmLabel := strings.TrimSpace(prompt.ConfirmLabel) + if confirmLabel == "" { + confirmLabel = "Continue" + } + + cancelLabel := strings.TrimSpace(prompt.CancelLabel) + if cancelLabel == "" { + cancelLabel = "Abort" + } + + var choice bool + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title(strings.TrimSpace(prompt.Title)). + Description(promptDescription(prompt.Description)). + Affirmative(confirmLabel). + Negative(cancelLabel). + Value(&choice), + ), + ) + + if err := form.RunWithContext(ctx); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return ErrInteractiveReviewAborted + } + return err + } + + if !choice { + return ErrInteractiveReviewAborted + } + + return nil +} + +func (p *HuhPrompter) Select(ctx context.Context, prompt SelectPrompt) (string, error) { + options := make([]huh.Option[string], 0, len(prompt.Options)) + for _, option := range prompt.Options { + label := option.Label + if description := strings.TrimSpace(option.Description); description != "" { + label += " - " + description + } + options = append(options, huh.NewOption(label, option.Value)) + } + + var choice string + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title(strings.TrimSpace(prompt.Title)). + Description(promptDescription(prompt.Description)). + Options(options...). + Value(&choice), + ), + ) + + if err := form.RunWithContext(ctx); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return "", ErrInteractiveReviewAborted + } + return "", err + } + + return choice, nil +} + +func promptDescription(description []string) string { + lines := make([]string, 0, len(description)) + for _, line := range description { + line = strings.TrimSpace(line) + if line == "" { + continue + } + lines = append(lines, line) + } + + return strings.Join(lines, "\n") +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_commit_confirmation.go b/otdfctl/migrations/namespacedpolicy/migration_commit_confirmation.go new file mode 100644 index 0000000000..c716291193 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_commit_confirmation.go @@ -0,0 +1,441 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" +) + +const ( + sourceIDText = "Source ID: " + actionText = "Action: " + actionsText = "Actions: " + resourceText = "Resource: " + targetNamespaceText = "Target namespace: " + attributeValueText = "Attribute value: " + obligationValueText = "Obligation value: " + valuesText = "Values: " + actionBindingsText = "Action bindings: " + subjectSetsTextFmt = "Subject sets: %d" + scsSourceText = "Subject condition set source: " + createActionDescription = "This will create a new namespaced action." + createSubjectConditionSetDesc = "This will create a new namespaced subject condition set." + createSubjectMappingDescription = "This will create a new namespaced subject mapping." + createRegisteredResourceDesc = "This will create a new namespaced registered resource and its values." + createObligationTriggerDesc = "This will create a new namespaced obligation trigger." + confirmMigrationLabel = "Confirm migration" + confirmMigrationDescription = "apply this create operation" + abortMigrationLabel = "Abort entire migration" + abortMigrationDescription = "stop without applying remaining changes" +) + +func ConfirmMigrationPlan(ctx context.Context, plan *MigrationPlan, prompter InteractivePrompter) error { + if plan == nil { + return nil + } + if prompter == nil { + prompter = &HuhPrompter{} + } + + state := interactiveCommitReviewState{ + skippedActions: make(map[string]map[string]string), + skippedSCS: make(map[string]map[string]string), + } + + for _, actionPlan := range plan.Actions { + if actionPlan == nil || actionPlan.Source == nil { + continue + } + for _, target := range actionPlan.Targets { + if target == nil || target.Status != TargetStatusCreate { + continue + } + switch err := applyInteractiveDecision(ctx, prompter, actionPrompt(actionPlan, target)); { + case err == nil: + case errors.Is(err, errInteractiveSkipSelected): + markActionTargetSkipped(actionPlan, target, skippedByUserReason) + state.recordSkippedAction(actionPlan.Source.GetId(), target.Namespace, skippedReason("action", actionPlan.Source.GetName(), target.Namespace, skippedByUserReason)) + default: + return err + } + } + } + + for _, scsPlan := range plan.SubjectConditionSets { + if scsPlan == nil || scsPlan.Source == nil { + continue + } + for _, target := range scsPlan.Targets { + if target == nil || target.Status != TargetStatusCreate { + continue + } + switch err := applyInteractiveDecision(ctx, prompter, subjectConditionSetPrompt(scsPlan, target)); { + case err == nil: + case errors.Is(err, errInteractiveSkipSelected): + markSubjectConditionSetTargetSkipped(scsPlan, target, skippedByUserReason) + state.recordSkippedSCS(scsPlan.Source.GetId(), target.Namespace, skippedReason("subject condition set", scsPlan.Source.GetId(), target.Namespace, skippedByUserReason)) + default: + return err + } + } + } + + for _, mappingPlan := range plan.SubjectMappings { + if mappingPlan == nil || mappingPlan.Source == nil || mappingPlan.Target == nil { + continue + } + if mappingPlan.Target.Status != TargetStatusCreate { + continue + } + if reason := state.subjectMappingSkipReason(mappingPlan); reason != "" { + markSubjectMappingTargetSkipped(mappingPlan, reason) + continue + } + switch err := applyInteractiveDecision(ctx, prompter, subjectMappingPrompt(plan, mappingPlan)); { + case err == nil: + case errors.Is(err, errInteractiveSkipSelected): + markSubjectMappingTargetSkipped(mappingPlan, skippedByUserReason) + default: + return err + } + } + + for _, resourcePlan := range plan.RegisteredResources { + if resourcePlan == nil || resourcePlan.Source == nil || resourcePlan.Target == nil { + continue + } + if resourcePlan.Target.Status != TargetStatusCreate { + continue + } + if reason := state.registeredResourceSkipReason(resourcePlan); reason != "" { + markRegisteredResourceTargetSkipped(resourcePlan, reason) + continue + } + switch err := applyInteractiveDecision(ctx, prompter, registeredResourcePrompt(plan, resourcePlan)); { + case err == nil: + case errors.Is(err, errInteractiveSkipSelected): + markRegisteredResourceTargetSkipped(resourcePlan, skippedByUserReason) + default: + return err + } + } + + for _, triggerPlan := range plan.ObligationTriggers { + if triggerPlan == nil || triggerPlan.Source == nil || triggerPlan.Target == nil { + continue + } + if triggerPlan.Target.Status != TargetStatusCreate { + continue + } + if reason := state.obligationTriggerSkipReason(triggerPlan); reason != "" { + markObligationTriggerTargetSkipped(triggerPlan, reason) + continue + } + switch err := applyInteractiveDecision(ctx, prompter, obligationTriggerPrompt(plan, triggerPlan)); { + case err == nil: + case errors.Is(err, errInteractiveSkipSelected): + markObligationTriggerTargetSkipped(triggerPlan, skippedByUserReason) + default: + return err + } + } + + return nil +} + +type interactiveCommitReviewState struct { + skippedActions map[string]map[string]string + skippedSCS map[string]map[string]string +} + +func (s *interactiveCommitReviewState) recordSkippedAction(sourceID string, namespace *policy.Namespace, reason string) { + recordSkippedTargetReason(s.skippedActions, sourceID, namespace, reason) +} + +func (s *interactiveCommitReviewState) recordSkippedSCS(sourceID string, namespace *policy.Namespace, reason string) { + recordSkippedTargetReason(s.skippedSCS, sourceID, namespace, reason) +} + +func (s *interactiveCommitReviewState) subjectMappingSkipReason(mappingPlan *SubjectMappingPlan) string { + if mappingPlan == nil || mappingPlan.Target == nil { + return "" + } + for _, sourceActionID := range mappingPlan.Target.ActionSourceIDs { + if reason := skippedTargetReason(s.skippedActions, sourceActionID, mappingPlan.Target.Namespace); reason != "" { + return reason + } + } + if reason := skippedTargetReason(s.skippedSCS, mappingPlan.Target.SubjectConditionSetSourceID, mappingPlan.Target.Namespace); reason != "" { + return reason + } + return "" +} + +func (s *interactiveCommitReviewState) registeredResourceSkipReason(resourcePlan *RegisteredResourcePlan) string { + if resourcePlan == nil || resourcePlan.Target == nil { + return "" + } + for _, valuePlan := range resourcePlan.Target.Values { + if valuePlan == nil { + continue + } + for _, binding := range valuePlan.ActionBindings { + if binding == nil { + continue + } + if reason := skippedTargetReason(s.skippedActions, binding.SourceActionID, resourcePlan.Target.Namespace); reason != "" { + return reason + } + } + } + return "" +} + +func (s *interactiveCommitReviewState) obligationTriggerSkipReason(triggerPlan *ObligationTriggerPlan) string { + if triggerPlan == nil || triggerPlan.Target == nil { + return "" + } + return skippedTargetReason(s.skippedActions, triggerPlan.Target.ActionSourceID, triggerPlan.Target.Namespace) +} + +func recordSkippedTargetReason(store map[string]map[string]string, sourceID string, namespace *policy.Namespace, reason string) { + if strings.TrimSpace(sourceID) == "" { + return + } + namespaceKey := interactiveReviewNamespaceKey(namespace) + if namespaceKey == "" { + return + } + if store[sourceID] == nil { + store[sourceID] = make(map[string]string) + } + store[sourceID][namespaceKey] = reason +} + +func skippedTargetReason(store map[string]map[string]string, sourceID string, namespace *policy.Namespace) string { + if strings.TrimSpace(sourceID) == "" { + return "" + } + namespaceKey := interactiveReviewNamespaceKey(namespace) + if namespaceKey == "" { + return "" + } + if store[sourceID] == nil { + return "" + } + return store[sourceID][namespaceKey] +} + +func interactiveReviewNamespaceKey(namespace *policy.Namespace) string { + if namespace == nil { + return "" + } + if id := strings.TrimSpace(namespace.GetId()); id != "" { + return id + } + return strings.ToLower(strings.TrimSpace(namespace.GetFqn())) +} + +func actionPrompt(actionPlan *ActionPlan, target *ActionTargetPlan) SelectPrompt { + return SelectPrompt{ + Title: fmt.Sprintf("Migrate action %q to %s?", actionPlan.Source.GetName(), namespaceDisplay(target.Namespace)), + Description: []string{ + sourceIDText + actionPlan.Source.GetId(), + actionText + actionPlan.Source.GetName(), + targetNamespaceText + namespaceDisplay(target.Namespace), + createActionDescription, + }, + Options: confirmSkipAbortOptions(), + } +} + +func subjectConditionSetPrompt(scsPlan *SubjectConditionSetPlan, target *SubjectConditionSetTargetPlan) SelectPrompt { + return SelectPrompt{ + Title: fmt.Sprintf("Migrate subject condition set %q to %s?", scsPlan.Source.GetId(), namespaceDisplay(target.Namespace)), + Description: []string{ + sourceIDText + scsPlan.Source.GetId(), + targetNamespaceText + namespaceDisplay(target.Namespace), + fmt.Sprintf(subjectSetsTextFmt, len(scsPlan.Source.GetSubjectSets())), + createSubjectConditionSetDesc, + }, + Options: confirmSkipAbortOptions(), + } +} + +func subjectMappingPrompt(plan *MigrationPlan, mappingPlan *SubjectMappingPlan) SelectPrompt { + return SelectPrompt{ + Title: fmt.Sprintf("Migrate subject mapping %q to %s?", mappingPlan.Source.GetId(), namespaceDisplay(mappingPlan.Target.Namespace)), + Description: []string{ + sourceIDText + mappingPlan.Source.GetId(), + targetNamespaceText + namespaceDisplay(mappingPlan.Target.Namespace), + attributeValueText + valueFQN(mappingPlan.Source.GetAttributeValue()), + actionsText + plainActionNamesSummary(plan, mappingPlan.Target.ActionSourceIDs), + scsSourceText + mappingPlan.Target.SubjectConditionSetSourceID, + createSubjectMappingDescription, + }, + Options: confirmSkipAbortOptions(), + } +} + +func registeredResourcePrompt(plan *MigrationPlan, resourcePlan *RegisteredResourcePlan) SelectPrompt { + description := []string{ + sourceIDText + resourcePlan.Source.GetId(), + resourceText + resourcePlan.Source.GetName(), + targetNamespaceText + namespaceDisplay(resourcePlan.Target.Namespace), + valuesText + plainRegisteredResourceValueFQNsSummary(resourcePlan), + actionBindingsText + plainRegisteredResourceActionBindingsSummary(plan, resourcePlan), + createRegisteredResourceDesc, + } + + return SelectPrompt{ + Title: fmt.Sprintf("Migrate registered resource %q to %s?", resourcePlan.Source.GetName(), namespaceDisplay(resourcePlan.Target.Namespace)), + Description: description, + Options: confirmSkipAbortOptions(), + } +} + +func obligationTriggerPrompt(plan *MigrationPlan, triggerPlan *ObligationTriggerPlan) SelectPrompt { + return SelectPrompt{ + Title: fmt.Sprintf("Migrate obligation trigger %q to %s?", triggerPlan.Source.GetId(), namespaceDisplay(triggerPlan.Target.Namespace)), + Description: []string{ + sourceIDText + triggerPlan.Source.GetId(), + targetNamespaceText + namespaceDisplay(triggerPlan.Target.Namespace), + actionText + plainActionNamesSummary(plan, []string{triggerPlan.Target.ActionSourceID}), + attributeValueText + valueFQN(triggerPlan.Source.GetAttributeValue()), + obligationValueText + obligationValueIDOrFQN(triggerPlan.Source.GetObligationValue()), + createObligationTriggerDesc, + }, + Options: confirmSkipAbortOptions(), + } +} + +func confirmSkipAbortOptions() []PromptOption { + return []PromptOption{ + {Label: confirmMigrationLabel, Value: namespacedPolicyCommitConfirm, Description: confirmMigrationDescription}, + {Label: skipObjectLabel, Value: namespacedPolicyCommitSkip, Description: skipObjectDescription}, + {Label: abortMigrationLabel, Value: namespacedPolicyCommitAbort, Description: abortMigrationDescription}, + } +} + +func plainActionNamesSummary(plan *MigrationPlan, sourceIDs []string) string { + names := make([]string, 0, len(sourceIDs)) + seen := make(map[string]struct{}, len(sourceIDs)) + for _, sourceID := range sourceIDs { + if strings.TrimSpace(sourceID) == "" { + continue + } + name := actionNameBySourceID(plan, sourceID) + if name == "" { + name = sourceID + } + if _, ok := seen[name]; ok { + continue + } + seen[name] = struct{}{} + names = append(names, strconvQuote(name)) + } + if len(names) == 0 { + return noneLabel + } + return strings.Join(names, ", ") +} + +func plainRegisteredResourceValueFQNsSummary(resource *RegisteredResourcePlan) string { + values := make([]string, 0, len(resource.Target.Values)) + seen := make(map[string]struct{}, len(resource.Target.Values)) + for _, valuePlan := range resource.Target.Values { + fqn := registeredResourceValueFQN(valuePlan) + if strings.TrimSpace(fqn) == "" { + continue + } + if _, ok := seen[fqn]; ok { + continue + } + seen[fqn] = struct{}{} + values = append(values, fqn) + } + if len(values) == 0 { + return noneLabel + } + return strings.Join(values, ", ") +} + +func plainRegisteredResourceActionBindingsSummary(plan *MigrationPlan, resource *RegisteredResourcePlan) string { + bindings := make([]string, 0) + seen := make(map[string]struct{}) + for _, valuePlan := range resource.Target.Values { + if valuePlan == nil { + continue + } + for _, binding := range valuePlan.ActionBindings { + if binding == nil { + continue + } + actionName := actionNameBySourceID(plan, binding.SourceActionID) + if actionName == "" { + actionName = binding.SourceActionID + } + label := fmt.Sprintf("%s -> %s", strconvQuote(actionName), valueFQN(binding.AttributeValue)) + if _, ok := seen[label]; ok { + continue + } + seen[label] = struct{}{} + bindings = append(bindings, label) + } + } + if len(bindings) == 0 { + return noneLabel + } + return strings.Join(bindings, ", ") +} + +func markActionTargetSkipped(actionPlan *ActionPlan, target *ActionTargetPlan, reason string) { + if actionPlan == nil || target == nil { + return + } + target.Status = TargetStatusSkipped + target.Reason = reason +} + +func markSubjectConditionSetTargetSkipped(scsPlan *SubjectConditionSetPlan, target *SubjectConditionSetTargetPlan, reason string) { + if scsPlan == nil || target == nil { + return + } + target.Status = TargetStatusSkipped + target.Reason = reason +} + +func markSubjectMappingTargetSkipped(mappingPlan *SubjectMappingPlan, reason string) { + if mappingPlan == nil || mappingPlan.Target == nil { + return + } + mappingPlan.Target.Status = TargetStatusSkipped + mappingPlan.Target.Reason = reason +} + +func markRegisteredResourceTargetSkipped(resourcePlan *RegisteredResourcePlan, reason string) { + if resourcePlan == nil || resourcePlan.Target == nil { + return + } + resourcePlan.Target.Status = TargetStatusSkipped + resourcePlan.Target.Reason = reason +} + +func markObligationTriggerTargetSkipped(triggerPlan *ObligationTriggerPlan, reason string) { + if triggerPlan == nil || triggerPlan.Target == nil { + return + } + triggerPlan.Target.Status = TargetStatusSkipped + triggerPlan.Target.Reason = reason +} + +func skippedReason(kind, label string, namespace *policy.Namespace, detail string) string { + base := fmt.Sprintf("depends on skipped %s %q in %s", kind, label, namespaceDisplay(namespace)) + if strings.TrimSpace(detail) == "" { + return base + } + return fmt.Sprintf("%s: %s", base, detail) +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_commit_confirmation_test.go b/otdfctl/migrations/namespacedpolicy/migration_commit_confirmation_test.go new file mode 100644 index 0000000000..1eceba35b3 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_commit_confirmation_test.go @@ -0,0 +1,419 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfirmNamespacedPolicyBackupMapsAbortToBackupError(t *testing.T) { + t.Parallel() + + prompter := &testInteractivePrompter{ + confirmErr: ErrInteractiveReviewAborted, + } + + err := ConfirmNamespacedPolicyBackup(t.Context(), prompter) + require.ErrorIs(t, err, ErrNamespacedPolicyBackupNotConfirmed) + + require.Equal(t, 1, prompter.confirmCalls) + require.NotNil(t, prompter.lastConfirmPrompt) + assert.Equal(t, backupConfirmTitle, prompter.lastConfirmPrompt.Title) + assert.Equal(t, []string{backupConfirmDetail, backupAbortDetail}, prompter.lastConfirmPrompt.Description) + assert.Equal(t, backupConfirmLabel, prompter.lastConfirmPrompt.ConfirmLabel) + assert.Equal(t, backupCancelLabel, prompter.lastConfirmPrompt.CancelLabel) +} + +func TestConfirmNamespacedPolicyPruneBackupUsesPrunePrompt(t *testing.T) { + t.Parallel() + + prompter := &testInteractivePrompter{} + + err := ConfirmNamespacedPolicyPruneBackup(t.Context(), prompter) + require.NoError(t, err) + + require.Equal(t, 1, prompter.confirmCalls) + require.NotNil(t, prompter.lastConfirmPrompt) + assert.Equal(t, backupConfirmTitle, prompter.lastConfirmPrompt.Title) + assert.Equal(t, []string{pruneBackupConfirmDetail, backupAbortDetail}, prompter.lastConfirmPrompt.Description) + assert.Equal(t, backupConfirmLabel, prompter.lastConfirmPrompt.ConfirmLabel) + assert.Equal(t, backupCancelLabel, prompter.lastConfirmPrompt.CancelLabel) +} + +func TestConfirmNamespacedPolicyPruneBackupMapsAbortToBackupError(t *testing.T) { + t.Parallel() + + prompter := &testInteractivePrompter{ + confirmErr: ErrInteractiveReviewAborted, + } + + err := ConfirmNamespacedPolicyPruneBackup(t.Context(), prompter) + require.ErrorIs(t, err, ErrNamespacedPolicyBackupNotConfirmed) + + require.Equal(t, 1, prompter.confirmCalls) + require.NotNil(t, prompter.lastConfirmPrompt) + assert.Equal(t, []string{pruneBackupConfirmDetail, backupAbortDetail}, prompter.lastConfirmPrompt.Description) +} + +func TestConfirmMigrationPlanSkipsDependentsOfSkippedAction(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", namespace) + plan := &MigrationPlan{ + Scopes: []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeRegisteredResources, + ScopeObligationTriggers, + }, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + }, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + }, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: attributeValue, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + ActionSourceIDs: []string{"action-1"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: testRegisteredResource("resource-1", "documents"), + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + Values: []*RegisteredResourceValuePlan{ + { + ActionBindings: []*RegisteredResourceActionBinding{ + { + SourceActionID: "action-1", + AttributeValue: attributeValue, + }, + }, + }, + }, + }, + }, + }, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + AttributeValue: attributeValue, + ObligationValue: &policy.ObligationValue{ + Id: "obligation-value-1", + }, + }, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + ActionSourceID: "action-1", + }, + }, + }, + } + + prompter := &queuedSelectPrompter{ + selectValues: []string{ + namespacedPolicyCommitSkip, + namespacedPolicyCommitConfirm, + }, + } + + err := ConfirmMigrationPlan(t.Context(), plan, prompter) + require.NoError(t, err) + + require.Equal(t, 2, prompter.selectCalls) + + actionTarget := plan.Actions[0].Targets[0] + assert.Equal(t, TargetStatusSkipped, actionTarget.Status) + assert.Equal(t, skippedByUserReason, actionTarget.Reason) + assert.Nil(t, actionTarget.Execution) + + scsTarget := plan.SubjectConditionSets[0].Targets[0] + assert.Equal(t, TargetStatusCreate, scsTarget.Status) + assert.Empty(t, scsTarget.Reason) + + mappingTarget := plan.SubjectMappings[0].Target + assert.Equal(t, TargetStatusSkipped, mappingTarget.Status) + assert.Contains(t, mappingTarget.Reason, `depends on skipped action "decrypt" in https://example.com`) + assert.Nil(t, mappingTarget.Execution) + + resourceTarget := plan.RegisteredResources[0].Target + assert.Equal(t, TargetStatusSkipped, resourceTarget.Status) + assert.Contains(t, resourceTarget.Reason, `depends on skipped action "decrypt" in https://example.com`) + assert.Nil(t, resourceTarget.Execution) + require.Len(t, resourceTarget.Values, 1) + assert.Nil(t, resourceTarget.Values[0].Execution) + + triggerTarget := plan.ObligationTriggers[0].Target + assert.Equal(t, TargetStatusSkipped, triggerTarget.Status) + assert.Contains(t, triggerTarget.Reason, `depends on skipped action "decrypt" in https://example.com`) + assert.Nil(t, triggerTarget.Execution) +} + +func TestConfirmMigrationPlanSkipsMappingsDependentOnSkippedSCS(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", namespace) + plan := &MigrationPlan{ + Scopes: []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + }, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + }, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + }, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: attributeValue, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + ActionSourceIDs: []string{"action-1"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + } + + prompter := &queuedSelectPrompter{ + selectValues: []string{ + namespacedPolicyCommitConfirm, + namespacedPolicyCommitSkip, + }, + } + + err := ConfirmMigrationPlan(t.Context(), plan, prompter) + require.NoError(t, err) + + require.Equal(t, 2, prompter.selectCalls) + + actionTarget := plan.Actions[0].Targets[0] + assert.Equal(t, TargetStatusCreate, actionTarget.Status) + assert.Empty(t, actionTarget.Reason) + assert.Nil(t, actionTarget.Execution) + + scsTarget := plan.SubjectConditionSets[0].Targets[0] + assert.Equal(t, TargetStatusSkipped, scsTarget.Status) + assert.Equal(t, skippedByUserReason, scsTarget.Reason) + assert.Nil(t, scsTarget.Execution) + + mappingTarget := plan.SubjectMappings[0].Target + assert.Equal(t, TargetStatusSkipped, mappingTarget.Status) + assert.Contains(t, mappingTarget.Reason, `depends on skipped subject condition set "scs-1" in https://example.com`) + assert.Nil(t, mappingTarget.Execution) +} + +func TestConfirmMigrationPlanPropagatesAbort(t *testing.T) { + t.Parallel() + + // The user aborts on the first action prompt. The reviewer must (a) return + // ErrInteractiveReviewAborted so the caller stops, and (b) not prompt for + // any later action or downstream object, so no half-applied migration is + // committed. + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", namespace) + plan := &MigrationPlan{ + Scopes: []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + }, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + {Namespace: namespace, Status: TargetStatusCreate}, + }, + }, + { + Source: &policy.Action{Id: "action-2", Name: "read"}, + Targets: []*ActionTargetPlan{ + {Namespace: namespace, Status: TargetStatusCreate}, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + {Namespace: namespace, Status: TargetStatusCreate}, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: attributeValue, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + ActionSourceIDs: []string{"action-1"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + } + + prompter := &queuedSelectPrompter{ + selectValues: []string{namespacedPolicyCommitAbort}, + } + + err := ConfirmMigrationPlan(t.Context(), plan, prompter) + require.ErrorIs(t, err, ErrInteractiveReviewAborted) + + // Only the first prompt should have fired; abort must halt the walkthrough. + require.Equal(t, 1, prompter.selectCalls) + + // Abort must not mutate target state — subsequent execution should be able + // to run or the user should be able to retry. + assert.Equal(t, TargetStatusCreate, plan.Actions[0].Targets[0].Status) + assert.Empty(t, plan.Actions[0].Targets[0].Reason) + assert.Equal(t, TargetStatusCreate, plan.Actions[1].Targets[0].Status) + assert.Equal(t, TargetStatusCreate, plan.SubjectConditionSets[0].Targets[0].Status) + assert.Equal(t, TargetStatusCreate, plan.SubjectMappings[0].Target.Status) +} + +func TestApplyInteractiveDecisionHandlesChoices(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + selectValue string + selectErr error + wantErr error + }{ + { + name: "confirm", + selectValue: namespacedPolicyCommitConfirm, + }, + { + name: "skip", + selectValue: namespacedPolicyCommitSkip, + wantErr: errInteractiveSkipSelected, + }, + { + name: "abort", + selectValue: namespacedPolicyCommitAbort, + wantErr: ErrInteractiveReviewAborted, + }, + { + name: "prompt error", + selectErr: errors.New("boom"), + wantErr: errors.New("boom"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + prompter := &queuedSelectPrompter{ + selectValues: []string{tt.selectValue}, + selectErr: tt.selectErr, + } + + err := applyInteractiveDecision(t.Context(), prompter, SelectPrompt{ + Title: "test prompt", + }) + if tt.wantErr == nil { + require.NoError(t, err) + return + } + require.EqualError(t, err, tt.wantErr.Error()) + }) + } +} + +type queuedSelectPrompter struct { + selectCalls int + selectValues []string + selectErr error +} + +func (p *queuedSelectPrompter) Confirm(_ context.Context, _ ConfirmPrompt) error { + return nil +} + +func (p *queuedSelectPrompter) Select(_ context.Context, _ SelectPrompt) (string, error) { + p.selectCalls++ + if p.selectErr != nil { + return "", p.selectErr + } + if len(p.selectValues) == 0 { + return "", nil + } + + value := p.selectValues[0] + p.selectValues = p.selectValues[1:] + return value, nil +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_execute.go b/otdfctl/migrations/namespacedpolicy/migration_execute.go new file mode 100644 index 0000000000..c6a33c2044 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_execute.go @@ -0,0 +1,144 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "strings" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" +) + +var ( + ErrNilExecutorHandler = errors.New("executor handler is required") + ErrNilExecutionPlan = errors.New("execution plan is required") + ErrPlanNotExecutable = errors.New("plan is not executable") + ErrExecutionPhaseNotImplemented = errors.New("execution phase is not implemented") + ErrMissingExistingTarget = errors.New("missing existing target") + ErrMissingMigratedTarget = errors.New("missing migrated target") + ErrMissingActionTarget = errors.New("missing action target") + ErrMissingSubjectConditionSetTarget = errors.New("missing subject condition set target") + ErrTargetNamespaceRequired = errors.New("target namespace is required") + ErrMissingCreatedTargetID = errors.New("missing created target id") + ErrUnsupportedStatus = errors.New("unsupported status") +) + +const ( + migrationLabelMigratedFrom = "migrated_from" + unknownLabel = "" +) + +type ExecutorHandler interface { + CreateAction(ctx context.Context, name string, namespace string, metadata *common.MetadataMutable) (*policy.Action, error) + CreateSubjectConditionSet(ctx context.Context, ss []*policy.SubjectSet, metadata *common.MetadataMutable, namespace string) (*policy.SubjectConditionSet, error) + CreateNewSubjectMapping(ctx context.Context, attrValID string, actions []*policy.Action, existingSCSId string, newScs *subjectmapping.SubjectConditionSetCreate, metadata *common.MetadataMutable, namespace string) (*policy.SubjectMapping, error) + CreateObligationTrigger(ctx context.Context, attributeValue, action, obligationValue, clientID string, metadata *common.MetadataMutable) (*policy.ObligationTrigger, error) + CreateRegisteredResource(ctx context.Context, namespace string, name string, values []string, metadata *common.MetadataMutable) (*policy.RegisteredResource, error) + CreateRegisteredResourceValue(ctx context.Context, resourceID string, value string, actionAttributeValues []*registeredresources.ActionAttributeValue, metadata *common.MetadataMutable) (*policy.RegisteredResourceValue, error) + GetRegisteredResource(ctx context.Context, id, name, namespace string) (*policy.RegisteredResource, error) + DeleteAction(ctx context.Context, id string) error + DeleteSubjectConditionSet(ctx context.Context, id string) error + DeleteSubjectMapping(ctx context.Context, id string) (*policy.SubjectMapping, error) + DeleteRegisteredResource(ctx context.Context, id string) error + DeleteRegisteredResourceValue(ctx context.Context, id string) error + DeleteObligationTrigger(ctx context.Context, id string) (*policy.ObligationTrigger, error) +} + +type MigrationExecutor struct { + handler ExecutorHandler + actionTargets map[string]map[string]*ActionTargetPlan + subjectConditionSets map[string]map[string]*SubjectConditionSetTargetPlan +} + +func NewMigrationExecutor(handler ExecutorHandler) (*MigrationExecutor, error) { + if handler == nil { + return nil, ErrNilExecutorHandler + } + + return &MigrationExecutor{ + handler: handler, + actionTargets: make(map[string]map[string]*ActionTargetPlan), + subjectConditionSets: make(map[string]map[string]*SubjectConditionSetTargetPlan), + }, nil +} + +func (e *MigrationExecutor) ExecuteMigration(ctx context.Context, plan *MigrationPlan) error { + if err := e.validateMigrationPlan(plan); err != nil { + return err + } + + if err := e.executeActions(ctx, plan.Actions); err != nil { + return err + } + if err := e.executeSubjectConditionSets(ctx, plan.SubjectConditionSets); err != nil { + return err + } + if err := e.executeSubjectMappings(ctx, plan.SubjectMappings); err != nil { + return err + } + if err := e.executeRegisteredResources(ctx, plan.RegisteredResources); err != nil { + return err + } + if err := e.executeObligationTriggers(ctx, plan.ObligationTriggers); err != nil { + return err + } + + return nil +} + +func (e *MigrationExecutor) validateMigrationPlan(plan *MigrationPlan) error { + if e == nil || e.handler == nil { + return ErrNilExecutorHandler + } + if plan == nil { + return ErrNilExecutionPlan + } + + return nil +} + +func metadataForCreate(sourceID string, sourceLabels map[string]string) *common.MetadataMutable { + labels := map[string]string{} + for key, value := range sourceLabels { + labels[key] = value + } + + labels[migrationLabelMigratedFrom] = sourceID + + return &common.MetadataMutable{ + Labels: labels, + } +} + +func metadataLabels(metadata *common.Metadata) map[string]string { + if metadata == nil { + return nil + } + + return metadata.GetLabels() +} + +func namespaceIdentifier(namespace *policy.Namespace) string { + if namespace == nil { + return "" + } + if id := strings.TrimSpace(namespace.GetId()); id != "" { + return id + } + return strings.TrimSpace(namespace.GetFqn()) +} + +func namespaceLabel(namespace *policy.Namespace) string { + if namespace == nil { + return unknownLabel + } + if fqn := strings.TrimSpace(namespace.GetFqn()); fqn != "" { + return fqn + } + if id := strings.TrimSpace(namespace.GetId()); id != "" { + return id + } + return unknownLabel +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_finalize_plan.go b/otdfctl/migrations/namespacedpolicy/migration_finalize_plan.go new file mode 100644 index 0000000000..70de34844b --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_finalize_plan.go @@ -0,0 +1,310 @@ +package namespacedpolicy + +import ( + "errors" +) + +var ErrNilResolvedTargets = errors.New("planner resolved state is required") + +// finalizePlan converts the fully resolved graph into the current migration plan shape. +// This is the last planner stage before artifact building/execution wiring. +func finalizePlan(resolved *ResolvedTargets) (*MigrationPlan, error) { + if resolved == nil { + return nil, ErrNilResolvedTargets + } + + scopes, err := normalizeScopes(resolved.Scopes) + if err != nil { + return nil, err + } + + finalizer := newPlanFinalizer(resolved) + + if scopes.requiresActions() { + for _, action := range resolved.Actions { + finalizer.addResolvedAction(action) + } + } + + if scopes.requiresSubjectConditionSets() { + for _, scs := range resolved.SubjectConditionSets { + finalizer.addResolvedSubjectConditionSet(scs) + } + } + + if scopes.has(ScopeSubjectMappings) { + for _, mapping := range resolved.SubjectMappings { + finalizer.addResolvedSubjectMapping(mapping) + } + } + + if scopes.has(ScopeRegisteredResources) { + for _, resource := range resolved.RegisteredResources { + finalizer.addResolvedRegisteredResource(resource) + } + } + + if scopes.has(ScopeObligationTriggers) { + for _, trigger := range resolved.ObligationTriggers { + finalizer.addResolvedObligationTrigger(trigger) + } + } + + return finalizer.build(), nil +} + +// planFinalizer folds resolved placements into an executable plan that +// preserves per-target status and dependency bindings for downstream creates. +type planFinalizer struct { + resolved *ResolvedTargets + actions []*ActionPlan + subjectConditionSets []*SubjectConditionSetPlan + subjectMappings []*SubjectMappingPlan + registeredResources []*RegisteredResourcePlan + obligationTriggers []*ObligationTriggerPlan +} + +func newPlanFinalizer(resolved *ResolvedTargets) *planFinalizer { + return &planFinalizer{ + resolved: resolved, + } +} + +func (f *planFinalizer) build() *MigrationPlan { + return &MigrationPlan{ + Scopes: append([]Scope(nil), f.resolved.Scopes...), + Actions: append([]*ActionPlan(nil), f.actions...), + SubjectConditionSets: append([]*SubjectConditionSetPlan(nil), f.subjectConditionSets...), + SubjectMappings: append([]*SubjectMappingPlan(nil), f.subjectMappings...), + RegisteredResources: append([]*RegisteredResourcePlan(nil), f.registeredResources...), + ObligationTriggers: append([]*ObligationTriggerPlan(nil), f.obligationTriggers...), + } +} + +func (f *planFinalizer) addResolvedAction(item *ResolvedAction) { + if item == nil || item.Source == nil { + return + } + + if len(item.Results) == 0 { + return + } + + actionPlan := &ActionPlan{ + Source: item.Source, + Targets: make([]*ActionTargetPlan, 0, len(item.Results)), + } + + for _, result := range item.Results { + target := newActionTargetPlan(result) + if target == nil { + continue + } + actionPlan.Targets = append(actionPlan.Targets, target) + } + + f.actions = append(f.actions, actionPlan) +} + +func (f *planFinalizer) addResolvedSubjectConditionSet(item *ResolvedSubjectConditionSet) { + if item == nil || item.Source == nil { + return + } + + scsPlan := &SubjectConditionSetPlan{ + Source: item.Source, + Targets: make([]*SubjectConditionSetTargetPlan, 0, len(item.Results)), + } + + for _, result := range item.Results { + target := newSubjectConditionSetTargetPlan(result) + if target == nil { + continue + } + scsPlan.Targets = append(scsPlan.Targets, target) + } + + f.subjectConditionSets = append(f.subjectConditionSets, scsPlan) +} + +func (f *planFinalizer) addResolvedSubjectMapping(item *ResolvedSubjectMapping) { + if item == nil || item.Source == nil { + return + } + + mappingPlan := &SubjectMappingPlan{Source: item.Source} + + target := f.newSubjectMappingTarget(item) + if target != nil { + mappingPlan.Target = target + } + + f.subjectMappings = append(f.subjectMappings, mappingPlan) +} + +func (f *planFinalizer) addResolvedRegisteredResource(item *ResolvedRegisteredResource) { + if item == nil || item.Source == nil { + return + } + + resourcePlan := &RegisteredResourcePlan{Source: item.Source} + if item.Unresolved != nil { + resourcePlan.Unresolved = item.Unresolved.Message + } + + target := f.newRegisteredResourceTarget(item) + if target != nil { + resourcePlan.Target = target + } + + f.registeredResources = append(f.registeredResources, resourcePlan) +} + +func (f *planFinalizer) addResolvedObligationTrigger(item *ResolvedObligationTrigger) { + if item == nil || item.Source == nil { + return + } + + triggerPlan := &ObligationTriggerPlan{Source: item.Source} + + target := f.newObligationTriggerTarget(item) + if target != nil { + triggerPlan.Target = target + } + + f.obligationTriggers = append(f.obligationTriggers, triggerPlan) +} + +func (f *planFinalizer) newSubjectMappingTarget(item *ResolvedSubjectMapping) *SubjectMappingTargetPlan { + if item == nil || item.Namespace == nil { + return nil + } + + target := &SubjectMappingTargetPlan{ + Namespace: item.Namespace, + } + + switch { + case item.AlreadyMigrated != nil: + target.Status = TargetStatusAlreadyMigrated + target.ExistingID = item.AlreadyMigrated.GetId() + return target + case item.NeedsCreate: + target.Status = TargetStatusCreate + default: + return nil + } + + target.ActionSourceIDs = make([]string, 0, len(item.Source.GetActions())) + for _, action := range item.Source.GetActions() { + target.ActionSourceIDs = append(target.ActionSourceIDs, action.GetId()) + } + target.SubjectConditionSetSourceID = item.Source.GetSubjectConditionSet().GetId() + + return target +} + +func (f *planFinalizer) newRegisteredResourceTarget(item *ResolvedRegisteredResource) *RegisteredResourceTargetPlan { + if item == nil || item.Namespace == nil { + return nil + } + + target := &RegisteredResourceTargetPlan{ + Namespace: item.Namespace, + } + + switch { + case item.AlreadyMigrated != nil: + target.Status = TargetStatusAlreadyMigrated + target.ExistingID = item.AlreadyMigrated.GetId() + return target + case item.NeedsCreate: + target.Status = TargetStatusCreate + default: + return nil + } + + target.Values = make([]*RegisteredResourceValuePlan, 0, len(item.Source.GetValues())) + for _, value := range item.Source.GetValues() { + valuePlan := &RegisteredResourceValuePlan{ + Source: value, + ActionBindings: make([]*RegisteredResourceActionBinding, 0, len(value.GetActionAttributeValues())), + } + for _, aav := range value.GetActionAttributeValues() { + if aav == nil { + continue + } + valuePlan.ActionBindings = append(valuePlan.ActionBindings, &RegisteredResourceActionBinding{ + SourceActionID: aav.GetAction().GetId(), + AttributeValue: aav.GetAttributeValue(), + }) + } + target.Values = append(target.Values, valuePlan) + } + + return target +} + +func (f *planFinalizer) newObligationTriggerTarget(item *ResolvedObligationTrigger) *ObligationTriggerTargetPlan { + if item == nil || item.Namespace == nil { + return nil + } + + target := &ObligationTriggerTargetPlan{ + Namespace: item.Namespace, + } + switch { + case item.AlreadyMigrated != nil: + target.Status = TargetStatusAlreadyMigrated + target.ExistingID = item.AlreadyMigrated.GetId() + return target + case item.NeedsCreate: + target.Status = TargetStatusCreate + default: + return nil + } + target.ActionSourceID = item.Source.GetAction().GetId() + + return target +} + +func newActionTargetPlan(result *ResolvedActionResult) *ActionTargetPlan { + if result == nil || result.Namespace == nil { + return nil + } + + target := &ActionTargetPlan{Namespace: result.Namespace} + switch { + case result.AlreadyMigrated != nil: + target.Status = TargetStatusAlreadyMigrated + target.ExistingID = result.AlreadyMigrated.GetId() + case result.ExistingStandard != nil: + target.Status = TargetStatusExistingStandard + target.ExistingID = result.ExistingStandard.GetId() + case result.NeedsCreate: + target.Status = TargetStatusCreate + default: + return nil + } + + return target +} + +func newSubjectConditionSetTargetPlan(result *ResolvedSubjectConditionSetResult) *SubjectConditionSetTargetPlan { + if result == nil || result.Namespace == nil { + return nil + } + + target := &SubjectConditionSetTargetPlan{Namespace: result.Namespace} + switch { + case result.AlreadyMigrated != nil: + target.Status = TargetStatusAlreadyMigrated + target.ExistingID = result.AlreadyMigrated.GetId() + case result.NeedsCreate: + target.Status = TargetStatusCreate + default: + return nil + } + + return target +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_finalize_plan_test.go b/otdfctl/migrations/namespacedpolicy/migration_finalize_plan_test.go new file mode 100644 index 0000000000..7b0153fff4 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_finalize_plan_test.go @@ -0,0 +1,183 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFinalizePlanBuildsBindingsForDependentObjects(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + + plan, err := finalizePlan(&ResolvedTargets{ + Scopes: []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeRegisteredResources, + ScopeObligationTriggers, + }, + Actions: []*ResolvedAction{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Results: []*ResolvedActionResult{ + {Namespace: namespace, NeedsCreate: true}, + }, + }, + }, + SubjectConditionSets: []*ResolvedSubjectConditionSet{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Results: []*ResolvedSubjectConditionSetResult{ + { + Namespace: namespace, + AlreadyMigrated: &policy.SubjectConditionSet{Id: "scs-target"}, + }, + }, + }, + }, + SubjectMappings: []*ResolvedSubjectMapping{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + Actions: []*policy.Action{ + {Id: "action-1", Name: "decrypt"}, + }, + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + }, + Namespace: namespace, + NeedsCreate: true, + }, + }, + RegisteredResources: []*ResolvedRegisteredResource{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", nil), + ), + ), + ), + Namespace: namespace, + NeedsCreate: true, + }, + }, + ObligationTriggers: []*ResolvedObligationTrigger{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + }, + Namespace: namespace, + NeedsCreate: true, + }, + }, + }) + require.NoError(t, err) + + require.Len(t, plan.SubjectMappings, 1) + require.NotNil(t, plan.SubjectMappings[0].Target) + assert.Equal(t, TargetStatusCreate, plan.SubjectMappings[0].Target.Status) + assert.Equal(t, []string{"action-1"}, plan.SubjectMappings[0].Target.ActionSourceIDs) + assert.Equal(t, "scs-1", plan.SubjectMappings[0].Target.SubjectConditionSetSourceID) + + require.Len(t, plan.RegisteredResources, 1) + require.NotNil(t, plan.RegisteredResources[0].Target) + require.Len(t, plan.RegisteredResources[0].Target.Values, 1) + require.Len(t, plan.RegisteredResources[0].Target.Values[0].ActionBindings, 1) + assert.Equal(t, "action-1", plan.RegisteredResources[0].Target.Values[0].ActionBindings[0].SourceActionID) + + require.Len(t, plan.ObligationTriggers, 1) + require.NotNil(t, plan.ObligationTriggers[0].Target) + assert.Equal(t, "action-1", plan.ObligationTriggers[0].Target.ActionSourceID) +} + +func TestFinalizePlanOmitsCreateOnlyBindingsForAlreadyMigratedTargets(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + + plan, err := finalizePlan(&ResolvedTargets{ + Scopes: []Scope{ + ScopeSubjectMappings, + ScopeRegisteredResources, + ScopeObligationTriggers, + }, + SubjectMappings: []*ResolvedSubjectMapping{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + Actions: []*policy.Action{ + {Id: "action-1", Name: "decrypt"}, + }, + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + }, + Namespace: namespace, + AlreadyMigrated: &policy.SubjectMapping{Id: "mapping-target"}, + }, + }, + RegisteredResources: []*ResolvedRegisteredResource{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", nil), + ), + ), + ), + Namespace: namespace, + AlreadyMigrated: &policy.RegisteredResource{Id: "resource-target"}, + }, + }, + ObligationTriggers: []*ResolvedObligationTrigger{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + }, + Namespace: namespace, + AlreadyMigrated: &policy.ObligationTrigger{Id: "trigger-target"}, + }, + }, + }) + require.NoError(t, err) + + require.Len(t, plan.SubjectMappings, 1) + require.NotNil(t, plan.SubjectMappings[0].Target) + assert.Equal(t, TargetStatusAlreadyMigrated, plan.SubjectMappings[0].Target.Status) + assert.Equal(t, "mapping-target", plan.SubjectMappings[0].Target.ExistingID) + assert.Nil(t, plan.SubjectMappings[0].Target.ActionSourceIDs) + assert.Empty(t, plan.SubjectMappings[0].Target.SubjectConditionSetSourceID) + + require.Len(t, plan.RegisteredResources, 1) + require.NotNil(t, plan.RegisteredResources[0].Target) + assert.Equal(t, TargetStatusAlreadyMigrated, plan.RegisteredResources[0].Target.Status) + assert.Equal(t, "resource-target", plan.RegisteredResources[0].Target.ExistingID) + assert.Nil(t, plan.RegisteredResources[0].Target.Values) + + require.Len(t, plan.ObligationTriggers, 1) + require.NotNil(t, plan.ObligationTriggers[0].Target) + assert.Equal(t, TargetStatusAlreadyMigrated, plan.ObligationTriggers[0].Target.Status) + assert.Equal(t, "trigger-target", plan.ObligationTriggers[0].Target.ExistingID) + assert.Empty(t, plan.ObligationTriggers[0].Target.ActionSourceID) +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_plan.go b/otdfctl/migrations/namespacedpolicy/migration_plan.go new file mode 100644 index 0000000000..d90f910c13 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_plan.go @@ -0,0 +1,309 @@ +package namespacedpolicy + +import ( + "errors" + "strings" + + identifier "github.com/opentdf/platform/lib/identifier" + "github.com/opentdf/platform/protocol/go/policy" +) + +var ( + ErrNilRetrieved = errors.New("planner retrieved state is required") + ErrMissingTargetNamespace = errors.New("missing target namespace") + ErrUndeterminedTargetMapping = errors.New("could not determine target namespace") + + ErrMissingActionID = errors.New("action reference missing id") + ErrMissingSubjectConditionSetID = errors.New("subject condition set reference missing id") + ErrUnresolvedActionDependency = errors.New("action dependency not resolved in target namespace") + ErrUnresolvedSubjectConditionSetDependency = errors.New("subject condition set dependency not resolved in target namespace") +) + +type UnresolvedReason string + +const ( + UnresolvedReasonRegisteredResourceConflictingNamespaces UnresolvedReason = "registered_resource_conflicting_namespaces" +) + +type Unresolved struct { + Reason UnresolvedReason + Message string +} + +type MigrationPlan struct { + Scopes []Scope `json:"scopes"` + Actions []*ActionPlan `json:"actions"` + SubjectConditionSets []*SubjectConditionSetPlan `json:"subject_condition_sets"` + SubjectMappings []*SubjectMappingPlan `json:"subject_mappings"` + RegisteredResources []*RegisteredResourcePlan `json:"registered_resources"` + ObligationTriggers []*ObligationTriggerPlan `json:"obligation_triggers"` +} + +type TargetStatus string + +const ( + TargetStatusCreate TargetStatus = "create" + TargetStatusAlreadyMigrated TargetStatus = "already_migrated" + TargetStatusExistingStandard TargetStatus = "existing_standard" + TargetStatusSkipped TargetStatus = "skipped" + TargetStatusUnresolved TargetStatus = "unresolved" +) + +type ExecutionResult struct { + Applied bool `json:"applied,omitempty"` + CreatedTargetID string `json:"created_target_id,omitempty"` + Failure string `json:"failure,omitempty"` +} + +type ActionPlan struct { + Source *policy.Action `json:"source"` + Targets []*ActionTargetPlan `json:"targets,omitempty"` +} + +type ActionTargetPlan struct { + Namespace *policy.Namespace `json:"namespace"` + Status TargetStatus `json:"status"` + ExistingID string `json:"existing_id,omitempty"` + Execution *ExecutionResult `json:"execution,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type SubjectConditionSetPlan struct { + Source *policy.SubjectConditionSet `json:"source"` + Targets []*SubjectConditionSetTargetPlan `json:"targets,omitempty"` +} + +type SubjectConditionSetTargetPlan struct { + Namespace *policy.Namespace `json:"namespace"` + Status TargetStatus `json:"status"` + ExistingID string `json:"existing_id,omitempty"` + Execution *ExecutionResult `json:"execution,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type SubjectMappingPlan struct { + Source *policy.SubjectMapping `json:"source"` + Target *SubjectMappingTargetPlan `json:"target,omitempty"` +} + +type SubjectMappingTargetPlan struct { + Namespace *policy.Namespace `json:"namespace"` + Status TargetStatus `json:"status"` + ExistingID string `json:"existing_id,omitempty"` + Execution *ExecutionResult `json:"execution,omitempty"` + Reason string `json:"reason,omitempty"` + ActionSourceIDs []string `json:"action_source_ids,omitempty"` + SubjectConditionSetSourceID string `json:"subject_condition_set_source_id,omitempty"` +} + +type RegisteredResourcePlan struct { + Source *policy.RegisteredResource `json:"source"` + Target *RegisteredResourceTargetPlan `json:"target,omitempty"` + Unresolved string `json:"unresolved,omitempty"` +} + +type RegisteredResourceTargetPlan struct { + Namespace *policy.Namespace `json:"namespace"` + Status TargetStatus `json:"status"` + ExistingID string `json:"existing_id,omitempty"` + Execution *ExecutionResult `json:"execution,omitempty"` + Reason string `json:"reason,omitempty"` + Values []*RegisteredResourceValuePlan `json:"values,omitempty"` +} + +type RegisteredResourceValuePlan struct { + Source *policy.RegisteredResourceValue `json:"source"` + ActionBindings []*RegisteredResourceActionBinding `json:"action_bindings,omitempty"` + Execution *ExecutionResult `json:"execution,omitempty"` +} + +type RegisteredResourceActionBinding struct { + SourceActionID string `json:"source_action_id"` + AttributeValue *policy.Value `json:"attribute_value,omitempty"` +} + +type ObligationTriggerPlan struct { + Source *policy.ObligationTrigger `json:"source"` + Target *ObligationTriggerTargetPlan `json:"target,omitempty"` +} + +type ObligationTriggerTargetPlan struct { + Namespace *policy.Namespace `json:"namespace"` + Status TargetStatus `json:"status"` + ExistingID string `json:"existing_id,omitempty"` + Execution *ExecutionResult `json:"execution,omitempty"` + Reason string `json:"reason,omitempty"` + ActionSourceID string `json:"action_source_id,omitempty"` +} + +func namespaceFromAttributeValue(value *policy.Value) *policy.Namespace { + if value == nil { + return nil + } + + if namespace := value.GetAttribute().GetNamespace(); namespaceRefKey(namespace) != "" { + return namespace + } + + parsed, err := identifier.Parse[*identifier.FullyQualifiedAttribute](strings.TrimSpace(value.GetFqn())) + if err != nil || parsed == nil || parsed.Namespace == "" { + return nil + } + + return &policy.Namespace{ + Fqn: (&identifier.FullyQualifiedAttribute{Namespace: parsed.Namespace}).FQN(), + } +} + +func namespaceFromObligationValue(value *policy.ObligationValue) *policy.Namespace { + if value == nil { + return nil + } + return value.GetObligation().GetNamespace() +} + +func hasRegisteredResourceActionAttributeValues(resource *policy.RegisteredResource) bool { + if resource == nil { + return false + } + + for _, value := range resource.GetValues() { + if len(value.GetActionAttributeValues()) > 0 { + return true + } + } + + return false +} + +func hasObject[T interface{ GetId() string }](items []T, id string) bool { + for _, item := range items { + if item.GetId() == id { + return true + } + } + return false +} + +// sameNamespace reports whether two namespace references identify the same +// namespace. IDs are compared with whitespace trimmed; FQNs are compared +// case-insensitively with whitespace trimmed. Two nil namespaces are +// considered equal (both represent legacy/global). +// +// NOTE: namespaceRefKey uses raw values without normalization for accumulator +// dedup keys. If normalization bugs surface there, consider unifying with +// this function's normalization logic. +func sameNamespace(left, right *policy.Namespace) bool { + if left == nil || right == nil { + return left == right + } + + leftID := strings.TrimSpace(left.GetId()) + rightID := strings.TrimSpace(right.GetId()) + if leftID != "" && rightID != "" { + return leftID == rightID + } + + leftFQN := strings.ToLower(strings.TrimSpace(left.GetFqn())) + rightFQN := strings.ToLower(strings.TrimSpace(right.GetFqn())) + if leftFQN != "" && rightFQN != "" { + return leftFQN == rightFQN + } + + return false +} + +func (t *ActionTargetPlan) TargetID() string { + if t == nil { + return "" + } + if t.Execution != nil && t.Execution.CreatedTargetID != "" { + return t.Execution.CreatedTargetID + } + return t.ExistingID +} + +func (t *SubjectConditionSetTargetPlan) TargetID() string { + if t == nil { + return "" + } + if t.Execution != nil && t.Execution.CreatedTargetID != "" { + return t.Execution.CreatedTargetID + } + return t.ExistingID +} + +func (t *SubjectMappingTargetPlan) TargetID() string { + if t == nil { + return "" + } + if t.Execution != nil && t.Execution.CreatedTargetID != "" { + return t.Execution.CreatedTargetID + } + return t.ExistingID +} + +func (t *RegisteredResourceTargetPlan) TargetID() string { + if t == nil { + return "" + } + if t.Execution != nil && t.Execution.CreatedTargetID != "" { + return t.Execution.CreatedTargetID + } + return t.ExistingID +} + +func (p *RegisteredResourceValuePlan) TargetID() string { + if p == nil || p.Execution == nil { + return "" + } + return p.Execution.CreatedTargetID +} + +func (t *ObligationTriggerTargetPlan) TargetID() string { + if t == nil { + return "" + } + if t.Execution != nil && t.Execution.CreatedTargetID != "" { + return t.Execution.CreatedTargetID + } + return t.ExistingID +} + +func (p *MigrationPlan) LookupActionTarget(sourceID, namespaceID string) *ActionTargetPlan { + if p == nil || sourceID == "" || namespaceID == "" { + return nil + } + + for _, action := range p.Actions { + if action == nil || action.Source == nil || action.Source.GetId() != sourceID { + continue + } + for _, target := range action.Targets { + if target != nil && target.Namespace != nil && target.Namespace.GetId() == namespaceID { + return target + } + } + } + + return nil +} + +func (p *MigrationPlan) LookupSubjectConditionSetTarget(sourceID, namespaceID string) *SubjectConditionSetTargetPlan { + if p == nil || sourceID == "" || namespaceID == "" { + return nil + } + + for _, scs := range p.SubjectConditionSets { + if scs == nil || scs.Source == nil || scs.Source.GetId() != sourceID { + continue + } + for _, target := range scs.Targets { + if target != nil && target.Namespace != nil && target.Namespace.GetId() == namespaceID { + return target + } + } + } + + return nil +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_plan_test.go b/otdfctl/migrations/namespacedpolicy/migration_plan_test.go new file mode 100644 index 0000000000..2a5366051b --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_plan_test.go @@ -0,0 +1,115 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNamespaceFromAttributeValueFallsBackToValueFQN(t *testing.T) { + t.Parallel() + + namespace := namespaceFromAttributeValue(&policy.Value{ + Fqn: "https://example.com/attr/classification/value/secret", + }) + require.NotNil(t, namespace) + assert.Equal(t, "https://example.com", namespace.GetFqn()) +} + +func TestPlanLookupActionTarget(t *testing.T) { + t.Parallel() + + nsA := &policy.Namespace{Id: "ns-a"} + nsB := &policy.Namespace{Id: "ns-b"} + targetA := &ActionTargetPlan{Namespace: nsA, Status: TargetStatusCreate} + targetB := &ActionTargetPlan{Namespace: nsB, Status: TargetStatusCreate} + + plan := &MigrationPlan{ + Actions: []*ActionPlan{ + nil, + {Source: nil, Targets: []*ActionTargetPlan{targetA}}, + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + nil, + {Namespace: nil}, + targetA, + targetB, + }, + }, + }, + } + + tests := []struct { + name string + plan *MigrationPlan + sourceID string + namespaceID string + want *ActionTargetPlan + }{ + {name: "nil plan", plan: nil, sourceID: "action-1", namespaceID: "ns-a", want: nil}, + {name: "empty sourceID", plan: plan, sourceID: "", namespaceID: "ns-a", want: nil}, + {name: "empty namespaceID", plan: plan, sourceID: "action-1", namespaceID: "", want: nil}, + {name: "match in namespace a", plan: plan, sourceID: "action-1", namespaceID: "ns-a", want: targetA}, + {name: "match in namespace b", plan: plan, sourceID: "action-1", namespaceID: "ns-b", want: targetB}, + {name: "unknown sourceID returns nil", plan: plan, sourceID: "action-missing", namespaceID: "ns-a", want: nil}, + {name: "unknown namespaceID returns nil", plan: plan, sourceID: "action-1", namespaceID: "ns-missing", want: nil}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Same(t, tc.want, tc.plan.LookupActionTarget(tc.sourceID, tc.namespaceID)) + }) + } +} + +func TestPlanLookupSubjectConditionSetTarget(t *testing.T) { + t.Parallel() + + nsA := &policy.Namespace{Id: "ns-a"} + nsB := &policy.Namespace{Id: "ns-b"} + targetA := &SubjectConditionSetTargetPlan{Namespace: nsA, Status: TargetStatusCreate} + targetB := &SubjectConditionSetTargetPlan{Namespace: nsB, Status: TargetStatusCreate} + + plan := &MigrationPlan{ + SubjectConditionSets: []*SubjectConditionSetPlan{ + nil, + {Source: nil, Targets: []*SubjectConditionSetTargetPlan{targetA}}, + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + nil, + {Namespace: nil}, + targetA, + targetB, + }, + }, + }, + } + + tests := []struct { + name string + plan *MigrationPlan + sourceID string + namespaceID string + want *SubjectConditionSetTargetPlan + }{ + {name: "nil plan", plan: nil, sourceID: "scs-1", namespaceID: "ns-a", want: nil}, + {name: "empty sourceID", plan: plan, sourceID: "", namespaceID: "ns-a", want: nil}, + {name: "empty namespaceID", plan: plan, sourceID: "scs-1", namespaceID: "", want: nil}, + {name: "match in namespace a", plan: plan, sourceID: "scs-1", namespaceID: "ns-a", want: targetA}, + {name: "match in namespace b", plan: plan, sourceID: "scs-1", namespaceID: "ns-b", want: targetB}, + {name: "unknown sourceID returns nil", plan: plan, sourceID: "scs-missing", namespaceID: "ns-a", want: nil}, + {name: "unknown namespaceID returns nil", plan: plan, sourceID: "scs-1", namespaceID: "ns-missing", want: nil}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Same(t, tc.want, tc.plan.LookupSubjectConditionSetTarget(tc.sourceID, tc.namespaceID)) + }) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_planner.go b/otdfctl/migrations/namespacedpolicy/migration_planner.go new file mode 100644 index 0000000000..f66fbec5c5 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_planner.go @@ -0,0 +1,193 @@ +package namespacedpolicy + +import ( + "context" + "errors" + + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/obligations" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" +) + +const defaultPlannerPageSize int32 = 100 + +var ErrNilPlannerHandler = errors.New("planner handler is required") + +type PolicyClient interface { + ListActions(ctx context.Context, limit, offset int32, namespace string) (*actions.ListActionsResponse, error) + ListSubjectConditionSets(ctx context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectConditionSetsResponse, error) + ListSubjectMappings(ctx context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectMappingsResponse, error) + ListRegisteredResources(ctx context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*registeredresources.ListRegisteredResourcesResponse, error) + ListRegisteredResourceValues(ctx context.Context, resourceID string, limit, offset int32) (*registeredresources.ListRegisteredResourceValuesResponse, error) + ListObligationTriggers(ctx context.Context, namespace string, limit, offset int32) (*obligations.ListObligationTriggersResponse, error) + ListNamespaces(ctx context.Context, state common.ActiveStateEnum, limit, offset int32, sort handlers.SortOption) (*namespaces.ListNamespacesResponse, error) +} + +type MigrationPlanner struct { + retriever *Retriever + requestedScopes scopeSet + expandedScopes scopeSet + reviewer InteractiveReviewer +} + +type Option func(*MigrationPlanner) + +type Retrieved struct { + Scopes []Scope + Candidates Candidates +} + +type Candidates struct { + Actions []*policy.Action + SubjectConditionSets []*policy.SubjectConditionSet + SubjectMappings []*policy.SubjectMapping + RegisteredResources []*policy.RegisteredResource + ObligationTriggers []*policy.ObligationTrigger +} + +type ExistingTargets struct { + CustomActions map[string][]*policy.Action + StandardActions map[string][]*policy.Action + SubjectConditionSets map[string][]*policy.SubjectConditionSet + SubjectMappings map[string][]*policy.SubjectMapping + RegisteredResources map[string][]*policy.RegisteredResource + ObligationTriggers map[string][]*policy.ObligationTrigger +} + +func NewMigrationPlanner(handler PolicyClient, scopeCSV string, opts ...Option) (*MigrationPlanner, error) { + if handler == nil { + return nil, ErrNilPlannerHandler + } + + scopes, err := ParseScopes(scopeCSV) + if err != nil { + return nil, err + } + + normalizedScopes, err := normalizeScopes(scopes) + if err != nil { + return nil, err + } + + planner := &MigrationPlanner{ + retriever: newRetriever(handler, defaultPlannerPageSize), + requestedScopes: normalizedScopes, + expandedScopes: expandScopes(normalizedScopes), + } + for _, opt := range opts { + opt(planner) + } + if planner.retriever.pageSize <= 0 { + planner.retriever.pageSize = defaultPlannerPageSize + } + + return planner, nil +} + +func WithPageSize(pageSize int32) Option { + return func(planner *MigrationPlanner) { + planner.retriever.pageSize = pageSize + } +} + +func WithInteractiveReviewer(reviewer InteractiveReviewer) Option { + return func(planner *MigrationPlanner) { + planner.reviewer = reviewer + } +} + +func (p *MigrationPlanner) Plan(ctx context.Context) (*MigrationPlan, error) { + resolved, err := p.resolve(ctx) + if err != nil { + return nil, err + } + + return finalizePlan(resolved) +} + +func (p *MigrationPlanner) resolve(ctx context.Context) (*ResolvedTargets, error) { + retrieved, err := p.retrieve(ctx) + if err != nil { + return nil, err + } + + namespaces, err := p.retriever.listNamespaces(ctx) + if err != nil { + return nil, err + } + + derived, err := deriveTargets(retrieved, namespaces) + if err != nil { + return nil, err + } + + existingTargets, err := p.retriever.listExistingTargets(ctx, p.requestedScopes, derived) + if err != nil { + return nil, err + } + + resolved, err := resolveExisting(derived, existingTargets) + if err != nil { + return nil, err + } + + if p.reviewer != nil { + if err := p.reviewer.Review(ctx, resolved, namespaces); err != nil { + return nil, err + } + } + + return resolved, nil +} + +// Retrieve the candidate policy constructs for items within scope or dependent +// on that scope. +func (p *MigrationPlanner) retrieve(ctx context.Context) (*Retrieved, error) { + if p == nil || p.retriever == nil || p.retriever.handler == nil { + return nil, ErrNilPlannerHandler + } + if len(p.requestedScopes) == 0 { + return nil, ErrEmptyPlannerScope + } + + retrieved, err := p.retriever.retrieve(ctx, p.requestedScopes) + if err != nil { + return nil, err + } + + reduceDependencies(retrieved, p.requestedScopes) + // Keep retrieval/reduction keyed off requestedScopes so "actions" does not + // implicitly pull in reverse-lookup scopes like registered resources or + // obligation triggers. The retrieved artifact still records expandedScopes to + // reflect the full dependency closure used by later planner stages. + retrieved.Scopes = p.expandedScopes.ordered() + + return retrieved, nil +} + +func newRetrieved(scopes []Scope) *Retrieved { + return &Retrieved{ + Scopes: append([]Scope(nil), scopes...), + Candidates: newCandidates(), + } +} + +func newCandidates() Candidates { + return Candidates{} +} + +func newExistingTargets() *ExistingTargets { + return &ExistingTargets{ + CustomActions: make(map[string][]*policy.Action), + StandardActions: make(map[string][]*policy.Action), + SubjectConditionSets: make(map[string][]*policy.SubjectConditionSet), + SubjectMappings: make(map[string][]*policy.SubjectMapping), + RegisteredResources: make(map[string][]*policy.RegisteredResource), + ObligationTriggers: make(map[string][]*policy.ObligationTrigger), + } +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_planner_test.go b/otdfctl/migrations/namespacedpolicy/migration_planner_test.go new file mode 100644 index 0000000000..0582d913c5 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_planner_test.go @@ -0,0 +1,1065 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "testing" + + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/obligations" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPlannerPlanMarksActionAlreadyMigratedWithoutMetadata(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + legacyAction := &policy.Action{ + Id: "action-legacy", + Name: "decrypt", + } + targetAction := &policy.Action{ + Id: "action-target", + Name: "decrypt", + Namespace: targetNamespace, + } + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-legacy", + Actions: []*policy.Action{ + { + Id: legacyAction.GetId(), + Name: legacyAction.GetName(), + }, + }, + AttributeValue: &policy.Value{ + Fqn: "https://example.com/attr/classification/value/secret", + }, + } + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{targetAction}, + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewMigrationPlanner(handler, "actions") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + require.Len(t, plan.Actions, 1) + require.Len(t, plan.Actions[0].Targets, 1) + + assert.Equal(t, TargetStatusAlreadyMigrated, plan.Actions[0].Targets[0].Status) + assert.Equal(t, targetAction.GetId(), plan.Actions[0].Targets[0].ExistingID) + assert.Equal(t, []string{"", targetNamespace.GetId()}, handler.actionCalls) + assert.Equal(t, []string{""}, handler.subjectMappingCalls) +} + +func TestPlannerPlanDoesNotLeakSupportSubjectMappingsIntoActionScope(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + legacyCreate := &policy.Action{ + Id: "action-create", + Name: "create", + } + legacyRead := &policy.Action{ + Id: "action-read", + Name: "read", + } + legacyCustom := &policy.Action{ + Id: "action-custom-1", + Name: "custom_action_1", + } + legacySCS := &policy.SubjectConditionSet{ + Id: "scs-1", + } + resourceValue := &policy.RegisteredResourceValue{ + Id: "resource-value-1", + Resource: &policy.RegisteredResource{Id: "resource-1"}, + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Id: "aav-create", + Action: &policy.Action{ + Id: legacyCreate.GetId(), + Name: legacyCreate.GetName(), + }, + AttributeValue: &policy.Value{ + Attribute: &policy.Attribute{ + Namespace: targetNamespace, + }, + }, + }, + { + Id: "aav-custom", + Action: &policy.Action{ + Id: legacyCustom.GetId(), + Name: legacyCustom.GetName(), + }, + AttributeValue: &policy.Value{ + Attribute: &policy.Attribute{ + Namespace: targetNamespace, + }, + }, + }, + }, + } + legacyResource := &policy.RegisteredResource{ + Id: "resource-1", + Name: "resource-1", + Values: []*policy.RegisteredResourceValue{resourceValue}, + } + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Attribute: &policy.Attribute{ + Namespace: targetNamespace, + }, + }, + SubjectConditionSet: &policy.SubjectConditionSet{ + Id: legacySCS.GetId(), + }, + Actions: []*policy.Action{ + { + Id: legacyRead.GetId(), + Name: legacyRead.GetName(), + }, + }, + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsStandard: []*policy.Action{legacyCreate, legacyRead}, + ActionsCustom: []*policy.Action{legacyCustom}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsStandard: []*policy.Action{ + { + Id: "target-create", + Name: legacyCreate.GetName(), + Namespace: targetNamespace, + }, + }, + Pagination: emptyPageResponse(), + }, + }, + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewMigrationPlanner(handler, "subject-condition-sets,registered-resources") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + assert.Equal(t, []Scope{ScopeActions, ScopeSubjectConditionSets, ScopeRegisteredResources}, plan.Scopes) + assert.ElementsMatch(t, []string{legacyCreate.GetId(), legacyCustom.GetId()}, actionSourceIDs(plan.Actions)) + assert.NotContains(t, actionSourceIDs(plan.Actions), legacyRead.GetId()) + require.Len(t, plan.SubjectConditionSets, 1) + assert.Equal(t, legacySCS.GetId(), plan.SubjectConditionSets[0].Source.GetId()) + assert.Equal(t, []string{"", targetNamespace.GetId()}, handler.actionCalls) + assert.Equal(t, []string{""}, handler.subjectMappingCalls) +} + +func TestPlannerRetrieveUsesRequestedScopeBoundaries(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + legacyAction := &policy.Action{ + Id: "action-1", + Name: "decrypt", + } + legacySCS := &policy.SubjectConditionSet{ + Id: "scs-1", + } + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: testAttributeValue( + "https://example.com/attr/classification/value/secret", + targetNamespace, + ), + SubjectConditionSet: &policy.SubjectConditionSet{ + Id: legacySCS.GetId(), + }, + Actions: []*policy.Action{ + { + Id: legacyAction.GetId(), + Name: legacyAction.GetName(), + }, + }, + } + legacyResource := testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace), + ), + ), + ) + legacyTrigger := &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + ObligationValue: &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{ + Namespace: targetNamespace, + }, + }, + } + + tests := []struct { + name string + scopeCSV string + expectedScopes []Scope + expectedActionCalls []string + expectedSubjectConditionCalls []string + expectedSubjectMappingCalls []string + expectedRegisteredResourceCall []string + expectedObligationCalls []string + expectedCandidateCounts Candidates + }{ + { + name: "subject mappings pull dependencies without reverse lookup scopes", + scopeCSV: "subject-mappings", + expectedScopes: []Scope{ScopeActions, ScopeSubjectConditionSets, ScopeSubjectMappings}, + expectedActionCalls: []string{ + "", + }, + expectedSubjectConditionCalls: []string{ + "", + }, + expectedSubjectMappingCalls: []string{ + "", + }, + expectedCandidateCounts: Candidates{ + Actions: []*policy.Action{legacyAction}, + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + }, + }, + { + name: "actions pull reverse lookup scopes without expanding artifact scopes", + scopeCSV: "actions", + expectedScopes: []Scope{ScopeActions}, + expectedActionCalls: []string{ + "", + }, + expectedSubjectMappingCalls: []string{ + "", + }, + expectedRegisteredResourceCall: []string{ + "", + }, + expectedObligationCalls: []string{ + "", + }, + expectedCandidateCounts: Candidates{ + Actions: []*policy.Action{legacyAction}, + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + RegisteredResources: []*policy.RegisteredResource{legacyResource}, + ObligationTriggers: []*policy.ObligationTrigger{legacyTrigger}, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + }, + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + }, + obligationTriggersByNamespace: map[string]*obligations.ListObligationTriggersResponse{ + "": { + Triggers: []*policy.ObligationTrigger{legacyTrigger}, + Pagination: emptyPageResponse(), + }, + }, + } + + planner, err := NewMigrationPlanner(handler, tt.scopeCSV) + require.NoError(t, err) + + retrieved, err := planner.retrieve(t.Context()) + require.NoError(t, err) + + assert.Equal(t, tt.expectedScopes, retrieved.Scopes) + assert.Equal(t, tt.expectedActionCalls, handler.actionCalls) + assert.Equal(t, tt.expectedSubjectConditionCalls, handler.subjectConditionSetCalls) + assert.Equal(t, tt.expectedSubjectMappingCalls, handler.subjectMappingCalls) + assert.Equal(t, tt.expectedRegisteredResourceCall, handler.registeredResourceCalls) + assert.Equal(t, tt.expectedObligationCalls, handler.obligationTriggerCalls) + assert.Len(t, retrieved.Candidates.Actions, len(tt.expectedCandidateCounts.Actions)) + assert.Len(t, retrieved.Candidates.SubjectConditionSets, len(tt.expectedCandidateCounts.SubjectConditionSets)) + assert.Len(t, retrieved.Candidates.SubjectMappings, len(tt.expectedCandidateCounts.SubjectMappings)) + assert.Len(t, retrieved.Candidates.RegisteredResources, len(tt.expectedCandidateCounts.RegisteredResources)) + assert.Len(t, retrieved.Candidates.ObligationTriggers, len(tt.expectedCandidateCounts.ObligationTriggers)) + }) + } +} + +func TestPlannerPlanAllScopesBuildsAllPlanSections(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + legacyAction := &policy.Action{ + Id: "action-1", + Name: "decrypt", + } + legacySCS := &policy.SubjectConditionSet{ + Id: "scs-1", + } + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: testAttributeValue( + "https://example.com/attr/classification/value/secret", + targetNamespace, + ), + SubjectConditionSet: &policy.SubjectConditionSet{ + Id: legacySCS.GetId(), + }, + Actions: []*policy.Action{ + { + Id: legacyAction.GetId(), + Name: legacyAction.GetName(), + }, + }, + } + legacyResource := testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace), + ), + ), + ) + legacyTrigger := &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + ObligationValue: &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{ + Namespace: targetNamespace, + }, + }, + } + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + obligationTriggersByNamespace: map[string]*obligations.ListObligationTriggersResponse{ + "": { + Triggers: []*policy.ObligationTrigger{legacyTrigger}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewMigrationPlanner(handler, "actions,subject-condition-sets,subject-mappings,registered-resources,obligation-triggers") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + assert.Equal(t, []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeRegisteredResources, + ScopeObligationTriggers, + }, plan.Scopes) + require.Len(t, plan.Actions, 1) + require.Len(t, plan.Actions[0].Targets, 1) + assert.Equal(t, TargetStatusCreate, plan.Actions[0].Targets[0].Status) + + require.Len(t, plan.SubjectConditionSets, 1) + require.Len(t, plan.SubjectConditionSets[0].Targets, 1) + assert.Equal(t, TargetStatusCreate, plan.SubjectConditionSets[0].Targets[0].Status) + + require.Len(t, plan.SubjectMappings, 1) + require.NotNil(t, plan.SubjectMappings[0].Target) + assert.Equal(t, TargetStatusCreate, plan.SubjectMappings[0].Target.Status) + assert.Equal(t, []string{legacyAction.GetId()}, plan.SubjectMappings[0].Target.ActionSourceIDs) + assert.Equal(t, legacySCS.GetId(), plan.SubjectMappings[0].Target.SubjectConditionSetSourceID) + + require.Len(t, plan.RegisteredResources, 1) + require.NotNil(t, plan.RegisteredResources[0].Target) + require.Len(t, plan.RegisteredResources[0].Target.Values, 1) + require.Len(t, plan.RegisteredResources[0].Target.Values[0].ActionBindings, 1) + assert.Equal(t, legacyAction.GetId(), plan.RegisteredResources[0].Target.Values[0].ActionBindings[0].SourceActionID) + + require.Len(t, plan.ObligationTriggers, 1) + require.NotNil(t, plan.ObligationTriggers[0].Target) + assert.Equal(t, legacyAction.GetId(), plan.ObligationTriggers[0].Target.ActionSourceID) + + assert.Equal(t, []string{"", targetNamespace.GetId()}, handler.actionCalls) + assert.Equal(t, []string{"", targetNamespace.GetId()}, handler.subjectConditionSetCalls) + assert.Equal(t, []string{"", targetNamespace.GetId()}, handler.subjectMappingCalls) + assert.Equal(t, []string{"", targetNamespace.GetId()}, handler.registeredResourceCalls) + assert.Equal(t, []string{"", targetNamespace.GetId()}, handler.obligationTriggerCalls) +} + +func TestPlannerPlanCarriesMultiActionMappingsAndMultiBindingRegisteredResources(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + legacyCustomAction := &policy.Action{ + Id: "action-custom", + Name: "decrypt", + } + legacyReadAction := &policy.Action{ + Id: "action-read", + Name: "read", + } + targetReadAction := &policy.Action{ + Id: "action-read-target", + Name: "read", + Namespace: targetNamespace, + } + legacySCS := &policy.SubjectConditionSet{ + Id: "scs-1", + } + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: testAttributeValue( + "https://example.com/attr/classification/value/secret", + targetNamespace, + ), + SubjectConditionSet: &policy.SubjectConditionSet{ + Id: legacySCS.GetId(), + }, + Actions: []*policy.Action{ + { + Id: legacyCustomAction.GetId(), + Name: legacyCustomAction.GetName(), + }, + { + Id: legacyReadAction.GetId(), + Name: legacyReadAction.GetName(), + }, + }, + } + legacyResource := testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + legacyCustomAction.GetId(), + legacyCustomAction.GetName(), + testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace), + ), + testActionAttributeValue( + legacyReadAction.GetId(), + legacyReadAction.GetName(), + testAttributeValue("https://example.com/attr/classification/value/restricted", targetNamespace), + ), + ), + ) + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsStandard: []*policy.Action{legacyReadAction}, + ActionsCustom: []*policy.Action{legacyCustomAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsStandard: []*policy.Action{targetReadAction}, + Pagination: emptyPageResponse(), + }, + }, + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewMigrationPlanner(handler, "subject-mappings,registered-resources") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + assert.Equal(t, []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeRegisteredResources, + }, plan.Scopes) + + require.Len(t, plan.Actions, 2) + assert.ElementsMatch(t, []string{legacyCustomAction.GetId(), legacyReadAction.GetId()}, actionSourceIDs(plan.Actions)) + + actionTargetsBySourceID := make(map[string]*ActionTargetPlan, len(plan.Actions)) + for _, actionPlan := range plan.Actions { + require.NotNil(t, actionPlan) + require.NotNil(t, actionPlan.Source) + require.Len(t, actionPlan.Targets, 1) + actionTargetsBySourceID[actionPlan.Source.GetId()] = actionPlan.Targets[0] + } + + require.Contains(t, actionTargetsBySourceID, legacyCustomAction.GetId()) + assert.Equal(t, TargetStatusCreate, actionTargetsBySourceID[legacyCustomAction.GetId()].Status) + assert.True(t, sameNamespace(targetNamespace, actionTargetsBySourceID[legacyCustomAction.GetId()].Namespace)) + + require.Contains(t, actionTargetsBySourceID, legacyReadAction.GetId()) + assert.Equal(t, TargetStatusExistingStandard, actionTargetsBySourceID[legacyReadAction.GetId()].Status) + assert.Equal(t, targetReadAction.GetId(), actionTargetsBySourceID[legacyReadAction.GetId()].ExistingID) + assert.True(t, sameNamespace(targetNamespace, actionTargetsBySourceID[legacyReadAction.GetId()].Namespace)) + + require.Len(t, plan.SubjectConditionSets, 1) + require.Len(t, plan.SubjectMappings, 1) + require.NotNil(t, plan.SubjectMappings[0].Target) + assert.Equal(t, TargetStatusCreate, plan.SubjectMappings[0].Target.Status) + assert.ElementsMatch(t, []string{legacyCustomAction.GetId(), legacyReadAction.GetId()}, plan.SubjectMappings[0].Target.ActionSourceIDs) + assert.Equal(t, legacySCS.GetId(), plan.SubjectMappings[0].Target.SubjectConditionSetSourceID) + + require.Len(t, plan.RegisteredResources, 1) + require.NotNil(t, plan.RegisteredResources[0].Target) + require.Len(t, plan.RegisteredResources[0].Target.Values, 1) + require.Len(t, plan.RegisteredResources[0].Target.Values[0].ActionBindings, 2) + + var resourceBindingSourceIDs []string + for _, binding := range plan.RegisteredResources[0].Target.Values[0].ActionBindings { + require.NotNil(t, binding) + resourceBindingSourceIDs = append(resourceBindingSourceIDs, binding.SourceActionID) + } + assert.ElementsMatch(t, []string{legacyCustomAction.GetId(), legacyReadAction.GetId()}, resourceBindingSourceIDs) +} + +func TestPlannerPlanInvokesInteractiveReviewerWhenConfigured(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + legacyAction := &policy.Action{ + Id: "action-legacy", + Name: "decrypt", + } + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-legacy", + Actions: []*policy.Action{ + { + Id: legacyAction.GetId(), + Name: legacyAction.GetName(), + }, + }, + AttributeValue: &policy.Value{ + Fqn: "https://example.com/attr/classification/value/secret", + }, + } + reviewer := &plannerTestReviewer{} + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewMigrationPlanner(handler, "actions", WithInteractiveReviewer(reviewer)) + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.Actions, 1) + assert.Equal(t, 1, reviewer.calls) + assert.NotNil(t, reviewer.lastResolved) +} + +func TestPlannerPlanPropagatesInteractiveReviewerError(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + legacyAction := &policy.Action{ + Id: "action-legacy", + Name: "decrypt", + } + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-legacy", + Actions: []*policy.Action{ + { + Id: legacyAction.GetId(), + Name: legacyAction.GetName(), + }, + }, + AttributeValue: &policy.Value{ + Fqn: "https://example.com/attr/classification/value/secret", + }, + } + reviewerErr := errors.New("review failed") + reviewer := &plannerTestReviewer{err: reviewerErr} + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewMigrationPlanner(handler, "actions", WithInteractiveReviewer(reviewer)) + require.NoError(t, err) + + _, err = planner.Plan(t.Context()) + require.ErrorIs(t, err, reviewerErr) + assert.Equal(t, 1, reviewer.calls) +} + +func TestPlannerPlanInteractiveReviewerLeavesCurrentUnresolvedPlanShapeUntouched(t *testing.T) { + t.Parallel() + + namespaceOne := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + namespaceTwo := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + legacyAction := &policy.Action{ + Id: "action-legacy", + Name: "decrypt", + } + legacyResource := testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + testAttributeValue("https://example.com/attr/classification/value/secret", namespaceOne), + ), + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + testAttributeValue("https://example.org/attr/classification/value/restricted", namespaceTwo), + ), + ), + ) + reviewer := &plannerTestReviewer{} + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{namespaceOne, namespaceTwo}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewMigrationPlanner(handler, "registered-resources", WithInteractiveReviewer(reviewer)) + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + assert.Equal(t, 1, reviewer.calls) + require.Len(t, plan.RegisteredResources, 1) + assert.Equal(t, ErrUndeterminedTargetMapping.Error()+": registered resource spans multiple target namespaces", plan.RegisteredResources[0].Unresolved) + assert.Nil(t, plan.RegisteredResources[0].Target) +} + +func TestPlannerPlanHuhInteractiveReviewerResolvesRegisteredResourceConflict(t *testing.T) { + t.Parallel() + + namespaceOne := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + namespaceTwo := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + legacyAction := &policy.Action{ + Id: "action-legacy", + Name: "decrypt", + } + legacyResource := testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + testAttributeValue("https://example.com/attr/classification/value/secret", namespaceOne), + ), + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + testAttributeValue("https://example.org/attr/classification/value/restricted", namespaceTwo), + ), + ), + ) + prompter := &testInteractivePrompter{ + selectValue: namespaceSelectionValue(namespaceOne), + } + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + namespaceOne.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + namespaceOne.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{namespaceOne, namespaceTwo}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewMigrationPlanner(handler, "registered-resources", WithInteractiveReviewer(NewHuhInteractiveReviewer(handler, prompter))) + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + assert.Equal(t, 1, prompter.selectCalls) + require.Len(t, plan.RegisteredResources, 1) + assert.Empty(t, plan.RegisteredResources[0].Unresolved) + require.NotNil(t, plan.RegisteredResources[0].Target) + assert.Equal(t, TargetStatusCreate, plan.RegisteredResources[0].Target.Status) + assert.True(t, sameNamespace(namespaceOne, plan.RegisteredResources[0].Target.Namespace)) + require.Len(t, plan.RegisteredResources[0].Target.Values, 1) + require.Len(t, plan.RegisteredResources[0].Target.Values[0].ActionBindings, 1) + assert.Equal(t, "action-legacy", plan.RegisteredResources[0].Target.Values[0].ActionBindings[0].SourceActionID) + require.Len(t, plan.Actions, 1) + require.Len(t, plan.Actions[0].Targets, 1) + assert.Equal(t, TargetStatusCreate, plan.Actions[0].Targets[0].Status) + assert.True(t, sameNamespace(namespaceOne, plan.Actions[0].Targets[0].Namespace)) +} + +type plannerTestHandler struct { + actionsByNamespace map[string]*actions.ListActionsResponse + subjectConditionSetsByNamespace map[string]*subjectmapping.ListSubjectConditionSetsResponse + subjectMappingsByNamespace map[string]*subjectmapping.ListSubjectMappingsResponse + registeredResourcesByNamespace map[string]*registeredresources.ListRegisteredResourcesResponse + registeredResourceValuesByResourceID map[string]*registeredresources.ListRegisteredResourceValuesResponse + obligationTriggersByNamespace map[string]*obligations.ListObligationTriggersResponse + namespacesResponse *namespaces.ListNamespacesResponse + actionCalls []string + subjectConditionSetCalls []string + subjectMappingCalls []string + registeredResourceCalls []string + registeredResourceValueCalls []string + obligationTriggerCalls []string +} + +func (h *plannerTestHandler) ListActions(_ context.Context, limit, offset int32, namespace string) (*actions.ListActionsResponse, error) { + h.actionCalls = append(h.actionCalls, namespace) + if resp, ok := h.actionsByNamespace[namespace]; ok { + return resp, nil + } + return &actions.ListActionsResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *plannerTestHandler) ListSubjectConditionSets(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectConditionSetsResponse, error) { + h.subjectConditionSetCalls = append(h.subjectConditionSetCalls, namespace) + if resp, ok := h.subjectConditionSetsByNamespace[namespace]; ok { + return resp, nil + } + return &subjectmapping.ListSubjectConditionSetsResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *plannerTestHandler) ListSubjectMappings(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectMappingsResponse, error) { + h.subjectMappingCalls = append(h.subjectMappingCalls, namespace) + if resp, ok := h.subjectMappingsByNamespace[namespace]; ok { + return resp, nil + } + return &subjectmapping.ListSubjectMappingsResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *plannerTestHandler) ListRegisteredResources(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*registeredresources.ListRegisteredResourcesResponse, error) { + h.registeredResourceCalls = append(h.registeredResourceCalls, namespace) + if resp, ok := h.registeredResourcesByNamespace[namespace]; ok { + return resp, nil + } + return ®isteredresources.ListRegisteredResourcesResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *plannerTestHandler) ListRegisteredResourceValues(_ context.Context, resourceID string, limit, offset int32) (*registeredresources.ListRegisteredResourceValuesResponse, error) { + h.registeredResourceValueCalls = append(h.registeredResourceValueCalls, resourceID) + if resp, ok := h.registeredResourceValuesByResourceID[resourceID]; ok { + return resp, nil + } + + for _, resp := range h.registeredResourcesByNamespace { + for _, resource := range resp.GetResources() { + if resource.GetId() != resourceID { + continue + } + return ®isteredresources.ListRegisteredResourceValuesResponse{ + Values: resource.GetValues(), + Pagination: emptyPageResponse(), + }, nil + } + } + + return ®isteredresources.ListRegisteredResourceValuesResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *plannerTestHandler) ListObligationTriggers(_ context.Context, namespace string, limit, offset int32) (*obligations.ListObligationTriggersResponse, error) { + h.obligationTriggerCalls = append(h.obligationTriggerCalls, namespace) + if resp, ok := h.obligationTriggersByNamespace[namespace]; ok { + return resp, nil + } + return &obligations.ListObligationTriggersResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *plannerTestHandler) ListNamespaces(_ context.Context, state common.ActiveStateEnum, limit, offset int32, sort handlers.SortOption) (*namespaces.ListNamespacesResponse, error) { + if h.namespacesResponse != nil { + return h.namespacesResponse, nil + } + return &namespaces.ListNamespacesResponse{Pagination: emptyPageResponse()}, nil +} + +func emptyPageResponse() *policy.PageResponse { + return &policy.PageResponse{} +} + +func actionSourceIDs(actions []*ActionPlan) []string { + ids := make([]string, 0, len(actions)) + for _, action := range actions { + if action == nil || action.Source == nil { + continue + } + ids = append(ids, action.Source.GetId()) + } + + return ids +} + +type plannerTestReviewer struct { + calls int + lastResolved *ResolvedTargets + err error +} + +func (r *plannerTestReviewer) Review(_ context.Context, resolved *ResolvedTargets, _ []*policy.Namespace) error { + r.calls++ + r.lastResolved = resolved + return r.err +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_review.go b/otdfctl/migrations/namespacedpolicy/migration_review.go new file mode 100644 index 0000000000..2e4011c50e --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_review.go @@ -0,0 +1,473 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" + "google.golang.org/protobuf/proto" +) + +const ( + interactiveReviewAbortOption = "__abort_interactive_review__" + minimumRegisteredResourceReviewNamespaces = 2 +) + +var ErrNilInteractiveReviewHandler = errors.New("interactive review handler is required") + +// InteractiveReviewer owns planner-time interactive review. It mutates +// resolved planner state before finalization when interactive review is enabled. +type InteractiveReviewer interface { + Review(context.Context, *ResolvedTargets, []*policy.Namespace) error +} + +// HuhInteractiveReviewer is the planner-owned interactive review entrypoint for +// `migrate namespaced-policy --interactive`. +// +// The only actionable planner-time review currently supported is resolving +// registered resources whose action-attribute-values span multiple namespaces. +type HuhInteractiveReviewer struct { + handler PolicyClient + prompter InteractivePrompter + pageSize int32 +} + +func NewHuhInteractiveReviewer(handler PolicyClient, prompter InteractivePrompter) *HuhInteractiveReviewer { + return &HuhInteractiveReviewer{ + handler: handler, + prompter: prompter, + pageSize: defaultPlannerPageSize, + } +} + +func (r *HuhInteractiveReviewer) Review(ctx context.Context, resolved *ResolvedTargets, namespaces []*policy.Namespace) error { + if resolved == nil { + return nil + } + + var retriever *Retriever + namespaceCache := make(map[string]*interactiveReviewNamespaceState) + for _, resource := range resolved.RegisteredResources { + if !isConflictingRegisteredResource(resource) { + continue + } + if retriever == nil { + var err error + retriever, err = r.retriever() + if err != nil { + return err + } + } + if err := r.reviewRegisteredResource(ctx, resolved, resource, namespaces, retriever, namespaceCache); err != nil { + return err + } + } + + return nil +} + +// reviewRegisteredResource prompts the reviewer to pick a target namespace for a +// conflicted registered resource and rewrites planner state to match that choice. +// +// In-place mutations: +// - resource.Source — replaced with the namespace-filtered clone +// - resource.Namespace — set to the chosen namespace +// - resource.Unresolved — cleared +// - resource.AlreadyMigrated — cleared, then set if an existing match is found +// - resource.NeedsCreate — cleared, then set true if no existing match +// - resolved.Actions — appended to via ensureRegisteredResourceActionResolution +// - namespaceCache — populated/read by reviewNamespaceState +func (r *HuhInteractiveReviewer) reviewRegisteredResource( + ctx context.Context, + resolved *ResolvedTargets, + resource *ResolvedRegisteredResource, + namespaces []*policy.Namespace, + retriever *Retriever, + namespaceCache map[string]*interactiveReviewNamespaceState, +) error { + if resource == nil || resource.Source == nil { + return nil + } + + candidates, err := registeredResourceCandidateNamespaces(resource.Source, namespaces) + if err != nil { + return fmt.Errorf("registered resource %q: %w", resource.Source.GetId(), err) + } + + selected, err := r.resolvePrompter().Select(ctx, registeredResourceConflictPrompt(resource.Source, candidates)) + if err != nil { + return err + } + // registeredResourceConflictPrompt appends interactiveReviewAbortOption to SelectPrompt.Options, + // but selectedNamespace only searches candidates, so this short-circuit must remain in place. + if selected == interactiveReviewAbortOption { + return ErrInteractiveReviewAborted + } + + chosen := selectedNamespace(candidates, selected) + if chosen == nil { + return fmt.Errorf("registered resource %q: invalid namespace choice %q", resource.Source.GetId(), selected) + } + + filtered, err := filterRegisteredResourceToNamespace(resource.Source, chosen) + if err != nil { + return fmt.Errorf("registered resource %q: %w", resource.Source.GetId(), err) + } + + namespaceState, err := reviewNamespaceState(ctx, retriever, chosen, namespaceCache) + if err != nil { + return fmt.Errorf("registered resource %q: %w", resource.Source.GetId(), err) + } + + resource.Source = filtered + resource.Namespace = chosen + // Reset planner state before re-resolving against registeredResources[chosen.GetId()] so AlreadyMigrated/NeedsCreate matches resolver.resolveRegisteredResource semantics for the chosen namespace. + resource.Unresolved = nil + resource.AlreadyMigrated = nil + resource.NeedsCreate = false + + if existing, found := resolveExistingRegisteredResource(filtered, namespaceState.registeredResources); found { + resource.AlreadyMigrated = existing + return nil + } + resource.NeedsCreate = true + + for _, value := range filtered.GetValues() { + for _, aav := range value.GetActionAttributeValues() { + if err := ensureRegisteredResourceActionResolution(resolved, chosen, aav.GetAction(), namespaceState.actionResolver); err != nil { + return fmt.Errorf("registered resource %q: %w", resource.Source.GetId(), err) + } + } + } + + return nil +} + +type interactiveReviewNamespaceState struct { + actionResolver *resolver + registeredResources []*policy.RegisteredResource +} + +func reviewNamespaceState( + ctx context.Context, + retriever *Retriever, + chosen *policy.Namespace, + namespaceCache map[string]*interactiveReviewNamespaceState, +) (*interactiveReviewNamespaceState, error) { + if chosen == nil { + return nil, fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + if retriever == nil { + return nil, ErrNilInteractiveReviewHandler + } + + key := chosen.GetId() + if state, ok := namespaceCache[key]; ok { + return state, nil + } + + customActions, standardActions, err := retriever.listActionsForNamespaces(ctx, []*policy.Namespace{chosen}) + if err != nil { + return nil, err + } + + registeredResources, err := retriever.listRegisteredResourcesForNamespaces(ctx, []*policy.Namespace{chosen}) + if err != nil { + return nil, err + } + + state := &interactiveReviewNamespaceState{ + actionResolver: &resolver{ + existing: &ExistingTargets{ + CustomActions: customActions, + StandardActions: standardActions, + }, + actionResultsByKey: make(map[string]*ResolvedActionResult), + scsResultsByKey: make(map[string]*ResolvedSubjectConditionSetResult), + }, + registeredResources: registeredResources[chosen.GetId()], + } + namespaceCache[key] = state + return state, nil +} + +func (r *HuhInteractiveReviewer) resolvePrompter() InteractivePrompter { + if r != nil && r.prompter != nil { + return r.prompter + } + + return &HuhPrompter{} +} + +func (r *HuhInteractiveReviewer) retriever() (*Retriever, error) { + if r == nil || r.handler == nil { + return nil, ErrNilInteractiveReviewHandler + } + + pageSize := r.pageSize + if pageSize <= 0 { + pageSize = defaultPlannerPageSize + } + + return newRetriever(r.handler, pageSize), nil +} + +func isConflictingRegisteredResource(resource *ResolvedRegisteredResource) bool { + if resource == nil || resource.Unresolved == nil { + return false + } + + return resource.Unresolved.Reason == UnresolvedReasonRegisteredResourceConflictingNamespaces +} + +func registeredResourceCandidateNamespaces(resource *policy.RegisteredResource, namespaces []*policy.Namespace) ([]*policy.Namespace, error) { + if resource == nil { + return nil, fmt.Errorf("%w: registered resource is empty", ErrUndeterminedTargetMapping) + } + + deriver := newTargetDeriver(namespaces) + ordered := newNamespaceAccumulator() + + for _, value := range resource.GetValues() { + for _, aav := range value.GetActionAttributeValues() { + namespace, err := deriver.resolveNamespace(namespaceFromAttributeValue(aav.GetAttributeValue())) + if err != nil { + return nil, err + } + ordered.add(namespace) + } + } + + candidates := ordered.slice() + if len(candidates) < minimumRegisteredResourceReviewNamespaces { + return nil, fmt.Errorf("%w: registered resource review requires multiple candidate namespaces", ErrUndeterminedTargetMapping) + } + + return candidates, nil +} + +func registeredResourceConflictPrompt(resource *policy.RegisteredResource, namespaces []*policy.Namespace) SelectPrompt { + description := []string{ + fmt.Sprintf("Registered resource: %s (%s)", strings.TrimSpace(resource.GetName()), resource.GetId()), + "Choose one target namespace for this registered resource.", + "Bindings for other namespaces will be removed from the reviewed RR.", + } + description = append(description, registeredResourceConflictLines(resource)...) + + options := make([]PromptOption, 0, len(namespaces)+1) + for _, namespace := range namespaces { + options = append(options, PromptOption{ + Label: namespaceLabel(namespace), + Value: namespaceSelectionValue(namespace), + Description: "migrate to this namespace", + }) + } + options = append(options, PromptOption{ + Label: "Abort run", + Value: interactiveReviewAbortOption, + Description: "stop planning without changing this RR", + }) + + return SelectPrompt{ + Title: fmt.Sprintf("Registered resource (name: %s, id: %s) spans multiple target namespaces.", resource.GetName(), resource.GetId()), + Description: description, + Options: options, + } +} + +func registeredResourceConflictLines(resource *policy.RegisteredResource) []string { + lines := make([]string, 0) + for _, value := range resource.GetValues() { + if value == nil { + continue + } + if len(value.GetActionAttributeValues()) == 0 { + lines = append(lines, fmt.Sprintf("Value %q has no action bindings.", value.GetValue())) + continue + } + for _, aav := range value.GetActionAttributeValues() { + if aav == nil { + continue + } + lines = append(lines, fmt.Sprintf( + "Value %q: action %q -> %s", + value.GetValue(), + actionLabel(aav.GetAction()), + namespaceLabel(namespaceFromAttributeValue(aav.GetAttributeValue())), + )) + } + } + + return lines +} + +func namespaceSelectionValue(namespace *policy.Namespace) string { + return namespaceRefKey(namespace) +} + +func selectedNamespace(candidates []*policy.Namespace, value string) *policy.Namespace { + for _, namespace := range candidates { + if namespaceSelectionValue(namespace) == value { + return namespace + } + } + + return nil +} + +func filterRegisteredResourceToNamespace(resource *policy.RegisteredResource, namespace *policy.Namespace) (*policy.RegisteredResource, error) { + if resource == nil { + return nil, fmt.Errorf("%w: registered resource is empty", ErrUndeterminedTargetMapping) + } + if namespace == nil { + return nil, fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + + cloned, ok := proto.Clone(resource).(*policy.RegisteredResource) + if !ok { + return nil, errors.New("could not clone registered resource") + } + + clonedValues := cloned.GetValues() + cloned.Values = make([]*policy.RegisteredResourceValue, 0, len(clonedValues)) + for _, value := range clonedValues { + if value == nil { + continue + } + + if len(value.GetActionAttributeValues()) == 0 { + cloned.Values = append(cloned.Values, value) + continue + } + + filteredAAVs := make([]*policy.RegisteredResourceValue_ActionAttributeValue, 0, len(value.GetActionAttributeValues())) + for _, aav := range value.GetActionAttributeValues() { + if aav == nil || !sameNamespace(namespaceFromAttributeValue(aav.GetAttributeValue()), namespace) { + continue + } + filteredAAVs = append(filteredAAVs, aav) + } + + if len(filteredAAVs) == 0 { + continue + } + + value.ActionAttributeValues = filteredAAVs + cloned.Values = append(cloned.Values, value) + } + + return cloned, nil +} + +// ensureRegisteredResourceActionResolution is the interactive-path counterpart +// to resolveRegisteredResourceDependencies: after the reviewer has picked a +// target namespace for a conflicted registered resource and filtered its AAVs +// to that namespace, this function guarantees every referenced action has a +// corresponding ResolvedAction entry that the executor can bind to at create +// time. +// +// For each action binding on the filtered resource, it: +// 1. Validates the action reference (non-nil, non-empty id). Missing or empty +// ids are a hard error — without an id the executor cannot bind the AAV, +// so the plan must fail here rather than at create time. +// 2. Finds or creates the matching ResolvedAction in resolved.Actions, keyed +// by source id. When creating, the input action is defensively cloned so +// later mutations to the plan do not alter the retriever's cached state. +// 3. Skips if the action is already resolved for this namespace (duplicate +// AAV bindings on the same resource resolve once). Otherwise runs the +// standard resolveActionTargetFromExisting path — same semantics as the +// initial-pass resolver — and appends the result. +// +// Note: this function writes only to resolved.Actions; it does NOT populate +// actionResolver.actionResultsByKey. That map is consumed only by the +// initial-pass dependency helpers (resolveRegisteredResourceDependencies, +// resolveObligationTriggerDependencies, resolveSubjectMappingDependencies) +// which are skipped for interactively-resolved resources because their +// Unresolved state causes resolveRegisteredResource to early-return. The +// final plan is built from resolved.Actions, so the two paths converge there. +func ensureRegisteredResourceActionResolution(resolved *ResolvedTargets, namespace *policy.Namespace, action *policy.Action, actionResolver *resolver) error { + if resolved == nil { + return ErrNilResolvedTargets + } + if namespace == nil { + return fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + if action == nil || strings.TrimSpace(action.GetId()) == "" { + return errors.New("registered resource binding action is missing") + } + if actionResolver == nil { + return errors.New("action resolver required for plan resolution") + } + + item := resolvedActionByID(resolved.Actions, action.GetId()) + if item == nil { + source, err := cloneAction(action) + if err != nil { + return err + } + + item = &ResolvedAction{ + Source: source, + Results: make([]*ResolvedActionResult, 0, 1), + } + resolved.Actions = append(resolved.Actions, item) + } else if item.Source == nil { + source, err := cloneAction(action) + if err != nil { + return err + } + + item.Source = source + } + + if resolvedActionResultForNamespace(item, namespace) != nil { + return nil + } + + result, err := actionResolver.resolveActionTargetFromExisting(item.Source, namespace) + if err != nil { + return fmt.Errorf("action %q in namespace %q: %w", item.Source.GetId(), namespace.GetId(), err) + } + + item.Results = append(item.Results, result) + return nil +} + +func resolvedActionByID(actions []*ResolvedAction, sourceID string) *ResolvedAction { + for _, action := range actions { + if action != nil && action.Source != nil && action.Source.GetId() == sourceID { + return action + } + } + + return nil +} + +func resolvedActionResultForNamespace(action *ResolvedAction, namespace *policy.Namespace) *ResolvedActionResult { + if action == nil || namespace == nil { + return nil + } + + for _, result := range action.Results { + if result != nil && sameNamespace(result.Namespace, namespace) { + return result + } + } + + return nil +} + +func cloneAction(action *policy.Action) (*policy.Action, error) { + if action == nil { + return nil, errors.New("action is nil") + } + + cloned, ok := proto.Clone(action).(*policy.Action) + if !ok { + return nil, fmt.Errorf("clone action %q: unexpected proto clone type", action.GetId()) + } + + return cloned, nil +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_review_test.go b/otdfctl/migrations/namespacedpolicy/migration_review_test.go new file mode 100644 index 0000000000..e539c8d62b --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_review_test.go @@ -0,0 +1,771 @@ +package namespacedpolicy + +import ( + "context" + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHuhInteractiveReviewerResolvesConflictingRegisteredResource(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + leftNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + leftNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{leftNamespace, rightNamespace}, + Pagination: emptyPageResponse(), + }, + } + prompter := &testInteractivePrompter{ + selectValue: namespaceSelectionValue(leftNamespace), + } + reviewer := NewHuhInteractiveReviewer(handler, prompter) + resolved := &ResolvedTargets{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*ResolvedRegisteredResource{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "encrypt", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + &policy.RegisteredResourceValue{ + Value: "shared", + }, + ), + Unresolved: &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + Message: "could not determine target namespace: registered resource spans multiple target namespaces", + }, + }, + }, + } + + err := reviewer.Review(t.Context(), resolved, []*policy.Namespace{leftNamespace, rightNamespace}) + require.NoError(t, err) + + require.Equal(t, 1, prompter.selectCalls) + require.NotNil(t, prompter.lastSelectPrompt) + assert.Equal(t, "Registered resource (name: documents, id: resource-1) spans multiple target namespaces.", prompter.lastSelectPrompt.Title) + require.Len(t, prompter.lastSelectPrompt.Options, 3) + + require.Len(t, resolved.RegisteredResources, 1) + resource := resolved.RegisteredResources[0] + require.NotNil(t, resource) + assert.Nil(t, resource.Unresolved) + assert.True(t, resource.NeedsCreate) + assert.Nil(t, resource.AlreadyMigrated) + require.True(t, sameNamespace(leftNamespace, resource.Namespace)) + require.Len(t, resource.Source.GetValues(), 2) + require.Len(t, resource.Source.GetValues()[0].GetActionAttributeValues(), 1) + assert.Equal(t, "action-1", resource.Source.GetValues()[0].GetActionAttributeValues()[0].GetAction().GetId()) + assert.Empty(t, resource.Source.GetValues()[1].GetActionAttributeValues()) + + require.Len(t, resolved.Actions, 1) + action := resolved.Actions[0] + require.NotNil(t, action.Source) + assert.Equal(t, "action-1", action.Source.GetId()) + require.Len(t, action.Results, 1) + assert.True(t, sameNamespace(leftNamespace, action.Results[0].Namespace)) + assert.True(t, action.Results[0].NeedsCreate) +} + +func TestHuhInteractiveReviewerSkipsActionResolutionWhenFilteredResourceAlreadyExists(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + + filteredExisting := testRegisteredResource( + "resource-existing", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-existing", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + ), + &policy.RegisteredResourceValue{Value: "shared"}, + ) + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + leftNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + leftNamespace.GetId(): { + Resources: []*policy.RegisteredResource{filteredExisting}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{leftNamespace, rightNamespace}, + Pagination: emptyPageResponse(), + }, + } + reviewer := NewHuhInteractiveReviewer(handler, &testInteractivePrompter{ + selectValue: namespaceSelectionValue(leftNamespace), + }) + resolved := &ResolvedTargets{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*ResolvedRegisteredResource{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "encrypt", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + &policy.RegisteredResourceValue{Value: "shared"}, + ), + Unresolved: &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + }, + }, + }, + } + + err := reviewer.Review(t.Context(), resolved, []*policy.Namespace{leftNamespace, rightNamespace}) + require.NoError(t, err) + + resource := resolved.RegisteredResources[0] + require.NotNil(t, resource) + assert.Nil(t, resource.Unresolved) + assert.False(t, resource.NeedsCreate) + require.NotNil(t, resource.AlreadyMigrated) + assert.Equal(t, filteredExisting.GetId(), resource.AlreadyMigrated.GetId()) + assert.Empty(t, resolved.Actions) +} + +func TestHuhInteractiveReviewerReusesPreviouslyResolvedActionForDuplicateBindings(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + leftNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + leftNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{leftNamespace, rightNamespace}, + Pagination: emptyPageResponse(), + }, + } + reviewer := NewHuhInteractiveReviewer(handler, &testInteractivePrompter{ + selectValue: namespaceSelectionValue(leftNamespace), + }) + resolved := &ResolvedTargets{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*ResolvedRegisteredResource{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/internal", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "encrypt", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + ), + Unresolved: &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + }, + }, + }, + } + + err := reviewer.Review(t.Context(), resolved, []*policy.Namespace{leftNamespace, rightNamespace}) + require.NoError(t, err) + + require.Len(t, resolved.RegisteredResources, 1) + reviewedResource := resolved.RegisteredResources[0] + require.NotNil(t, reviewedResource) + require.True(t, sameNamespace(leftNamespace, reviewedResource.Namespace)) + require.Len(t, reviewedResource.Source.GetValues(), 1) + require.Len(t, reviewedResource.Source.GetValues()[0].GetActionAttributeValues(), 2) + + require.Len(t, resolved.Actions, 1) + resolvedAction := resolved.Actions[0] + require.NotNil(t, resolvedAction.Source) + assert.Equal(t, "action-1", resolvedAction.Source.GetId()) + require.Len(t, resolvedAction.Results, 1) + duplicateBindingActionResult := resolvedAction.Results[0] + assert.True(t, sameNamespace(leftNamespace, duplicateBindingActionResult.Namespace)) + assert.True(t, duplicateBindingActionResult.NeedsCreate) + assert.Nil(t, duplicateBindingActionResult.AlreadyMigrated) +} + +func TestEnsureRegisteredResourceActionResolutionReusesExistingNamespaceResult(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + resolved := &ResolvedTargets{ + Actions: []*ResolvedAction{ + { + Source: &policy.Action{ + Id: "action-1", + Name: "decrypt", + }, + Results: []*ResolvedActionResult{ + { + Namespace: namespace, + NeedsCreate: true, + }, + }, + }, + }, + } + + err := ensureRegisteredResourceActionResolution( + resolved, + namespace, + &policy.Action{Id: "action-1", Name: "decrypt"}, + &resolver{ + existing: &ExistingTargets{}, + }, + ) + require.NoError(t, err) + + require.Len(t, resolved.Actions, 1) + require.Len(t, resolved.Actions[0].Results, 1) +} + +func TestEnsureRegisteredResourceActionResolutionCreatesNewActionResolution(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + sourceAction := &policy.Action{ + Id: "action-1", + Name: "decrypt_custom", + } + resolved := &ResolvedTargets{} + + err := ensureRegisteredResourceActionResolution( + resolved, + namespace, + sourceAction, + &resolver{ + existing: newExistingTargets(), + }, + ) + require.NoError(t, err) + + require.Len(t, resolved.Actions, 1) + resolvedAction := resolved.Actions[0] + require.NotNil(t, resolvedAction.Source) + assert.NotSame(t, sourceAction, resolvedAction.Source) + assert.Equal(t, sourceAction.GetId(), resolvedAction.Source.GetId()) + assert.Equal(t, sourceAction.GetName(), resolvedAction.Source.GetName()) + + require.Len(t, resolvedAction.Results, 1) + createdActionResult := resolvedAction.Results[0] + assert.True(t, sameNamespace(namespace, createdActionResult.Namespace)) + assert.True(t, createdActionResult.NeedsCreate) + assert.Nil(t, createdActionResult.AlreadyMigrated) + assert.Nil(t, createdActionResult.ExistingStandard) +} + +func TestEnsureRegisteredResourceActionResolutionAppendsMissingNamespaceResult(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + sourceAction := &policy.Action{ + Id: "action-1", + Name: "decrypt_custom", + } + existingMigratedAction := &policy.Action{ + Id: "action-existing", + Name: "decrypt_custom", + } + resolved := &ResolvedTargets{ + Actions: []*ResolvedAction{ + { + Source: sourceAction, + Results: []*ResolvedActionResult{ + { + Namespace: leftNamespace, + NeedsCreate: true, + }, + }, + }, + }, + } + actionResolver := &resolver{ + existing: newExistingTargets(), + } + actionResolver.existing.CustomActions[rightNamespace.GetId()] = []*policy.Action{existingMigratedAction} + + err := ensureRegisteredResourceActionResolution( + resolved, + rightNamespace, + sourceAction, + actionResolver, + ) + require.NoError(t, err) + + require.Len(t, resolved.Actions, 1) + resolvedAction := resolved.Actions[0] + require.Len(t, resolvedAction.Results, 2) + + existingNamespaceActionResult := resolvedAction.Results[0] + assert.True(t, sameNamespace(leftNamespace, existingNamespaceActionResult.Namespace)) + assert.True(t, existingNamespaceActionResult.NeedsCreate) + + appendedActionResult := resolvedAction.Results[1] + assert.True(t, sameNamespace(rightNamespace, appendedActionResult.Namespace)) + assert.False(t, appendedActionResult.NeedsCreate) + assert.Same(t, existingMigratedAction, appendedActionResult.AlreadyMigrated) + assert.Nil(t, appendedActionResult.ExistingStandard) +} + +func TestEnsureRegisteredResourceActionResolutionResolvesStandardActionTarget(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + sourceAction := &policy.Action{ + Id: "action-1", + Name: "read", + } + existingStandardAction := &policy.Action{ + Id: "action-standard", + Name: "read", + Namespace: namespace, + } + actionResolver := &resolver{ + existing: newExistingTargets(), + } + actionResolver.existing.StandardActions[namespace.GetId()] = []*policy.Action{existingStandardAction} + resolved := &ResolvedTargets{} + + err := ensureRegisteredResourceActionResolution( + resolved, + namespace, + sourceAction, + actionResolver, + ) + require.NoError(t, err) + + require.Len(t, resolved.Actions, 1) + require.Len(t, resolved.Actions[0].Results, 1) + standardActionResult := resolved.Actions[0].Results[0] + assert.True(t, sameNamespace(namespace, standardActionResult.Namespace)) + assert.Same(t, existingStandardAction, standardActionResult.ExistingStandard) + assert.False(t, standardActionResult.NeedsCreate) + assert.Nil(t, standardActionResult.AlreadyMigrated) +} + +func TestEnsureRegisteredResourceActionResolutionWrapsResolverError(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + sourceAction := &policy.Action{ + Id: "action-1", + Name: "read", + } + + err := ensureRegisteredResourceActionResolution( + &ResolvedTargets{}, + namespace, + sourceAction, + &resolver{ + existing: newExistingTargets(), + }, + ) + require.ErrorContains(t, err, `action "action-1" in namespace "ns-1"`) + require.ErrorContains(t, err, "matching standard action not found in target namespace") +} + +func TestFilterRegisteredResourceToNamespaceRetainsUnboundValues(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + + filtered, err := filterRegisteredResourceToNamespace( + testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "encrypt", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + &policy.RegisteredResourceValue{Value: "shared"}, + ), + leftNamespace, + ) + require.NoError(t, err) + + require.Len(t, filtered.GetValues(), 2) + require.Len(t, filtered.GetValues()[0].GetActionAttributeValues(), 1) + assert.Equal(t, "action-1", filtered.GetValues()[0].GetActionAttributeValues()[0].GetAction().GetId()) + assert.Empty(t, filtered.GetValues()[1].GetActionAttributeValues()) +} + +func TestRegisteredResourceCandidateNamespacesDeduplicatesNamespaces(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + + candidates, err := registeredResourceCandidateNamespaces( + testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "decrypt-again", + testAttributeValue("https://example.com/attr/classification/value/internal", leftNamespace), + ), + testActionAttributeValue( + "action-3", + "encrypt", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + ), + []*policy.Namespace{leftNamespace, rightNamespace}, + ) + require.NoError(t, err) + + require.Len(t, candidates, 2) + assert.True(t, sameNamespace(leftNamespace, candidates[0])) + assert.True(t, sameNamespace(rightNamespace, candidates[1])) +} + +func TestHuhInteractiveReviewerReturnsAbortWhenPromptAborts(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + reviewer := NewHuhInteractiveReviewer( + &plannerTestHandler{}, + &testInteractivePrompter{selectErr: ErrInteractiveReviewAborted}, + ) + + err := reviewer.Review(t.Context(), &ResolvedTargets{ + RegisteredResources: []*ResolvedRegisteredResource{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "encrypt", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + ), + Unresolved: &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + }, + }, + }, + }, []*policy.Namespace{leftNamespace, rightNamespace}) + require.ErrorIs(t, err, ErrInteractiveReviewAborted) +} + +func TestHuhInteractiveReviewerReturnsNilForNilResolvedTargets(t *testing.T) { + t.Parallel() + + reviewer := NewHuhInteractiveReviewer(&plannerTestHandler{}, &testInteractivePrompter{}) + + err := reviewer.Review(t.Context(), nil, nil) + require.NoError(t, err) +} + +func TestHuhInteractiveReviewerRequiresHandlerForConflictingReview(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + reviewer := NewHuhInteractiveReviewer(nil, &testInteractivePrompter{ + selectValue: namespaceSelectionValue(leftNamespace), + }) + + err := reviewer.Review(t.Context(), &ResolvedTargets{ + RegisteredResources: []*ResolvedRegisteredResource{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "encrypt", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + ), + Unresolved: &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + }, + }, + }, + }, []*policy.Namespace{leftNamespace, rightNamespace}) + require.ErrorIs(t, err, ErrNilInteractiveReviewHandler) +} + +func TestHuhInteractiveReviewerCachesNamespaceLookupsWithinReview(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + rightNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://example.org", + } + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + leftNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + leftNamespace.GetId(): { + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{leftNamespace, rightNamespace}, + Pagination: emptyPageResponse(), + }, + } + prompter := &testInteractivePrompter{ + selectValue: namespaceSelectionValue(leftNamespace), + } + reviewer := NewHuhInteractiveReviewer(handler, prompter) + resolved := &ResolvedTargets{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*ResolvedRegisteredResource{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", leftNamespace), + ), + testActionAttributeValue( + "action-2", + "encrypt", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + ), + Unresolved: &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + }, + }, + { + Source: testRegisteredResource( + "resource-2", + "records", + testRegisteredResourceValue( + "stage", + testActionAttributeValue( + "action-3", + "review_records", + testAttributeValue("https://example.com/attr/classification/value/internal", leftNamespace), + ), + testActionAttributeValue( + "action-4", + "publish_records", + testAttributeValue("https://example.org/attr/classification/value/restricted", rightNamespace), + ), + ), + ), + Unresolved: &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + }, + }, + }, + } + + err := reviewer.Review(t.Context(), resolved, []*policy.Namespace{leftNamespace, rightNamespace}) + require.NoError(t, err) + assert.Equal(t, 2, prompter.selectCalls) + assert.Equal(t, []string{leftNamespace.GetId()}, handler.actionCalls) + assert.Equal(t, []string{leftNamespace.GetId()}, handler.registeredResourceCalls) +} + +type testInteractivePrompter struct { + confirmCalls int + lastConfirmPrompt *ConfirmPrompt + confirmErr error + selectCalls int + lastSelectPrompt *SelectPrompt + selectValue string + selectErr error +} + +func (p *testInteractivePrompter) Confirm(_ context.Context, prompt ConfirmPrompt) error { + p.confirmCalls++ + p.lastConfirmPrompt = &prompt + return p.confirmErr +} + +func (p *testInteractivePrompter) Select(_ context.Context, prompt SelectPrompt) (string, error) { + p.selectCalls++ + p.lastSelectPrompt = &prompt + return p.selectValue, p.selectErr +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_summary.go b/otdfctl/migrations/namespacedpolicy/migration_summary.go new file mode 100644 index 0000000000..6537e4ce52 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_summary.go @@ -0,0 +1,581 @@ +package namespacedpolicy + +import ( + "fmt" + "strings" + + identifier "github.com/opentdf/platform/lib/identifier" + "github.com/opentdf/platform/otdfctl/migrations" + "github.com/opentdf/platform/protocol/go/policy" +) + +type summaryLines struct { + applied string + pending string + failed string + skipped string + unresolved string +} + +const unexpectedNilTargetReasonFormat = "received unexpected nil target for %s" + +const ( + migrationAppliedCountLabel = "created" + migrationPendingCountLabel = "to_create" + migrationAppliedSectionLabel = "Created" + migrationPendingSectionLabel = "Will Create" +) + +func RenderNamespacedPolicySummary(plan *MigrationPlan, commit bool) string { + return renderNamespacedPolicySummary(plan, commit, "success") +} + +func RenderNamespacedPolicySummaryWithResult(plan *MigrationPlan, commit bool, result string) string { + return renderNamespacedPolicySummary(plan, commit, result) +} + +func planScopes(plan *MigrationPlan) []Scope { + if plan == nil { + return nil + } + return plan.Scopes +} + +func renderNamespacedPolicySummary(plan *MigrationPlan, commit bool, result string) string { + styles := migrations.NewDisplayStyles() + return renderSummaryDocument(styles, summaryDocument{ + plannedTitle: "Namespaced Policy Migration Plan", + committedTitle: "Namespaced Policy Migration Committed", + operation: summaryOperationMigration, + scopes: planScopes(plan), + commit: commit, + result: result, + summaries: []constructSummary{ + summarizeActions(plan, commit, styles), + summarizeSubjectConditionSets(plan, commit, styles), + summarizeSubjectMappings(plan, commit, styles), + summarizeRegisteredResources(plan, commit, styles), + summarizeObligationTriggers(plan, commit, styles), + }, + }) +} + +func appendMigrationSummaryCountParts(parts []string, counts summaryCounts) []string { + return append(parts, + fmt.Sprintf("existing_standard=%d", counts.existingStandard), + fmt.Sprintf("already_migrated=%d", counts.alreadyMigrated), + ) +} + +func summarizeActions(plan *MigrationPlan, commit bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Actions", + include: includesScope(planScopes(plan), ScopeActions), + } + if plan == nil { + return summary + } + + for _, action := range plan.Actions { + if action == nil || action.Source == nil { + continue + } + for _, target := range action.Targets { + if target == nil { + appendTargetlessUnresolved(&summary, styles, actionKind, action.Source.GetName(), unexpectedNilTargetReason(actionKind)) + continue + } + appendTargetStatusSummary(&summary, target.Status, classifyCreateExecution(commit, target.Execution), summaryLines{ + applied: formatCreatedLine(styles, actionKind, action.Source.GetName(), target.Namespace, target.TargetID(), true), + pending: formatCreatedLine(styles, actionKind, action.Source.GetName(), target.Namespace, target.TargetID(), false), + failed: formatFailedLine(styles, actionKind, action.Source.GetName(), target.Namespace, executionFailure(target.Execution)), + skipped: formatSkippedLine(styles, actionKind, action.Source.GetName(), target.Namespace, target.Reason), + unresolved: formatUnresolvedLine(styles, actionKind, action.Source.GetName(), target.Namespace, target.Reason), + }) + } + } + + return summary +} + +func summarizeSubjectConditionSets(plan *MigrationPlan, commit bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Subject Condition Sets", + include: includesScope(planScopes(plan), ScopeSubjectConditionSets), + } + if plan == nil { + return summary + } + + for _, scs := range plan.SubjectConditionSets { + if scs == nil || scs.Source == nil { + continue + } + for _, target := range scs.Targets { + if target == nil { + appendTargetlessUnresolved(&summary, styles, subjectConditionSetKind, scs.Source.GetId(), unexpectedNilTargetReason(subjectConditionSetKind)) + continue + } + appendTargetStatusSummary(&summary, target.Status, classifyCreateExecution(commit, target.Execution), summaryLines{ + applied: formatSubjectConditionSetCreatedLine(styles, scs, target, true), + pending: formatSubjectConditionSetCreatedLine(styles, scs, target, false), + failed: formatFailedLine(styles, subjectConditionSetKind, scs.Source.GetId(), target.Namespace, executionFailure(target.Execution)), + skipped: formatSkippedLine(styles, subjectConditionSetKind, scs.Source.GetId(), target.Namespace, target.Reason), + unresolved: formatUnresolvedLine(styles, subjectConditionSetKind, scs.Source.GetId(), target.Namespace, target.Reason), + }) + } + } + + return summary +} + +func summarizeSubjectMappings(plan *MigrationPlan, commit bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Subject Mappings", + include: includesScope(planScopes(plan), ScopeSubjectMappings), + } + if plan == nil { + return summary + } + + for _, mapping := range plan.SubjectMappings { + if mapping == nil || mapping.Source == nil { + continue + } + if mapping.Target == nil { + appendTargetlessUnresolved(&summary, styles, subjectMappingKind, mapping.Source.GetId(), unexpectedNilTargetReason(subjectMappingKind)) + continue + } + + appendTargetStatusSummary(&summary, mapping.Target.Status, classifyCreateExecution(commit, mapping.Target.Execution), summaryLines{ + applied: formatSubjectMappingCreatedLine(styles, plan, mapping, true), + pending: formatSubjectMappingCreatedLine(styles, plan, mapping, false), + failed: formatFailedLine(styles, subjectMappingKind, mapping.Source.GetId(), mapping.Target.Namespace, executionFailure(mapping.Target.Execution)), + skipped: formatSkippedLine(styles, subjectMappingKind, mapping.Source.GetId(), mapping.Target.Namespace, mapping.Target.Reason), + unresolved: formatUnresolvedLine(styles, subjectMappingKind, mapping.Source.GetId(), mapping.Target.Namespace, mapping.Target.Reason), + }) + } + + return summary +} + +func summarizeRegisteredResources(plan *MigrationPlan, commit bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Registered Resources", + include: includesScope(planScopes(plan), ScopeRegisteredResources), + } + if plan == nil { + return summary + } + + for _, resource := range plan.RegisteredResources { + if resource == nil || resource.Source == nil { + continue + } + if resource.Target == nil { + appendTargetlessUnresolved(&summary, styles, registeredResourceKind, resource.Source.GetName(), unresolvedRegisteredResourceReason(resource)) + continue + } + + state := operationExecutionStatePending + failure := "" + if resource.Target.Status == TargetStatusCreate { + state, failure = classifyRegisteredResourceExecution(commit, resource.Target) + } + appendTargetStatusSummary(&summary, resource.Target.Status, state, summaryLines{ + applied: formatRegisteredResourceCreatedLine(styles, plan, resource, true), + pending: formatRegisteredResourceCreatedLine(styles, plan, resource, false), + failed: formatRegisteredResourceFailedLine(styles, resource, failure), + skipped: formatSkippedLine(styles, registeredResourceKind, resource.Source.GetName(), resource.Target.Namespace, resource.Target.Reason), + unresolved: formatUnresolvedLine(styles, registeredResourceKind, resource.Source.GetName(), resource.Target.Namespace, registeredResourceUnresolvedReason(resource)), + }) + } + + return summary +} + +func summarizeObligationTriggers(plan *MigrationPlan, commit bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Obligation Triggers", + include: includesScope(planScopes(plan), ScopeObligationTriggers), + } + if plan == nil { + return summary + } + + for _, trigger := range plan.ObligationTriggers { + if trigger == nil || trigger.Source == nil { + continue + } + if trigger.Target == nil { + appendTargetlessUnresolved(&summary, styles, obligationTriggerKind, trigger.Source.GetId(), unexpectedNilTargetReason(obligationTriggerKind)) + continue + } + + appendTargetStatusSummary(&summary, trigger.Target.Status, classifyCreateExecution(commit, trigger.Target.Execution), summaryLines{ + applied: formatObligationTriggerCreatedLine(styles, plan, trigger, true), + pending: formatObligationTriggerCreatedLine(styles, plan, trigger, false), + failed: formatFailedLine(styles, obligationTriggerKind, trigger.Source.GetId(), trigger.Target.Namespace, executionFailure(trigger.Target.Execution)), + skipped: formatSkippedLine(styles, obligationTriggerKind, trigger.Source.GetId(), trigger.Target.Namespace, trigger.Target.Reason), + unresolved: formatUnresolvedLine(styles, obligationTriggerKind, trigger.Source.GetId(), trigger.Target.Namespace, trigger.Target.Reason), + }) + } + + return summary +} + +func recordConstructTargetStatus(counts *summaryCounts, status TargetStatus) { + if status == TargetStatusExistingStandard { + counts.existingStandard++ + return + } + if status == TargetStatusAlreadyMigrated { + counts.alreadyMigrated++ + return + } + if status == TargetStatusSkipped { + counts.skipped++ + return + } + if status == TargetStatusUnresolved { + counts.unresolved++ + } +} + +func appendTargetStatusSummary(summary *constructSummary, status TargetStatus, createState operationExecutionState, lines summaryLines) { + switch status { + case TargetStatusCreate: + switch createState { + case operationExecutionStateApplied: + summary.counts.applied++ + summary.applied = append(summary.applied, lines.applied) + case operationExecutionStateFailed: + summary.counts.failed++ + summary.failed = append(summary.failed, lines.failed) + case operationExecutionStatePending: + summary.counts.pending++ + summary.pending = append(summary.pending, lines.pending) + } + case TargetStatusExistingStandard, TargetStatusAlreadyMigrated: + recordConstructTargetStatus(&summary.counts, status) + case TargetStatusSkipped: + recordConstructTargetStatus(&summary.counts, status) + summary.skipped = append(summary.skipped, lines.skipped) + case TargetStatusUnresolved: + recordConstructTargetStatus(&summary.counts, status) + summary.unresolved = append(summary.unresolved, lines.unresolved) + } +} + +func classifyCreateExecution(commit bool, execution *ExecutionResult) operationExecutionState { + if !commit || execution == nil { + return operationExecutionStatePending + } + if strings.TrimSpace(execution.Failure) != "" { + return operationExecutionStateFailed + } + if execution.Applied || strings.TrimSpace(execution.CreatedTargetID) != "" { + return operationExecutionStateApplied + } + return operationExecutionStatePending +} + +func formatCreatedLine(styles *migrations.DisplayStyles, kind, label string, namespace *policy.Namespace, targetID string, commit bool) string { + line := fmt.Sprintf( + "%s %s -> %s", + styles.Info().Render(kind), + styles.Name().Render(strconvQuote(label)), + styles.Namespace().Render(namespaceDisplay(namespace)), + ) + if commit && targetID != "" { + line = fmt.Sprintf("%s (id: %s)", line, styles.ID().Render(targetID)) + } + return line +} + +func formatFailedLine(styles *migrations.DisplayStyles, kind, label string, namespace *policy.Namespace, reason string) string { + line := fmt.Sprintf( + "%s %s -> %s", + styles.Info().Render(kind), + styles.Name().Render(strconvQuote(label)), + styles.Namespace().Render(namespaceDisplay(namespace)), + ) + if strings.TrimSpace(reason) == "" { + return line + } + return fmt.Sprintf("%s: %s", line, styles.Warning().Render(reason)) +} + +func formatSubjectConditionSetCreatedLine(styles *migrations.DisplayStyles, scs *SubjectConditionSetPlan, target *SubjectConditionSetTargetPlan, commit bool) string { + line := formatCreatedLine(styles, subjectConditionSetKind, scs.Source.GetId(), target.Namespace, target.TargetID(), commit) + return appendDetails(line, + fmt.Sprintf("subject_sets=%d", len(scs.Source.GetSubjectSets())), + ) +} + +func formatSubjectMappingCreatedLine(styles *migrations.DisplayStyles, plan *MigrationPlan, mapping *SubjectMappingPlan, commit bool) string { + line := formatCreatedLine(styles, subjectMappingKind, mapping.Source.GetId(), mapping.Target.Namespace, mapping.Target.TargetID(), commit) + return appendDetails(line, + "attribute_value="+styles.Namespace().Render(valueFQN(mapping.Source.GetAttributeValue())), + "actions="+actionNamesSummary(styles, plan, mapping.Target.ActionSourceIDs), + "scs_source="+styles.ID().Render(mapping.Target.SubjectConditionSetSourceID), + ) +} + +func formatRegisteredResourceCreatedLine(styles *migrations.DisplayStyles, plan *MigrationPlan, resource *RegisteredResourcePlan, commit bool) string { + line := formatCreatedLine(styles, registeredResourceKind, resource.Source.GetName(), resource.Target.Namespace, resource.Target.TargetID(), commit) + + return appendDetails(line, + "values="+registeredResourceValueFQNsSummary(styles, resource), + "action_bindings="+registeredResourceActionBindingsSummary(styles, plan, resource), + ) +} + +func formatRegisteredResourceFailedLine(styles *migrations.DisplayStyles, resource *RegisteredResourcePlan, reason string) string { + line := formatFailedLine(styles, registeredResourceKind, resource.Source.GetName(), resource.Target.Namespace, reason) + if failedValue := registeredResourceFailedValue(resource); failedValue != "" { + return appendDetails(line, "value="+styles.Namespace().Render(failedValue)) + } + return line +} + +func formatObligationTriggerCreatedLine(styles *migrations.DisplayStyles, plan *MigrationPlan, trigger *ObligationTriggerPlan, commit bool) string { + line := formatCreatedLine(styles, obligationTriggerKind, trigger.Source.GetId(), trigger.Target.Namespace, trigger.Target.TargetID(), commit) + return appendDetails(line, + "action="+actionNamesSummary(styles, plan, []string{trigger.Target.ActionSourceID}), + "attribute_value="+styles.Namespace().Render(valueFQN(trigger.Source.GetAttributeValue())), + "obligation_value="+styles.ID().Render(obligationValueIDOrFQN(trigger.Source.GetObligationValue())), + ) +} + +func formatUnresolvedLine(styles *migrations.DisplayStyles, kind, label string, namespace *policy.Namespace, reason string) string { + line := fmt.Sprintf( + "%s %s -> %s", + styles.Info().Render(kind), + styles.Name().Render(strconvQuote(label)), + styles.Namespace().Render(namespaceDisplay(namespace)), + ) + if strings.TrimSpace(reason) == "" { + return line + } + return fmt.Sprintf("%s: %s", line, styles.Warning().Render(reason)) +} + +func formatUnresolvedLineWithoutNamespace(styles *migrations.DisplayStyles, kind, label string, reason string) string { + line := fmt.Sprintf( + "%s %s", + styles.Info().Render(kind), + styles.Name().Render(strconvQuote(label)), + ) + if strings.TrimSpace(reason) == "" { + return line + } + return fmt.Sprintf("%s: %s", line, styles.Warning().Render(reason)) +} + +func formatSkippedLine(styles *migrations.DisplayStyles, kind, label string, namespace *policy.Namespace, reason string) string { + line := fmt.Sprintf( + "%s %s -> %s", + styles.Info().Render(kind), + styles.Name().Render(strconvQuote(label)), + styles.Namespace().Render(namespaceDisplay(namespace)), + ) + if strings.TrimSpace(reason) == "" { + return line + } + return fmt.Sprintf("%s: %s", line, styles.Warning().Render(reason)) +} + +func registeredResourceUnresolvedReason(resource *RegisteredResourcePlan) string { + if resource == nil || resource.Target == nil { + return "" + } + if resource.Target.Reason != "" { + return resource.Target.Reason + } + return resource.Unresolved +} + +func classifyRegisteredResourceExecution(commit bool, target *RegisteredResourceTargetPlan) (operationExecutionState, string) { + if !commit { + return operationExecutionStatePending, "" + } + if target.Execution != nil && strings.TrimSpace(target.Execution.Failure) != "" { + return operationExecutionStateFailed, target.Execution.Failure + } + if target.Execution == nil || (!target.Execution.Applied && strings.TrimSpace(target.Execution.CreatedTargetID) == "") { + return operationExecutionStatePending, "" + } + + pendingValues := false + for _, valuePlan := range target.Values { + if valuePlan == nil { + continue + } + if valuePlan.Execution != nil && strings.TrimSpace(valuePlan.Execution.Failure) != "" { + return operationExecutionStateFailed, valuePlan.Execution.Failure + } + if valuePlan.Execution == nil || (!valuePlan.Execution.Applied && strings.TrimSpace(valuePlan.Execution.CreatedTargetID) == "") { + pendingValues = true + } + } + if pendingValues { + return operationExecutionStatePending, "" + } + return operationExecutionStateApplied, "" +} + +func appendTargetlessUnresolved(summary *constructSummary, styles *migrations.DisplayStyles, kind, label, reason string) { + if summary == nil { + return + } + summary.counts.unresolved++ + summary.unresolved = append(summary.unresolved, formatUnresolvedLineWithoutNamespace(styles, kind, label, reason)) +} + +func registeredResourceValueFQNsSummary(styles *migrations.DisplayStyles, resource *RegisteredResourcePlan) string { + values := make([]string, 0, len(resource.Target.Values)) + seen := make(map[string]struct{}, len(resource.Target.Values)) + for _, valuePlan := range resource.Target.Values { + fqn := registeredResourceValueFQN(valuePlan) + if strings.TrimSpace(fqn) == "" { + continue + } + if _, ok := seen[fqn]; ok { + continue + } + seen[fqn] = struct{}{} + values = append(values, styles.Namespace().Render(fqn)) + } + return strings.Join(values, ", ") +} + +func registeredResourceFailedValue(resource *RegisteredResourcePlan) string { + if resource == nil || resource.Target == nil { + return "" + } + for _, valuePlan := range resource.Target.Values { + if valuePlan == nil || valuePlan.Execution == nil || strings.TrimSpace(valuePlan.Execution.Failure) == "" { + continue + } + return registeredResourceValueFQN(valuePlan) + } + return "" +} + +func registeredResourceActionBindingsSummary(styles *migrations.DisplayStyles, plan *MigrationPlan, resource *RegisteredResourcePlan) string { + bindings := make([]string, 0) + seen := make(map[string]struct{}) + for _, valuePlan := range resource.Target.Values { + if valuePlan == nil { + continue + } + for _, binding := range valuePlan.ActionBindings { + if binding == nil { + continue + } + actionName := actionNameBySourceID(plan, binding.SourceActionID) + if actionName == "" { + actionName = binding.SourceActionID + } + attrValue := valueFQN(binding.AttributeValue) + label := fmt.Sprintf( + "%s -> %s", + styles.Name().Render(strconvQuote(actionName)), + styles.Namespace().Render(attrValue), + ) + if _, ok := seen[label]; ok { + continue + } + seen[label] = struct{}{} + bindings = append(bindings, label) + } + } + return strings.Join(bindings, ", ") +} + +func actionNamesSummary(styles *migrations.DisplayStyles, plan *MigrationPlan, sourceIDs []string) string { + names := make([]string, 0, len(sourceIDs)) + seen := make(map[string]struct{}, len(sourceIDs)) + for _, sourceID := range sourceIDs { + if strings.TrimSpace(sourceID) == "" { + continue + } + name := actionNameBySourceID(plan, sourceID) + if name == "" { + name = sourceID + } + if _, ok := seen[name]; ok { + continue + } + seen[name] = struct{}{} + names = append(names, styles.Name().Render(strconvQuote(name))) + } + if len(names) == 0 { + return "" + } + return strings.Join(names, ", ") +} + +func actionNameBySourceID(plan *MigrationPlan, sourceID string) string { + if plan == nil { + return "" + } + for _, action := range plan.Actions { + if action == nil || action.Source == nil { + continue + } + if action.Source.GetId() == sourceID { + return action.Source.GetName() + } + } + return "" +} + +func unresolvedRegisteredResourceReason(resource *RegisteredResourcePlan) string { + if resource == nil { + return "" + } + return strings.TrimSpace(resource.Unresolved) +} + +func unexpectedNilTargetReason(kind string) string { + return fmt.Sprintf(unexpectedNilTargetReasonFormat, kind) +} + +func registeredResourceValueFQN(valuePlan *RegisteredResourceValuePlan) string { + if valuePlan == nil || valuePlan.Source == nil { + return "" + } + resource := valuePlan.Source.GetResource() + if resource == nil { + return valuePlan.Source.GetValue() + } + + namespace := "" + if resource.GetNamespace() != nil { + namespace = strings.TrimPrefix(strings.TrimSpace(resource.GetNamespace().GetFqn()), "https://") + } + + return (&identifier.FullyQualifiedRegisteredResourceValue{ + Namespace: namespace, + Name: resource.GetName(), + Value: valuePlan.Source.GetValue(), + }).FQN() +} + +func namespaceDisplay(namespace *policy.Namespace) string { + if namespace == nil { + return "(global)" + } + if fqn := strings.TrimSpace(namespace.GetFqn()); fqn != "" { + return fqn + } + if name := strings.TrimSpace(namespace.GetName()); name != "" { + return name + } + if id := strings.TrimSpace(namespace.GetId()); id != "" { + return id + } + return "(unknown namespace)" +} diff --git a/otdfctl/migrations/namespacedpolicy/migration_summary_test.go b/otdfctl/migrations/namespacedpolicy/migration_summary_test.go new file mode 100644 index 0000000000..ca49af8572 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/migration_summary_test.go @@ -0,0 +1,338 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" +) + +func TestRenderNamespacedPolicySummaryCommitIncludesCountsAndCreatedDetails(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + otherNamespace := &policy.Namespace{Id: "ns-2", Fqn: "https://example.org"} + classificationValue := testAttributeValue("https://example.com/attr/classification/value/secret", namespace) + + plan := &MigrationPlan{ + Scopes: []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeRegisteredResources, + ScopeObligationTriggers, + }, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-create", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{CreatedTargetID: "created-action-1"}, + }, + }, + }, + { + Source: &policy.Action{Id: "action-skip", Name: "download"}, + Targets: []*ActionTargetPlan{ + { + Namespace: otherNamespace, + Status: TargetStatusSkipped, + Reason: skippedByUserReason, + }, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{ + Id: "scs-1", + SubjectSets: []*policy.SubjectSet{ + {}, + {}, + }, + }, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{CreatedTargetID: "created-scs-1"}, + }, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: classificationValue, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{CreatedTargetID: "created-mapping-1"}, + ActionSourceIDs: []string{"action-create"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue("action-create", "decrypt", classificationValue), + ), + ), + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{CreatedTargetID: "created-resource-1"}, + Values: []*RegisteredResourceValuePlan{ + { + Source: &policy.RegisteredResourceValue{ + Value: "prod", + Resource: &policy.RegisteredResource{ + Name: "documents", + Namespace: namespace, + }, + }, + Execution: &ExecutionResult{CreatedTargetID: "created-resource-value-1"}, + ActionBindings: []*RegisteredResourceActionBinding{ + { + SourceActionID: "action-create", + AttributeValue: classificationValue, + }, + }, + }, + }, + }, + }, + { + Source: testRegisteredResource("resource-2", "finance"), + Unresolved: "conflicting namespaces", + }, + }, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-create", Name: "decrypt"}, + AttributeValue: classificationValue, + ObligationValue: &policy.ObligationValue{ + Fqn: "https://example.com/obligation/log/value/default", + }, + }, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{CreatedTargetID: "created-trigger-1"}, + ActionSourceID: "action-create", + }, + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicySummaryWithResult(plan, true, "success")) + + assert.Contains(t, summary, "Namespaced Policy Migration Committed") + assert.Contains(t, summary, "Scopes: actions, subject-condition-sets, subject-mappings, registered-resources, obligation-triggers") + assert.Contains(t, summary, "Commit: true") + assert.Contains(t, summary, "Result: success") + assert.Contains(t, summary, "Actions") + assert.Contains(t, summary, "Counts: created=1 existing_standard=0 already_migrated=0 skipped=1 failed=0 unresolved=0") + assert.Contains(t, summary, `action "decrypt" -> https://example.com (id: created-action-1)`) + assert.Contains(t, summary, `action "download" -> https://example.org: skipped by user`) + assert.Contains(t, summary, "Subject Condition Sets") + assert.Contains(t, summary, `subject condition set "scs-1" -> https://example.com (id: created-scs-1) (subject_sets=2)`) + assert.Contains(t, summary, "Subject Mappings") + assert.Contains(t, summary, `subject mapping "mapping-1" -> https://example.com (id: created-mapping-1) (attribute_value=https://example.com/attr/classification/value/secret, actions="decrypt", scs_source=scs-1)`) + assert.Contains(t, summary, "Registered Resources") + assert.Contains(t, summary, `registered resource "documents" -> https://example.com (id: created-resource-1) (values=https://example.com/reg_res/documents/value/prod, action_bindings="decrypt" -> https://example.com/attr/classification/value/secret)`) + assert.Contains(t, summary, `registered resource "finance": conflicting namespaces`) + assert.Contains(t, summary, "Obligation Triggers") + assert.Contains(t, summary, `obligation trigger "trigger-1" -> https://example.com (id: created-trigger-1) (action="decrypt", attribute_value=https://example.com/attr/classification/value/secret, obligation_value=https://example.com/obligation/log/value/default)`) + assert.Contains(t, summary, "Created") + assert.Contains(t, summary, "Skipped") + assert.Contains(t, summary, "Unresolved") +} + +func TestRenderNamespacedPolicySummaryDryRunUsesToCreateLabel(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + plan := &MigrationPlan{ + Scopes: []Scope{ScopeActions}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{CreatedTargetID: "created-action-1"}, + }, + }, + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicySummary(plan, false)) + + assert.Contains(t, summary, "Namespaced Policy Migration Plan") + assert.Contains(t, summary, "Commit: false") + assert.Contains(t, summary, "Result: success") + assert.Contains(t, summary, "Counts: to_create=1 existing_standard=0 already_migrated=0 skipped=0 failed=0 unresolved=0") + assert.Contains(t, summary, "Will Create") + assert.NotContains(t, summary, "\nCreated\n") + assert.Contains(t, summary, `action "decrypt" -> https://example.com`) + assert.NotContains(t, summary, "(id: created-action-1)") +} + +func TestRenderNamespacedPolicySummaryIncludesTargetlessUnresolvedEntries(t *testing.T) { + t.Parallel() + + plan := &MigrationPlan{ + Scopes: []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeObligationTriggers, + }, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + nil, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + nil, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{Id: "mapping-1"}, + }, + }, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{Id: "trigger-1"}, + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicySummaryWithResult(plan, true, "success")) + + assert.Contains(t, summary, "Actions") + assert.Contains(t, summary, "Counts: created=0 existing_standard=0 already_migrated=0 skipped=0 failed=0 unresolved=1") + assert.Contains(t, summary, `action "decrypt": received unexpected nil target for action`) + assert.Contains(t, summary, "Subject Condition Sets") + assert.Contains(t, summary, "Counts: created=0 existing_standard=0 already_migrated=0 skipped=0 failed=0 unresolved=1") + assert.Contains(t, summary, `subject condition set "scs-1": received unexpected nil target for subject condition set`) + assert.Contains(t, summary, "Subject Mappings") + assert.Contains(t, summary, "Counts: created=0 existing_standard=0 already_migrated=0 skipped=0 failed=0 unresolved=1") + assert.Contains(t, summary, `subject mapping "mapping-1": received unexpected nil target for subject mapping`) + assert.Contains(t, summary, "Obligation Triggers") + assert.Contains(t, summary, "Counts: created=0 existing_standard=0 already_migrated=0 skipped=0 failed=0 unresolved=1") + assert.Contains(t, summary, `obligation trigger "trigger-1": received unexpected nil target for obligation trigger`) +} + +func TestRenderNamespacedPolicySummaryCommitFailureShowsFailedAndPendingCreates(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + + plan := &MigrationPlan{ + Scopes: []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeRegisteredResources, + }, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-created", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{Applied: true, CreatedTargetID: "created-action-1"}, + }, + }, + }, + { + Source: &policy.Action{Id: "action-failed", Name: "download"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{Failure: "boom"}, + }, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-pending"}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace, + Status: TargetStatusCreate, + }, + }, + }, + }, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue("prod"), + ), + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace, + Status: TargetStatusCreate, + Execution: &ExecutionResult{Applied: true, CreatedTargetID: "created-resource-1"}, + Values: []*RegisteredResourceValuePlan{ + { + Source: &policy.RegisteredResourceValue{ + Id: "rrv-1", + Value: "prod", + Resource: &policy.RegisteredResource{ + Name: "documents", + Namespace: namespace, + }, + }, + Execution: &ExecutionResult{Failure: "value boom"}, + }, + }, + }, + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicySummaryWithResult(plan, true, "failure")) + + assert.Contains(t, summary, "Result: failure") + assert.Contains(t, summary, "Actions") + assert.Contains(t, summary, "Counts: created=1 existing_standard=0 already_migrated=0 skipped=0 failed=1 unresolved=0") + assert.Contains(t, summary, "Created") + assert.Contains(t, summary, `action "decrypt" -> https://example.com (id: created-action-1)`) + assert.Contains(t, summary, "Failed") + assert.Contains(t, summary, `action "download" -> https://example.com: boom`) + assert.Contains(t, summary, "Subject Condition Sets") + assert.Contains(t, summary, "Counts: created=0 to_create=1 existing_standard=0 already_migrated=0 skipped=0 failed=0 unresolved=0") + assert.Contains(t, summary, "Will Create") + assert.Contains(t, summary, `subject condition set "scs-pending" -> https://example.com (subject_sets=0)`) + assert.Contains(t, summary, "Registered Resources") + assert.Contains(t, summary, "Counts: created=0 existing_standard=0 already_migrated=0 skipped=0 failed=1 unresolved=0") + assert.Contains(t, summary, `registered resource "documents" -> https://example.com: value boom (value=https://example.com/reg_res/documents/value/prod)`) +} diff --git a/otdfctl/migrations/namespacedpolicy/obligation_triggers_execute.go b/otdfctl/migrations/namespacedpolicy/obligation_triggers_execute.go new file mode 100644 index 0000000000..773325f851 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/obligation_triggers_execute.go @@ -0,0 +1,135 @@ +package namespacedpolicy + +import ( + "context" + "fmt" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" +) + +func (e *MigrationExecutor) executeObligationTriggers(ctx context.Context, plans []*ObligationTriggerPlan) error { + if len(plans) == 0 { + return nil + } + + for _, triggerPlan := range plans { + if triggerPlan == nil || triggerPlan.Source == nil { + continue + } + + if triggerPlan.Target == nil { + continue + } + + if err := e.executeObligationTriggerTarget(ctx, triggerPlan, triggerPlan.Target); err != nil { + return err + } + } + + return nil +} + +func (e *MigrationExecutor) executeObligationTriggerTarget(ctx context.Context, triggerPlan *ObligationTriggerPlan, target *ObligationTriggerTargetPlan) error { + //nolint:exhaustive // Obligation-trigger execution only handles create and already-migrated explicitly; all other statuses are unsupported. + switch target.Status { + case TargetStatusAlreadyMigrated: + if target.TargetID() == "" { + return fmt.Errorf("%w: obligation trigger %q target %q", ErrMissingMigratedTarget, triggerPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + return nil + case TargetStatusSkipped: + return nil + case TargetStatusCreate: + return e.createObligationTriggerTarget(ctx, triggerPlan, target) + case TargetStatusUnresolved: + return nil + default: + return fmt.Errorf("%w: obligation trigger %q target %q has unsupported status %q", ErrUnsupportedStatus, triggerPlan.Source.GetId(), namespaceLabel(target.Namespace), target.Status) + } +} + +func (e *MigrationExecutor) createObligationTriggerTarget(ctx context.Context, triggerPlan *ObligationTriggerPlan, target *ObligationTriggerTargetPlan) error { + actionID, err := e.requireActionTargetID(target.ActionSourceID, target.Namespace, triggerPlan.Source.GetId()) + if err != nil { + return err + } + + created, err := e.handler.CreateObligationTrigger( + ctx, + valueIDOrFQN(triggerPlan.Source.GetAttributeValue()), + actionID, + obligationValueIDOrFQN(triggerPlan.Source.GetObligationValue()), + triggerClientID(triggerPlan.Source.GetContext()), + metadataForCreate( + triggerPlan.Source.GetId(), + metadataLabels(triggerPlan.Source.GetMetadata()), + ), + ) + if err != nil { + target.Execution = &ExecutionResult{ + Failure: err.Error(), + } + return fmt.Errorf("create obligation trigger %q in namespace %q: %w", triggerPlan.Source.GetId(), namespaceLabel(target.Namespace), err) + } + if created.GetId() == "" { + target.Execution = &ExecutionResult{ + Failure: ErrMissingCreatedTargetID.Error(), + } + return fmt.Errorf("%w: obligation trigger %q target %q", ErrMissingCreatedTargetID, triggerPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + target.Execution = &ExecutionResult{ + Applied: true, + CreatedTargetID: created.GetId(), + } + + return nil +} + +// TODO: Eventually make this generic when we merge sm / rr +func (e *MigrationExecutor) requireActionTargetID(sourceID string, targetNamespace *policy.Namespace, ownerID string) (string, error) { + if sourceID == "" { + return "", fmt.Errorf("%w: obligation trigger %q action source id is missing", ErrMissingMigratedTarget, ownerID) + } + + actionID := e.cachedActionTargetID(sourceID, targetNamespace) + if actionID != "" { + return actionID, nil + } + + return "", fmt.Errorf("%w: obligation trigger %q action %q target %q", ErrMissingMigratedTarget, ownerID, sourceID, namespaceLabel(targetNamespace)) +} + +func valueIDOrFQN(value *policy.Value) string { + if value == nil { + return "" + } + if fqn := strings.TrimSpace(value.GetFqn()); fqn != "" { + return fqn + } + return strings.TrimSpace(value.GetId()) +} + +func obligationValueIDOrFQN(value *policy.ObligationValue) string { + if value == nil { + return "" + } + if fqn := strings.TrimSpace(value.GetFqn()); fqn != "" { + return fqn + } + return strings.TrimSpace(value.GetId()) +} + +func triggerClientID(contexts []*policy.RequestContext) string { + for _, requestContext := range contexts { + if requestContext == nil || requestContext.GetPep() == nil { + continue + } + if clientID := strings.TrimSpace(requestContext.GetPep().GetClientId()); clientID != "" { + return clientID + } + } + + return "" +} diff --git a/otdfctl/migrations/namespacedpolicy/obligation_triggers_execute_test.go b/otdfctl/migrations/namespacedpolicy/obligation_triggers_execute_test.go new file mode 100644 index 0000000000..90cde505cc --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/obligation_triggers_execute_test.go @@ -0,0 +1,392 @@ +package namespacedpolicy + +import ( + "errors" + "testing" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExecuteObligationTriggers(t *testing.T) { + t.Parallel() + + namespace1 := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + namespace2 := &policy.Namespace{Id: "ns-2", Fqn: "https://example.net"} + errBoom := errors.New("boom") + + tests := []struct { + name string + plan *MigrationPlan + handler *mockExecutorHandler + wantErr *expectedError + assert func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) + }{ + { + name: "handles created and already migrated obligation trigger targets", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeObligationTriggers}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + { + Namespace: namespace2, + Status: TargetStatusExistingStandard, + ExistingID: "existing-standard-action", + }, + }, + }, + }, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-1"}, + AttributeValue: &policy.Value{ + Id: "attribute-value-1", + Fqn: "https://example.com/attr/department/value/eng", + }, + ObligationValue: &policy.ObligationValue{ + Id: "obligation-value-1", + Fqn: "https://example.com/obligation/log/value/default", + }, + Context: []*policy.RequestContext{ + {Pep: &policy.PolicyEnforcementPoint{ClientId: "client-a"}}, + }, + Metadata: &common.Metadata{ + Labels: map[string]string{ + "owner": "policy-team", + "env": "dev", + }, + }, + }, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceID: "action-1", + }, + }, + { + Source: &policy.ObligationTrigger{ + Id: "trigger-2", + Action: &policy.Action{Id: "action-1"}, + }, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace2, + Status: TargetStatusAlreadyMigrated, + ExistingID: "migrated-trigger-2", + ActionSourceID: "action-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{ + results: map[string]map[string]*policy.Action{ + "decrypt": { + "ns-1": {Id: "created-action-1", Name: "decrypt"}, + }, + }, + obligationTriggerResult: map[string]map[string]*policy.ObligationTrigger{ + "trigger-1": { + "created-action-1": {Id: "created-trigger-1"}, + }, + }, + }, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + require.Contains(t, handler.createdObligationTriggers, "trigger-1") + require.Contains(t, handler.createdObligationTriggers["trigger-1"], "created-action-1") + + createdCall := handler.createdObligationTriggers["trigger-1"]["created-action-1"] + assert.Equal(t, "https://example.com/attr/department/value/eng", createdCall.AttributeValue) + assert.Equal(t, "created-action-1", createdCall.Action) + assert.Equal(t, "https://example.com/obligation/log/value/default", createdCall.ObligationValue) + assert.Equal(t, "client-a", createdCall.ClientID) + assert.Equal(t, map[string]string{ + "owner": "policy-team", + "env": "dev", + migrationLabelMigratedFrom: "trigger-1", + }, createdCall.Metadata.GetLabels()) + + createdTarget := plan.ObligationTriggers[0].Target + require.NotNil(t, createdTarget.Execution) + assert.True(t, createdTarget.Execution.Applied) + assert.Equal(t, "created-trigger-1", createdTarget.Execution.CreatedTargetID) + assert.Equal(t, "created-trigger-1", createdTarget.TargetID()) + + migratedTarget := plan.ObligationTriggers[1].Target + assert.Equal(t, "migrated-trigger-2", migratedTarget.TargetID()) + assert.Nil(t, migratedTarget.Execution) + }, + }, + { + name: "skips skipped obligation trigger targets", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeObligationTriggers}, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{Id: "trigger-1"}, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace1, + Status: TargetStatusSkipped, + Reason: skippedByUserReason, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Empty(t, handler.createdObligationTriggers) + assert.Nil(t, plan.ObligationTriggers[0].Target.Execution) + }, + }, + { + name: "ignores unresolved target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeObligationTriggers}, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{Id: "trigger-1"}, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace1, + Status: TargetStatusUnresolved, + Reason: "missing target namespace mapping", + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Empty(t, handler.createdObligationTriggers) + }, + }, + { + name: "returns error when migrated action target is unavailable", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeObligationTriggers}, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-1"}, + AttributeValue: &policy.Value{Id: "attribute-value-1"}, + ObligationValue: &policy.ObligationValue{Id: "obligation-value-1"}, + }, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceID: "action-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrMissingMigratedTarget, `obligation trigger %q action %q target %q`, "trigger-1", "action-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdObligationTriggers) + assert.Nil(t, plan.ObligationTriggers[0].Target.Execution) + }, + }, + { + name: "returns error for missing already migrated trigger id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeObligationTriggers}, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{Id: "trigger-1"}, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrMissingMigratedTarget, `obligation trigger %q target %q`, "trigger-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdObligationTriggers) + }, + }, + { + name: "returns error for missing created target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeObligationTriggers}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-1"}, + AttributeValue: &policy.Value{Id: "attribute-value-1"}, + ObligationValue: &policy.ObligationValue{Id: "obligation-value-1"}, + }, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceID: "action-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{ + results: map[string]map[string]*policy.Action{ + "decrypt": { + "ns-1": {Id: "created-action-1", Name: "decrypt"}, + }, + }, + obligationTriggerResult: map[string]map[string]*policy.ObligationTrigger{ + "trigger-1": { + "created-action-1": {}, + }, + }, + }, + wantErr: wantError(ErrMissingCreatedTargetID, `obligation trigger %q target %q`, "trigger-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.createdObligationTriggers, "trigger-1") + require.NotNil(t, plan.ObligationTriggers[0].Target.Execution) + assert.Equal(t, ErrMissingCreatedTargetID.Error(), plan.ObligationTriggers[0].Target.Execution.Failure) + }, + }, + { + name: "records create failure from handler", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeObligationTriggers}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-1"}, + AttributeValue: &policy.Value{Id: "attribute-value-1"}, + ObligationValue: &policy.ObligationValue{Id: "obligation-value-1"}, + }, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceID: "action-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{ + results: map[string]map[string]*policy.Action{ + "decrypt": { + "ns-1": {Id: "created-action-1", Name: "decrypt"}, + }, + }, + obligationTriggerErrs: map[string]map[string]error{ + "trigger-1": { + "created-action-1": errBoom, + }, + }, + }, + wantErr: &expectedError{ + is: errBoom, + message: `create obligation trigger "trigger-1" in namespace "https://example.com": boom`, + }, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.createdObligationTriggers, "trigger-1") + require.NotNil(t, plan.ObligationTriggers[0].Target.Execution) + assert.Equal(t, "boom", plan.ObligationTriggers[0].Target.Execution.Failure) + }, + }, + { + name: "returns error for unsupported target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeObligationTriggers}, + ObligationTriggers: []*ObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{Id: "trigger-1"}, + Target: &ObligationTriggerTargetPlan{ + Namespace: namespace1, + Status: TargetStatus("bogus"), + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError( + ErrUnsupportedStatus, + `obligation trigger %q target %q has unsupported status %q`, + "trigger-1", + namespace1.GetFqn(), + TargetStatus("bogus"), + ), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdObligationTriggers) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + executor, err := NewMigrationExecutor(tt.handler) + require.NoError(t, err) + + err = executor.ExecuteMigration(t.Context(), tt.plan) + switch { + case tt.wantErr != nil: + require.Error(t, err) + require.ErrorIs(t, err, tt.wantErr.is) + require.EqualError(t, err, tt.wantErr.message) + default: + require.NoError(t, err) + } + + tt.assert(t, err, executor, tt.handler, tt.plan) + }) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/plan_utils.go b/otdfctl/migrations/namespacedpolicy/plan_utils.go new file mode 100644 index 0000000000..c538013447 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/plan_utils.go @@ -0,0 +1,33 @@ +package namespacedpolicy + +import ( + "strings" + + "github.com/opentdf/platform/protocol/go/policy" +) + +func objectIDSet[T interface{ GetId() string }](items []T) map[string]struct{} { + ids := make(map[string]struct{}, len(items)) + for _, item := range items { + if id := item.GetId(); id != "" { + ids[id] = struct{}{} + } + } + return ids +} + +func isStandardAction(action *policy.Action) bool { + if action == nil { + return false + } + if action.GetStandard() != policy.Action_STANDARD_ACTION_UNSPECIFIED { + return true + } + + switch strings.ToLower(strings.TrimSpace(action.GetName())) { + case "create", "read", "update", "delete": + return true + default: + return false + } +} diff --git a/otdfctl/migrations/namespacedpolicy/policy_formatting.go b/otdfctl/migrations/namespacedpolicy/policy_formatting.go new file mode 100644 index 0000000000..06370e35e4 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/policy_formatting.go @@ -0,0 +1,245 @@ +package namespacedpolicy + +import ( + "fmt" + "strings" + + "github.com/opentdf/platform/otdfctl/migrations" + "github.com/opentdf/platform/protocol/go/policy" +) + +const ( + actionKind = "action" + subjectConditionSetKind = "subject condition set" + subjectMappingKind = "subject mapping" + registeredResourceKind = "registered resource" + obligationTriggerKind = "obligation trigger" +) + +func actionLabel(action *policy.Action) string { + if action == nil { + return unknownLabel + } + if name := strings.TrimSpace(action.GetName()); name != "" { + return name + } + if id := strings.TrimSpace(action.GetId()); id != "" { + return id + } + return unknownLabel +} + +func plainPolicyActionNamesSummary(actions []*policy.Action) string { + names := make([]string, 0, len(actions)) + seen := make(map[string]struct{}, len(actions)) + for _, action := range actions { + if action == nil { + continue + } + name := actionLabel(action) + if strings.TrimSpace(name) == "" || name == unknownLabel { + continue + } + if _, ok := seen[name]; ok { + continue + } + seen[name] = struct{}{} + names = append(names, strconvQuote(name)) + } + return plainListSummary(names) +} + +func styledPolicyActionNamesSummary(styles *migrations.DisplayStyles, actions []*policy.Action) string { + names := make([]string, 0, len(actions)) + seen := make(map[string]struct{}, len(actions)) + for _, action := range actions { + if action == nil { + continue + } + name := actionLabel(action) + if strings.TrimSpace(name) == "" || name == unknownLabel { + continue + } + if _, ok := seen[name]; ok { + continue + } + seen[name] = struct{}{} + names = append(names, styles.Name().Render(strconvQuote(name))) + } + return strings.Join(names, ", ") +} + +func plainRegisteredResourceSourceSummary(resource *policy.RegisteredResource) string { + return appendDetails( + "values="+plainRegisteredResourceValuesSummary(resource), + "action_bindings="+plainRegisteredResourceActionAttributeValuesSummary(resource), + ) +} + +func styledRegisteredResourceSourceSummary(styles *migrations.DisplayStyles, resource *policy.RegisteredResource) string { + return appendDetails( + "values="+styledRegisteredResourceValuesSummary(styles, resource), + "action_bindings="+styledRegisteredResourceActionAttributeValuesSummary(styles, resource), + ) +} + +func appendDetails(line string, details ...string) string { + filtered := make([]string, 0, len(details)) + for _, detail := range details { + if strings.TrimSpace(detail) != "" { + filtered = append(filtered, detail) + } + } + if len(filtered) == 0 { + return line + } + return fmt.Sprintf("%s (%s)", line, strings.Join(filtered, ", ")) +} + +func strconvQuote(value string) string { + return fmt.Sprintf("%q", value) +} + +func valueFQN(value *policy.Value) string { + if value == nil { + return "" + } + if value.GetFqn() != "" { + return value.GetFqn() + } + return value.GetId() +} + +func plainRegisteredResourceValuesSummary(resource *policy.RegisteredResource) string { + values := make([]string, 0, len(resource.GetValues())) + seen := make(map[string]struct{}, len(resource.GetValues())) + for _, value := range resource.GetValues() { + if value == nil { + continue + } + label := strings.TrimSpace(value.GetValue()) + if label == "" { + label = strings.TrimSpace(value.GetId()) + } + if label == "" { + continue + } + if _, ok := seen[label]; ok { + continue + } + seen[label] = struct{}{} + values = append(values, strconvQuote(label)) + } + return plainListSummary(values) +} + +func styledRegisteredResourceValuesSummary(styles *migrations.DisplayStyles, resource *policy.RegisteredResource) string { + values := make([]string, 0, len(resource.GetValues())) + seen := make(map[string]struct{}, len(resource.GetValues())) + for _, value := range resource.GetValues() { + if value == nil { + continue + } + label := strings.TrimSpace(value.GetValue()) + if label == "" { + label = strings.TrimSpace(value.GetId()) + } + if label == "" { + continue + } + if _, ok := seen[label]; ok { + continue + } + seen[label] = struct{}{} + values = append(values, styles.Name().Render(strconvQuote(label))) + } + return strings.Join(values, ", ") +} + +func plainRegisteredResourceActionAttributeValuesSummary(resource *policy.RegisteredResource) string { + bindings := make([]string, 0) + seen := make(map[string]struct{}) + for _, value := range resource.GetValues() { + if value == nil { + continue + } + for _, binding := range value.GetActionAttributeValues() { + if binding == nil { + continue + } + label := fmt.Sprintf("%s -> %s", strconvQuote(actionLabel(binding.GetAction())), valueFQN(binding.GetAttributeValue())) + if _, ok := seen[label]; ok { + continue + } + seen[label] = struct{}{} + bindings = append(bindings, label) + } + } + return plainListSummary(bindings) +} + +func styledRegisteredResourceActionAttributeValuesSummary(styles *migrations.DisplayStyles, resource *policy.RegisteredResource) string { + bindings := make([]string, 0) + seen := make(map[string]struct{}) + for _, value := range resource.GetValues() { + if value == nil { + continue + } + for _, binding := range value.GetActionAttributeValues() { + if binding == nil { + continue + } + label := fmt.Sprintf( + "%s -> %s", + styles.Name().Render(strconvQuote(actionLabel(binding.GetAction()))), + styles.Namespace().Render(valueFQN(binding.GetAttributeValue())), + ) + if _, ok := seen[label]; ok { + continue + } + seen[label] = struct{}{} + bindings = append(bindings, label) + } + } + return strings.Join(bindings, ", ") +} + +func obligationLabel(obligation *policy.Obligation) string { + if obligation == nil { + return noneLabel + } + if fqn := strings.TrimSpace(obligation.GetFqn()); fqn != "" { + return fqn + } + if name := strings.TrimSpace(obligation.GetName()); name != "" { + return name + } + if id := strings.TrimSpace(obligation.GetId()); id != "" { + return id + } + return noneLabel +} + +func plainRequestContextsSummary(contexts []*policy.RequestContext) string { + clientIDs := make([]string, 0, len(contexts)) + seen := make(map[string]struct{}, len(contexts)) + for _, requestContext := range contexts { + clientID := strings.TrimSpace(requestContext.GetPep().GetClientId()) + if clientID == "" { + continue + } + if _, ok := seen[clientID]; ok { + continue + } + seen[clientID] = struct{}{} + clientIDs = append(clientIDs, "client_id: "+strconvQuote(clientID)) + } + return plainListSummary(clientIDs) +} + +func plainListSummary(items []string) string { + if len(items) == 0 { + return noneLabel + } + return strings.Join(items, ", ") +} diff --git a/otdfctl/migrations/namespacedpolicy/policy_formatting_test.go b/otdfctl/migrations/namespacedpolicy/policy_formatting_test.go new file mode 100644 index 0000000000..98d1d3bb81 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/policy_formatting_test.go @@ -0,0 +1,176 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" +) + +func TestPlainPolicyActionNamesSummary(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + actions []*policy.Action + want string + }{ + {name: "empty", want: noneLabel}, + { + name: "skips nil and deduplicates labels", + actions: []*policy.Action{ + nil, + {Id: "action-1", Name: "read"}, + {Id: "action-2", Name: "write"}, + {Id: "action-3", Name: "read"}, + }, + want: `"read", "write"`, + }, + { + name: "falls back to id", + actions: []*policy.Action{ + {Id: "action-1"}, + }, + want: `"action-1"`, + }, + { + name: "ignores empty labels", + actions: []*policy.Action{ + {}, + }, + want: noneLabel, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, plainPolicyActionNamesSummary(tc.actions)) + }) + } +} + +func TestPlainRegisteredResourceSourceSummary(t *testing.T) { + t.Parallel() + + secretValue := testAttributeValue("https://example.com/attr/classification/value/secret", testNamespace("https://example.com")) + + tests := []struct { + name string + resource *policy.RegisteredResource + want string + }{ + {name: "nil", want: "values=(none) (action_bindings=(none))"}, + { + name: "empty values", + resource: &policy.RegisteredResource{}, + want: "values=(none) (action_bindings=(none))", + }, + { + name: "deduplicates values and bindings", + resource: testRegisteredResource( + "resource-1", + "dataset", + testRegisteredResourceValue("prod", testActionAttributeValue("action-1", "read", secretValue)), + testRegisteredResourceValue("prod", testActionAttributeValue("action-1", "read", secretValue)), + testRegisteredResourceValue("dev", testActionAttributeValue("action-2", "write", secretValue)), + ), + want: `values="prod", "dev" (action_bindings="read" -> https://example.com/attr/classification/value/secret, "write" -> https://example.com/attr/classification/value/secret)`, + }, + { + name: "value falls back to id", + resource: testRegisteredResource( + "resource-1", + "dataset", + &policy.RegisteredResourceValue{Id: "value-1"}, + ), + want: `values="value-1" (action_bindings=(none))`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, plainRegisteredResourceSourceSummary(tc.resource)) + }) + } +} + +func TestObligationLabel(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + obligation *policy.Obligation + want string + }{ + {name: "nil", want: noneLabel}, + { + name: "uses fqn first", + obligation: &policy.Obligation{ + Id: "obligation-1", + Name: "watermark", + Fqn: "https://example.com/obl/watermark", + }, + want: "https://example.com/obl/watermark", + }, + { + name: "falls back to name", + obligation: &policy.Obligation{ + Id: "obligation-1", + Name: "watermark", + }, + want: "watermark", + }, + { + name: "falls back to id", + obligation: &policy.Obligation{ + Id: "obligation-1", + }, + want: "obligation-1", + }, + { + name: "empty", + obligation: &policy.Obligation{}, + want: noneLabel, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, obligationLabel(tc.obligation)) + }) + } +} + +func TestPlainRequestContextsSummary(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + contexts []*policy.RequestContext + want string + }{ + {name: "empty", want: noneLabel}, + { + name: "skips empty and deduplicates client ids", + contexts: []*policy.RequestContext{ + nil, + {}, + {Pep: &policy.PolicyEnforcementPoint{}}, + {Pep: &policy.PolicyEnforcementPoint{ClientId: "tdf-client"}}, + {Pep: &policy.PolicyEnforcementPoint{ClientId: "tdf-client"}}, + {Pep: &policy.PolicyEnforcementPoint{ClientId: "admin-client"}}, + }, + want: `client_id: "tdf-client", client_id: "admin-client"`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, plainRequestContextsSummary(tc.contexts)) + }) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_commit_confirmation.go b/otdfctl/migrations/namespacedpolicy/prune_commit_confirmation.go new file mode 100644 index 0000000000..ecb6be9a46 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_commit_confirmation.go @@ -0,0 +1,103 @@ +package namespacedpolicy + +import ( + "context" + "fmt" +) + +const ( + confirmPruneDeleteLabel = "Confirm delete" + confirmPruneDeleteDescription = "delete this source object" + abortPruneDeleteLabel = "Abort prune commit" + abortPruneDeleteDescription = "stop before deleting any objects" +) + +// ConfirmPrunePlanDeletes prompts for every delete-status prune item before +// commit execution and lets the user confirm, skip, or abort. +func ConfirmPrunePlanDeletes(ctx context.Context, plan *PrunePlan, prompter InteractivePrompter) error { + if plan == nil { + return nil + } + if prompter == nil { + prompter = &HuhPrompter{} + } + + if err := confirmDeletePruneItems(ctx, prompter, plan.Actions); err != nil { + return err + } + if err := confirmDeletePruneItems(ctx, prompter, plan.SubjectConditionSets); err != nil { + return err + } + if err := confirmDeletePruneItems(ctx, prompter, plan.SubjectMappings); err != nil { + return err + } + if err := confirmDeletePruneItems(ctx, prompter, plan.RegisteredResources); err != nil { + return err + } + + return confirmDeletePruneItems(ctx, prompter, plan.ObligationTriggers) +} + +func confirmDeletePruneItems[T pruneReviewItem]( + ctx context.Context, + prompter InteractivePrompter, + items []T, +) error { + for _, item := range items { + if !confirmablePruneDeleteItem(item) { + continue + } + prompt := pruneDeleteConfirmationPrompt(item) + if err := applyPruneDeleteConfirmationDecision(ctx, prompter, prompt, func() { markPruneItemSkipped(item) }); err != nil { + return err + } + } + + return nil +} + +func confirmablePruneDeleteItem(item pruneReviewItem) bool { + return item.hasSource() && item.status() == PruneStatusDelete +} + +func markPruneItemSkipped(item pruneReviewItem) { + item.setStatus(PruneStatusSkipped) + item.setReason(newPruneReason(PruneStatusReasonTypeSkippedByUser, pruneStatusReasonMessageSkippedByUser)) +} + +func applyPruneDeleteConfirmationDecision(ctx context.Context, prompter InteractivePrompter, prompt SelectPrompt, markSkipped func()) error { + choice, err := prompter.Select(ctx, prompt) + if err != nil { + return err + } + + switch choice { + case namespacedPolicyCommitConfirm: + return nil + case namespacedPolicyCommitSkip: + markSkipped() + return nil + case namespacedPolicyCommitAbort: + return ErrInteractiveReviewAborted + default: + return fmt.Errorf("invalid prune commit selection %q", choice) + } +} + +func pruneDeleteConfirmationPrompt(item pruneReviewItem) SelectPrompt { + summary := item.reviewSummary() + + return SelectPrompt{ + Title: fmt.Sprintf("Delete %s %q?", summary.Kind, summary.Label), + Description: summary.Description, + Options: pruneDeleteConfirmationOptions(), + } +} + +func pruneDeleteConfirmationOptions() []PromptOption { + return []PromptOption{ + {Label: confirmPruneDeleteLabel, Value: namespacedPolicyCommitConfirm, Description: confirmPruneDeleteDescription}, + {Label: skipObjectLabel, Value: namespacedPolicyCommitSkip, Description: skipObjectDescription}, + {Label: abortPruneDeleteLabel, Value: namespacedPolicyCommitAbort, Description: abortPruneDeleteDescription}, + } +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_commit_confirmation_test.go b/otdfctl/migrations/namespacedpolicy/prune_commit_confirmation_test.go new file mode 100644 index 0000000000..d90bf55f0f --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_commit_confirmation_test.go @@ -0,0 +1,224 @@ +package namespacedpolicy + +import ( + "errors" + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfirmPrunePlanDeletesConfirmsDeleteItems(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "archive"}, + Status: PruneStatusDelete, + MigratedTargets: []TargetRef{ + {ID: "target-action-1", NamespaceFQN: "https://example.com"}, + }, + }, + }, + } + prompter := &queuedSelectPrompter{ + selectValues: []string{namespacedPolicyCommitConfirm}, + } + + err := ConfirmPrunePlanDeletes(t.Context(), plan, prompter) + require.NoError(t, err) + + require.Equal(t, 1, prompter.selectCalls) + assert.Equal(t, PruneStatusDelete, plan.Actions[0].Status) + assert.True(t, plan.Actions[0].Reason.IsZero()) +} + +func TestConfirmPrunePlanDeletesSkipsDeleteItems(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "archive"}, + Status: PruneStatusDelete, + MigratedTargets: []TargetRef{{ID: "target-action-1", NamespaceFQN: "https://example.com"}}, + }, + { + Source: &policy.Action{Id: "action-2", Name: "export"}, + Status: PruneStatusDelete, + MigratedTargets: []TargetRef{{ID: "target-action-2", NamespaceFQN: "https://example.com"}}, + }, + }, + } + prompter := &queuedSelectPrompter{ + selectValues: []string{ + namespacedPolicyCommitSkip, + namespacedPolicyCommitConfirm, + }, + } + + err := ConfirmPrunePlanDeletes(t.Context(), plan, prompter) + require.NoError(t, err) + + require.Equal(t, 2, prompter.selectCalls) + assert.Equal(t, PruneStatusSkipped, plan.Actions[0].Status) + assert.Equal(t, PruneStatusReasonTypeSkippedByUser, plan.Actions[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageSkippedByUser, plan.Actions[0].Reason.Message) + assert.Equal(t, PruneStatusDelete, plan.Actions[1].Status) +} + +func TestConfirmPrunePlanDeletesAbortStopsWithoutMutatingCurrentItem(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "archive"}, + Status: PruneStatusDelete, + }, + { + Source: &policy.Action{Id: "action-2", Name: "export"}, + Status: PruneStatusDelete, + }, + }, + } + prompter := &queuedSelectPrompter{ + selectValues: []string{namespacedPolicyCommitAbort}, + } + + err := ConfirmPrunePlanDeletes(t.Context(), plan, prompter) + require.ErrorIs(t, err, ErrInteractiveReviewAborted) + + require.Equal(t, 1, prompter.selectCalls) + assert.Equal(t, PruneStatusDelete, plan.Actions[0].Status) + assert.Equal(t, PruneStatusDelete, plan.Actions[1].Status) +} + +func TestConfirmPrunePlanDeletesSkipsNilSourceAndNonDeleteItems(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + nil, + {Status: PruneStatusDelete}, + { + Source: &policy.Action{Id: "action-blocked", Name: "archive"}, + Status: PruneStatusBlocked, + }, + { + Source: &policy.Action{Id: "action-unresolved", Name: "export"}, + Status: PruneStatusUnresolved, + }, + { + Source: &policy.Action{Id: "action-skipped", Name: "share"}, + Status: PruneStatusSkipped, + }, + }, + } + prompter := &queuedSelectPrompter{ + selectValues: []string{namespacedPolicyCommitSkip}, + } + + err := ConfirmPrunePlanDeletes(t.Context(), plan, prompter) + require.NoError(t, err) + + assert.Equal(t, 0, prompter.selectCalls) + assert.Equal(t, PruneStatusDelete, plan.Actions[1].Status) + assert.Equal(t, PruneStatusBlocked, plan.Actions[2].Status) + assert.Equal(t, PruneStatusUnresolved, plan.Actions[3].Status) + assert.Equal(t, PruneStatusSkipped, plan.Actions[4].Status) +} + +func TestConfirmPrunePlanDeletesPromptsAllConstructs(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + {Source: &policy.Action{Id: "action-1", Name: "archive"}, Status: PruneStatusDelete}, + }, + SubjectConditionSets: []*PruneSubjectConditionSetPlan{ + {Source: &policy.SubjectConditionSet{Id: "scs-1"}, Status: PruneStatusDelete}, + }, + SubjectMappings: []*PruneSubjectMappingPlan{ + {Source: &policy.SubjectMapping{Id: "mapping-1"}, Status: PruneStatusDelete}, + }, + RegisteredResources: []*PruneRegisteredResourcePlan{ + {Source: testRegisteredResource("resource-1", "dataset"), Status: PruneStatusDelete}, + }, + ObligationTriggers: []*PruneObligationTriggerPlan{ + {Source: &policy.ObligationTrigger{Id: "trigger-1"}, Status: PruneStatusDelete}, + }, + } + prompter := &queuedSelectPrompter{ + selectValues: []string{ + namespacedPolicyCommitConfirm, + namespacedPolicyCommitConfirm, + namespacedPolicyCommitConfirm, + namespacedPolicyCommitConfirm, + namespacedPolicyCommitConfirm, + }, + } + + err := ConfirmPrunePlanDeletes(t.Context(), plan, prompter) + require.NoError(t, err) + + assert.Equal(t, 5, prompter.selectCalls) +} + +func TestApplyPruneDeleteConfirmationDecisionHandlesChoices(t *testing.T) { + t.Parallel() + + promptErr := errors.New("boom") + tests := []struct { + name string + selectValue string + selectErr error + wantSkipped bool + wantErr error + }{ + { + name: "confirm", + selectValue: namespacedPolicyCommitConfirm, + }, + { + name: "skip", + selectValue: namespacedPolicyCommitSkip, + wantSkipped: true, + }, + { + name: "abort", + selectValue: namespacedPolicyCommitAbort, + wantErr: ErrInteractiveReviewAborted, + }, + { + name: "prompt error", + selectErr: promptErr, + wantErr: promptErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + prompter := &queuedSelectPrompter{ + selectValues: []string{tt.selectValue}, + selectErr: tt.selectErr, + } + skipped := false + + err := applyPruneDeleteConfirmationDecision(t.Context(), prompter, SelectPrompt{Title: "test prompt"}, func() { + skipped = true + }) + if tt.wantErr == nil { + require.NoError(t, err) + assert.Equal(t, tt.wantSkipped, skipped) + return + } + require.ErrorIs(t, err, tt.wantErr) + assert.False(t, skipped) + }) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_details.go b/otdfctl/migrations/namespacedpolicy/prune_details.go new file mode 100644 index 0000000000..fc81d6c0b1 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_details.go @@ -0,0 +1,204 @@ +package namespacedpolicy + +import ( + "strconv" + "strings" + + "github.com/opentdf/platform/otdfctl/migrations" + "github.com/opentdf/platform/protocol/go/policy" +) + +func renderPruneReviewDetails(details []string, result string) []string { + lines := make([]string, 0, len(details)+1) + for _, detail := range details { + if strings.TrimSpace(detail) == "" { + continue + } + lines = append(lines, detail) + } + if strings.TrimSpace(result) != "" { + lines = append(lines, result) + } + return lines +} + +func renderPruneReviewDescription(details []string, reason PruneStatusReason, execution *ExecutionResult) []string { + return renderPruneReviewDetails(details, renderResultDetail(false, nil, reason, execution)) +} + +func renderPruneSummaryLine(base string, details []string, result string) string { + line := appendDetails(base, details...) + if strings.TrimSpace(result) == "" { + return line + } + return line + ": " + result +} + +func renderResultDetail(styled bool, styles *migrations.DisplayStyles, reason PruneStatusReason, execution *ExecutionResult) string { + if !reason.IsZero() { + return formatPruneDetail("reason", pruneWarningValue(styled, styles, reason.String())) + } + + if failure := strings.TrimSpace(executionFailure(execution)); failure != "" { + return formatPruneDetail("execution_failure", pruneWarningValue(styled, styles, failure)) + } + + return "" +} + +func formatPruneDetail(label, value string) string { + if strings.TrimSpace(value) == "" { + return "" + } + return label + "=" + value +} + +func pruneIDValue(styled bool, styles *migrations.DisplayStyles, value string) string { + if styled { + return styles.ID().Render(value) + } + return value +} + +func pruneNameValue(styled bool, styles *migrations.DisplayStyles, value string) string { + if styled { + return styles.Name().Render(value) + } + return value +} + +func pruneNamespaceValue(styled bool, styles *migrations.DisplayStyles, value string) string { + if styled { + return styles.Namespace().Render(value) + } + return value +} + +func pruneWarningValue(styled bool, styles *migrations.DisplayStyles, value string) string { + if styled { + return styles.Warning().Render(value) + } + return value +} + +func targetRefsSummary(targets []TargetRef) string { + labels := make([]string, 0, len(targets)) + for _, target := range targets { + label := target.String() + if label == noneLabel { + continue + } + labels = append(labels, label) + } + if len(labels) == 0 { + return noneLabel + } + return "[" + strings.Join(labels, ", ") + "]" +} + +func styledTargetRefsSummary(styles *migrations.DisplayStyles, targets []TargetRef) string { + labels := make([]string, 0, len(targets)) + for _, target := range targets { + if target.IsZero() { + continue + } + labels = append(labels, styledTargetRefSummary(styles, target)) + } + if len(labels) == 0 { + return styles.ID().Render(noneLabel) + } + return "[" + strings.Join(labels, ", ") + "]" +} + +func styledTargetRefSummary(styles *migrations.DisplayStyles, target TargetRef) string { + if target.IsZero() { + return styles.ID().Render(noneLabel) + } + + parts := make([]string, 0, targetRefSummaryPartCapacity) + if id := strings.TrimSpace(target.ID); id != "" { + parts = append(parts, "id: "+styles.ID().Render(strconvQuote(id))) + } + + namespace := strings.TrimSpace(target.NamespaceFQN) + if namespace == "" { + namespace = strings.TrimSpace(target.NamespaceID) + } + if namespace != "" { + parts = append(parts, "namespace: "+styles.Namespace().Render(strconvQuote(namespace))) + } + + if len(parts) == 0 { + return styles.ID().Render(noneLabel) + } + return strings.Join(parts, " ") +} + +func targetRefsPruneDetail(label string, targets []TargetRef, styled bool, styles *migrations.DisplayStyles) string { + if styled { + return formatPruneDetail(label, styledTargetRefsSummary(styles, targets)) + } + return formatPruneDetail(label, targetRefsSummary(targets)) +} + +func targetRefPruneDetail(label string, target TargetRef, styled bool, styles *migrations.DisplayStyles) string { + if styled { + return formatPruneDetail(label, styledTargetRefSummary(styles, target)) + } + return formatPruneDetail(label, target.String()) +} + +func policyActionsPruneDetail(label string, actions []*policy.Action, styled bool, styles *migrations.DisplayStyles) string { + if styled { + return formatPruneDetail(label, styledPolicyActionNamesSummary(styles, actions)) + } + return formatPruneDetail(label, plainPolicyActionNamesSummary(actions)) +} + +func registeredResourceSourcePruneDetail(label string, resource *policy.RegisteredResource, styled bool, styles *migrations.DisplayStyles) string { + if styled { + return formatPruneDetail(label, styledRegisteredResourceSourceSummary(styles, resource)) + } + return formatPruneDetail(label, plainRegisteredResourceSourceSummary(resource)) +} + +func (p *PruneActionPlan) pruneDetails(styled bool, styles *migrations.DisplayStyles) []string { + return []string{ + formatPruneDetail("source_id", pruneIDValue(styled, styles, p.Source.GetId())), + targetRefsPruneDetail("found_migrated_targets", p.MigratedTargets, styled, styles), + } +} + +func (p *PruneSubjectConditionSetPlan) pruneDetails(styled bool, styles *migrations.DisplayStyles) []string { + return []string{ + formatPruneDetail("subject_sets", strconv.Itoa(len(p.Source.GetSubjectSets()))), + targetRefsPruneDetail("found_migrated_targets", p.MigratedTargets, styled, styles), + } +} + +func (p *PruneSubjectMappingPlan) pruneDetails(styled bool, styles *migrations.DisplayStyles) []string { + return []string{ + formatPruneDetail("attribute_value", pruneNamespaceValue(styled, styles, valueFQN(p.Source.GetAttributeValue()))), + policyActionsPruneDetail("actions", p.Source.GetActions(), styled, styles), + formatPruneDetail("scs_source", pruneIDValue(styled, styles, p.Source.GetSubjectConditionSet().GetId())), + targetRefPruneDetail("found_migrated_target", p.MigratedTarget, styled, styles), + } +} + +func (p *PruneRegisteredResourcePlan) pruneDetails(styled bool, styles *migrations.DisplayStyles) []string { + return []string{ + formatPruneDetail("source_id", pruneIDValue(styled, styles, p.Source.GetId())), + registeredResourceSourcePruneDetail("source", p.Source, styled, styles), + targetRefPruneDetail("found_migrated_target", p.MigratedTarget, styled, styles), + } +} + +func (p *PruneObligationTriggerPlan) pruneDetails(styled bool, styles *migrations.DisplayStyles) []string { + return []string{ + formatPruneDetail("attribute_value", pruneNamespaceValue(styled, styles, valueFQN(p.Source.GetAttributeValue()))), + formatPruneDetail("action", pruneNameValue(styled, styles, strconvQuote(actionLabel(p.Source.GetAction())))), + formatPruneDetail("obligation_value", pruneIDValue(styled, styles, obligationValueIDOrFQN(p.Source.GetObligationValue()))), + formatPruneDetail("context", pruneIDValue(styled, styles, plainRequestContextsSummary(p.Source.GetContext()))), + targetRefPruneDetail("found_migrated_target", p.MigratedTarget, styled, styles), + } +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_execute.go b/otdfctl/migrations/namespacedpolicy/prune_execute.go new file mode 100644 index 0000000000..609e1f5b04 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_execute.go @@ -0,0 +1,131 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "fmt" +) + +type ( + pruneDeleteFunc[T prunePlanItem] func(context.Context, T, string) error +) + +var ( + ErrNilPruneExecutionPlan = errors.New("prune plan is required") + ErrMissingPruneSourceID = errors.New("missing prune source id") +) + +type PruneExecutor struct { + handler ExecutorHandler +} + +func NewPruneExecutor(handler ExecutorHandler) (*PruneExecutor, error) { + if handler == nil { + return nil, ErrNilExecutorHandler + } + + return &PruneExecutor{handler: handler}, nil +} + +func (e *PruneExecutor) ExecutePrune(ctx context.Context, plan *PrunePlan) error { + if err := e.validatePrunePlan(plan); err != nil { + return err + } + + switch plan.Scope { + case ScopeObligationTriggers: + return e.executePruneObligationTriggers(ctx, plan.ObligationTriggers) + case ScopeSubjectMappings: + return e.executePruneSubjectMappings(ctx, plan.SubjectMappings) + case ScopeRegisteredResources: + return e.executePruneRegisteredResources(ctx, plan.RegisteredResources) + case ScopeSubjectConditionSets: + return e.executePruneSubjectConditionSets(ctx, plan.SubjectConditionSets) + case ScopeActions: + return e.executePruneActions(ctx, plan.Actions) + default: + return fmt.Errorf("%w: %s", ErrInvalidScope, plan.Scope) + } +} + +func (e *PruneExecutor) validatePrunePlan(plan *PrunePlan) error { + if e == nil || e.handler == nil { + return ErrNilExecutorHandler + } + if plan == nil { + return ErrNilPruneExecutionPlan + } + if plan.Scope == "" { + return ErrEmptyPlannerScope + } + + return nil +} + +func (e *PruneExecutor) executePruneActions(ctx context.Context, plans []*PruneActionPlan) error { + return executePruneItems(ctx, e, plans, "action", func(ctx context.Context, _ *PruneActionPlan, sourceID string) error { + return e.handler.DeleteAction(ctx, sourceID) + }) +} + +func (e *PruneExecutor) executePruneSubjectConditionSets(ctx context.Context, plans []*PruneSubjectConditionSetPlan) error { + return executePruneItems(ctx, e, plans, "subject condition set", func(ctx context.Context, _ *PruneSubjectConditionSetPlan, sourceID string) error { + return e.handler.DeleteSubjectConditionSet(ctx, sourceID) + }) +} + +func (e *PruneExecutor) executePruneSubjectMappings(ctx context.Context, plans []*PruneSubjectMappingPlan) error { + return executePruneItems(ctx, e, plans, "subject mapping", func(ctx context.Context, _ *PruneSubjectMappingPlan, sourceID string) error { + _, err := e.handler.DeleteSubjectMapping(ctx, sourceID) + return err + }) +} + +func (e *PruneExecutor) executePruneRegisteredResources(ctx context.Context, plans []*PruneRegisteredResourcePlan) error { + return executePruneItems(ctx, e, plans, "registered resource", func(ctx context.Context, _ *PruneRegisteredResourcePlan, sourceID string) error { + return e.handler.DeleteRegisteredResource(ctx, sourceID) + }) +} + +func (e *PruneExecutor) executePruneObligationTriggers(ctx context.Context, plans []*PruneObligationTriggerPlan) error { + return executePruneItems(ctx, e, plans, "obligation trigger", func(ctx context.Context, _ *PruneObligationTriggerPlan, sourceID string) error { + _, err := e.handler.DeleteObligationTrigger(ctx, sourceID) + return err + }) +} + +func executePruneItems[T prunePlanItem]( + ctx context.Context, + executor *PruneExecutor, + items []T, + kind string, + deleteSource pruneDeleteFunc[T], +) error { + for _, item := range items { + if item.status() != PruneStatusDelete { + continue + } + + id := item.sourceID() + if id == "" { + return executor.recordPruneFailure(item, fmt.Errorf("%w: %s", ErrMissingPruneSourceID, kind)) + } + + if err := deleteSource(ctx, item, id); err != nil { + return executor.recordPruneFailure(item, fmt.Errorf("delete %s %q: %w", kind, id, err)) + } + + item.setExecution(&ExecutionResult{ + Applied: true, + }) + } + + return nil +} + +func (e *PruneExecutor) recordPruneFailure(item prunePlanItem, err error) error { + item.setExecution(&ExecutionResult{ + Failure: err.Error(), + }) + return err +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_execute_test.go b/otdfctl/migrations/namespacedpolicy/prune_execute_test.go new file mode 100644 index 0000000000..0e4077501f --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_execute_test.go @@ -0,0 +1,188 @@ +package namespacedpolicy + +import ( + "errors" + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExecutePruneDispatchesOnlyPlanScope(t *testing.T) { + tests := []struct { + name string + scope Scope + wantCalls []string + verify func(*testing.T, *PrunePlan) + }{ + { + name: "actions", + scope: ScopeActions, + wantCalls: []string{"action:action-delete-1", "action:action-delete-2"}, + verify: verifyPruneActionsExecuted, + }, + { + name: "subject condition sets", + scope: ScopeSubjectConditionSets, + wantCalls: []string{"subject-condition-set:scs-delete-1", "subject-condition-set:scs-delete-2"}, + verify: verifyPruneSubjectConditionSetsExecuted, + }, + { + name: "subject mappings", + scope: ScopeSubjectMappings, + wantCalls: []string{"subject-mapping:mapping-delete-1", "subject-mapping:mapping-delete-2"}, + verify: verifyPruneSubjectMappingsExecuted, + }, + { + name: "registered resources", + scope: ScopeRegisteredResources, + wantCalls: []string{"registered-resource:resource-delete-1", "registered-resource:resource-delete-2"}, + verify: verifyPruneRegisteredResourcesExecuted, + }, + { + name: "obligation triggers", + scope: ScopeObligationTriggers, + wantCalls: []string{"obligation-trigger:trigger-delete-1", "obligation-trigger:trigger-delete-2"}, + verify: verifyPruneObligationTriggersExecuted, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := &mockExecutorHandler{} + plan := mixedPrunePlan(tt.scope) + + executor, err := NewPruneExecutor(handler) + require.NoError(t, err) + + err = executor.ExecutePrune(t.Context(), plan) + require.NoError(t, err) + + assert.Equal(t, tt.wantCalls, handler.deleteCalls) + tt.verify(t, plan) + }) + } +} + +func TestExecutePruneRecordsFailureAndStops(t *testing.T) { + deleteErr := errors.New("delete denied") + handler := &mockExecutorHandler{ + deleteActionErrs: map[string]error{ + "action-delete": deleteErr, + }, + } + plan := &PrunePlan{ + Scope: ScopeActions, + Actions: []*PruneActionPlan{ + {Source: &policy.Action{Id: "action-delete"}, Status: PruneStatusDelete}, + {Source: &policy.Action{Id: "action-pending"}, Status: PruneStatusDelete}, + }, + } + + executor, err := NewPruneExecutor(handler) + require.NoError(t, err) + + err = executor.ExecutePrune(t.Context(), plan) + require.ErrorIs(t, err, deleteErr) + require.EqualError(t, err, `delete action "action-delete": delete denied`) + + require.NotNil(t, plan.Actions[0].Execution) + assert.False(t, plan.Actions[0].Execution.Applied) + assert.Equal(t, `delete action "action-delete": delete denied`, plan.Actions[0].Execution.Failure) + assert.Nil(t, plan.Actions[1].Execution) + assert.Equal(t, []string{"action:action-delete"}, handler.deleteCalls) +} + +func TestExecutePruneRequiresSingleScope(t *testing.T) { + handler := &mockExecutorHandler{} + executor, err := NewPruneExecutor(handler) + require.NoError(t, err) + + err = executor.ExecutePrune(t.Context(), &PrunePlan{}) + require.ErrorIs(t, err, ErrEmptyPlannerScope) +} + +func verifyPruneActionsExecuted(t *testing.T, plan *PrunePlan) { + t.Helper() + + assert.True(t, plan.Actions[0].Execution.Applied) + assert.True(t, plan.Actions[1].Execution.Applied) + assert.Nil(t, plan.Actions[2].Execution) + assert.Nil(t, plan.Actions[3].Execution) +} + +func verifyPruneSubjectConditionSetsExecuted(t *testing.T, plan *PrunePlan) { + t.Helper() + + assert.True(t, plan.SubjectConditionSets[0].Execution.Applied) + assert.True(t, plan.SubjectConditionSets[1].Execution.Applied) + assert.Nil(t, plan.SubjectConditionSets[2].Execution) + assert.Nil(t, plan.SubjectConditionSets[3].Execution) +} + +func verifyPruneSubjectMappingsExecuted(t *testing.T, plan *PrunePlan) { + t.Helper() + + assert.True(t, plan.SubjectMappings[0].Execution.Applied) + assert.True(t, plan.SubjectMappings[1].Execution.Applied) + assert.Nil(t, plan.SubjectMappings[2].Execution) +} + +func verifyPruneRegisteredResourcesExecuted(t *testing.T, plan *PrunePlan) { + t.Helper() + + assert.True(t, plan.RegisteredResources[0].Execution.Applied) + assert.True(t, plan.RegisteredResources[1].Execution.Applied) + assert.Nil(t, plan.RegisteredResources[2].Execution) +} + +func verifyPruneObligationTriggersExecuted(t *testing.T, plan *PrunePlan) { + t.Helper() + + assert.True(t, plan.ObligationTriggers[0].Execution.Applied) + assert.True(t, plan.ObligationTriggers[1].Execution.Applied) + assert.Nil(t, plan.ObligationTriggers[2].Execution) +} + +func mixedPrunePlan(scope Scope) *PrunePlan { + return &PrunePlan{ + Scope: scope, + Actions: []*PruneActionPlan{ + {Source: &policy.Action{Id: "action-delete-1"}, Status: PruneStatusDelete}, + {Source: &policy.Action{Id: "action-delete-2"}, Status: PruneStatusDelete}, + {Source: &policy.Action{Id: "action-blocked"}, Status: PruneStatusBlocked}, + {Source: &policy.Action{Id: "action-skipped"}, Status: PruneStatusSkipped}, + }, + SubjectConditionSets: []*PruneSubjectConditionSetPlan{ + {Source: &policy.SubjectConditionSet{Id: "scs-delete-1"}, Status: PruneStatusDelete}, + {Source: &policy.SubjectConditionSet{Id: "scs-delete-2"}, Status: PruneStatusDelete}, + {Source: &policy.SubjectConditionSet{Id: "scs-unresolved"}, Status: PruneStatusUnresolved}, + {Source: &policy.SubjectConditionSet{Id: "scs-skipped"}, Status: PruneStatusSkipped}, + }, + SubjectMappings: []*PruneSubjectMappingPlan{ + {Source: &policy.SubjectMapping{Id: "mapping-delete-1"}, Status: PruneStatusDelete}, + {Source: &policy.SubjectMapping{Id: "mapping-delete-2"}, Status: PruneStatusDelete}, + {Source: &policy.SubjectMapping{Id: "mapping-skipped"}, Status: PruneStatusSkipped}, + }, + RegisteredResources: []*PruneRegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{Id: "resource-delete-1"}, + Status: PruneStatusDelete, + }, + { + Source: &policy.RegisteredResource{Id: "resource-delete-2"}, + Status: PruneStatusDelete, + }, + { + Source: &policy.RegisteredResource{Id: "resource-skipped"}, + Status: PruneStatusSkipped, + }, + }, + ObligationTriggers: []*PruneObligationTriggerPlan{ + {Source: &policy.ObligationTrigger{Id: "trigger-delete-1"}, Status: PruneStatusDelete}, + {Source: &policy.ObligationTrigger{Id: "trigger-delete-2"}, Status: PruneStatusDelete}, + {Source: &policy.ObligationTrigger{Id: "trigger-skipped"}, Status: PruneStatusSkipped}, + }, + } +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_plan.go b/otdfctl/migrations/namespacedpolicy/prune_plan.go new file mode 100644 index 0000000000..103a4a111e --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_plan.go @@ -0,0 +1,430 @@ +package namespacedpolicy + +import ( + "fmt" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" +) + +type PruneStatus string + +const ( + PruneStatusDelete PruneStatus = "delete" + PruneStatusBlocked PruneStatus = "blocked" + PruneStatusUnresolved PruneStatus = "unresolved" + PruneStatusSkipped PruneStatus = "skipped" + targetRefSummaryPartCapacity = 2 +) + +type PruneStatusReasonType string + +const ( + PruneStatusReasonTypeMigratedTargetNotFound PruneStatusReasonType = "MigratedTargetNotFound" + PruneStatusReasonTypeNoMatchingLabelsFound PruneStatusReasonType = "NoMatchingLabelsFound" + PruneStatusReasonTypeMismatchedMigrationLabel PruneStatusReasonType = "MismatchedMigrationLabel" + PruneStatusReasonTypeMissingMigrationLabel PruneStatusReasonType = "MissingMigrationLabel" + PruneStatusReasonTypeInUse PruneStatusReasonType = "InUse" + PruneStatusReasonTypeNeedsMigration PruneStatusReasonType = "NeedsMigration" + PruneStatusReasonTypeMultiNamespaceManualDelete PruneStatusReasonType = "MultiNamespaceManualDelete" + PruneStatusReasonTypeSkippedByUser PruneStatusReasonType = "SkippedByUser" + + pruneStatusReasonMessageMigratedTargetNotFound = "no migrated target was found for this source" + pruneStatusReasonMessageInUse = "source object is still referenced by legacy policy" + pruneStatusReasonMessageNoMatchingLabelsFound = "canonical migrated targets were found, but none carry migrated_from for this source" + pruneStatusReasonMessageMismatchedMigrationLabel = "migrated target carries migrated_from metadata for a different source" + pruneStatusReasonMessageMissingMigrationLabel = "migrated target is missing migrated_from metadata for this source" + pruneStatusReasonMessageNeedsMigration = "source object does not have a migrated target yet" + pruneStatusReasonMessageMultiNamespaceManualDelete = "registered resource spans multiple target namespaces and was not migrated; must be deleted manually" + pruneStatusReasonMessageSkippedByUser = "skipped by user" +) + +type PruneStatusReason struct { + Type PruneStatusReasonType `json:"type"` + Message string `json:"message"` +} + +// TargetRef identifies the migrated target object that the planner +// matched to a source object. For objects that resolve to a single migrated +// target, the prune plan uses `TargetRef`. For objects that may still be +// referenced across multiple migrated namespaces, the prune plan uses +// `TargetRefs`. +type TargetRef struct { + ID string `json:"id"` + NamespaceID string `json:"namespace_id,omitempty"` + NamespaceFQN string `json:"namespace_fqn,omitempty"` +} + +func (t TargetRef) IsZero() bool { + return len(t.ID) == 0 && len(t.NamespaceID) == 0 && len(t.NamespaceFQN) == 0 +} + +func (t TargetRef) String() string { + return targetRefSummary(t) +} + +func targetRefSummary(target TargetRef) string { + if target.IsZero() { + return noneLabel + } + + parts := make([]string, 0, targetRefSummaryPartCapacity) + if id := strings.TrimSpace(target.ID); id != "" { + parts = append(parts, "id: "+strconvQuote(id)) + } + + namespace := strings.TrimSpace(target.NamespaceFQN) + if namespace == "" { + namespace = strings.TrimSpace(target.NamespaceID) + } + if namespace != "" { + parts = append(parts, "namespace: "+strconvQuote(namespace)) + } + + if len(parts) == 0 { + return noneLabel + } + return strings.Join(parts, " ") +} + +func (r PruneStatusReason) IsZero() bool { + return len(r.Type) == 0 && len(r.Message) == 0 +} + +func (r PruneStatusReason) String() string { + if r.IsZero() { + return noneLabel + } + if strings.TrimSpace(r.Message) == "" { + return string(r.Type) + } + if r.Type == "" { + return r.Message + } + return fmt.Sprintf("%s: %s", r.Type, r.Message) +} + +type PrunePlan struct { + Scope Scope `json:"scope"` + Actions []*PruneActionPlan `json:"actions"` + SubjectConditionSets []*PruneSubjectConditionSetPlan `json:"subject_condition_sets"` + SubjectMappings []*PruneSubjectMappingPlan `json:"subject_mappings"` + RegisteredResources []*PruneRegisteredResourcePlan `json:"registered_resources"` + ObligationTriggers []*PruneObligationTriggerPlan `json:"obligation_triggers"` +} + +type prunePlanItem interface { + hasSource() bool + sourceID() string + status() PruneStatus + setStatus(PruneStatus) + reason() PruneStatusReason + setReason(PruneStatusReason) + execution() *ExecutionResult + setExecution(*ExecutionResult) +} + +// PruneActionPlan records the source action being considered for deletion and +// any migrated target actions that still reference or replace it. +type PruneActionPlan struct { + Source *policy.Action `json:"source"` + Status PruneStatus `json:"status"` + MigratedTargets []TargetRef `json:"migrated_targets,omitempty"` + Reason PruneStatusReason `json:"reason,omitzero"` + Execution *ExecutionResult `json:"execution,omitempty"` // The CreatedTargetID is not used for the PrunePlans. +} + +func (p *PruneActionPlan) sourceID() string { + if !p.hasSource() { + return "" + } + return p.Source.GetId() +} + +func (p *PruneActionPlan) hasSource() bool { + return p != nil && p.Source != nil +} + +func (p *PruneActionPlan) status() PruneStatus { + if p == nil { + return "" + } + return p.Status +} + +func (p *PruneActionPlan) setStatus(status PruneStatus) { + if p != nil { + p.Status = status + } +} + +func (p *PruneActionPlan) reason() PruneStatusReason { + if p == nil { + return PruneStatusReason{} + } + return p.Reason +} + +func (p *PruneActionPlan) setReason(reason PruneStatusReason) { + if p != nil { + p.Reason = reason + } +} + +func (p *PruneActionPlan) execution() *ExecutionResult { + if p == nil { + return nil + } + return p.Execution +} + +func (p *PruneActionPlan) setExecution(execution *ExecutionResult) { + if p != nil { + p.Execution = execution + } +} + +// PruneSubjectConditionSetPlan records the source SCS being considered for +// deletion and any migrated target subject condition sets that still reference +// or replace it. +type PruneSubjectConditionSetPlan struct { + Source *policy.SubjectConditionSet `json:"source"` + Status PruneStatus `json:"status"` + MigratedTargets []TargetRef `json:"migrated_targets,omitempty"` + Reason PruneStatusReason `json:"reason,omitzero"` + Execution *ExecutionResult `json:"execution,omitempty"` +} + +func (p *PruneSubjectConditionSetPlan) sourceID() string { + if !p.hasSource() { + return "" + } + return p.Source.GetId() +} + +func (p *PruneSubjectConditionSetPlan) hasSource() bool { + return p != nil && p.Source != nil +} + +func (p *PruneSubjectConditionSetPlan) status() PruneStatus { + if p == nil { + return "" + } + return p.Status +} + +func (p *PruneSubjectConditionSetPlan) setStatus(status PruneStatus) { + if p != nil { + p.Status = status + } +} + +func (p *PruneSubjectConditionSetPlan) reason() PruneStatusReason { + if p == nil { + return PruneStatusReason{} + } + return p.Reason +} + +func (p *PruneSubjectConditionSetPlan) setReason(reason PruneStatusReason) { + if p != nil { + p.Reason = reason + } +} + +func (p *PruneSubjectConditionSetPlan) execution() *ExecutionResult { + if p == nil { + return nil + } + return p.Execution +} + +func (p *PruneSubjectConditionSetPlan) setExecution(execution *ExecutionResult) { + if p != nil { + p.Execution = execution + } +} + +// PruneSubjectMappingPlan records the source subject mapping being considered +// for deletion and the single migrated target subject mapping matched to it by +// migration metadata. +type PruneSubjectMappingPlan struct { + Source *policy.SubjectMapping `json:"source"` + Status PruneStatus `json:"status"` + MigratedTarget TargetRef `json:"migrated_target,omitzero"` + Reason PruneStatusReason `json:"reason,omitzero"` + Execution *ExecutionResult `json:"execution,omitempty"` +} + +func (p *PruneSubjectMappingPlan) sourceID() string { + if !p.hasSource() { + return "" + } + return p.Source.GetId() +} + +func (p *PruneSubjectMappingPlan) hasSource() bool { + return p != nil && p.Source != nil +} + +func (p *PruneSubjectMappingPlan) status() PruneStatus { + if p == nil { + return "" + } + return p.Status +} + +func (p *PruneSubjectMappingPlan) setStatus(status PruneStatus) { + if p != nil { + p.Status = status + } +} + +func (p *PruneSubjectMappingPlan) reason() PruneStatusReason { + if p == nil { + return PruneStatusReason{} + } + return p.Reason +} + +func (p *PruneSubjectMappingPlan) setReason(reason PruneStatusReason) { + if p != nil { + p.Reason = reason + } +} + +func (p *PruneSubjectMappingPlan) execution() *ExecutionResult { + if p == nil { + return nil + } + return p.Execution +} + +func (p *PruneSubjectMappingPlan) setExecution(execution *ExecutionResult) { + if p != nil { + p.Execution = execution + } +} + +// PruneRegisteredResourcePlan records the resolved RR source being considered +// for deletion and the single migrated target RR matched to it by migration +// metadata. +type PruneRegisteredResourcePlan struct { + // Source is the legacy RR source being considered for deletion. + Source *policy.RegisteredResource `json:"source"` + Status PruneStatus `json:"status"` + MigratedTarget TargetRef `json:"migrated_target,omitzero"` + Reason PruneStatusReason `json:"reason,omitzero"` + Execution *ExecutionResult `json:"execution,omitempty"` +} + +func (p *PruneRegisteredResourcePlan) sourceID() string { + if !p.hasSource() { + return "" + } + return p.Source.GetId() +} + +func (p *PruneRegisteredResourcePlan) hasSource() bool { + return p != nil && p.Source != nil +} + +func (p *PruneRegisteredResourcePlan) status() PruneStatus { + if p == nil { + return "" + } + return p.Status +} + +func (p *PruneRegisteredResourcePlan) setStatus(status PruneStatus) { + if p != nil { + p.Status = status + } +} + +func (p *PruneRegisteredResourcePlan) reason() PruneStatusReason { + if p == nil { + return PruneStatusReason{} + } + return p.Reason +} + +func (p *PruneRegisteredResourcePlan) setReason(reason PruneStatusReason) { + if p != nil { + p.Reason = reason + } +} + +func (p *PruneRegisteredResourcePlan) execution() *ExecutionResult { + if p == nil { + return nil + } + return p.Execution +} + +func (p *PruneRegisteredResourcePlan) setExecution(execution *ExecutionResult) { + if p != nil { + p.Execution = execution + } +} + +// PruneObligationTriggerPlan records the source obligation trigger being +// considered for deletion and the single migrated target obligation trigger +// matched to it by migration metadata. +type PruneObligationTriggerPlan struct { + Source *policy.ObligationTrigger `json:"source"` + Status PruneStatus `json:"status"` + MigratedTarget TargetRef `json:"migrated_target,omitzero"` + Reason PruneStatusReason `json:"reason,omitzero"` + Execution *ExecutionResult `json:"execution,omitempty"` +} + +func (p *PruneObligationTriggerPlan) sourceID() string { + if !p.hasSource() { + return "" + } + return p.Source.GetId() +} + +func (p *PruneObligationTriggerPlan) hasSource() bool { + return p != nil && p.Source != nil +} + +func (p *PruneObligationTriggerPlan) status() PruneStatus { + if p == nil { + return "" + } + return p.Status +} + +func (p *PruneObligationTriggerPlan) setStatus(status PruneStatus) { + if p != nil { + p.Status = status + } +} + +func (p *PruneObligationTriggerPlan) reason() PruneStatusReason { + if p == nil { + return PruneStatusReason{} + } + return p.Reason +} + +func (p *PruneObligationTriggerPlan) setReason(reason PruneStatusReason) { + if p != nil { + p.Reason = reason + } +} + +func (p *PruneObligationTriggerPlan) execution() *ExecutionResult { + if p == nil { + return nil + } + return p.Execution +} + +func (p *PruneObligationTriggerPlan) setExecution(execution *ExecutionResult) { + if p != nil { + p.Execution = execution + } +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_plan_test.go b/otdfctl/migrations/namespacedpolicy/prune_plan_test.go new file mode 100644 index 0000000000..82bee1fcc7 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_plan_test.go @@ -0,0 +1,165 @@ +package namespacedpolicy + +import ( + "encoding/json" + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTargetRefString(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + target TargetRef + want string + }{ + {name: "empty", target: TargetRef{}, want: noneLabel}, + { + name: "id only", + target: TargetRef{ + ID: "target-1", + }, + want: `id: "target-1"`, + }, + { + name: "id with namespace id", + target: TargetRef{ + ID: "target-1", + NamespaceID: "namespace-1", + }, + want: `id: "target-1" namespace: "namespace-1"`, + }, + { + name: "id with namespace fqn", + target: TargetRef{ + ID: "target-1", + NamespaceID: "namespace-1", + NamespaceFQN: "https://example.com", + }, + want: `id: "target-1" namespace: "https://example.com"`, + }, + { + name: "namespace without id", + target: TargetRef{ + NamespaceFQN: "https://example.com", + }, + want: `namespace: "https://example.com"`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, tc.target.String()) + }) + } +} + +func TestPrunePlanItemSourceID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + item prunePlanItem + want string + }{ + { + name: "action", + item: &PruneActionPlan{Source: &policy.Action{Id: "action-1"}}, + want: "action-1", + }, + { + name: "action without source", + item: &PruneActionPlan{}, + want: "", + }, + { + name: "nil action plan", + item: (*PruneActionPlan)(nil), + want: "", + }, + { + name: "subject condition set", + item: &PruneSubjectConditionSetPlan{Source: &policy.SubjectConditionSet{Id: "scs-1"}}, + want: "scs-1", + }, + { + name: "subject condition set without source", + item: &PruneSubjectConditionSetPlan{}, + want: "", + }, + { + name: "nil subject condition set plan", + item: (*PruneSubjectConditionSetPlan)(nil), + want: "", + }, + { + name: "subject mapping", + item: &PruneSubjectMappingPlan{Source: &policy.SubjectMapping{Id: "mapping-1"}}, + want: "mapping-1", + }, + { + name: "subject mapping without source", + item: &PruneSubjectMappingPlan{}, + want: "", + }, + { + name: "nil subject mapping plan", + item: (*PruneSubjectMappingPlan)(nil), + want: "", + }, + { + name: "registered resource", + item: &PruneRegisteredResourcePlan{Source: &policy.RegisteredResource{Id: "resource-1"}}, + want: "resource-1", + }, + { + name: "registered resource without source", + item: &PruneRegisteredResourcePlan{}, + want: "", + }, + { + name: "nil registered resource plan", + item: (*PruneRegisteredResourcePlan)(nil), + want: "", + }, + { + name: "obligation trigger", + item: &PruneObligationTriggerPlan{Source: &policy.ObligationTrigger{Id: "trigger-1"}}, + want: "trigger-1", + }, + { + name: "obligation trigger without source", + item: &PruneObligationTriggerPlan{}, + want: "", + }, + { + name: "nil obligation trigger plan", + item: (*PruneObligationTriggerPlan)(nil), + want: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, tc.item.sourceID()) + }) + } +} + +func TestPrunePlanMarshalsSingleScopeField(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{Scope: ScopeActions} + + data, err := json.Marshal(plan) + require.NoError(t, err) + + assert.Contains(t, string(data), `"scope":"actions"`) + assert.NotContains(t, string(data), `"scopes"`) +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_planner.go b/otdfctl/migrations/namespacedpolicy/prune_planner.go new file mode 100644 index 0000000000..9af84c85fb --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_planner.go @@ -0,0 +1,645 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" +) + +var ( + ErrMultiplePruneScopes = errors.New("prune planner accepts exactly one scope") + ErrInvalidPruneResolvedTarget = errors.New("invalid prune resolved target") +) + +// PrunePlanner classifies whether legacy policy objects can be deleted after +// migration. It accepts exactly one scope and uses one of two strategies: +// +// - actions and subject condition sets are planned directly from currently +// listed source objects because they are expected to be deleted last, after +// their legacy dependents are gone +// - subject mappings, registered resources, and obligation triggers reuse the +// migration planner's resolved view because prune decisions for those scopes +// still depend on resolved migrated targets +// +// Each prune item is classified as delete, blocked, or unresolved and carries +// the migrated target context that justified that decision. +type PrunePlanner struct { + planner *MigrationPlanner + scope Scope +} + +type prunePlannerConfig struct { + pageSize int32 +} + +type PruneOption func(*prunePlannerConfig) + +type pruneObject interface { + GetId() string + GetMetadata() *common.Metadata +} + +type pruneSourceObject interface { + GetId() string +} + +type pruneMigratedObject interface { + pruneObject + *policy.SubjectMapping | *policy.RegisteredResource | *policy.ObligationTrigger +} + +// NewPrunePlanner constructs a single-scope prune planner on top of the shared +// migration planner infrastructure. Direct-prune scopes reuse the same +// retriever and namespace discovery logic, while resolved-object scopes reuse +// the resolver state without planner-time interactive review. +func NewPrunePlanner(handler PolicyClient, scopeCSV string, opts ...PruneOption) (*PrunePlanner, error) { + if handler == nil { + return nil, ErrNilPlannerHandler + } + + scopes, err := ParseScopes(scopeCSV) + if err != nil { + return nil, err + } + + normalizedScopes, err := normalizeScopes(scopes) + if err != nil { + return nil, err + } + if len(normalizedScopes) != 1 { + return nil, ErrMultiplePruneScopes + } + + config := prunePlannerConfig{pageSize: defaultPlannerPageSize} + for _, opt := range opts { + opt(&config) + } + if config.pageSize <= 0 { + config.pageSize = defaultPlannerPageSize + } + + planner, err := NewMigrationPlanner(handler, scopeCSV, WithPageSize(config.pageSize)) + if err != nil { + return nil, err + } + + return &PrunePlanner{ + planner: planner, + scope: normalizedScopes.ordered()[0], + }, nil +} + +func WithPrunePageSize(pageSize int32) PruneOption { + return func(config *prunePlannerConfig) { + config.pageSize = pageSize + } +} + +// Plan produces a prune plan for the configured scope. +// +// Actions and subject condition sets bypass resolved migration output and are +// classified directly from current legacy usage plus canonical migrated target +// lookup. Subject mappings, registered resources, and obligation triggers first +// resolve through the migration planner and then translate that resolved state +// into prune statuses. +func (p *PrunePlanner) Plan(ctx context.Context) (*PrunePlan, error) { + if p == nil || p.planner == nil { + return nil, ErrNilPlannerHandler + } + if p.scope == "" { + return nil, ErrEmptyPlannerScope + } + switch p.scope { + case ScopeActions: + return p.planActions(ctx) + case ScopeSubjectConditionSets: + return p.planSubjectConditionSets(ctx) + case ScopeSubjectMappings, ScopeRegisteredResources, ScopeObligationTriggers: + resolved, err := p.planner.resolve(ctx) + if err != nil { + return nil, err + } + if resolved == nil { + return nil, ErrNilResolvedTargets + } + + return buildPrunePlanFromResolved(p.scope, resolved) + default: + return nil, fmt.Errorf("%w: %s", ErrInvalidScope, p.scope) + } +} + +func (p *PrunePlanner) planActions(ctx context.Context) (*PrunePlan, error) { + sourceActions, err := p.planner.retriever.retrieveActions(ctx) + if err != nil { + return nil, err + } + sourceActions = customLegacyActions(sourceActions) + + plan := &PrunePlan{ + Scope: p.scope, + Actions: make([]*PruneActionPlan, 0, len(sourceActions)), + } + if len(sourceActions) == 0 { + return plan, nil + } + + usedByID, err := p.usedLegacyActionsByID(ctx, objectIDSet(sourceActions)) + if err != nil { + return nil, err + } + + namespaces, err := p.planner.retriever.listNamespaces(ctx) + if err != nil { + return nil, err + } + targetNamespaces := dedupeTargetNamespaces(namespaces) + + customActionsByNamespace, _, err := p.planner.retriever.listActionsForNamespaces(ctx, targetNamespaces) + if err != nil { + return nil, err + } + + for _, source := range sourceActions { + if source.GetId() == "" { + continue + } + + status, reason, targets, err := pruneStatusForAction(source, usedByID, targetNamespaces, customActionsByNamespace) + if err != nil { + return nil, fmt.Errorf("action %q: %w", source.GetId(), err) + } + + plan.Actions = append(plan.Actions, &PruneActionPlan{ + Source: source, + Status: status, + MigratedTargets: targets, + Reason: reason, + }) + } + + return plan, nil +} + +func (p *PrunePlanner) planSubjectConditionSets(ctx context.Context) (*PrunePlan, error) { + sourceSCS, err := p.planner.retriever.retrieveSubjectConditionSets(ctx) + if err != nil { + return nil, err + } + + plan := &PrunePlan{ + Scope: p.scope, + SubjectConditionSets: make([]*PruneSubjectConditionSetPlan, 0, len(sourceSCS)), + } + if len(sourceSCS) == 0 { + return plan, nil + } + + usedByID, err := p.usedLegacySubjectConditionSetsByID(ctx, objectIDSet(sourceSCS)) + if err != nil { + return nil, err + } + + namespaces, err := p.planner.retriever.listNamespaces(ctx) + if err != nil { + return nil, err + } + targetNamespaces := dedupeTargetNamespaces(namespaces) + + scsByNamespace, err := p.planner.retriever.listSubjectConditionSetsForNamespaces(ctx, targetNamespaces) + if err != nil { + return nil, err + } + + for _, source := range sourceSCS { + if source.GetId() == "" { + continue + } + + status, reason, targets, err := pruneStatusForSubjectConditionSet(source, usedByID, targetNamespaces, scsByNamespace) + if err != nil { + return nil, fmt.Errorf("subject condition set %q: %w", source.GetId(), err) + } + + plan.SubjectConditionSets = append(plan.SubjectConditionSets, &PruneSubjectConditionSetPlan{ + Source: source, + Status: status, + MigratedTargets: targets, + Reason: reason, + }) + } + + return plan, nil +} + +func (p *PrunePlanner) usedLegacyActionsByID(ctx context.Context, sourceIDs map[string]struct{}) (map[string]struct{}, error) { + used := make(map[string]struct{}, len(sourceIDs)) + if len(sourceIDs) == 0 { + return used, nil + } + + subjectMappings, err := p.planner.retriever.retrieveSubjectMappings(ctx) + if err != nil { + return nil, err + } + for _, mapping := range subjectMappings { + if mapping == nil { + continue + } + for _, action := range mapping.GetActions() { + if action == nil { + continue + } + if _, ok := sourceIDs[action.GetId()]; ok { + used[action.GetId()] = struct{}{} + } + } + } + + registeredResources, err := p.planner.retriever.retrieveRegisteredResources(ctx) + if err != nil { + return nil, err + } + for _, resource := range registeredResources { + if resource == nil { + continue + } + for _, value := range resource.GetValues() { + if value == nil { + continue + } + for _, aav := range value.GetActionAttributeValues() { + if aav == nil || aav.GetAction() == nil { + continue + } + if _, ok := sourceIDs[aav.GetAction().GetId()]; ok { + used[aav.GetAction().GetId()] = struct{}{} + } + } + } + } + + obligationTriggers, err := p.planner.retriever.retrieveObligationTriggers(ctx, sourceIDs) + if err != nil { + return nil, err + } + for _, trigger := range obligationTriggers { + if trigger == nil || trigger.GetAction() == nil { + continue + } + if _, ok := sourceIDs[trigger.GetAction().GetId()]; ok { + used[trigger.GetAction().GetId()] = struct{}{} + } + } + + return used, nil +} + +func (p *PrunePlanner) usedLegacySubjectConditionSetsByID(ctx context.Context, sourceIDs map[string]struct{}) (map[string]struct{}, error) { + used := make(map[string]struct{}, len(sourceIDs)) + if len(sourceIDs) == 0 { + return used, nil + } + + subjectMappings, err := p.planner.retriever.retrieveSubjectMappings(ctx) + if err != nil { + return nil, err + } + for _, mapping := range subjectMappings { + if mapping == nil || mapping.GetSubjectConditionSet() == nil { + continue + } + scsID := mapping.GetSubjectConditionSet().GetId() + if _, ok := sourceIDs[scsID]; ok { + used[scsID] = struct{}{} + } + } + + return used, nil +} + +func buildPrunePlanFromResolved(scope Scope, resolved *ResolvedTargets) (*PrunePlan, error) { + if resolved == nil { + return &PrunePlan{}, nil + } + + builder := newPrunePlanBuilder(scope, resolved) + return builder.build() +} + +type prunePlanBuilder struct { + scope Scope + resolved *ResolvedTargets +} + +func newPrunePlanBuilder(scope Scope, resolved *ResolvedTargets) *prunePlanBuilder { + return &prunePlanBuilder{ + scope: scope, + resolved: resolved, + } +} + +func (b *prunePlanBuilder) build() (*PrunePlan, error) { + plan := &PrunePlan{ + Scope: b.scope, + } + if b.scope == ScopeSubjectMappings { + subjectMappings, err := b.subjectMappings() + if err != nil { + return nil, err + } + plan.SubjectMappings = subjectMappings + } + if b.scope == ScopeRegisteredResources { + registeredResources, err := b.registeredResources() + if err != nil { + return nil, err + } + plan.RegisteredResources = registeredResources + } + if b.scope == ScopeObligationTriggers { + obligationTriggers, err := b.obligationTriggers() + if err != nil { + return nil, err + } + plan.ObligationTriggers = obligationTriggers + } + return plan, nil +} + +func (b *prunePlanBuilder) subjectMappings() ([]*PruneSubjectMappingPlan, error) { + plans := make([]*PruneSubjectMappingPlan, 0, len(b.resolved.SubjectMappings)) + + for _, mapping := range b.resolved.SubjectMappings { + if mapping == nil || mapping.Source == nil { + continue + } + + status, reason, err := pruneStatusForResolvedObject(mapping.Source, mapping.AlreadyMigrated) + if err != nil { + return nil, fmt.Errorf("subject mapping %q: %w", mapping.Source.GetId(), err) + } + plans = append(plans, &PruneSubjectMappingPlan{ + Source: mapping.Source, + Status: status, + MigratedTarget: migratedTarget(mapping.AlreadyMigrated, mapping.Namespace), + Reason: reason, + }) + } + + return plans, nil +} + +// registeredResources classifies each resolved RR prune decision directly from +// the resolved migration state. Multi-namespace legacy RRs are blocked when no +// migrated target exists because the migration planner cannot determine a single +// target namespace for them and prune cannot safely auto-delete them. +func (b *prunePlanBuilder) registeredResources() ([]*PruneRegisteredResourcePlan, error) { + plans := make([]*PruneRegisteredResourcePlan, 0, len(b.resolved.RegisteredResources)) + + for _, resource := range b.resolved.RegisteredResources { + if resource == nil { + continue + } + if resource.Source == nil { + continue + } + + if pruneManualDeleteRequired(resource) { + plans = append(plans, &PruneRegisteredResourcePlan{ + Source: resource.Source, + Status: PruneStatusBlocked, + MigratedTarget: migratedTarget(resource.AlreadyMigrated, resource.Namespace), + Reason: newPruneReason( + PruneStatusReasonTypeMultiNamespaceManualDelete, + pruneStatusReasonMessageMultiNamespaceManualDelete, + ), + }) + continue + } + + status, reason, err := pruneStatusForRegisteredResource(resource.Source, resource.AlreadyMigrated) + if err != nil { + return nil, fmt.Errorf("registered resource %q: %w", resource.Source.GetId(), err) + } + + plans = append(plans, &PruneRegisteredResourcePlan{ + Source: resource.Source, + Status: status, + MigratedTarget: migratedTarget(resource.AlreadyMigrated, resource.Namespace), + Reason: reason, + }) + } + + return plans, nil +} + +func pruneManualDeleteRequired(resource *ResolvedRegisteredResource) bool { + return resource != nil && + resource.AlreadyMigrated == nil && + resource.Unresolved != nil && + resource.Unresolved.Reason == UnresolvedReasonRegisteredResourceConflictingNamespaces +} + +func (b *prunePlanBuilder) obligationTriggers() ([]*PruneObligationTriggerPlan, error) { + plans := make([]*PruneObligationTriggerPlan, 0, len(b.resolved.ObligationTriggers)) + + for _, trigger := range b.resolved.ObligationTriggers { + if trigger == nil || trigger.Source == nil { + continue + } + + status, reason, err := pruneStatusForResolvedObject(trigger.Source, trigger.AlreadyMigrated) + if err != nil { + return nil, fmt.Errorf("obligation trigger %q: %w", trigger.Source.GetId(), err) + } + plans = append(plans, &PruneObligationTriggerPlan{ + Source: trigger.Source, + Status: status, + MigratedTarget: migratedTarget(trigger.AlreadyMigrated, trigger.Namespace), + Reason: reason, + }) + } + + return plans, nil +} + +func customLegacyActions(actions []*policy.Action) []*policy.Action { + custom := make([]*policy.Action, 0, len(actions)) + for _, action := range actions { + if action.GetId() == "" || isStandardAction(action) { + continue + } + custom = append(custom, action) + } + return custom +} + +func pruneStatusForAction(source *policy.Action, usedByID map[string]struct{}, targetNamespaces []*policy.Namespace, actionsByNamespace map[string][]*policy.Action) (PruneStatus, PruneStatusReason, []TargetRef, error) { + targets, foundCanonical, labelsMatch, err := matchedActionTargets(source, targetNamespaces, actionsByNamespace) + if err != nil { + return "", PruneStatusReason{}, nil, err + } + _, used := usedByID[source.GetId()] + status, reason, migratedTargets := pruneStatusForCanonicalTargets(used, foundCanonical, labelsMatch, targets) + return status, reason, migratedTargets, nil +} + +func matchedActionTargets(source *policy.Action, targetNamespaces []*policy.Namespace, actionsByNamespace map[string][]*policy.Action) ([]TargetRef, bool, bool, error) { + targets := make([]TargetRef, 0) + foundCanonical := false + labelsMatch := false + + for _, namespace := range targetNamespaces { + for _, target := range actionsByNamespace[namespace.GetId()] { + if target == nil || !actionCanonicalEqual(source, target) { + continue + } + if target.GetId() == "" { + return nil, false, false, fmt.Errorf("%w: migrated target for source %q has empty id", ErrInvalidPruneResolvedTarget, source.GetId()) + } + foundCanonical = true + targets = append(targets, singleMigratedTarget(target.GetId(), namespace)) + if migratedFromID(target) == source.GetId() { + labelsMatch = true + } + break + } + } + + return targets, foundCanonical, labelsMatch, nil +} + +func pruneStatusForSubjectConditionSet(source *policy.SubjectConditionSet, usedByID map[string]struct{}, targetNamespaces []*policy.Namespace, scsByNamespace map[string][]*policy.SubjectConditionSet) (PruneStatus, PruneStatusReason, []TargetRef, error) { + targets, foundCanonical, labelsMatch, err := matchedSubjectConditionSetTargets(source, targetNamespaces, scsByNamespace) + if err != nil { + return "", PruneStatusReason{}, nil, err + } + _, used := usedByID[source.GetId()] + status, reason, migratedTargets := pruneStatusForCanonicalTargets(used, foundCanonical, labelsMatch, targets) + return status, reason, migratedTargets, nil +} + +func matchedSubjectConditionSetTargets(source *policy.SubjectConditionSet, targetNamespaces []*policy.Namespace, scsByNamespace map[string][]*policy.SubjectConditionSet) ([]TargetRef, bool, bool, error) { + targets := make([]TargetRef, 0) + foundCanonical := false + labelsMatch := false + + for _, namespace := range targetNamespaces { + for _, target := range scsByNamespace[namespace.GetId()] { + if target == nil || !subjectConditionSetCanonicalEqual(source, target) { + continue + } + if target.GetId() == "" { + return nil, false, false, fmt.Errorf("%w: migrated target for source %q has empty id", ErrInvalidPruneResolvedTarget, source.GetId()) + } + foundCanonical = true + targets = append(targets, singleMigratedTarget(target.GetId(), namespace)) + if migratedFromID(target) == source.GetId() { + labelsMatch = true + } + break + } + } + + return targets, foundCanonical, labelsMatch, nil +} + +// For actions and subject condition sets, prune runs after legacy policy graph +// objects are expected to be gone, so the planner can no longer reliably infer +// which target namespace a source object was intended to migrate into. In that +// state, a canonical match is only operator context. Delete requires that no +// legacy object is still using the source and that at least one canonical match +// carries the expected migrated_from label; additional canonical matches may +// still appear in the returned targets without that label. +func pruneStatusForCanonicalTargets(used, foundCanonical, labelsMatch bool, targets []TargetRef) (PruneStatus, PruneStatusReason, []TargetRef) { + if used { + return PruneStatusBlocked, newPruneReason(PruneStatusReasonTypeInUse, pruneStatusReasonMessageInUse), targets + } + // No canonical migrated target means the source object was not represented in + // any target namespace. For actions/SCS, that is more precise than + // needs-migration because these objects may have been left unmigrated simply + // because nothing depended on them. + if !foundCanonical { + return PruneStatusBlocked, newPruneReason(PruneStatusReasonTypeMigratedTargetNotFound, pruneStatusReasonMessageMigratedTargetNotFound), nil + } + if !labelsMatch { + return PruneStatusUnresolved, newPruneReason(PruneStatusReasonTypeNoMatchingLabelsFound, pruneStatusReasonMessageNoMatchingLabelsFound), targets + } + return PruneStatusDelete, PruneStatusReason{}, targets +} + +// target is expected to already be canonically equal to the source object. +// For subject mappings and obligation triggers, that canonical check happens in +// the resolver before AlreadyMigrated is set. For registered resources, prune +// verifies canonical equality against the authoritative full source in +// registeredResources before calling this helper. +func pruneStatusForMigratedObject(target pruneObject, sourceID string) (PruneStatus, PruneStatusReason, error) { + if target.GetId() == "" { + return "", PruneStatusReason{}, fmt.Errorf("%w: migrated target for source %q has empty id", ErrInvalidPruneResolvedTarget, sourceID) + } + if migratedFromID(target) != sourceID { + return PruneStatusUnresolved, pruneStatusReasonForMigrationLabel(target), nil + } + return PruneStatusDelete, PruneStatusReason{}, nil +} + +func pruneStatusForResolvedObject[S pruneSourceObject, T pruneMigratedObject](source S, alreadyMigrated T) (PruneStatus, PruneStatusReason, error) { + if alreadyMigrated == nil { + return PruneStatusBlocked, newPruneReason(PruneStatusReasonTypeNeedsMigration, pruneStatusReasonMessageNeedsMigration), nil + } + return pruneStatusForMigratedObject(alreadyMigrated, source.GetId()) +} + +func pruneStatusForRegisteredResource(source, alreadyMigrated *policy.RegisteredResource) (PruneStatus, PruneStatusReason, error) { + if alreadyMigrated == nil { + return PruneStatusBlocked, newPruneReason(PruneStatusReasonTypeNeedsMigration, pruneStatusReasonMessageNeedsMigration), nil + } + return pruneStatusForMigratedObject(alreadyMigrated, source.GetId()) +} + +func migratedFromID(item pruneObject) string { + if item == nil { + return "" + } + return strings.TrimSpace(item.GetMetadata().GetLabels()[migrationLabelMigratedFrom]) +} + +func singleMigratedTarget(existingID string, namespace *policy.Namespace) TargetRef { + if existingID == "" { + return TargetRef{} + } + return TargetRef{ + ID: existingID, + NamespaceID: namespace.GetId(), + NamespaceFQN: namespace.GetFqn(), + } +} + +func migratedTarget(target pruneObject, namespace *policy.Namespace) TargetRef { + if target == nil { + return TargetRef{} + } + return singleMigratedTarget(target.GetId(), namespace) +} + +func newPruneReason(reasonType PruneStatusReasonType, message string) PruneStatusReason { + if reasonType == "" && strings.TrimSpace(message) == "" { + return PruneStatusReason{} + } + return PruneStatusReason{ + Type: reasonType, + Message: message, + } +} + +func pruneStatusReasonForMigrationLabel(target pruneObject) PruneStatusReason { + if migratedFromID(target) == "" { + return newPruneReason(PruneStatusReasonTypeMissingMigrationLabel, pruneStatusReasonMessageMissingMigrationLabel) + } + return newPruneReason(PruneStatusReasonTypeMismatchedMigrationLabel, pruneStatusReasonMessageMismatchedMigrationLabel) +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_planner_test.go b/otdfctl/migrations/namespacedpolicy/prune_planner_test.go new file mode 100644 index 0000000000..db052e9382 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_planner_test.go @@ -0,0 +1,1158 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/obligations" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewPrunePlannerRejectsMultipleScopes(t *testing.T) { + t.Parallel() + + _, err := NewPrunePlanner(&plannerTestHandler{}, "actions,subject-mappings") + + require.Error(t, err) + assert.ErrorIs(t, err, ErrMultiplePruneScopes) +} + +// Scope: actions. +func TestPrunePlannerPlanBlocksActionWhenInUse(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace) + subjectSets := testSubjectSets() + legacySCS := &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets} + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: attributeValue, + Actions: []*policy.Action{ + {Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + }, + SubjectConditionSet: legacySCS, + } + legacyValue := testRegisteredResourceValue( + "value-1", + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + attributeValue, + ), + ) + legacyValue.Id = "value-1" + legacyResource := testRegisteredResource("resource-1", "documents", legacyValue) + obligationValue := &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{ + Namespace: targetNamespace, + }, + } + legacyTrigger := &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + AttributeValue: attributeValue, + ObligationValue: obligationValue, + } + + targetResourceValue := &policy.RegisteredResourceValue{ + Id: "target-value-1", + Value: legacyValue.GetValue(), + Metadata: migratedMetadata(legacyValue.GetId()), + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + testActionAttributeValue("target-action-1", legacyAction.GetName(), attributeValue), + }, + } + targetResource := &policy.RegisteredResource{ + Id: "target-resource-1", + Name: legacyResource.GetName(), + Metadata: migratedMetadata(legacyResource.GetId()), + Values: []*policy.RegisteredResourceValue{targetResourceValue}, + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{ + { + Id: "target-action-1", + Name: legacyAction.GetName(), + Namespace: targetNamespace, + Metadata: migratedMetadata(legacyAction.GetId()), + }, + }, + Pagination: emptyPageResponse(), + }, + }, + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + SubjectConditionSets: []*policy.SubjectConditionSet{ + { + Id: "target-scs-1", + SubjectSets: subjectSets, + Metadata: migratedMetadata(legacySCS.GetId()), + }, + }, + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + SubjectMappings: []*policy.SubjectMapping{ + { + Id: "target-mapping-1", + AttributeValue: attributeValue, + Actions: []*policy.Action{ + {Id: "target-action-1", Name: legacyAction.GetName()}, + }, + SubjectConditionSet: &policy.SubjectConditionSet{ + Id: "target-scs-1", + SubjectSets: subjectSets, + }, + Metadata: migratedMetadata(legacyMapping.GetId()), + }, + }, + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Resources: []*policy.RegisteredResource{targetResource}, + Pagination: emptyPageResponse(), + }, + }, + obligationTriggersByNamespace: map[string]*obligations.ListObligationTriggersResponse{ + "": { + Triggers: []*policy.ObligationTrigger{legacyTrigger}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Triggers: []*policy.ObligationTrigger{ + { + Id: "target-trigger-1", + Action: &policy.Action{Id: "target-action-1", Name: legacyAction.GetName()}, + AttributeValue: attributeValue, + ObligationValue: obligationValue, + Metadata: migratedMetadata(legacyTrigger.GetId()), + }, + }, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "actions") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.Actions, 1) + assert.Equal(t, PruneStatusBlocked, plan.Actions[0].Status) + assertPruneMigratedTargets(t, plan.Actions[0].MigratedTargets, targetNamespace, "target-action-1") + assert.Equal(t, PruneStatusReasonTypeInUse, plan.Actions[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageInUse, plan.Actions[0].Reason.Message) + assert.Empty(t, plan.SubjectConditionSets) + assert.Empty(t, plan.SubjectMappings) + assert.Empty(t, plan.RegisteredResources) + assert.Empty(t, plan.ObligationTriggers) +} + +func TestPrunePlannerPlanDeletesUnusedActionWhenCanonicalMigratedTargetExists(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + targetAction := &policy.Action{ + Id: "target-action-1", + Name: legacyAction.GetName(), + Namespace: targetNamespace, + Metadata: migratedMetadata(legacyAction.GetId()), + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{targetAction}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "actions") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.Actions, 1) + assert.Equal(t, PruneStatusDelete, plan.Actions[0].Status) + assertPruneMigratedTargets(t, plan.Actions[0].MigratedTargets, targetNamespace, targetAction.GetId()) + assert.True(t, plan.Actions[0].Reason.IsZero()) +} + +func TestPrunePlannerPlanMarksUnusedActionWithNoMatchingMigrationLabelsAsUnresolved(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + targetAction := &policy.Action{ + Id: "target-action-1", + Name: legacyAction.GetName(), + Namespace: targetNamespace, + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{targetAction}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "actions") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.Actions, 1) + assert.Equal(t, PruneStatusUnresolved, plan.Actions[0].Status) + assertPruneMigratedTargets(t, plan.Actions[0].MigratedTargets, targetNamespace, targetAction.GetId()) + assert.Equal(t, PruneStatusReasonTypeNoMatchingLabelsFound, plan.Actions[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageNoMatchingLabelsFound, plan.Actions[0].Reason.Message) +} + +func TestPrunePlannerPlanBlocksUnusedActionWhenMigratedTargetIsNotFound(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{ + { + Id: "target-action-1", + Name: "different", + Namespace: targetNamespace, + }, + }, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "actions") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.Actions, 1) + assert.Equal(t, PruneStatusBlocked, plan.Actions[0].Status) + assert.Empty(t, plan.Actions[0].MigratedTargets) + assert.Equal(t, PruneStatusReasonTypeMigratedTargetNotFound, plan.Actions[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageMigratedTargetNotFound, plan.Actions[0].Reason.Message) +} + +// Scope: subject condition sets. +func TestPrunePlannerPlanBlocksSubjectConditionSetWhenInUse(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace) + subjectSets := testSubjectSets() + legacySCS := &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets} + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: attributeValue, + Actions: []*policy.Action{ + {Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + }, + SubjectConditionSet: legacySCS, + } + legacyResource := testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + attributeValue, + ), + ), + ) + obligationValue := &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{ + Namespace: targetNamespace, + }, + } + legacyTrigger := &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + AttributeValue: attributeValue, + ObligationValue: obligationValue, + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{ + { + Id: "target-action-1", + Name: legacyAction.GetName(), + Namespace: targetNamespace, + Metadata: migratedMetadata(legacyAction.GetId()), + }, + }, + Pagination: emptyPageResponse(), + }, + }, + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + SubjectConditionSets: []*policy.SubjectConditionSet{ + { + Id: "target-scs-1", + SubjectSets: subjectSets, + Metadata: migratedMetadata(legacySCS.GetId()), + }, + }, + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + }, + obligationTriggersByNamespace: map[string]*obligations.ListObligationTriggersResponse{ + "": { + Triggers: []*policy.ObligationTrigger{legacyTrigger}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "subject-condition-sets") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.SubjectConditionSets, 1) + assert.Equal(t, PruneStatusBlocked, plan.SubjectConditionSets[0].Status) + assertPruneMigratedTargets(t, plan.SubjectConditionSets[0].MigratedTargets, targetNamespace, "target-scs-1") + assert.Equal(t, PruneStatusReasonTypeInUse, plan.SubjectConditionSets[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageInUse, plan.SubjectConditionSets[0].Reason.Message) + assert.Empty(t, plan.Actions) + assert.Empty(t, plan.SubjectMappings) + assert.Empty(t, plan.RegisteredResources) + assert.Empty(t, plan.ObligationTriggers) +} + +func TestPrunePlannerPlanDeletesUnusedSubjectConditionSetWhenCanonicalMigratedTargetExists(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + subjectSets := testSubjectSets() + legacySCS := &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets} + targetSCS := &policy.SubjectConditionSet{ + Id: "target-scs-1", + SubjectSets: subjectSets, + Metadata: migratedMetadata(legacySCS.GetId()), + } + handler := &plannerTestHandler{ + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + SubjectConditionSets: []*policy.SubjectConditionSet{targetSCS}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "subject-condition-sets") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.SubjectConditionSets, 1) + assert.Equal(t, PruneStatusDelete, plan.SubjectConditionSets[0].Status) + assertPruneMigratedTargets(t, plan.SubjectConditionSets[0].MigratedTargets, targetNamespace, targetSCS.GetId()) + assert.True(t, plan.SubjectConditionSets[0].Reason.IsZero()) +} + +func TestPrunePlannerPlanMarksUnusedSubjectConditionSetWithNoMatchingMigrationLabelsAsUnresolved(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + subjectSets := testSubjectSets() + legacySCS := &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets} + targetSCS := &policy.SubjectConditionSet{ + Id: "target-scs-1", + SubjectSets: subjectSets, + } + handler := &plannerTestHandler{ + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + SubjectConditionSets: []*policy.SubjectConditionSet{targetSCS}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "subject-condition-sets") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.SubjectConditionSets, 1) + assert.Equal(t, PruneStatusUnresolved, plan.SubjectConditionSets[0].Status) + assertPruneMigratedTargets(t, plan.SubjectConditionSets[0].MigratedTargets, targetNamespace, targetSCS.GetId()) + assert.Equal(t, PruneStatusReasonTypeNoMatchingLabelsFound, plan.SubjectConditionSets[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageNoMatchingLabelsFound, plan.SubjectConditionSets[0].Reason.Message) +} + +func TestPrunePlannerPlanBlocksUnusedSubjectConditionSetWhenMigratedTargetIsNotFound(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacySCS := &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: testSubjectSets()} + targetSCS := &policy.SubjectConditionSet{ + Id: "target-scs-1", + SubjectSets: []*policy.SubjectSet{}, + } + handler := &plannerTestHandler{ + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + SubjectConditionSets: []*policy.SubjectConditionSet{targetSCS}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "subject-condition-sets") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.SubjectConditionSets, 1) + assert.Equal(t, PruneStatusBlocked, plan.SubjectConditionSets[0].Status) + assert.Empty(t, plan.SubjectConditionSets[0].MigratedTargets) + assert.Equal(t, PruneStatusReasonTypeMigratedTargetNotFound, plan.SubjectConditionSets[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageMigratedTargetNotFound, plan.SubjectConditionSets[0].Reason.Message) +} + +// Scope: subject mappings. +func TestPrunePlannerPlanClassifiesUnmigratedSubjectMappingAsNeedsMigration(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + subjectSets := testSubjectSets() + legacySCS := &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets} + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace), + Actions: []*policy.Action{ + {Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + }, + SubjectConditionSet: legacySCS, + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + }, + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "subject-mappings") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.SubjectMappings, 1) + assert.Equal(t, PruneStatusBlocked, plan.SubjectMappings[0].Status) + assert.Equal(t, PruneStatusReasonTypeNeedsMigration, plan.SubjectMappings[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageNeedsMigration, plan.SubjectMappings[0].Reason.Message) + assert.True(t, plan.SubjectMappings[0].MigratedTarget.IsZero()) +} + +func TestPrunePlannerPlanClassifiesMissingMigrationLabelAsUnresolved(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + targetAction := &policy.Action{ + Id: "target-action-1", + Name: legacyAction.GetName(), + Namespace: targetNamespace, + Metadata: migratedMetadata(legacyAction.GetId()), + } + subjectSets := testSubjectSets() + legacySCS := &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets} + targetSCS := &policy.SubjectConditionSet{ + Id: "target-scs-1", + SubjectSets: subjectSets, + Metadata: migratedMetadata(legacySCS.GetId()), + } + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace) + legacyMapping := &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: attributeValue, + Actions: []*policy.Action{ + {Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + }, + SubjectConditionSet: legacySCS, + } + targetMapping := &policy.SubjectMapping{ + Id: "target-mapping-1", + AttributeValue: attributeValue, + Actions: []*policy.Action{ + {Id: targetAction.GetId(), Name: targetAction.GetName()}, + }, + SubjectConditionSet: targetSCS, + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{targetAction}, + Pagination: emptyPageResponse(), + }, + }, + subjectConditionSetsByNamespace: map[string]*subjectmapping.ListSubjectConditionSetsResponse{ + "": { + SubjectConditionSets: []*policy.SubjectConditionSet{legacySCS}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + SubjectConditionSets: []*policy.SubjectConditionSet{targetSCS}, + Pagination: emptyPageResponse(), + }, + }, + subjectMappingsByNamespace: map[string]*subjectmapping.ListSubjectMappingsResponse{ + "": { + SubjectMappings: []*policy.SubjectMapping{legacyMapping}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + SubjectMappings: []*policy.SubjectMapping{targetMapping}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "subject-mappings") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.SubjectMappings, 1) + assert.Equal(t, PruneStatusUnresolved, plan.SubjectMappings[0].Status) + assertPruneMigratedTarget(t, plan.SubjectMappings[0].MigratedTarget, targetNamespace, targetMapping.GetId()) + assert.Equal(t, PruneStatusReasonTypeMissingMigrationLabel, plan.SubjectMappings[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageMissingMigrationLabel, plan.SubjectMappings[0].Reason.Message) +} + +func TestPrunePlannerPlanFailsWhenMigratedTargetIDIsEmpty(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyMapping := &policy.SubjectMapping{Id: "mapping-1"} + resolved := &ResolvedTargets{ + SubjectMappings: []*ResolvedSubjectMapping{ + { + Source: legacyMapping, + Namespace: targetNamespace, + AlreadyMigrated: &policy.SubjectMapping{ + Metadata: migratedMetadata(legacyMapping.GetId()), + }, + }, + }, + } + + _, err := buildPrunePlanFromResolved(ScopeSubjectMappings, resolved) + + require.Error(t, err) + require.ErrorIs(t, err, ErrInvalidPruneResolvedTarget) + assert.Contains(t, err.Error(), `subject mapping "mapping-1"`) +} + +func TestPrunePlannerPlanClassifiesMismatchedMigrationLabelAsUnresolved(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyMapping := &policy.SubjectMapping{Id: "mapping-1"} + resolved := &ResolvedTargets{ + SubjectMappings: []*ResolvedSubjectMapping{ + { + Source: legacyMapping, + Namespace: targetNamespace, + AlreadyMigrated: &policy.SubjectMapping{ + Id: "target-mapping-1", + Metadata: &common.Metadata{ + Labels: map[string]string{ + migrationLabelMigratedFrom: "different-source-id", + }, + }, + }, + }, + }, + } + + plan, err := buildPrunePlanFromResolved(ScopeSubjectMappings, resolved) + + require.NoError(t, err) + require.Len(t, plan.SubjectMappings, 1) + assert.Equal(t, PruneStatusUnresolved, plan.SubjectMappings[0].Status) + assert.Equal(t, PruneStatusReasonTypeMismatchedMigrationLabel, plan.SubjectMappings[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageMismatchedMigrationLabel, plan.SubjectMappings[0].Reason.Message) +} + +// Scope: registered resources. +func TestPrunePlannerPlanSkipsRegisteredResourceWhenResolvedSourceIsMissing(t *testing.T) { + t.Parallel() + + resolved := &ResolvedTargets{ + RegisteredResources: []*ResolvedRegisteredResource{{}}, + } + + plan, err := buildPrunePlanFromResolved( + ScopeRegisteredResources, + resolved, + ) + + require.NoError(t, err) + require.NotNil(t, plan) + assert.Empty(t, plan.RegisteredResources) +} + +func TestPrunePlannerPlanClassifiesUnmigratedRegisteredResourceAsNeedsMigration(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace) + legacyResource := testRegisteredResource( + "resource-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue(legacyAction.GetId(), legacyAction.GetName(), attributeValue), + ), + ) + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "registered-resources") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.RegisteredResources, 1) + assert.Equal(t, PruneStatusBlocked, plan.RegisteredResources[0].Status) + assert.Equal(t, PruneStatusReasonTypeNeedsMigration, plan.RegisteredResources[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageNeedsMigration, plan.RegisteredResources[0].Reason.Message) + assert.True(t, plan.RegisteredResources[0].MigratedTarget.IsZero()) +} + +func TestPrunePlannerPlanBlocksUnmigratedMultiNamespaceRegisteredResourceForManualDelete(t *testing.T) { + t.Parallel() + + leftNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://left.example.com"} + rightNamespace := &policy.Namespace{Id: "ns-2", Fqn: "https://right.example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + leftValue := testRegisteredResourceValue( + "left", + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + testAttributeValue("https://left.example.com/attr/classification/value/secret", leftNamespace), + ), + ) + rightValue := testRegisteredResourceValue( + "right", + testActionAttributeValue( + legacyAction.GetId(), + legacyAction.GetName(), + testAttributeValue("https://right.example.com/attr/classification/value/secret", rightNamespace), + ), + ) + legacyResource := testRegisteredResource("resource-1", "documents", leftValue, rightValue) + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{leftNamespace, rightNamespace}, + Pagination: emptyPageResponse(), + }, + } + planner, err := NewPrunePlanner(handler, "registered-resources") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.RegisteredResources, 1) + assert.Equal(t, PruneStatusBlocked, plan.RegisteredResources[0].Status) + assert.True(t, plan.RegisteredResources[0].MigratedTarget.IsZero()) + assert.Equal(t, PruneStatusReasonTypeMultiNamespaceManualDelete, plan.RegisteredResources[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageMultiNamespaceManualDelete, plan.RegisteredResources[0].Reason.Message) + require.Len(t, plan.RegisteredResources[0].Source.GetValues(), 2) +} + +func TestPrunePlannerPlanDeletesRegisteredResourceWhenMigratedTargetMatches(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + targetAction := &policy.Action{ + Id: "target-action-1", + Name: legacyAction.GetName(), + Namespace: targetNamespace, + Metadata: migratedMetadata(legacyAction.GetId()), + } + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace) + legacyValue := testRegisteredResourceValue( + "prod", + testActionAttributeValue(legacyAction.GetId(), legacyAction.GetName(), attributeValue), + ) + legacyResource := testRegisteredResource("resource-1", "documents", legacyValue) + targetValue := testRegisteredResourceValue( + "prod", + testActionAttributeValue(targetAction.GetId(), targetAction.GetName(), attributeValue), + ) + targetResource := testRegisteredResource("target-resource-1", legacyResource.GetName(), targetValue) + targetResource.Metadata = migratedMetadata(legacyResource.GetId()) + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{targetAction}, + Pagination: emptyPageResponse(), + }, + }, + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + "": { + Resources: []*policy.RegisteredResource{legacyResource}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Resources: []*policy.RegisteredResource{targetResource}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "registered-resources") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.RegisteredResources, 1) + assert.Equal(t, PruneStatusDelete, plan.RegisteredResources[0].Status) + assertPruneMigratedTarget(t, plan.RegisteredResources[0].MigratedTarget, targetNamespace, targetResource.GetId()) + assert.True(t, plan.RegisteredResources[0].Reason.IsZero()) + require.NotNil(t, plan.RegisteredResources[0].Source) +} + +// Scope: obligation triggers. +func TestPrunePlannerPlanClassifiesUnmigratedObligationTriggerAsNeedsMigration(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace) + obligationValue := &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{ + Namespace: targetNamespace, + }, + } + legacyTrigger := &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + AttributeValue: attributeValue, + ObligationValue: obligationValue, + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + }, + obligationTriggersByNamespace: map[string]*obligations.ListObligationTriggersResponse{ + "": { + Triggers: []*policy.ObligationTrigger{legacyTrigger}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "obligation-triggers") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.ObligationTriggers, 1) + assert.Equal(t, PruneStatusBlocked, plan.ObligationTriggers[0].Status) + assert.Equal(t, PruneStatusReasonTypeNeedsMigration, plan.ObligationTriggers[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageNeedsMigration, plan.ObligationTriggers[0].Reason.Message) + assert.True(t, plan.ObligationTriggers[0].MigratedTarget.IsZero()) +} + +func TestPrunePlannerPlanDeletesObligationTriggerWhenMigratedTargetExists(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + targetAction := &policy.Action{ + Id: "target-action-1", + Name: legacyAction.GetName(), + Namespace: targetNamespace, + Metadata: migratedMetadata(legacyAction.GetId()), + } + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace) + obligationValue := &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{ + Namespace: targetNamespace, + }, + } + legacyTrigger := &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + AttributeValue: attributeValue, + ObligationValue: obligationValue, + } + targetTrigger := &policy.ObligationTrigger{ + Id: "target-trigger-1", + Action: &policy.Action{Id: targetAction.GetId(), Name: targetAction.GetName()}, + AttributeValue: attributeValue, + ObligationValue: obligationValue, + Metadata: migratedMetadata(legacyTrigger.GetId()), + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{targetAction}, + Pagination: emptyPageResponse(), + }, + }, + obligationTriggersByNamespace: map[string]*obligations.ListObligationTriggersResponse{ + "": { + Triggers: []*policy.ObligationTrigger{legacyTrigger}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Triggers: []*policy.ObligationTrigger{targetTrigger}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "obligation-triggers") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.ObligationTriggers, 1) + assert.Equal(t, PruneStatusDelete, plan.ObligationTriggers[0].Status) + assertPruneMigratedTarget(t, plan.ObligationTriggers[0].MigratedTarget, targetNamespace, targetTrigger.GetId()) + assert.True(t, plan.ObligationTriggers[0].Reason.IsZero()) +} + +func TestPrunePlannerPlanMarksObligationTriggerWithMissingMigrationLabelAsUnresolved(t *testing.T) { + t.Parallel() + + targetNamespace := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + legacyAction := &policy.Action{Id: "action-1", Name: "decrypt"} + targetAction := &policy.Action{ + Id: "target-action-1", + Name: legacyAction.GetName(), + Namespace: targetNamespace, + Metadata: migratedMetadata(legacyAction.GetId()), + } + attributeValue := testAttributeValue("https://example.com/attr/classification/value/secret", targetNamespace) + obligationValue := &policy.ObligationValue{ + Id: "ov-1", + Fqn: "https://example.com/obl/notify/value/email", + Obligation: &policy.Obligation{ + Namespace: targetNamespace, + }, + } + legacyTrigger := &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: legacyAction.GetId(), Name: legacyAction.GetName()}, + AttributeValue: attributeValue, + ObligationValue: obligationValue, + } + targetTrigger := &policy.ObligationTrigger{ + Id: "target-trigger-1", + Action: &policy.Action{Id: targetAction.GetId(), Name: targetAction.GetName()}, + AttributeValue: attributeValue, + ObligationValue: obligationValue, + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsCustom: []*policy.Action{legacyAction}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + ActionsCustom: []*policy.Action{targetAction}, + Pagination: emptyPageResponse(), + }, + }, + obligationTriggersByNamespace: map[string]*obligations.ListObligationTriggersResponse{ + "": { + Triggers: []*policy.ObligationTrigger{legacyTrigger}, + Pagination: emptyPageResponse(), + }, + targetNamespace.GetId(): { + Triggers: []*policy.ObligationTrigger{targetTrigger}, + Pagination: emptyPageResponse(), + }, + }, + namespacesResponse: &namespaces.ListNamespacesResponse{ + Namespaces: []*policy.Namespace{targetNamespace}, + Pagination: emptyPageResponse(), + }, + } + + planner, err := NewPrunePlanner(handler, "obligation-triggers") + require.NoError(t, err) + + plan, err := planner.Plan(t.Context()) + require.NoError(t, err) + + require.Len(t, plan.ObligationTriggers, 1) + assert.Equal(t, PruneStatusUnresolved, plan.ObligationTriggers[0].Status) + assertPruneMigratedTarget(t, plan.ObligationTriggers[0].MigratedTarget, targetNamespace, targetTrigger.GetId()) + assert.Equal(t, PruneStatusReasonTypeMissingMigrationLabel, plan.ObligationTriggers[0].Reason.Type) + assert.Equal(t, pruneStatusReasonMessageMissingMigrationLabel, plan.ObligationTriggers[0].Reason.Message) +} + +func migratedMetadata(sourceID string) *common.Metadata { + return &common.Metadata{ + Labels: map[string]string{ + migrationLabelMigratedFrom: sourceID, + }, + } +} + +func testSubjectSets() []*policy.SubjectSet { + return []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: "email", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"user@example.com"}, + }, + }, + }, + }, + }, + } +} + +func assertPruneMigratedTargets(t *testing.T, actual []TargetRef, namespace *policy.Namespace, ids ...string) { + t.Helper() + + require.Len(t, actual, len(ids)) + for i, id := range ids { + assertPruneMigratedTarget(t, actual[i], namespace, id) + } +} + +func assertPruneMigratedTarget(t *testing.T, actual TargetRef, namespace *policy.Namespace, id string) { + t.Helper() + + assert.Equal(t, id, actual.ID) + assert.Equal(t, namespace.GetId(), actual.NamespaceID) + assert.Equal(t, namespace.GetFqn(), actual.NamespaceFQN) +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_review.go b/otdfctl/migrations/namespacedpolicy/prune_review.go new file mode 100644 index 0000000000..cb75adc227 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_review.go @@ -0,0 +1,160 @@ +package namespacedpolicy + +import ( + "context" + "fmt" +) + +const ( + pruneReviewDelete = "delete" + pruneReviewSkip = "skip" + pruneReviewAbort = "abort" + + deletePruneLabel = "Delete source object" + deletePruneDescription = "mark this unresolved item for deletion" + skipPruneLabel = "Skip this object" + skipPruneDescription = "leave this item unresolved and untouched" + abortPruneLabel = "Abort prune review" + abortPruneDescription = "stop without applying remaining decisions" +) + +type pruneReviewItem interface { + prunePlanItem + reviewSummary() pruneReviewSummary +} + +type pruneReviewSummary struct { + Kind string + Label string + Description []string +} + +// ReviewPrunePlan prompts for every unresolved prune item and lets the user +// explicitly promote it to delete, leave it unresolved, or abort the run. +func ReviewPrunePlan(ctx context.Context, plan *PrunePlan, prompter InteractivePrompter) error { + if plan == nil { + return nil + } + if prompter == nil { + prompter = &HuhPrompter{} + } + + if err := reviewUnresolvedPruneItems(ctx, prompter, plan.Actions); err != nil { + return err + } + if err := reviewUnresolvedPruneItems(ctx, prompter, plan.SubjectConditionSets); err != nil { + return err + } + if err := reviewUnresolvedPruneItems(ctx, prompter, plan.SubjectMappings); err != nil { + return err + } + if err := reviewUnresolvedPruneItems(ctx, prompter, plan.RegisteredResources); err != nil { + return err + } + + return reviewUnresolvedPruneItems(ctx, prompter, plan.ObligationTriggers) +} + +func reviewUnresolvedPruneItems[T pruneReviewItem]( + ctx context.Context, + prompter InteractivePrompter, + items []T, +) error { + for _, item := range items { + if !reviewablePruneItem(item) { + continue + } + prompt := pruneReviewPrompt(item) + if err := applyPruneReviewDecision(ctx, prompter, prompt, func() { markPruneItemDelete(item) }); err != nil { + return err + } + } + + return nil +} + +func reviewablePruneItem(item pruneReviewItem) bool { + return item.hasSource() && item.status() == PruneStatusUnresolved +} + +func markPruneItemDelete(item pruneReviewItem) { + item.setStatus(PruneStatusDelete) + item.setReason(PruneStatusReason{}) +} + +func applyPruneReviewDecision(ctx context.Context, prompter InteractivePrompter, prompt SelectPrompt, markDelete func()) error { + choice, err := prompter.Select(ctx, prompt) + if err != nil { + return err + } + + switch choice { + case pruneReviewDelete: + markDelete() + return nil + case pruneReviewSkip: + return nil + case pruneReviewAbort: + return ErrInteractiveReviewAborted + default: + return fmt.Errorf("invalid prune review selection %q", choice) + } +} + +func pruneReviewPrompt(item pruneReviewItem) SelectPrompt { + summary := item.reviewSummary() + + return SelectPrompt{ + Title: fmt.Sprintf("Delete unresolved %s %q?", summary.Kind, summary.Label), + Description: summary.Description, + Options: pruneReviewOptions(), + } +} + +func (p *PruneActionPlan) reviewSummary() pruneReviewSummary { + return pruneReviewSummary{ + Kind: "action", + Label: p.Source.GetName(), + Description: renderPruneReviewDescription(p.pruneDetails(false, nil), p.Reason, p.Execution), + } +} + +func (p *PruneSubjectConditionSetPlan) reviewSummary() pruneReviewSummary { + return pruneReviewSummary{ + Kind: "subject condition set", + Label: p.Source.GetId(), + Description: renderPruneReviewDescription(p.pruneDetails(false, nil), p.Reason, p.Execution), + } +} + +func (p *PruneSubjectMappingPlan) reviewSummary() pruneReviewSummary { + return pruneReviewSummary{ + Kind: "subject mapping", + Label: p.Source.GetId(), + Description: renderPruneReviewDescription(p.pruneDetails(false, nil), p.Reason, p.Execution), + } +} + +func (p *PruneRegisteredResourcePlan) reviewSummary() pruneReviewSummary { + return pruneReviewSummary{ + Kind: "registered resource", + Label: p.Source.GetName(), + Description: renderPruneReviewDescription(p.pruneDetails(false, nil), p.Reason, p.Execution), + } +} + +func (p *PruneObligationTriggerPlan) reviewSummary() pruneReviewSummary { + return pruneReviewSummary{ + Kind: "obligation trigger", + Label: p.Source.GetId(), + Description: renderPruneReviewDescription(p.pruneDetails(false, nil), p.Reason, p.Execution), + } +} + +func pruneReviewOptions() []PromptOption { + return []PromptOption{ + {Label: deletePruneLabel, Value: pruneReviewDelete, Description: deletePruneDescription}, + {Label: skipPruneLabel, Value: pruneReviewSkip, Description: skipPruneDescription}, + {Label: abortPruneLabel, Value: pruneReviewAbort, Description: abortPruneDescription}, + } +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_review_test.go b/otdfctl/migrations/namespacedpolicy/prune_review_test.go new file mode 100644 index 0000000000..358a348768 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_review_test.go @@ -0,0 +1,303 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReviewPrunePlanMarksUnresolvedActionForDeletion(t *testing.T) { + t.Parallel() + + reason := newPruneReason(PruneStatusReasonTypeNoMatchingLabelsFound, pruneStatusReasonMessageNoMatchingLabelsFound) + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{ + Id: "action-1", + Name: "archive", + }, + Status: PruneStatusUnresolved, + MigratedTargets: []TargetRef{ + { + ID: "target-action-1", + NamespaceID: "namespace-1", + NamespaceFQN: "https://example.com", + }, + }, + Reason: reason, + }, + }, + } + prompter := &testInteractivePrompter{selectValue: pruneReviewDelete} + + err := ReviewPrunePlan(t.Context(), plan, prompter) + require.NoError(t, err) + + require.Equal(t, 1, prompter.selectCalls) + require.NotNil(t, prompter.lastSelectPrompt) + assert.Equal(t, `Delete unresolved action "archive"?`, prompter.lastSelectPrompt.Title) + assert.Equal(t, []string{ + "source_id=action-1", + `found_migrated_targets=[id: "target-action-1" namespace: "https://example.com"]`, + "reason=NoMatchingLabelsFound: canonical migrated targets were found, but none carry migrated_from for this source", + }, prompter.lastSelectPrompt.Description) + require.Len(t, prompter.lastSelectPrompt.Options, 3) + assert.Equal(t, PruneStatusDelete, plan.Actions[0].Status) + assert.True(t, plan.Actions[0].Reason.IsZero()) +} + +func TestReviewPrunePlanSkipLeavesUnresolvedSubjectMappingUntouched(t *testing.T) { + t.Parallel() + + reason := newPruneReason(PruneStatusReasonTypeMissingMigrationLabel, pruneStatusReasonMessageMissingMigrationLabel) + target := TargetRef{ + ID: "target-mapping-1", + NamespaceID: "namespace-1", + NamespaceFQN: "https://example.com", + } + plan := &PrunePlan{ + SubjectMappings: []*PruneSubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Fqn: "https://example.com/attr/classification/value/secret", + }, + }, + Status: PruneStatusUnresolved, + MigratedTarget: target, + Reason: reason, + }, + }, + } + prompter := &testInteractivePrompter{selectValue: pruneReviewSkip} + + err := ReviewPrunePlan(t.Context(), plan, prompter) + require.NoError(t, err) + + assert.Equal(t, 1, prompter.selectCalls) + assert.Equal(t, PruneStatusUnresolved, plan.SubjectMappings[0].Status) + assert.Equal(t, target, plan.SubjectMappings[0].MigratedTarget) + assert.Equal(t, reason, plan.SubjectMappings[0].Reason) +} + +func TestReviewPrunePlanSubjectConditionSetPromptIncludesTargetsAndReason(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + SubjectConditionSets: []*PruneSubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{ + Id: "scs-1", + }, + Status: PruneStatusUnresolved, + MigratedTargets: []TargetRef{ + { + ID: "target-scs-1", + NamespaceFQN: "https://example.com", + }, + }, + Reason: newPruneReason(PruneStatusReasonTypeNoMatchingLabelsFound, pruneStatusReasonMessageNoMatchingLabelsFound), + }, + }, + } + prompter := &testInteractivePrompter{selectValue: pruneReviewSkip} + + err := ReviewPrunePlan(t.Context(), plan, prompter) + require.NoError(t, err) + + require.Equal(t, 1, prompter.selectCalls) + require.NotNil(t, prompter.lastSelectPrompt) + assert.Equal(t, `Delete unresolved subject condition set "scs-1"?`, prompter.lastSelectPrompt.Title) + assert.Equal(t, []string{ + "subject_sets=0", + `found_migrated_targets=[id: "target-scs-1" namespace: "https://example.com"]`, + "reason=NoMatchingLabelsFound: canonical migrated targets were found, but none carry migrated_from for this source", + }, prompter.lastSelectPrompt.Description) +} + +func TestReviewPrunePlanSubjectMappingPromptIncludesActionNames(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + SubjectMappings: []*PruneSubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Fqn: "https://example.com/attr/classification/value/secret", + }, + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + Actions: []*policy.Action{ + {Id: "action-1", Name: "archive"}, + {Id: "action-2", Name: "export"}, + }, + }, + Status: PruneStatusUnresolved, + MigratedTarget: TargetRef{ + ID: "target-mapping-1", + NamespaceFQN: "https://example.com", + }, + Reason: newPruneReason(PruneStatusReasonTypeMissingMigrationLabel, pruneStatusReasonMessageMissingMigrationLabel), + }, + }, + } + prompter := &testInteractivePrompter{selectValue: pruneReviewSkip} + + err := ReviewPrunePlan(t.Context(), plan, prompter) + require.NoError(t, err) + + require.Equal(t, 1, prompter.selectCalls) + require.NotNil(t, prompter.lastSelectPrompt) + assert.Equal(t, `Delete unresolved subject mapping "mapping-1"?`, prompter.lastSelectPrompt.Title) + assert.Equal(t, []string{ + "attribute_value=https://example.com/attr/classification/value/secret", + `actions="archive", "export"`, + "scs_source=scs-1", + `found_migrated_target=id: "target-mapping-1" namespace: "https://example.com"`, + "reason=MissingMigrationLabel: migrated target is missing migrated_from metadata for this source", + }, prompter.lastSelectPrompt.Description) +} + +func TestReviewPrunePlanObligationTriggerPromptIncludesTriggerContext(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + ObligationTriggers: []*PruneObligationTriggerPlan{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + AttributeValue: testAttributeValue("https://example.com/attr/classification/value/secret", testNamespace("https://example.com")), + Action: &policy.Action{Id: "action-1", Name: "read"}, + ObligationValue: &policy.ObligationValue{ + Id: "obligation-value-1", + Fqn: "https://example.com/obl/watermark/value/footer", + Value: "footer", + Obligation: &policy.Obligation{ + Id: "obligation-1", + Fqn: "https://example.com/obl/watermark", + Name: "watermark", + }, + }, + Context: []*policy.RequestContext{ + {Pep: &policy.PolicyEnforcementPoint{ClientId: "tdf-client"}}, + }, + }, + Status: PruneStatusUnresolved, + MigratedTarget: TargetRef{ + ID: "target-trigger-1", + NamespaceFQN: "https://example.com", + }, + Reason: newPruneReason(PruneStatusReasonTypeMissingMigrationLabel, pruneStatusReasonMessageMissingMigrationLabel), + }, + }, + } + prompter := &testInteractivePrompter{selectValue: pruneReviewSkip} + + err := ReviewPrunePlan(t.Context(), plan, prompter) + require.NoError(t, err) + + require.Equal(t, 1, prompter.selectCalls) + require.NotNil(t, prompter.lastSelectPrompt) + assert.Equal(t, `Delete unresolved obligation trigger "trigger-1"?`, prompter.lastSelectPrompt.Title) + assert.Equal(t, []string{ + "attribute_value=https://example.com/attr/classification/value/secret", + `action="read"`, + "obligation_value=https://example.com/obl/watermark/value/footer", + `context=client_id: "tdf-client"`, + `found_migrated_target=id: "target-trigger-1" namespace: "https://example.com"`, + "reason=MissingMigrationLabel: migrated target is missing migrated_from metadata for this source", + }, prompter.lastSelectPrompt.Description) +} + +func TestReviewPrunePlanAbortReturnsAbortedAndStops(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{ + Id: "action-1", + Name: "archive", + }, + Status: PruneStatusUnresolved, + Reason: newPruneReason(PruneStatusReasonTypeNoMatchingLabelsFound, pruneStatusReasonMessageNoMatchingLabelsFound), + }, + { + Source: &policy.Action{ + Id: "action-2", + Name: "export", + }, + Status: PruneStatusUnresolved, + Reason: newPruneReason(PruneStatusReasonTypeNoMatchingLabelsFound, pruneStatusReasonMessageNoMatchingLabelsFound), + }, + }, + } + prompter := &testInteractivePrompter{selectValue: pruneReviewAbort} + + err := ReviewPrunePlan(t.Context(), plan, prompter) + require.ErrorIs(t, err, ErrInteractiveReviewAborted) + + assert.Equal(t, 1, prompter.selectCalls) + assert.Equal(t, PruneStatusUnresolved, plan.Actions[0].Status) + assert.Equal(t, PruneStatusUnresolved, plan.Actions[1].Status) +} + +func TestReviewPrunePlanIgnoresBlockedPruneItems(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{ + Id: "action-1", + Name: "archive", + }, + Status: PruneStatusBlocked, + Reason: newPruneReason(PruneStatusReasonTypeInUse, pruneStatusReasonMessageInUse), + }, + }, + } + prompter := &testInteractivePrompter{selectValue: pruneReviewDelete} + + err := ReviewPrunePlan(t.Context(), plan, prompter) + require.NoError(t, err) + + assert.Equal(t, 0, prompter.selectCalls) + assert.Equal(t, PruneStatusBlocked, plan.Actions[0].Status) +} + +func TestReviewPrunePlanSkipsNilSourceAndNonUnresolvedItems(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Actions: []*PruneActionPlan{ + nil, + { + Status: PruneStatusUnresolved, + Reason: newPruneReason( + PruneStatusReasonTypeNoMatchingLabelsFound, + pruneStatusReasonMessageNoMatchingLabelsFound, + ), + }, + { + Source: &policy.Action{ + Id: "action-1", + Name: "archive", + }, + Status: PruneStatusDelete, + }, + }, + } + prompter := &testInteractivePrompter{selectValue: pruneReviewDelete} + + err := ReviewPrunePlan(t.Context(), plan, prompter) + require.NoError(t, err) + + assert.Equal(t, 0, prompter.selectCalls) + assert.Equal(t, PruneStatusUnresolved, plan.Actions[1].Status) + assert.Equal(t, PruneStatusDelete, plan.Actions[2].Status) +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_summary.go b/otdfctl/migrations/namespacedpolicy/prune_summary.go new file mode 100644 index 0000000000..72818d62b9 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_summary.go @@ -0,0 +1,235 @@ +package namespacedpolicy + +import ( + "fmt" + "strings" + + "github.com/opentdf/platform/otdfctl/migrations" +) + +type pruneSummaryItem interface { + prunePlanItem + summaryLine(*migrations.DisplayStyles) string +} + +const ( + pruneAppliedCountLabel = "deleted" + prunePendingCountLabel = "to_delete" + pruneAppliedSectionLabel = "Deleted" + prunePendingSectionLabel = "Will Delete" +) + +// PruneSummaryResult identifies the overall prune command outcome shown in the +// rendered summary. +type PruneSummaryResult string + +const ( + // PruneSummaryResultSuccess indicates the prune command completed without error. + PruneSummaryResultSuccess PruneSummaryResult = "success" + // PruneSummaryResultFailure indicates prune commit execution failed. + PruneSummaryResultFailure PruneSummaryResult = "failure" + // PruneSummaryResultAborted indicates the user aborted an interactive prune flow. + PruneSummaryResultAborted PruneSummaryResult = "aborted" +) + +func prunePlanScopes(plan *PrunePlan) []Scope { + if plan == nil || plan.Scope == "" { + return nil + } + return []Scope{plan.Scope} +} + +func RenderNamespacedPolicyPruneSummary(plan *PrunePlan, executed bool, result PruneSummaryResult) string { + styles := migrations.NewDisplayStyles() + return renderSummaryDocument(styles, summaryDocument{ + plannedTitle: "Namespaced Policy Prune Plan", + committedTitle: "Namespaced Policy Prune Committed", + operation: summaryOperationPrune, + scopes: prunePlanScopes(plan), + commit: executed, + result: string(result), + summaries: []constructSummary{ + summarizePruneActions(plan, executed, styles), + summarizePruneSubjectConditionSets(plan, executed, styles), + summarizePruneSubjectMappings(plan, executed, styles), + summarizePruneRegisteredResources(plan, executed, styles), + summarizePruneObligationTriggers(plan, executed, styles), + }, + }) +} + +func appendPruneSummaryCountParts(parts []string, counts summaryCounts) []string { + return append(parts, fmt.Sprintf("blocked=%d", counts.blocked)) +} + +func summarizePruneActions(plan *PrunePlan, executed bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Actions", + include: includesScope(prunePlanScopes(plan), ScopeActions), + } + if plan == nil { + return summary + } + for _, action := range plan.Actions { + if action == nil || action.Source == nil { + continue + } + appendPruneStatusSummary(&summary, action, executed, styles) + } + return summary +} + +func summarizePruneSubjectConditionSets(plan *PrunePlan, executed bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Subject Condition Sets", + include: includesScope(prunePlanScopes(plan), ScopeSubjectConditionSets), + } + if plan == nil { + return summary + } + for _, scs := range plan.SubjectConditionSets { + if scs == nil || scs.Source == nil { + continue + } + appendPruneStatusSummary(&summary, scs, executed, styles) + } + return summary +} + +func summarizePruneSubjectMappings(plan *PrunePlan, executed bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Subject Mappings", + include: includesScope(prunePlanScopes(plan), ScopeSubjectMappings), + } + if plan == nil { + return summary + } + for _, mapping := range plan.SubjectMappings { + if mapping == nil || mapping.Source == nil { + continue + } + appendPruneStatusSummary(&summary, mapping, executed, styles) + } + return summary +} + +func summarizePruneRegisteredResources(plan *PrunePlan, executed bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Registered Resources", + include: includesScope(prunePlanScopes(plan), ScopeRegisteredResources), + } + if plan == nil { + return summary + } + for _, resource := range plan.RegisteredResources { + if resource == nil || resource.Source == nil { + continue + } + appendPruneStatusSummary(&summary, resource, executed, styles) + } + return summary +} + +func summarizePruneObligationTriggers(plan *PrunePlan, executed bool, styles *migrations.DisplayStyles) constructSummary { + summary := constructSummary{ + label: "Obligation Triggers", + include: includesScope(prunePlanScopes(plan), ScopeObligationTriggers), + } + if plan == nil { + return summary + } + for _, trigger := range plan.ObligationTriggers { + if trigger == nil || trigger.Source == nil { + continue + } + appendPruneStatusSummary(&summary, trigger, executed, styles) + } + return summary +} + +func appendPruneStatusSummary[T pruneSummaryItem](summary *constructSummary, item T, executed bool, styles *migrations.DisplayStyles) { + switch item.status() { + case PruneStatusDelete: + switch classifyPruneExecution(executed, item.execution()) { + case operationExecutionStateApplied: + summary.counts.applied++ + summary.applied = append(summary.applied, item.summaryLine(styles)) + case operationExecutionStateFailed: + summary.counts.failed++ + summary.failed = append(summary.failed, item.summaryLine(styles)) + case operationExecutionStatePending: + summary.counts.pending++ + summary.pending = append(summary.pending, item.summaryLine(styles)) + } + case PruneStatusBlocked: + summary.counts.blocked++ + summary.blocked = append(summary.blocked, item.summaryLine(styles)) + case PruneStatusUnresolved: + summary.counts.unresolved++ + summary.unresolved = append(summary.unresolved, item.summaryLine(styles)) + case PruneStatusSkipped: + summary.counts.skipped++ + summary.skipped = append(summary.skipped, item.summaryLine(styles)) + } +} + +func classifyPruneExecution(executed bool, execution *ExecutionResult) operationExecutionState { + if !executed || execution == nil { + return operationExecutionStatePending + } + if len(strings.TrimSpace(execution.Failure)) != 0 { + return operationExecutionStateFailed + } + if execution.Applied { + return operationExecutionStateApplied + } + return operationExecutionStatePending +} + +func (p *PruneActionPlan) summaryLine(styles *migrations.DisplayStyles) string { + return renderPruneSummaryLine( + formatPruneSourceLine(styles, actionKind, p.Source.GetName()), + p.pruneDetails(true, styles), + renderResultDetail(true, styles, p.Reason, p.Execution), + ) +} + +func (p *PruneSubjectConditionSetPlan) summaryLine(styles *migrations.DisplayStyles) string { + return renderPruneSummaryLine( + formatPruneSourceLine(styles, subjectConditionSetKind, p.Source.GetId()), + p.pruneDetails(true, styles), + renderResultDetail(true, styles, p.Reason, p.Execution), + ) +} + +func (p *PruneSubjectMappingPlan) summaryLine(styles *migrations.DisplayStyles) string { + return renderPruneSummaryLine( + formatPruneSourceLine(styles, subjectMappingKind, p.Source.GetId()), + p.pruneDetails(true, styles), + renderResultDetail(true, styles, p.Reason, p.Execution), + ) +} + +func (p *PruneRegisteredResourcePlan) summaryLine(styles *migrations.DisplayStyles) string { + return renderPruneSummaryLine( + formatPruneSourceLine(styles, registeredResourceKind, p.Source.GetName()), + p.pruneDetails(true, styles), + renderResultDetail(true, styles, p.Reason, p.Execution), + ) +} + +func (p *PruneObligationTriggerPlan) summaryLine(styles *migrations.DisplayStyles) string { + return renderPruneSummaryLine( + formatPruneSourceLine(styles, obligationTriggerKind, p.Source.GetId()), + p.pruneDetails(true, styles), + renderResultDetail(true, styles, p.Reason, p.Execution), + ) +} + +func formatPruneSourceLine(styles *migrations.DisplayStyles, kind, label string) string { + return fmt.Sprintf( + "%s %s", + styles.Info().Render(kind), + styles.Name().Render(strconvQuote(label)), + ) +} diff --git a/otdfctl/migrations/namespacedpolicy/prune_summary_test.go b/otdfctl/migrations/namespacedpolicy/prune_summary_test.go new file mode 100644 index 0000000000..29bd2997e4 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/prune_summary_test.go @@ -0,0 +1,467 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" +) + +type pruneSummaryStatusLines struct { + deleted string + pending string + failed string + blocked string + unresolved string +} + +func TestRenderNamespacedPolicyPruneSummaryDryRunShowsWillDelete(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeActions, + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{Id: "action-delete", Name: "archive"}, + Status: PruneStatusDelete, + MigratedTargets: []TargetRef{ + {ID: "migrated-action-1", NamespaceID: "ns-1", NamespaceFQN: "https://example.com"}, + }, + }, + { + Source: &policy.Action{Id: "action-blocked", Name: "share"}, + Status: PruneStatusBlocked, + Reason: newPruneReason( + PruneStatusReasonTypeInUse, + pruneStatusReasonMessageInUse, + ), + }, + { + Source: &policy.Action{Id: "action-unresolved", Name: "preview"}, + Status: PruneStatusUnresolved, + Reason: newPruneReason( + PruneStatusReasonTypeNoMatchingLabelsFound, + pruneStatusReasonMessageNoMatchingLabelsFound, + ), + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, false, PruneSummaryResultSuccess)) + + assert.Contains(t, summary, "Namespaced Policy Prune Plan") + assert.Contains(t, summary, "Scopes: actions") + assert.Contains(t, summary, "Commit: false") + assert.Contains(t, summary, "Result: success") + assert.Contains(t, summary, "Counts: to_delete=1 skipped=0 blocked=1 failed=0 unresolved=1") + assert.Contains(t, summary, "Will Delete") + assert.NotContains(t, summary, "\nDeleted\n") + assert.Contains(t, summary, `action "archive" (source_id=action-delete, found_migrated_targets=[id: "migrated-action-1" namespace: "https://example.com"])`) + assert.Contains(t, summary, "Blocked") + assert.Contains(t, summary, `action "share" (source_id=action-blocked, found_migrated_targets=(none)): reason=InUse: source object is still referenced by legacy policy`) + assert.Contains(t, summary, "Unresolved") + assert.Contains(t, summary, `action "preview" (source_id=action-unresolved, found_migrated_targets=(none)): reason=NoMatchingLabelsFound: canonical migrated targets were found, but none carry migrated_from for this source`) +} + +func TestRenderNamespacedPolicyPruneSummaryCommitSeparatesDeletedPendingAndFailed(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeActions, + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{Id: "action-deleted", Name: "archive"}, + Status: PruneStatusDelete, + Execution: &ExecutionResult{Applied: true}, + }, + { + Source: &policy.Action{Id: "action-pending", Name: "preview"}, + Status: PruneStatusDelete, + }, + { + Source: &policy.Action{Id: "action-failed", Name: "share"}, + Status: PruneStatusDelete, + Execution: &ExecutionResult{Failure: "boom"}, + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, true, PruneSummaryResultFailure)) + + assert.Contains(t, summary, "Namespaced Policy Prune Committed") + assert.Contains(t, summary, "Result: failure") + assert.Contains(t, summary, "Counts: deleted=1 to_delete=1 skipped=0 blocked=0 failed=1 unresolved=0") + assert.Contains(t, summary, "Deleted") + assert.Contains(t, summary, `action "archive" (source_id=action-deleted, found_migrated_targets=(none))`) + assert.Contains(t, summary, "Will Delete") + assert.Contains(t, summary, `action "preview" (source_id=action-pending, found_migrated_targets=(none))`) + assert.Contains(t, summary, "Failed") + assert.Contains(t, summary, `action "share" (source_id=action-failed, found_migrated_targets=(none)): execution_failure=boom`) +} + +func TestRenderNamespacedPolicyPruneSummaryCommitShowsSkippedDeletes(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeActions, + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{Id: "action-skipped", Name: "archive"}, + Status: PruneStatusSkipped, + Reason: newPruneReason( + PruneStatusReasonTypeSkippedByUser, + pruneStatusReasonMessageSkippedByUser, + ), + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, true, PruneSummaryResultSuccess)) + + assert.Contains(t, summary, "Counts: deleted=0 skipped=1 blocked=0 failed=0 unresolved=0") + assert.Contains(t, summary, "Skipped") + assert.Contains(t, summary, `action "archive" (source_id=action-skipped, found_migrated_targets=(none)): reason=SkippedByUser: skipped by user`) + assert.NotContains(t, summary, "Will Delete") + assert.NotContains(t, summary, "\nDeleted\n") +} + +func TestRenderNamespacedPolicyPruneSummaryActionsCoversEveryStatus(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeActions, + Actions: []*PruneActionPlan{ + { + Source: &policy.Action{Id: "action-deleted", Name: "archive"}, + Status: PruneStatusDelete, + Execution: &ExecutionResult{Applied: true}, + }, + { + Source: &policy.Action{Id: "action-pending", Name: "preview"}, + Status: PruneStatusDelete, + }, + { + Source: &policy.Action{Id: "action-failed", Name: "share"}, + Status: PruneStatusDelete, + Execution: &ExecutionResult{Failure: "action boom"}, + }, + { + Source: &policy.Action{Id: "action-blocked", Name: "download"}, + Status: PruneStatusBlocked, + Reason: pruneSummaryBlockedReason(), + }, + { + Source: &policy.Action{Id: "action-unresolved", Name: "export"}, + Status: PruneStatusUnresolved, + Reason: pruneSummaryUnresolvedReason(), + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, true, PruneSummaryResultFailure)) + + assertPruneSummaryCoversEveryStatus(t, summary, "Actions", pruneSummaryStatusLines{ + deleted: `action "archive" (source_id=action-deleted, found_migrated_targets=(none))`, + pending: `action "preview" (source_id=action-pending, found_migrated_targets=(none))`, + failed: `action "share" (source_id=action-failed, found_migrated_targets=(none)): execution_failure=action boom`, + blocked: `action "download" (source_id=action-blocked, found_migrated_targets=(none)): reason=InUse: source object is still referenced by legacy policy`, + unresolved: `action "export" (source_id=action-unresolved, found_migrated_targets=(none)): reason=NoMatchingLabelsFound: canonical migrated targets were found, but none carry migrated_from for this source`, + }) +} + +func TestRenderNamespacedPolicyPruneSummarySubjectConditionSetsCoversEveryStatus(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeSubjectConditionSets, + SubjectConditionSets: []*PruneSubjectConditionSetPlan{ + { + Source: pruneSummarySubjectConditionSet("scs-deleted"), + Status: PruneStatusDelete, + Execution: &ExecutionResult{Applied: true}, + }, + { + Source: pruneSummarySubjectConditionSet("scs-pending"), + Status: PruneStatusDelete, + }, + { + Source: pruneSummarySubjectConditionSet("scs-failed"), + Status: PruneStatusDelete, + Execution: &ExecutionResult{Failure: "subject condition set boom"}, + }, + { + Source: pruneSummarySubjectConditionSet("scs-blocked"), + Status: PruneStatusBlocked, + Reason: pruneSummaryBlockedReason(), + }, + { + Source: pruneSummarySubjectConditionSet("scs-unresolved"), + Status: PruneStatusUnresolved, + Reason: pruneSummaryUnresolvedReason(), + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, true, PruneSummaryResultFailure)) + + assertPruneSummaryCoversEveryStatus(t, summary, "Subject Condition Sets", pruneSummaryStatusLines{ + deleted: `subject condition set "scs-deleted" (subject_sets=1, found_migrated_targets=(none))`, + pending: `subject condition set "scs-pending" (subject_sets=1, found_migrated_targets=(none))`, + failed: `subject condition set "scs-failed" (subject_sets=1, found_migrated_targets=(none)): execution_failure=subject condition set boom`, + blocked: `subject condition set "scs-blocked" (subject_sets=1, found_migrated_targets=(none)): reason=InUse: source object is still referenced by legacy policy`, + unresolved: `subject condition set "scs-unresolved" (subject_sets=1, found_migrated_targets=(none)): reason=NoMatchingLabelsFound: canonical migrated targets were found, but none carry migrated_from for this source`, + }) +} + +func TestRenderNamespacedPolicyPruneSummarySubjectMappingsCoversEveryStatus(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeSubjectMappings, + SubjectMappings: []*PruneSubjectMappingPlan{ + { + Source: pruneSummarySubjectMapping("mapping-deleted", "read"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-mapping-deleted"), + Execution: &ExecutionResult{Applied: true}, + }, + { + Source: pruneSummarySubjectMapping("mapping-pending", "write"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-mapping-pending"), + }, + { + Source: pruneSummarySubjectMapping("mapping-failed", "share"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-mapping-failed"), + Execution: &ExecutionResult{Failure: "subject mapping boom"}, + }, + { + Source: pruneSummarySubjectMapping("mapping-blocked", "download"), + Status: PruneStatusBlocked, + MigratedTarget: pruneSummaryTarget("target-mapping-blocked"), + Reason: pruneSummaryBlockedReason(), + }, + { + Source: pruneSummarySubjectMapping("mapping-unresolved", "export"), + Status: PruneStatusUnresolved, + MigratedTarget: pruneSummaryTarget("target-mapping-unresolved"), + Reason: pruneSummaryUnresolvedReason(), + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, true, PruneSummaryResultFailure)) + + assertPruneSummaryCoversEveryStatus(t, summary, "Subject Mappings", pruneSummaryStatusLines{ + deleted: `subject mapping "mapping-deleted" (attribute_value=https://example.com/attr/classification/value/secret, actions="read", scs_source=scs-source, found_migrated_target=id: "target-mapping-deleted" namespace: "https://example.com")`, + pending: `subject mapping "mapping-pending" (attribute_value=https://example.com/attr/classification/value/secret, actions="write", scs_source=scs-source, found_migrated_target=id: "target-mapping-pending" namespace: "https://example.com")`, + failed: `subject mapping "mapping-failed" (attribute_value=https://example.com/attr/classification/value/secret, actions="share", scs_source=scs-source, found_migrated_target=id: "target-mapping-failed" namespace: "https://example.com"): execution_failure=subject mapping boom`, + blocked: `subject mapping "mapping-blocked" (attribute_value=https://example.com/attr/classification/value/secret, actions="download", scs_source=scs-source, found_migrated_target=id: "target-mapping-blocked" namespace: "https://example.com"): reason=InUse: source object is still referenced by legacy policy`, + unresolved: `subject mapping "mapping-unresolved" (attribute_value=https://example.com/attr/classification/value/secret, actions="export", scs_source=scs-source, found_migrated_target=id: "target-mapping-unresolved" namespace: "https://example.com"): reason=NoMatchingLabelsFound: canonical migrated targets were found, but none carry migrated_from for this source`, + }) +} + +func TestRenderNamespacedPolicyPruneSummaryRegisteredResourcesCoversEveryStatus(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeRegisteredResources, + RegisteredResources: []*PruneRegisteredResourcePlan{ + { + Source: pruneSummaryRegisteredResource("resource-deleted", "dataset-deleted", "read"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-resource-deleted"), + Execution: &ExecutionResult{Applied: true}, + }, + { + Source: pruneSummaryRegisteredResource("resource-pending", "dataset-pending", "write"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-resource-pending"), + }, + { + Source: pruneSummaryRegisteredResource("resource-failed", "dataset-failed", "share"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-resource-failed"), + Execution: &ExecutionResult{Failure: "registered resource boom"}, + }, + { + Source: pruneSummaryRegisteredResource("resource-blocked", "dataset-blocked", "download"), + Status: PruneStatusBlocked, + MigratedTarget: pruneSummaryTarget("target-resource-blocked"), + Reason: pruneSummaryBlockedReason(), + }, + { + Source: pruneSummaryRegisteredResource("resource-unresolved", "dataset-unresolved", "export"), + Status: PruneStatusUnresolved, + MigratedTarget: pruneSummaryTarget("target-resource-unresolved"), + Reason: pruneSummaryUnresolvedReason(), + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, true, PruneSummaryResultFailure)) + + assertPruneSummaryCoversEveryStatus(t, summary, "Registered Resources", pruneSummaryStatusLines{ + deleted: `registered resource "dataset-deleted" (source_id=resource-deleted, source=values="prod" (action_bindings="read" -> https://example.com/attr/classification/value/secret), found_migrated_target=id: "target-resource-deleted" namespace: "https://example.com")`, + pending: `registered resource "dataset-pending" (source_id=resource-pending, source=values="prod" (action_bindings="write" -> https://example.com/attr/classification/value/secret), found_migrated_target=id: "target-resource-pending" namespace: "https://example.com")`, + failed: `registered resource "dataset-failed" (source_id=resource-failed, source=values="prod" (action_bindings="share" -> https://example.com/attr/classification/value/secret), found_migrated_target=id: "target-resource-failed" namespace: "https://example.com"): execution_failure=registered resource boom`, + blocked: `registered resource "dataset-blocked" (source_id=resource-blocked, source=values="prod" (action_bindings="download" -> https://example.com/attr/classification/value/secret), found_migrated_target=id: "target-resource-blocked" namespace: "https://example.com"): reason=InUse: source object is still referenced by legacy policy`, + unresolved: `registered resource "dataset-unresolved" (source_id=resource-unresolved, source=values="prod" (action_bindings="export" -> https://example.com/attr/classification/value/secret), found_migrated_target=id: "target-resource-unresolved" namespace: "https://example.com"): reason=NoMatchingLabelsFound: canonical migrated targets were found, but none carry migrated_from for this source`, + }) +} + +func TestRenderNamespacedPolicyPruneSummaryRegisteredResourceManualDeleteReason(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeRegisteredResources, + RegisteredResources: []*PruneRegisteredResourcePlan{ + { + Source: pruneSummaryRegisteredResource("resource-1", "dataset", "read"), + Status: PruneStatusBlocked, + Reason: newPruneReason( + PruneStatusReasonTypeMultiNamespaceManualDelete, + pruneStatusReasonMessageMultiNamespaceManualDelete, + ), + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, false, PruneSummaryResultSuccess)) + + assert.Contains(t, summary, `registered resource "dataset" (source_id=resource-1, source=values="prod" (action_bindings="read" -> https://example.com/attr/classification/value/secret), found_migrated_target=(none)): reason=MultiNamespaceManualDelete: registered resource spans multiple target namespaces and was not migrated; must be deleted manually`) + assert.NotContains(t, summary, "full_source=") +} + +func TestRenderNamespacedPolicyPruneSummaryObligationTriggersCoversEveryStatus(t *testing.T) { + t.Parallel() + + plan := &PrunePlan{ + Scope: ScopeObligationTriggers, + ObligationTriggers: []*PruneObligationTriggerPlan{ + { + Source: pruneSummaryObligationTrigger("trigger-deleted", "read"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-trigger-deleted"), + Execution: &ExecutionResult{Applied: true}, + }, + { + Source: pruneSummaryObligationTrigger("trigger-pending", "write"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-trigger-pending"), + }, + { + Source: pruneSummaryObligationTrigger("trigger-failed", "share"), + Status: PruneStatusDelete, + MigratedTarget: pruneSummaryTarget("target-trigger-failed"), + Execution: &ExecutionResult{Failure: "obligation trigger boom"}, + }, + { + Source: pruneSummaryObligationTrigger("trigger-blocked", "download"), + Status: PruneStatusBlocked, + MigratedTarget: pruneSummaryTarget("target-trigger-blocked"), + Reason: pruneSummaryBlockedReason(), + }, + { + Source: pruneSummaryObligationTrigger("trigger-unresolved", "export"), + Status: PruneStatusUnresolved, + MigratedTarget: pruneSummaryTarget("target-trigger-unresolved"), + Reason: pruneSummaryUnresolvedReason(), + }, + }, + } + + summary := stripANSI(RenderNamespacedPolicyPruneSummary(plan, true, PruneSummaryResultFailure)) + + assertPruneSummaryCoversEveryStatus(t, summary, "Obligation Triggers", pruneSummaryStatusLines{ + deleted: `obligation trigger "trigger-deleted" (attribute_value=https://example.com/attr/classification/value/secret, action="read", obligation_value=https://example.com/obligation/log/value/default, context=client_id: "tdf-client", found_migrated_target=id: "target-trigger-deleted" namespace: "https://example.com")`, + pending: `obligation trigger "trigger-pending" (attribute_value=https://example.com/attr/classification/value/secret, action="write", obligation_value=https://example.com/obligation/log/value/default, context=client_id: "tdf-client", found_migrated_target=id: "target-trigger-pending" namespace: "https://example.com")`, + failed: `obligation trigger "trigger-failed" (attribute_value=https://example.com/attr/classification/value/secret, action="share", obligation_value=https://example.com/obligation/log/value/default, context=client_id: "tdf-client", found_migrated_target=id: "target-trigger-failed" namespace: "https://example.com"): execution_failure=obligation trigger boom`, + blocked: `obligation trigger "trigger-blocked" (attribute_value=https://example.com/attr/classification/value/secret, action="download", obligation_value=https://example.com/obligation/log/value/default, context=client_id: "tdf-client", found_migrated_target=id: "target-trigger-blocked" namespace: "https://example.com"): reason=InUse: source object is still referenced by legacy policy`, + unresolved: `obligation trigger "trigger-unresolved" (attribute_value=https://example.com/attr/classification/value/secret, action="export", obligation_value=https://example.com/obligation/log/value/default, context=client_id: "tdf-client", found_migrated_target=id: "target-trigger-unresolved" namespace: "https://example.com"): reason=NoMatchingLabelsFound: canonical migrated targets were found, but none carry migrated_from for this source`, + }) +} + +func assertPruneSummaryCoversEveryStatus(t *testing.T, summary, constructTitle string, lines pruneSummaryStatusLines) { + t.Helper() + + assert.Contains(t, summary, constructTitle) + assert.Contains(t, summary, "Counts: deleted=1 to_delete=1 skipped=0 blocked=1 failed=1 unresolved=1") + assert.Contains(t, summary, "Deleted") + assert.Contains(t, summary, lines.deleted) + assert.Contains(t, summary, "Will Delete") + assert.Contains(t, summary, lines.pending) + assert.Contains(t, summary, "Failed") + assert.Contains(t, summary, lines.failed) + assert.Contains(t, summary, "Blocked") + assert.Contains(t, summary, lines.blocked) + assert.Contains(t, summary, "Unresolved") + assert.Contains(t, summary, lines.unresolved) +} + +func pruneSummaryBlockedReason() PruneStatusReason { + return newPruneReason(PruneStatusReasonTypeInUse, pruneStatusReasonMessageInUse) +} + +func pruneSummaryUnresolvedReason() PruneStatusReason { + return newPruneReason(PruneStatusReasonTypeNoMatchingLabelsFound, pruneStatusReasonMessageNoMatchingLabelsFound) +} + +func pruneSummaryTarget(id string) TargetRef { + return TargetRef{ + ID: id, + NamespaceFQN: "https://example.com", + } +} + +func pruneSummaryAttributeValue() *policy.Value { + return testAttributeValue("https://example.com/attr/classification/value/secret", testNamespace("https://example.com")) +} + +func pruneSummarySubjectConditionSet(id string) *policy.SubjectConditionSet { + return &policy.SubjectConditionSet{ + Id: id, + SubjectSets: []*policy.SubjectSet{ + {}, + }, + } +} + +func pruneSummarySubjectMapping(id, actionName string) *policy.SubjectMapping { + return &policy.SubjectMapping{ + Id: id, + AttributeValue: pruneSummaryAttributeValue(), + SubjectConditionSet: &policy.SubjectConditionSet{ + Id: "scs-source", + }, + Actions: []*policy.Action{ + {Id: "action-" + actionName, Name: actionName}, + }, + } +} + +func pruneSummaryRegisteredResource(id, name, actionName string) *policy.RegisteredResource { + return testRegisteredResource( + id, + name, + testRegisteredResourceValue("prod", testActionAttributeValue("action-"+actionName, actionName, pruneSummaryAttributeValue())), + ) +} + +func pruneSummaryObligationTrigger(id, actionName string) *policy.ObligationTrigger { + return &policy.ObligationTrigger{ + Id: id, + AttributeValue: pruneSummaryAttributeValue(), + Action: &policy.Action{ + Id: "action-" + actionName, + Name: actionName, + }, + ObligationValue: &policy.ObligationValue{ + Fqn: "https://example.com/obligation/log/value/default", + }, + Context: []*policy.RequestContext{ + {Pep: &policy.PolicyEnforcementPoint{ClientId: "tdf-client"}}, + }, + } +} diff --git a/otdfctl/migrations/namespacedpolicy/reduce.go b/otdfctl/migrations/namespacedpolicy/reduce.go new file mode 100644 index 0000000000..190af09471 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/reduce.go @@ -0,0 +1,137 @@ +package namespacedpolicy + +import ( + "github.com/opentdf/platform/protocol/go/policy" +) + +// reduceDependencies filters dependency-backed candidate slices in place on the +// provided Retrieved. Callers should assume retrieved.Candidates is modified. +func reduceDependencies(retrieved *Retrieved, scopes scopeSet) { + if retrieved == nil { + return + } + + retrieved.Candidates.Actions = reduceActions(scopes, retrieved.Candidates) + retrieved.Candidates.SubjectConditionSets = reduceSubjectConditionSets(scopes, retrieved.Candidates) +} + +func reduceActions(scopes scopeSet, candidates Candidates) []*policy.Action { + if !scopes.requiresActions() || scopes.has(ScopeActions) { + return candidates.Actions + } + + required := make(map[string]struct{}) + + if scopes.has(ScopeSubjectMappings) { + for _, mapping := range candidates.SubjectMappings { + for _, action := range mapping.GetActions() { + if id := action.GetId(); id != "" { + required[id] = struct{}{} + } + } + } + } + + if scopes.has(ScopeRegisteredResources) { + for _, resource := range candidates.RegisteredResources { + if _, ok := registeredResourceNamespaceRef(resource); !ok { + continue + } + for _, value := range resource.GetValues() { + for _, aav := range value.GetActionAttributeValues() { + if id := aav.GetAction().GetId(); id != "" { + required[id] = struct{}{} + } + } + } + } + } + + if scopes.has(ScopeObligationTriggers) { + for _, trigger := range candidates.ObligationTriggers { + if id := trigger.GetAction().GetId(); id != "" { + required[id] = struct{}{} + } + } + } + + return filterActions(candidates.Actions, required) +} + +// Determine if a registered resource can be derived to one namespace based on +// whether or not the attribute values are all under one namespace. +func registeredResourceNamespaceRef(resource *policy.RegisteredResource) (*policy.Namespace, bool) { + if resource == nil { + return nil, false + } + + var observed *policy.Namespace + for _, value := range resource.GetValues() { + for _, aav := range value.GetActionAttributeValues() { + namespace := namespaceFromAttributeValue(aav.GetAttributeValue()) + if namespaceRefKey(namespace) == "" { + return nil, false + } + if observed == nil { + observed = namespace + continue + } + if !sameNamespace(observed, namespace) { + return nil, false + } + } + } + + return observed, observed != nil +} + +func reduceSubjectConditionSets(scopes scopeSet, candidates Candidates) []*policy.SubjectConditionSet { + if !scopes.requiresSubjectConditionSets() || scopes.has(ScopeSubjectConditionSets) { + return candidates.SubjectConditionSets + } + + required := make(map[string]struct{}) + for _, mapping := range candidates.SubjectMappings { + if id := mapping.GetSubjectConditionSet().GetId(); id != "" { + required[id] = struct{}{} + } + } + + return filterSubjectConditionSets(candidates.SubjectConditionSets, required) +} + +func filterActions(actions []*policy.Action, required map[string]struct{}) []*policy.Action { + if len(required) == 0 { + return nil + } + + filtered := make([]*policy.Action, 0, len(actions)) + for _, action := range actions { + if action == nil { + continue + } + if _, ok := required[action.GetId()]; ok { + filtered = append(filtered, action) + } + } + + return filtered +} + +func filterSubjectConditionSets(sets []*policy.SubjectConditionSet, required map[string]struct{}) []*policy.SubjectConditionSet { + if len(required) == 0 { + return nil + } + + filtered := make([]*policy.SubjectConditionSet, 0, len(sets)) + for _, scs := range sets { + if scs == nil { + continue + } + if _, ok := required[scs.GetId()]; ok { + filtered = append(filtered, scs) + } + } + + return filtered +} diff --git a/otdfctl/migrations/namespacedpolicy/reduce_test.go b/otdfctl/migrations/namespacedpolicy/reduce_test.go new file mode 100644 index 0000000000..be55fae96e --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/reduce_test.go @@ -0,0 +1,158 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReduceDependenciesKeepsOnlySubjectMappingDependencies(t *testing.T) { + t.Parallel() + + scopes, err := normalizeScopes([]Scope{ScopeSubjectMappings}) + require.NoError(t, err) + + retrieved := &Retrieved{ + Candidates: Candidates{ + Actions: []*policy.Action{ + {Id: "action-keep", Name: "decrypt"}, + {Id: "action-drop", Name: "upload"}, + }, + SubjectConditionSets: []*policy.SubjectConditionSet{ + {Id: "scs-keep"}, + {Id: "scs-drop"}, + }, + SubjectMappings: []*policy.SubjectMapping{ + { + Id: "mapping-1", + Actions: []*policy.Action{ + {Id: "action-keep"}, + }, + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-keep"}, + }, + }, + }, + } + + reduceDependencies(retrieved, scopes) + + require.Len(t, retrieved.Candidates.Actions, 1) + assert.Equal(t, "action-keep", retrieved.Candidates.Actions[0].GetId()) + require.Len(t, retrieved.Candidates.SubjectConditionSets, 1) + assert.Equal(t, "scs-keep", retrieved.Candidates.SubjectConditionSets[0].GetId()) +} + +func TestReduceActionsIgnoresRegisteredResourcesWithoutSingleNamespace(t *testing.T) { + t.Parallel() + + scopes, err := normalizeScopes([]Scope{ScopeRegisteredResources}) + require.NoError(t, err) + + resourceWithSingleNamespace := testRegisteredResource( + "resource-keep", + "keep", + testRegisteredResourceValue( + "value-1", + testActionAttributeValue( + "action-keep", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", testNamespace("https://example.com")), + ), + ), + ) + resourceWithConflictingNamespaces := testRegisteredResource( + "resource-drop", + "drop", + testRegisteredResourceValue( + "value-2", + testActionAttributeValue( + "action-drop", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", testNamespace("https://example.com")), + ), + ), + testRegisteredResourceValue( + "value-3", + testActionAttributeValue( + "action-drop", + "decrypt", + testAttributeValue("https://other.example.com/attr/classification/value/secret", testNamespace("https://other.example.com")), + ), + ), + ) + + actions := reduceActions(scopes, Candidates{ + Actions: []*policy.Action{ + {Id: "action-keep", Name: "decrypt"}, + {Id: "action-drop", Name: "decrypt"}, + }, + RegisteredResources: []*policy.RegisteredResource{ + resourceWithSingleNamespace, + resourceWithConflictingNamespaces, + }, + }) + + require.Len(t, actions, 1) + assert.Equal(t, "action-keep", actions[0].GetId()) +} + +func TestRegisteredResourceNamespaceRefAcceptsEquivalentFQNs(t *testing.T) { + t.Parallel() + + resource := testRegisteredResource( + "resource-1", + "resource", + testRegisteredResourceValue( + "value-1", + testActionAttributeValue( + "action-1", + "read", + testAttributeValue("", testNamespace(" https://Example.COM ")), + ), + ), + testRegisteredResourceValue( + "value-2", + testActionAttributeValue( + "action-2", + "read", + testAttributeValue("", testNamespace("https://example.com")), + ), + ), + ) + + namespace, ok := registeredResourceNamespaceRef(resource) + require.True(t, ok) + require.NotNil(t, namespace) + assert.Equal(t, " https://Example.COM ", namespace.GetFqn()) +} + +func TestReduceActionsKeepsOnlyObligationTriggerDependencies(t *testing.T) { + t.Parallel() + + scopes, err := normalizeScopes([]Scope{ScopeObligationTriggers}) + require.NoError(t, err) + + actions := reduceActions(scopes, Candidates{ + Actions: []*policy.Action{ + {Id: "action-keep", Name: "decrypt"}, + {Id: "action-drop", Name: "upload"}, + }, + ObligationTriggers: []*policy.ObligationTrigger{ + { + Id: "trigger-1", + Action: &policy.Action{ + Id: "action-keep", + }, + }, + { + Id: "trigger-2", + Action: &policy.Action{}, + }, + }, + }) + + require.Len(t, actions, 1) + assert.Equal(t, "action-keep", actions[0].GetId()) +} diff --git a/otdfctl/migrations/namespacedpolicy/registered_resources_execute.go b/otdfctl/migrations/namespacedpolicy/registered_resources_execute.go new file mode 100644 index 0000000000..668ec3c81a --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/registered_resources_execute.go @@ -0,0 +1,237 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" +) + +func (e *MigrationExecutor) executeRegisteredResources(ctx context.Context, plans []*RegisteredResourcePlan) error { + if len(plans) == 0 { + return nil + } + + for _, plan := range plans { + if plan == nil || plan.Source == nil { + continue + } + + if plan.Target == nil { + continue + } + + if err := e.executeRegisteredResourceTarget(ctx, plan, plan.Target); err != nil { + return err + } + } + + return nil +} + +func (e *MigrationExecutor) executeRegisteredResourceTarget(ctx context.Context, plan *RegisteredResourcePlan, target *RegisteredResourceTargetPlan) error { + //nolint:exhaustive // Registered-resource execution only handles create and already-migrated explicitly; all other statuses are unsupported. + switch target.Status { + case TargetStatusAlreadyMigrated: + if target.TargetID() == "" { + return fmt.Errorf("%w: registered resource %q target %q", ErrMissingMigratedTarget, plan.Source.GetId(), namespaceLabel(target.Namespace)) + } + return nil + case TargetStatusSkipped: + return nil + case TargetStatusCreate: + return e.createRegisteredResourceTarget(ctx, plan, target) + case TargetStatusUnresolved: + return nil + default: + return fmt.Errorf("%w: registered resource %q target %q has unsupported status %q", ErrUnsupportedStatus, plan.Source.GetId(), namespaceLabel(target.Namespace), target.Status) + } +} + +func (e *MigrationExecutor) createRegisteredResourceTarget(ctx context.Context, plan *RegisteredResourcePlan, target *RegisteredResourceTargetPlan) error { + namespace := namespaceIdentifier(target.Namespace) + if namespace == "" { + return fmt.Errorf("%w: registered resource %q", ErrTargetNamespaceRequired, plan.Source.GetId()) + } + + created, err := e.handler.CreateRegisteredResource( + ctx, + namespace, + plan.Source.GetName(), + nil, + metadataForCreate( + plan.Source.GetId(), + metadataLabels(plan.Source.GetMetadata()), + ), + ) + if err != nil { + target.Execution = &ExecutionResult{ + Failure: err.Error(), + } + return fmt.Errorf("%w: create registered resource %q in namespace %q", err, plan.Source.GetId(), namespaceLabel(target.Namespace)) + } + if created == nil { + target.Execution = &ExecutionResult{ + Failure: ErrMissingCreatedTargetID.Error(), + } + return fmt.Errorf("%w: registered resource %q target %q", ErrMissingCreatedTargetID, plan.Source.GetId(), namespaceLabel(target.Namespace)) + } + if created.GetId() == "" { + target.Execution = &ExecutionResult{ + Failure: ErrMissingCreatedTargetID.Error(), + } + return fmt.Errorf("%w: registered resource %q target %q", ErrMissingCreatedTargetID, plan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + target.Execution = &ExecutionResult{ + Applied: true, + CreatedTargetID: created.GetId(), + } + + existingValues := registeredResourceValueIDsByValue(created) + for _, valuePlan := range target.Values { + if valuePlan == nil || valuePlan.Source == nil { + continue + } + + // RR values are reconciled at runtime so explicit parent reuse can skip + // values that already exist on the chosen parent RR. + if existingID := existingValues[registeredResourceValueKey(valuePlan.Source.GetValue())]; existingID != "" { + valuePlan.Execution = &ExecutionResult{ + Applied: true, + CreatedTargetID: existingID, + } + continue + } + + if err := e.createRegisteredResourceValue(ctx, target, valuePlan); err != nil { + return err + } + } + + return nil +} + +func (e *MigrationExecutor) createRegisteredResourceValue(ctx context.Context, target *RegisteredResourceTargetPlan, valuePlan *RegisteredResourceValuePlan) error { + actionAttributeValues, err := e.registeredResourceActionAttributeValues(target.Namespace, valuePlan) + if err != nil { + valuePlan.Execution = &ExecutionResult{ + Failure: err.Error(), + } + return fmt.Errorf("%w: build registered resource value %q action bindings for namespace %q", err, valuePlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + created, err := e.handler.CreateRegisteredResourceValue( + ctx, + target.TargetID(), + valuePlan.Source.GetValue(), + actionAttributeValues, + metadataForCreate( + valuePlan.Source.GetId(), + metadataLabels(valuePlan.Source.GetMetadata()), + ), + ) + if err != nil { + valuePlan.Execution = &ExecutionResult{ + Failure: err.Error(), + } + return fmt.Errorf("%w: create registered resource value %q for resource %q in namespace %q", err, valuePlan.Source.GetId(), target.TargetID(), namespaceLabel(target.Namespace)) + } + if created.GetId() == "" { + valuePlan.Execution = &ExecutionResult{ + Failure: ErrMissingCreatedTargetID.Error(), + } + return fmt.Errorf("%w: registered resource value %q for target %q", ErrMissingCreatedTargetID, valuePlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + valuePlan.Execution = &ExecutionResult{ + Applied: true, + CreatedTargetID: created.GetId(), + } + + return nil +} + +func (e *MigrationExecutor) registeredResourceActionAttributeValues(namespace *policy.Namespace, valuePlan *RegisteredResourceValuePlan) ([]*registeredresources.ActionAttributeValue, error) { + if valuePlan == nil { + return nil, nil + } + + actionAttributeValues := make([]*registeredresources.ActionAttributeValue, 0, len(valuePlan.ActionBindings)) + for _, binding := range valuePlan.ActionBindings { + if binding == nil { + continue + } + + if binding.SourceActionID == "" { + return nil, fmt.Errorf("%w: action source id is missing", ErrPlanNotExecutable) + } + + actionID := e.cachedActionTargetID(binding.SourceActionID, namespace) + if actionID == "" { + return nil, fmt.Errorf("%w: action %q target %q", ErrMissingMigratedTarget, binding.SourceActionID, namespaceLabel(namespace)) + } + + actionAttributeValue, err := registeredResourceActionAttributeValue(actionID, binding.AttributeValue) + if err != nil { + return nil, fmt.Errorf("registered resource value %q binding action %q: %w", valuePlan.Source.GetId(), binding.SourceActionID, err) + } + actionAttributeValues = append(actionAttributeValues, actionAttributeValue) + } + + return actionAttributeValues, nil +} + +func registeredResourceActionAttributeValue(actionID string, attributeValue *policy.Value) (*registeredresources.ActionAttributeValue, error) { + if strings.TrimSpace(actionID) == "" { + return nil, errors.New("action target id is empty") + } + if attributeValue == nil { + return nil, errors.New("attribute value is missing") + } + + actionAttributeValue := ®isteredresources.ActionAttributeValue{ + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionId{ + ActionId: actionID, + }, + } + + if attributeValueID := strings.TrimSpace(attributeValue.GetId()); attributeValueID != "" { + actionAttributeValue.AttributeValueIdentifier = ®isteredresources.ActionAttributeValue_AttributeValueId{ + AttributeValueId: attributeValueID, + } + return actionAttributeValue, nil + } + + if attributeValueFQN := strings.TrimSpace(attributeValue.GetFqn()); attributeValueFQN != "" { + actionAttributeValue.AttributeValueIdentifier = ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: attributeValueFQN, + } + return actionAttributeValue, nil + } + + return nil, errors.New("attribute value identifier is missing") +} + +func registeredResourceValueIDsByValue(resource *policy.RegisteredResource) map[string]string { + valueIDs := make(map[string]string) + if resource == nil { + return valueIDs + } + + for _, value := range resource.GetValues() { + if value == nil { + continue + } + valueIDs[registeredResourceValueKey(value.GetValue())] = value.GetId() + } + + return valueIDs +} + +func registeredResourceValueKey(value string) string { + return strings.ToLower(strings.TrimSpace(value)) +} diff --git a/otdfctl/migrations/namespacedpolicy/registered_resources_execute_test.go b/otdfctl/migrations/namespacedpolicy/registered_resources_execute_test.go new file mode 100644 index 0000000000..b61afb0011 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/registered_resources_execute_test.go @@ -0,0 +1,439 @@ +package namespacedpolicy + +import ( + "errors" + "testing" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExecuteRegisteredResources(t *testing.T) { + t.Parallel() + + namespace1 := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + errBoom := errors.New("boom") + + tests := []struct { + name string + plan *MigrationPlan + handler *mockExecutorHandler + wantErr *expectedError + assert func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) + }{ + { + name: "creates registered resource shell and values with authoritative action target ids", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeRegisteredResources}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "read"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + { + Source: &policy.Action{Id: "action-2", Name: "standard-read"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusExistingStandard, + ExistingID: "existing-standard-action-2", + }, + }, + }, + }, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{ + Id: "rr-1", + Name: "repo", + Metadata: &common.Metadata{ + Labels: map[string]string{ + "owner": "policy-team", + }, + }, + }, + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + Values: []*RegisteredResourceValuePlan{ + { + Source: &policy.RegisteredResourceValue{ + Id: "rrv-1", + Value: "repo-a", + Metadata: &common.Metadata{ + Labels: map[string]string{ + "classification": "secret", + }, + }, + }, + ActionBindings: []*RegisteredResourceActionBinding{ + { + SourceActionID: "action-1", + AttributeValue: &policy.Value{ + Id: "attribute-value-id-1", + Fqn: "https://example.com/attr/classification/value/secret", + }, + }, + { + SourceActionID: "action-2", + AttributeValue: &policy.Value{ + Fqn: "https://example.com/attr/project/value/apollo", + }, + }, + }, + }, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + results: map[string]map[string]*policy.Action{ + "read": { + "ns-1": {Id: "created-action-1", Name: "read"}, + }, + }, + registeredResourceResult: map[string]map[string]*policy.RegisteredResource{ + "rr-1": { + "ns-1": {Id: "created-rr-1", Name: "repo"}, + }, + }, + registeredResourceValueResult: map[string]map[string]*policy.RegisteredResourceValue{ + "rrv-1": { + "created-rr-1": {Id: "created-rrv-1", Value: "repo-a"}, + }, + }, + }, + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + + require.Contains(t, handler.createdRegisteredResources, "rr-1") + require.Contains(t, handler.createdRegisteredResources["rr-1"], "ns-1") + resourceCall := handler.createdRegisteredResources["rr-1"]["ns-1"] + assert.Equal(t, "repo", resourceCall.Name) + assert.Equal(t, "ns-1", resourceCall.Namespace) + assert.Empty(t, resourceCall.Values) + assert.Equal(t, map[string]string{ + "owner": "policy-team", + migrationLabelMigratedFrom: "rr-1", + }, resourceCall.Metadata.GetLabels()) + + require.Contains(t, handler.createdRegisteredResourceValues, "rrv-1") + require.Contains(t, handler.createdRegisteredResourceValues["rrv-1"], "created-rr-1") + valueCall := handler.createdRegisteredResourceValues["rrv-1"]["created-rr-1"] + assert.Equal(t, "created-rr-1", valueCall.ResourceID) + assert.Equal(t, "repo-a", valueCall.Value) + assert.Equal(t, map[string]string{ + "classification": "secret", + migrationLabelMigratedFrom: "rrv-1", + }, valueCall.Metadata.GetLabels()) + require.Len(t, valueCall.ActionAttributeValues, 2) + assert.Equal(t, "created-action-1", valueCall.ActionAttributeValues[0].GetActionId()) + assert.Equal(t, "attribute-value-id-1", valueCall.ActionAttributeValues[0].GetAttributeValueId()) + assert.Equal(t, "existing-standard-action-2", valueCall.ActionAttributeValues[1].GetActionId()) + assert.Equal(t, "https://example.com/attr/project/value/apollo", valueCall.ActionAttributeValues[1].GetAttributeValueFqn()) + + resourceTarget := plan.RegisteredResources[0].Target + require.NotNil(t, resourceTarget.Execution) + assert.True(t, resourceTarget.Execution.Applied) + assert.Equal(t, "created-rr-1", resourceTarget.Execution.CreatedTargetID) + assert.Equal(t, "created-rr-1", resourceTarget.TargetID()) + + valueTarget := plan.RegisteredResources[0].Target.Values[0] + require.NotNil(t, valueTarget.Execution) + assert.True(t, valueTarget.Execution.Applied) + assert.Equal(t, "created-rrv-1", valueTarget.Execution.CreatedTargetID) + assert.Equal(t, "created-rrv-1", valueTarget.TargetID()) + + assert.Equal(t, "created-action-1", executor.cachedActionTargetID("action-1", namespace1)) + assert.Equal(t, "existing-standard-action-2", executor.cachedActionTargetID("action-2", namespace1)) + }, + }, + { + name: "skips already migrated registered resource targets", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeRegisteredResources}, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{Id: "rr-1", Name: "repo"}, + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + ExistingID: "migrated-rr-1", + Values: []*RegisteredResourceValuePlan{ + { + Source: &policy.RegisteredResourceValue{Id: "rrv-1", Value: "repo-a"}, + }, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Nil(t, handler.createdRegisteredResources) + assert.Nil(t, handler.createdRegisteredResourceValues) + assert.Equal(t, "migrated-rr-1", plan.RegisteredResources[0].Target.TargetID()) + assert.Nil(t, plan.RegisteredResources[0].Target.Execution) + assert.Nil(t, plan.RegisteredResources[0].Target.Values[0].Execution) + }, + }, + { + name: "skips skipped registered resource targets", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{Id: "rr-1", Name: "repo"}, + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace1, + Status: TargetStatusSkipped, + Reason: skippedByUserReason, + Values: []*RegisteredResourceValuePlan{ + { + Source: &policy.RegisteredResourceValue{Id: "rrv-1", Value: "repo-a"}, + }, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Nil(t, handler.createdRegisteredResources) + assert.Nil(t, handler.createdRegisteredResourceValues) + assert.Nil(t, plan.RegisteredResources[0].Target.Execution) + assert.Nil(t, plan.RegisteredResources[0].Target.Values[0].Execution) + }, + }, + { + name: "ignores unresolved target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeRegisteredResources}, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{Id: "rr-1", Name: "repo"}, + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace1, + Status: TargetStatusUnresolved, + Reason: "ambiguous target namespace", + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Nil(t, handler.createdRegisteredResources) + assert.Nil(t, handler.createdRegisteredResourceValues) + }, + }, + { + name: "ignores unresolved registered resource entry without target", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{Id: "rr-1", Name: "repo"}, + Unresolved: "registered resource spans multiple target namespaces", + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Nil(t, handler.createdRegisteredResources) + assert.Nil(t, handler.createdRegisteredResourceValues) + }, + }, + { + name: "records shell creation failures on the target", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeRegisteredResources}, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{Id: "rr-1", Name: "repo"}, + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + registeredResourceErrs: map[string]map[string]error{ + "rr-1": { + "ns-1": errBoom, + }, + }, + }, + wantErr: wantError(errBoom, `create registered resource %q in namespace %q`, "rr-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.createdRegisteredResources, "rr-1") + require.NotNil(t, plan.RegisteredResources[0].Target.Execution) + assert.Equal(t, "boom", plan.RegisteredResources[0].Target.Execution.Failure) + }, + }, + { + name: "stops when a registered resource value cannot resolve its migrated action target", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeRegisteredResources}, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{Id: "rr-1", Name: "repo"}, + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + Values: []*RegisteredResourceValuePlan{ + { + Source: &policy.RegisteredResourceValue{Id: "rrv-1", Value: "repo-a"}, + ActionBindings: []*RegisteredResourceActionBinding{ + { + SourceActionID: "missing-action", + AttributeValue: &policy.Value{Id: "attribute-value-id-1"}, + }, + }, + }, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + registeredResourceResult: map[string]map[string]*policy.RegisteredResource{ + "rr-1": { + "ns-1": {Id: "created-rr-1", Name: "repo"}, + }, + }, + }, + wantErr: wantError( + ErrMissingMigratedTarget, + `action %q target %q: build registered resource value %q action bindings for namespace %q`, + "missing-action", + namespace1.GetFqn(), + "rrv-1", + namespace1.GetFqn(), + ), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.createdRegisteredResources, "rr-1") + assert.Nil(t, handler.createdRegisteredResourceValues) + require.NotNil(t, plan.RegisteredResources[0].Target.Execution) + assert.True(t, plan.RegisteredResources[0].Target.Execution.Applied) + require.NotNil(t, plan.RegisteredResources[0].Target.Values[0].Execution) + assert.Contains(t, plan.RegisteredResources[0].Target.Values[0].Execution.Failure, `missing migrated target: action "missing-action" target "https://example.com"`) + }, + }, + { + name: "records value creation failures on the value target", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeRegisteredResources}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "read"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + RegisteredResources: []*RegisteredResourcePlan{ + { + Source: &policy.RegisteredResource{Id: "rr-1", Name: "repo"}, + Target: &RegisteredResourceTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + Values: []*RegisteredResourceValuePlan{ + { + Source: &policy.RegisteredResourceValue{Id: "rrv-1", Value: "repo-a"}, + ActionBindings: []*RegisteredResourceActionBinding{ + { + SourceActionID: "action-1", + AttributeValue: &policy.Value{Id: "attribute-value-id-1"}, + }, + }, + }, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + results: map[string]map[string]*policy.Action{ + "read": { + "ns-1": {Id: "created-action-1", Name: "read"}, + }, + }, + registeredResourceResult: map[string]map[string]*policy.RegisteredResource{ + "rr-1": { + "ns-1": {Id: "created-rr-1", Name: "repo"}, + }, + }, + registeredResourceValueErrs: map[string]map[string]error{ + "rrv-1": { + "created-rr-1": errBoom, + }, + }, + }, + wantErr: wantError(errBoom, `create registered resource value %q for resource %q in namespace %q`, "rrv-1", "created-rr-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.createdRegisteredResourceValues, "rrv-1") + require.NotNil(t, plan.RegisteredResources[0].Target.Execution) + assert.True(t, plan.RegisteredResources[0].Target.Execution.Applied) + require.NotNil(t, plan.RegisteredResources[0].Target.Values[0].Execution) + assert.Equal(t, "boom", plan.RegisteredResources[0].Target.Values[0].Execution.Failure) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + executor, err := NewMigrationExecutor(tt.handler) + require.NoError(t, err) + + err = executor.ExecuteMigration(t.Context(), tt.plan) + switch { + case tt.wantErr != nil: + require.Error(t, err) + require.ErrorIs(t, err, tt.wantErr.is) + require.EqualError(t, err, tt.wantErr.message) + default: + require.NoError(t, err) + } + + tt.assert(t, err, executor, tt.handler, tt.plan) + }) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/resolved.go b/otdfctl/migrations/namespacedpolicy/resolved.go new file mode 100644 index 0000000000..22687a93aa --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/resolved.go @@ -0,0 +1,485 @@ +package namespacedpolicy + +import ( + "errors" + "fmt" + + "github.com/opentdf/platform/protocol/go/policy" +) + +type ResolvedTargets struct { + Scopes []Scope + Actions []*ResolvedAction + SubjectConditionSets []*ResolvedSubjectConditionSet + SubjectMappings []*ResolvedSubjectMapping + RegisteredResources []*ResolvedRegisteredResource + ObligationTriggers []*ResolvedObligationTrigger +} + +type ResolvedAction struct { + Source *policy.Action + Results []*ResolvedActionResult +} + +type ResolvedActionResult struct { + Namespace *policy.Namespace + AlreadyMigrated *policy.Action + ExistingStandard *policy.Action + NeedsCreate bool +} + +type ResolvedSubjectConditionSet struct { + Source *policy.SubjectConditionSet + Results []*ResolvedSubjectConditionSetResult +} + +type ResolvedSubjectConditionSetResult struct { + Namespace *policy.Namespace + AlreadyMigrated *policy.SubjectConditionSet + NeedsCreate bool +} + +type ResolvedSubjectMapping struct { + Source *policy.SubjectMapping + Namespace *policy.Namespace + AlreadyMigrated *policy.SubjectMapping + NeedsCreate bool +} + +type ResolvedRegisteredResource struct { + Source *policy.RegisteredResource + Namespace *policy.Namespace + AlreadyMigrated *policy.RegisteredResource + NeedsCreate bool + Unresolved *Unresolved +} + +type ResolvedObligationTrigger struct { + Source *policy.ObligationTrigger + Namespace *policy.Namespace + AlreadyMigrated *policy.ObligationTrigger + NeedsCreate bool +} + +type resolver struct { + derived *DerivedTargets + existing *ExistingTargets + scopes scopeSet + actionResultsByKey map[string]*ResolvedActionResult + scsResultsByKey map[string]*ResolvedSubjectConditionSetResult +} + +// resolveExisting classifies each derived source/target placement as already +// migrated, satisfied by an existing target object, needing creation, or still +// unresolved. This is the phase that ties the derived namespace targets to live +// target-side state before the final per-namespace plan is built. +func resolveExisting(derived *DerivedTargets, existing *ExistingTargets) (*ResolvedTargets, error) { + if existing == nil { + existing = newExistingTargets() + } + + r := &resolver{ + derived: derived, + existing: existing, + scopes: scopesFromSlice(derived.Scopes), + actionResultsByKey: make(map[string]*ResolvedActionResult), + scsResultsByKey: make(map[string]*ResolvedSubjectConditionSetResult), + } + + resolvedActions, err := r.resolveActions() + if err != nil { + return nil, err + } + resolvedSubjectConditionSets, err := r.resolveSubjectConditionSets() + if err != nil { + return nil, err + } + resolvedSubjectMappings, err := r.resolveSubjectMappings() + if err != nil { + return nil, err + } + resolvedRegisteredResources, err := r.resolveRegisteredResources() + if err != nil { + return nil, err + } + resolvedObligationTriggers, err := r.resolveObligationTriggers() + if err != nil { + return nil, err + } + + return &ResolvedTargets{ + Scopes: append([]Scope(nil), derived.Scopes...), + Actions: resolvedActions, + SubjectConditionSets: resolvedSubjectConditionSets, + SubjectMappings: resolvedSubjectMappings, + RegisteredResources: resolvedRegisteredResources, + ObligationTriggers: resolvedObligationTriggers, + }, nil +} + +func (r *resolver) resolveActions() ([]*ResolvedAction, error) { + if r == nil || r.derived == nil { + return nil, nil + } + + resolved := make([]*ResolvedAction, 0, len(r.derived.Actions)) + for _, action := range r.derived.Actions { + item, err := r.resolveAction(action) + if err != nil { + return nil, err + } + resolved = append(resolved, item) + } + return resolved, nil +} + +func (r *resolver) resolveAction(derived *DerivedAction) (*ResolvedAction, error) { + if derived == nil || derived.Source == nil { + return nil, fmt.Errorf("%w: empty action candidate", ErrUndeterminedTargetMapping) + } + + item := &ResolvedAction{ + Source: derived.Source, + Results: make([]*ResolvedActionResult, 0, len(derived.Targets)), + } + for _, namespace := range derived.Targets { + result, err := r.resolveActionTargetFromExisting(derived.Source, namespace) + if err != nil { + return nil, fmt.Errorf("action %q in namespace %q: %w", derived.Source.GetId(), namespace.GetId(), err) + } + item.Results = append(item.Results, result) + r.addActionResult(derived.Source.GetId(), result) + } + + return item, nil +} + +func (r *resolver) resolveActionTargetFromExisting(source *policy.Action, namespace *policy.Namespace) (*ResolvedActionResult, error) { + if namespace.GetId() == "" { + return nil, fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + + result := &ResolvedActionResult{Namespace: namespace} + if isStandardAction(source) { + return r.resolveStandardActionTarget(source, namespace) + } + + if existing, found := resolveExistingAction(source, r.existing.CustomActions[namespace.GetId()]); found { + result.AlreadyMigrated = existing + return result, nil + } + + result.NeedsCreate = true + return result, nil +} + +func (r *resolver) resolveStandardActionTarget(source *policy.Action, namespace *policy.Namespace) (*ResolvedActionResult, error) { + for _, action := range r.existing.StandardActions[namespace.GetId()] { + if actionCanonicalEqual(source, action) { + return &ResolvedActionResult{Namespace: namespace, ExistingStandard: action}, nil + } + } + return nil, errors.New("matching standard action not found in target namespace") +} + +func (r *resolver) resolveSubjectConditionSets() ([]*ResolvedSubjectConditionSet, error) { + if r == nil || r.derived == nil { + return nil, nil + } + + resolved := make([]*ResolvedSubjectConditionSet, 0, len(r.derived.SubjectConditionSets)) + for _, scs := range r.derived.SubjectConditionSets { + item, err := r.resolveSubjectConditionSet(scs) + if err != nil { + return nil, err + } + resolved = append(resolved, item) + } + return resolved, nil +} + +func (r *resolver) resolveSubjectConditionSet(derived *DerivedSubjectConditionSet) (*ResolvedSubjectConditionSet, error) { + if derived == nil || derived.Source == nil { + return nil, fmt.Errorf("%w: empty subject condition set candidate", ErrUndeterminedTargetMapping) + } + + item := &ResolvedSubjectConditionSet{ + Source: derived.Source, + Results: make([]*ResolvedSubjectConditionSetResult, 0, len(derived.Targets)), + } + for _, namespace := range derived.Targets { + result, err := r.resolveSubjectConditionSetTargetFromExisting(derived.Source, namespace) + if err != nil { + return nil, fmt.Errorf("subject condition set %q in namespace %q: %w", derived.Source.GetId(), namespace.GetId(), err) + } + item.Results = append(item.Results, result) + r.addSubjectConditionSetResult(derived.Source.GetId(), result) + } + + return item, nil +} + +func (r *resolver) resolveSubjectConditionSetTargetFromExisting(source *policy.SubjectConditionSet, namespace *policy.Namespace) (*ResolvedSubjectConditionSetResult, error) { + if namespace.GetId() == "" { + return nil, fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + result := &ResolvedSubjectConditionSetResult{Namespace: namespace} + if existing, found := resolveExistingSubjectConditionSet(source, r.existing.SubjectConditionSets[namespace.GetId()]); found { + result.AlreadyMigrated = existing + } else { + result.NeedsCreate = true + } + + return result, nil +} + +func (r *resolver) resolveSubjectMappings() ([]*ResolvedSubjectMapping, error) { + if r == nil || r.derived == nil || !r.scopes.has(ScopeSubjectMappings) { + return nil, nil + } + + resolved := make([]*ResolvedSubjectMapping, 0, len(r.derived.SubjectMappings)) + for _, mapping := range r.derived.SubjectMappings { + item, err := r.resolveSubjectMapping(mapping) + if err != nil { + return nil, err + } + resolved = append(resolved, item) + } + return resolved, nil +} + +func (r *resolver) resolveSubjectMapping(derived *DerivedSubjectMapping) (*ResolvedSubjectMapping, error) { + if derived == nil || derived.Source == nil { + return nil, fmt.Errorf("%w: empty subject mapping candidate", ErrUndeterminedTargetMapping) + } + + if derived.Target == nil { + return nil, fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + + item := &ResolvedSubjectMapping{ + Source: derived.Source, + Namespace: derived.Target, + } + // Subject mappings are only safe to resolve once their action and subject + // condition set dependencies are themselves resolvable in the same target + // namespace. This keeps the plan graph internally consistent. + if err := r.resolveSubjectMappingDependencies(item.Source, item.Namespace); err != nil { + return nil, fmt.Errorf("subject mapping %q in namespace %q: %w", item.Source.GetId(), item.Namespace.GetId(), err) + } + + if existing, found := resolveExistingSubjectMapping(item.Source, r.existing.SubjectMappings[item.Namespace.GetId()]); found { + item.AlreadyMigrated = existing + } else { + item.NeedsCreate = true + } + + return item, nil +} + +func (r *resolver) resolveRegisteredResources() ([]*ResolvedRegisteredResource, error) { + if r == nil || r.derived == nil || !r.scopes.has(ScopeRegisteredResources) { + return nil, nil + } + + resolved := make([]*ResolvedRegisteredResource, 0, len(r.derived.RegisteredResources)) + for _, resource := range r.derived.RegisteredResources { + item, err := r.resolveRegisteredResource(resource) + if err != nil { + return nil, err + } + resolved = append(resolved, item) + } + return resolved, nil +} + +func (r *resolver) resolveRegisteredResource(derived *DerivedRegisteredResource) (*ResolvedRegisteredResource, error) { + item := &ResolvedRegisteredResource{} + if derived == nil { + return item, nil + } + + item.Source = derived.Source + item.Namespace = derived.Target + item.Unresolved = derived.Unresolved + + if item.Unresolved != nil { + return item, nil + } + if item.Source == nil { + return nil, fmt.Errorf("%w: registered resource is empty", ErrUndeterminedTargetMapping) + } + if item.Namespace == nil { + return nil, fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + if err := r.resolveRegisteredResourceDependencies(item.Source, item.Namespace); err != nil { + return nil, fmt.Errorf("registered resource %q in namespace %q: %w", item.Source.GetId(), item.Namespace.GetId(), err) + } + if existing, found := resolveExistingRegisteredResource(item.Source, r.existing.RegisteredResources[item.Namespace.GetId()]); found { + item.AlreadyMigrated = existing + } else { + item.NeedsCreate = true + } + + return item, nil +} + +func (r *resolver) resolveObligationTriggers() ([]*ResolvedObligationTrigger, error) { + if r == nil || r.derived == nil || !r.scopes.has(ScopeObligationTriggers) { + return nil, nil + } + + resolved := make([]*ResolvedObligationTrigger, 0, len(r.derived.ObligationTriggers)) + for _, trigger := range r.derived.ObligationTriggers { + item, err := r.resolveObligationTrigger(trigger) + if err != nil { + return nil, err + } + resolved = append(resolved, item) + } + return resolved, nil +} + +func (r *resolver) resolveObligationTrigger(derived *DerivedObligationTrigger) (*ResolvedObligationTrigger, error) { + if derived == nil || derived.Source == nil { + return nil, fmt.Errorf("%w: empty obligation trigger candidate", ErrUndeterminedTargetMapping) + } + if derived.Target == nil { + return nil, fmt.Errorf("%w: empty namespace reference", ErrUndeterminedTargetMapping) + } + + item := &ResolvedObligationTrigger{ + Source: derived.Source, + Namespace: derived.Target, + } + if err := r.resolveObligationTriggerDependencies(item.Source, item.Namespace); err != nil { + return nil, fmt.Errorf("obligation trigger %q in namespace %q: %w", item.Source.GetId(), item.Namespace.GetId(), err) + } + if existing, found := resolveExistingObligationTrigger(item.Source, r.existing.ObligationTriggers[item.Namespace.GetId()]); found { + item.AlreadyMigrated = existing + } else { + item.NeedsCreate = true + } + + return item, nil +} + +func (r *resolver) addActionResult(sourceID string, result *ResolvedActionResult) { + if sourceID == "" || result == nil || result.Namespace == nil || result.Namespace.GetId() == "" { + return + } + r.actionResultsByKey[resolvedResultKey(sourceID, result.Namespace.GetId())] = result +} + +func (r *resolver) addSubjectConditionSetResult(sourceID string, result *ResolvedSubjectConditionSetResult) { + if sourceID == "" || result == nil || result.Namespace == nil || result.Namespace.GetId() == "" { + return + } + r.scsResultsByKey[resolvedResultKey(sourceID, result.Namespace.GetId())] = result +} + +func (r *resolver) resolveSubjectMappingDependencies(mapping *policy.SubjectMapping, namespace *policy.Namespace) error { + for _, action := range mapping.GetActions() { + actionID := action.GetId() + if actionID == "" { + return ErrMissingActionID + } + if r.actionResultsByKey[resolvedResultKey(actionID, namespace.GetId())] == nil { + return fmt.Errorf("%w: action %q", ErrUnresolvedActionDependency, actionID) + } + } + + scsID := mapping.GetSubjectConditionSet().GetId() + if scsID == "" { + return ErrMissingSubjectConditionSetID + } + if r.scsResultsByKey[resolvedResultKey(scsID, namespace.GetId())] == nil { + return fmt.Errorf("%w: subject condition set %q", ErrUnresolvedSubjectConditionSetDependency, scsID) + } + + return nil +} + +func (r *resolver) resolveRegisteredResourceDependencies(resource *policy.RegisteredResource, namespace *policy.Namespace) error { + for _, value := range resource.GetValues() { + for _, aav := range value.GetActionAttributeValues() { + actionID := aav.GetAction().GetId() + if actionID == "" { + return ErrMissingActionID + } + if r.actionResultsByKey[resolvedResultKey(actionID, namespace.GetId())] == nil { + return fmt.Errorf("%w: action %q", ErrUnresolvedActionDependency, actionID) + } + } + } + return nil +} + +func (r *resolver) resolveObligationTriggerDependencies(trigger *policy.ObligationTrigger, namespace *policy.Namespace) error { + actionID := trigger.GetAction().GetId() + if actionID == "" { + return ErrMissingActionID + } + if r.actionResultsByKey[resolvedResultKey(actionID, namespace.GetId())] == nil { + return fmt.Errorf("%w: action %q", ErrUnresolvedActionDependency, actionID) + } + return nil +} + +func resolveExistingAction(source *policy.Action, existing []*policy.Action) (*policy.Action, bool) { + for _, action := range existing { + if actionCanonicalEqual(source, action) { + return action, true + } + } + return nil, false +} + +func resolveExistingSubjectConditionSet(source *policy.SubjectConditionSet, existing []*policy.SubjectConditionSet) (*policy.SubjectConditionSet, bool) { + for _, scs := range existing { + if subjectConditionSetCanonicalEqual(source, scs) { + return scs, true + } + } + return nil, false +} + +func resolveExistingSubjectMapping(source *policy.SubjectMapping, existing []*policy.SubjectMapping) (*policy.SubjectMapping, bool) { + for _, mapping := range existing { + if subjectMappingCanonicalEqual(source, mapping) { + return mapping, true + } + } + return nil, false +} + +func resolveExistingRegisteredResource(source *policy.RegisteredResource, existing []*policy.RegisteredResource) (*policy.RegisteredResource, bool) { + for _, resource := range existing { + if registeredResourceCanonicalEqual(source, resource) { + return resource, true + } + } + return nil, false +} + +func resolveExistingObligationTrigger(source *policy.ObligationTrigger, existing []*policy.ObligationTrigger) (*policy.ObligationTrigger, bool) { + for _, trigger := range existing { + if obligationTriggerCanonicalEqual(source, trigger) { + return trigger, true + } + } + return nil, false +} + +func resolvedResultKey(sourceID, namespaceID string) string { + return sourceID + "|" + namespaceID +} + +func scopesFromSlice(scopes []Scope) scopeSet { + set := make(scopeSet, len(scopes)) + for _, scope := range scopes { + set[scope] = struct{}{} + } + return set +} diff --git a/otdfctl/migrations/namespacedpolicy/resolved_test.go b/otdfctl/migrations/namespacedpolicy/resolved_test.go new file mode 100644 index 0000000000..f00ec421d9 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/resolved_test.go @@ -0,0 +1,890 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveExistingUsesExistingStandardAction(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + existing := newExistingTargets() + existing.StandardActions[namespace.GetId()] = []*policy.Action{ + {Id: "read-target", Name: "read", Namespace: namespace}, + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "legacy-read", Name: "read"}, + Targets: []*policy.Namespace{namespace}, + }, + }, + }, + existing, + ) + require.NoError(t, err) + + require.Len(t, resolved.Actions, 1) + require.Len(t, resolved.Actions[0].Results, 1) + assert.Equal(t, "read-target", resolved.Actions[0].Results[0].ExistingStandard.GetId()) + assert.False(t, resolved.Actions[0].Results[0].NeedsCreate) +} + +func TestResolveExistingFailsWhenActionCandidateIsNil(t *testing.T) { + t.Parallel() + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + Actions: []*DerivedAction{nil}, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + assert.EqualError(t, err, "could not determine target namespace: empty action candidate") +} + +func TestResolveExistingFailsWhenSubjectConditionSetCandidateIsNil(t *testing.T) { + t.Parallel() + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*DerivedSubjectConditionSet{nil}, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + assert.EqualError(t, err, "could not determine target namespace: empty subject condition set candidate") +} + +func TestResolveExistingFailsWhenSubjectMappingCandidateIsNil(t *testing.T) { + t.Parallel() + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*DerivedSubjectMapping{nil}, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + assert.EqualError(t, err, "could not determine target namespace: empty subject mapping candidate") +} + +func TestResolveExistingFailsWhenSubjectMappingActionDependencyMissing(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions, ScopeSubjectConditionSets, ScopeSubjectMappings}, + SubjectConditionSets: []*DerivedSubjectConditionSet{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*policy.Namespace{namespace}, + }, + }, + SubjectMappings: []*DerivedSubjectMapping{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + Actions: []*policy.Action{ + {Id: "action-1", Name: "decrypt"}, + }, + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + }, + Target: namespace, + }, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrUnresolvedActionDependency) + assert.Contains(t, err.Error(), `subject mapping "mapping-1" in namespace "ns-1"`) + assert.Contains(t, err.Error(), `action "action-1"`) +} + +func TestResolveExistingKeepsRegisteredResourceConflictUnresolved(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*DerivedRegisteredResource{ + { + Source: &policy.RegisteredResource{Id: "resource-1", Name: "documents"}, + Target: namespace, + Unresolved: &Unresolved{ + Reason: UnresolvedReasonRegisteredResourceConflictingNamespaces, + Message: "could not determine target namespace: registered resource spans multiple target namespaces", + }, + }, + }, + }, + nil, + ) + require.NoError(t, err) + require.Len(t, resolved.RegisteredResources, 1) + require.NotNil(t, resolved.RegisteredResources[0].Unresolved) + assert.Equal(t, UnresolvedReasonRegisteredResourceConflictingNamespaces, resolved.RegisteredResources[0].Unresolved.Reason) + assert.Equal( + t, + "could not determine target namespace: registered resource spans multiple target namespaces", + resolved.RegisteredResources[0].Unresolved.Message, + ) + assert.False(t, resolved.RegisteredResources[0].NeedsCreate) +} + +func TestResolveExistingCustomActionReturnsFirstCanonicalMatch(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + existing := newExistingTargets() + // Two canonically-equal custom actions. Pinning first-match-wins here + // guards against a future refactor silently reintroducing ambiguous + // ordering (e.g. switching to map iteration). + existing.CustomActions[namespace.GetId()] = []*policy.Action{ + {Id: "first-match", Name: "decrypt-custom"}, + {Id: "second-match", Name: "DECRYPT-CUSTOM"}, + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "legacy", Name: "decrypt-custom"}, + Targets: []*policy.Namespace{namespace}, + }, + }, + }, + existing, + ) + require.NoError(t, err) + require.Len(t, resolved.Actions, 1) + require.Len(t, resolved.Actions[0].Results, 1) + assert.Equal(t, "first-match", resolved.Actions[0].Results[0].AlreadyMigrated.GetId()) + assert.False(t, resolved.Actions[0].Results[0].NeedsCreate) +} + +func TestResolveExistingStandardActionReturnsFirstCanonicalMatch(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + existing := newExistingTargets() + existing.StandardActions[namespace.GetId()] = []*policy.Action{ + {Id: "first-match", Name: "read", Namespace: namespace}, + {Id: "second-match", Name: "READ", Namespace: namespace}, + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "legacy", Name: "read"}, + Targets: []*policy.Namespace{namespace}, + }, + }, + }, + existing, + ) + require.NoError(t, err) + require.Len(t, resolved.Actions, 1) + require.Len(t, resolved.Actions[0].Results, 1) + assert.Equal(t, "first-match", resolved.Actions[0].Results[0].ExistingStandard.GetId()) + assert.False(t, resolved.Actions[0].Results[0].NeedsCreate) +} + +func TestResolveExistingStandardActionFailsWhenNoMatchInTargetNamespace(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + existing := newExistingTargets() + existing.StandardActions[namespace.GetId()] = []*policy.Action{ + {Id: "non-matching", Name: "write", Namespace: namespace}, + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "legacy", Name: "read"}, + Targets: []*policy.Namespace{namespace}, + }, + }, + }, + existing, + ) + require.Error(t, err) + assert.Nil(t, resolved) + assert.EqualError( + t, + err, + `action "legacy" in namespace "ns-1": matching standard action not found in target namespace`, + ) +} + +func TestResolveExistingRoutesStandardActionsByName(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + sourceName string + }{ + {name: "create", sourceName: "create"}, + {name: "read", sourceName: "read"}, + {name: "update", sourceName: "update"}, + {name: "delete", sourceName: "delete"}, + {name: "uppercase routes to standard path", sourceName: "CREATE"}, + {name: "whitespace-padded routes to standard path", sourceName: " read "}, + {name: "mixed case routes to standard path", sourceName: "UpDaTe"}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + existing := newExistingTargets() + // Populating only StandardActions forces this to fail if the + // source is misrouted through the custom-action matcher. + existing.StandardActions[namespace.GetId()] = []*policy.Action{ + {Id: "standard-target", Name: tc.sourceName, Namespace: namespace}, + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "legacy", Name: tc.sourceName}, + Targets: []*policy.Namespace{namespace}, + }, + }, + }, + existing, + ) + require.NoError(t, err) + require.Len(t, resolved.Actions, 1) + require.Len(t, resolved.Actions[0].Results, 1) + assert.Equal(t, "standard-target", resolved.Actions[0].Results[0].ExistingStandard.GetId()) + assert.False(t, resolved.Actions[0].Results[0].NeedsCreate) + }) + } +} + +func TestResolveExistingRoutesStandardActionsByEnumRegardlessOfName(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + existing := newExistingTargets() + // Source name is not one of create/read/update/delete, so routing relies + // entirely on the proto Standard enum to reach the standard-action path. + existing.StandardActions[namespace.GetId()] = []*policy.Action{ + { + Id: "standard-target", + Name: "decrypt", + Value: &policy.Action_Standard{Standard: policy.Action_STANDARD_ACTION_DECRYPT}, + Namespace: namespace, + }, + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{ + Id: "legacy", + Name: "decrypt", + Value: &policy.Action_Standard{Standard: policy.Action_STANDARD_ACTION_DECRYPT}, + }, + Targets: []*policy.Namespace{namespace}, + }, + }, + }, + existing, + ) + require.NoError(t, err) + require.Len(t, resolved.Actions, 1) + require.Len(t, resolved.Actions[0].Results, 1) + assert.Equal(t, "standard-target", resolved.Actions[0].Results[0].ExistingStandard.GetId()) +} + +func TestResolveExistingSubjectMappingFailsWhenDerivedTargetIsNil(t *testing.T) { + t.Parallel() + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*DerivedSubjectMapping{ + {Source: &policy.SubjectMapping{Id: "mapping-1"}}, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrUndeterminedTargetMapping) + assert.EqualError(t, err, "could not determine target namespace: empty namespace reference") +} + +func TestResolveExistingRegisteredResourceFailsWhenSourceIsNil(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*DerivedRegisteredResource{ + {Target: namespace}, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrUndeterminedTargetMapping) + assert.EqualError(t, err, "could not determine target namespace: registered resource is empty") +} + +func TestResolveExistingRegisteredResourceFailsWhenNamespaceIsNil(t *testing.T) { + t.Parallel() + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*DerivedRegisteredResource{ + {Source: &policy.RegisteredResource{Id: "resource-1", Name: "documents"}}, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrUndeterminedTargetMapping) + assert.EqualError(t, err, "could not determine target namespace: empty namespace reference") +} + +func TestResolveExistingObligationTriggerFailsWhenDerivedTargetIsNil(t *testing.T) { + t.Parallel() + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeObligationTriggers}, + ObligationTriggers: []*DerivedObligationTrigger{ + {Source: &policy.ObligationTrigger{Id: "trigger-1"}}, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrUndeterminedTargetMapping) + assert.EqualError(t, err, "could not determine target namespace: empty namespace reference") +} + +func TestResolveExistingSubjectMappingFailsWhenSubjectConditionSetHasNoID(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + // Mapping has no actions (skipping the action dependency loop) and no + // SubjectConditionSet, so GetSubjectConditionSet().GetId() is "". + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*DerivedSubjectMapping{ + { + Source: &policy.SubjectMapping{Id: "mapping-1"}, + Target: namespace, + }, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrMissingSubjectConditionSetID) + assert.Contains(t, err.Error(), `subject mapping "mapping-1" in namespace "ns-1"`) +} + +func TestResolveExistingSubjectMappingFailsWhenSubjectConditionSetNotResolvedInNamespace(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + otherNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://other.example.com", + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions, ScopeSubjectConditionSets, ScopeSubjectMappings}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*policy.Namespace{namespace}, + }, + }, + SubjectConditionSets: []*DerivedSubjectConditionSet{ + // SCS is resolved only against otherNamespace, so the mapping's + // lookup against "scs-1|ns-1" comes back nil. + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*policy.Namespace{otherNamespace}, + }, + }, + SubjectMappings: []*DerivedSubjectMapping{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + Actions: []*policy.Action{{Id: "action-1", Name: "decrypt"}}, + SubjectConditionSet: &policy.SubjectConditionSet{Id: "scs-1"}, + }, + Target: namespace, + }, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrUnresolvedSubjectConditionSetDependency) + assert.Contains(t, err.Error(), `subject mapping "mapping-1" in namespace "ns-1"`) + assert.Contains(t, err.Error(), `subject condition set "scs-1"`) +} + +func TestResolveExistingDropsSubjectMappingsOutsideScope(t *testing.T) { + t.Parallel() + + // Scopes omit ScopeSubjectMappings. A mapping that would otherwise fail + // validation (missing SCS id) must be dropped rather than surfaced. + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + SubjectMappings: []*DerivedSubjectMapping{ + { + Source: &policy.SubjectMapping{Id: "mapping-1"}, + Target: &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"}, + }, + }, + }, + nil, + ) + require.NoError(t, err) + assert.Empty(t, resolved.SubjectMappings) +} + +func TestResolveExistingDropsRegisteredResourcesOutsideScope(t *testing.T) { + t.Parallel() + + // An empty derived resource would error on nil source if reached; clean + // NoError proves the scope gate drops it before validation. + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + RegisteredResources: []*DerivedRegisteredResource{{}}, + }, + nil, + ) + require.NoError(t, err) + assert.Empty(t, resolved.RegisteredResources) +} + +func TestResolveExistingDropsObligationTriggersOutsideScope(t *testing.T) { + t.Parallel() + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions}, + ObligationTriggers: []*DerivedObligationTrigger{ + {Source: &policy.ObligationTrigger{Id: "trigger-1"}}, + }, + }, + nil, + ) + require.NoError(t, err) + assert.Empty(t, resolved.ObligationTriggers) +} + +func TestResolveExistingRegisteredResourceFailsWhenActionDependencyMissingID(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + // AAV references an action with no ID — planner cannot wire the RR create + // to a specific action target, so the plan must fail here rather than + // surface the error at execution time. + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeRegisteredResources}, + RegisteredResources: []*DerivedRegisteredResource{ + { + Source: testRegisteredResource( + "rr-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue("", "decrypt", testAttributeValue("https://example.com/attr/foo/value/bar", namespace)), + ), + ), + Target: namespace, + }, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrMissingActionID) + assert.Contains(t, err.Error(), `registered resource "rr-1" in namespace "ns-1"`) +} + +func TestResolveExistingRegisteredResourceFailsWhenActionDependencyNotResolvedInNamespace(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + otherNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://other.example.com", + } + // Action resolves only against otherNamespace, so the RR's lookup at + // "action-1|ns-1" comes back nil — catch this at plan time rather than + // letting the executor fail on a missing action at create time. + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions, ScopeRegisteredResources}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*policy.Namespace{otherNamespace}, + }, + }, + RegisteredResources: []*DerivedRegisteredResource{ + { + Source: testRegisteredResource( + "rr-1", + "documents", + testRegisteredResourceValue( + "prod", + testActionAttributeValue("action-1", "decrypt", testAttributeValue("https://example.com/attr/foo/value/bar", namespace)), + ), + ), + Target: namespace, + }, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrUnresolvedActionDependency) + assert.Contains(t, err.Error(), `registered resource "rr-1" in namespace "ns-1"`) + assert.Contains(t, err.Error(), `action "action-1"`) +} + +func TestResolveExistingObligationTriggerFailsWhenActionDependencyMissingID(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeObligationTriggers}, + ObligationTriggers: []*DerivedObligationTrigger{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Name: "decrypt"}, + }, + Target: namespace, + }, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrMissingActionID) + assert.Contains(t, err.Error(), `obligation trigger "trigger-1" in namespace "ns-1"`) +} + +func TestResolveExistingObligationTriggerFailsWhenActionDependencyNotResolvedInNamespace(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + otherNamespace := &policy.Namespace{ + Id: "ns-2", + Fqn: "https://other.example.com", + } + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions, ScopeObligationTriggers}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*policy.Namespace{otherNamespace}, + }, + }, + ObligationTriggers: []*DerivedObligationTrigger{ + { + Source: &policy.ObligationTrigger{ + Id: "trigger-1", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + }, + Target: namespace, + }, + }, + }, + nil, + ) + require.Error(t, err) + assert.Nil(t, resolved) + require.ErrorIs(t, err, ErrUnresolvedActionDependency) + assert.Contains(t, err.Error(), `obligation trigger "trigger-1" in namespace "ns-1"`) + assert.Contains(t, err.Error(), `action "action-1"`) +} + +func TestResolveExistingReusesAlreadyMigratedSubjectConditionSet(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + + scs := func(id string, values ...string) *policy.SubjectConditionSet { + return &policy.SubjectConditionSet{ + Id: id, + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: values, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + }, + }}, + }, + } + } + + existing := newExistingTargets() + // Existing SCS differs only in condition-value order — canonical equality + // must match so the resolver routes to AlreadyMigrated instead of create. + existing.SubjectConditionSets[namespace.GetId()] = []*policy.SubjectConditionSet{ + scs("existing-scs", "editor", "admin"), + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*DerivedSubjectConditionSet{ + { + Source: scs("legacy-scs", "admin", "editor"), + Targets: []*policy.Namespace{namespace}, + }, + }, + }, + existing, + ) + require.NoError(t, err) + require.Len(t, resolved.SubjectConditionSets, 1) + require.Len(t, resolved.SubjectConditionSets[0].Results, 1) + result := resolved.SubjectConditionSets[0].Results[0] + require.NotNil(t, result.AlreadyMigrated) + assert.Equal(t, "existing-scs", result.AlreadyMigrated.GetId()) + assert.False(t, result.NeedsCreate) +} + +func TestResolveExistingReusesAlreadyMigratedSubjectMapping(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + sourceSCS := &policy.SubjectConditionSet{ + Id: "scs-1", + SubjectSets: []*policy.SubjectSet{ + {ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"admin"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + }, + }}, + }, + } + attributeFQN := "https://example.com/attr/classification/value/secret" + + existing := newExistingTargets() + // Existing SM differs only in case/whitespace on the attribute value FQN and + // action names — canonical equality must still identify it as a match. + existing.SubjectMappings[namespace.GetId()] = []*policy.SubjectMapping{ + { + Id: "existing-mapping", + AttributeValue: &policy.Value{Fqn: " " + attributeFQN + " "}, + Actions: []*policy.Action{{Id: "action-1", Name: "DECRYPT"}}, + SubjectConditionSet: sourceSCS, + }, + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions, ScopeSubjectConditionSets, ScopeSubjectMappings}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*policy.Namespace{namespace}, + }, + }, + SubjectConditionSets: []*DerivedSubjectConditionSet{ + { + Source: sourceSCS, + Targets: []*policy.Namespace{namespace}, + }, + }, + SubjectMappings: []*DerivedSubjectMapping{ + { + Source: &policy.SubjectMapping{ + Id: "legacy-mapping", + AttributeValue: &policy.Value{Fqn: attributeFQN}, + Actions: []*policy.Action{{Id: "action-1", Name: "decrypt"}}, + SubjectConditionSet: sourceSCS, + }, + Target: namespace, + }, + }, + }, + existing, + ) + require.NoError(t, err) + require.Len(t, resolved.SubjectMappings, 1) + require.NotNil(t, resolved.SubjectMappings[0].AlreadyMigrated) + assert.Equal(t, "existing-mapping", resolved.SubjectMappings[0].AlreadyMigrated.GetId()) + assert.False(t, resolved.SubjectMappings[0].NeedsCreate) +} + +func TestResolveExistingReusesAlreadyMigratedObligationTrigger(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + attributeFQN := "https://example.com/attr/classification/value/secret" + obligationFQN := "https://example.com/obligation/notify/value/default" + + existing := newExistingTargets() + // Existing trigger differs only in case and whitespace — canonical equality + // must still identify it as a match. + existing.ObligationTriggers[namespace.GetId()] = []*policy.ObligationTrigger{ + { + Id: "existing-trigger", + Action: &policy.Action{Id: "action-1", Name: "DECRYPT"}, + AttributeValue: &policy.Value{Fqn: " " + attributeFQN + " "}, + ObligationValue: &policy.ObligationValue{Fqn: obligationFQN}, + }, + } + + resolved, err := resolveExisting( + &DerivedTargets{ + Scopes: []Scope{ScopeActions, ScopeObligationTriggers}, + Actions: []*DerivedAction{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*policy.Namespace{namespace}, + }, + }, + ObligationTriggers: []*DerivedObligationTrigger{ + { + Source: &policy.ObligationTrigger{ + Id: "legacy-trigger", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + AttributeValue: &policy.Value{Fqn: attributeFQN}, + ObligationValue: &policy.ObligationValue{Fqn: obligationFQN}, + }, + Target: namespace, + }, + }, + }, + existing, + ) + require.NoError(t, err) + require.Len(t, resolved.ObligationTriggers, 1) + require.NotNil(t, resolved.ObligationTriggers[0].AlreadyMigrated) + assert.Equal(t, "existing-trigger", resolved.ObligationTriggers[0].AlreadyMigrated.GetId()) + assert.False(t, resolved.ObligationTriggers[0].NeedsCreate) +} diff --git a/otdfctl/migrations/namespacedpolicy/retrieve.go b/otdfctl/migrations/namespacedpolicy/retrieve.go new file mode 100644 index 0000000000..2cb0f2d9dd --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/retrieve.go @@ -0,0 +1,695 @@ +package namespacedpolicy + +import ( + "context" + "errors" + "fmt" + + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/obligations" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" + "google.golang.org/protobuf/proto" +) + +var ( + _ pagedResponse = (*actions.ListActionsResponse)(nil) + _ pagedResponse = (*subjectmapping.ListSubjectConditionSetsResponse)(nil) + _ pagedResponse = (*subjectmapping.ListSubjectMappingsResponse)(nil) + _ pagedResponse = (*registeredresources.ListRegisteredResourcesResponse)(nil) + _ pagedResponse = (*registeredresources.ListRegisteredResourceValuesResponse)(nil) + _ pagedResponse = (*obligations.ListObligationTriggersResponse)(nil) + _ pagedResponse = (*namespaces.ListNamespacesResponse)(nil) +) + +type pagedResponse interface { + GetPagination() *policy.PageResponse +} + +type Retriever struct { + handler PolicyClient + pageSize int32 +} + +func newRetriever(handler PolicyClient, pageSize int32) *Retriever { + return &Retriever{ + handler: handler, + pageSize: pageSize, + } +} + +func (r *Retriever) retrieve(ctx context.Context, scopes scopeSet) (*Retrieved, error) { + retrieved := newRetrieved(scopes.ordered()) + + if scopes.requiresSubjectMappings() { + candidates, err := r.retrieveSubjectMappings(ctx) + if err != nil { + return nil, err + } + retrieved.Candidates.SubjectMappings = candidates + } + + if scopes.requiresSubjectConditionSets() { + candidates, err := r.retrieveSubjectConditionSets(ctx) + if err != nil { + return nil, err + } + retrieved.Candidates.SubjectConditionSets = candidates + } + + if scopes.requiresActions() { + candidates, err := r.retrieveActions(ctx) + if err != nil { + return nil, err + } + retrieved.Candidates.Actions = candidates + } + + if scopes.requiresRegisteredResources() { + candidates, err := r.retrieveRegisteredResources(ctx) + if err != nil { + return nil, err + } + retrieved.Candidates.RegisteredResources = candidates + } + + if scopes.requiresObligationTriggers() { + candidates, err := r.retrieveObligationTriggers(ctx, objectIDSet(retrieved.Candidates.Actions)) + if err != nil { + return nil, err + } + retrieved.Candidates.ObligationTriggers = candidates + } + + return retrieved, nil +} + +func (r *Retriever) listNamespaces(ctx context.Context) ([]*policy.Namespace, error) { + var ( + all []*policy.Namespace + offset int32 + ) + + for { + resp, err := r.handler.ListNamespaces(ctx, common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE, r.pageSize, offset, handlers.SortOption{}) + if err != nil { + return nil, fmt.Errorf("list namespaces: %w", err) + } + + items := resp.GetNamespaces() + if len(items) == 0 { + break + } + + all = append(all, items...) + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list namespaces: %w", err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + + return all, nil +} + +func (r *Retriever) listExistingTargets(ctx context.Context, scopes scopeSet, derived *DerivedTargets) (*ExistingTargets, error) { + existing := newExistingTargets() + var ( + customActions map[string][]*policy.Action + standardActions map[string][]*policy.Action + ) + + if scopes.requiresActions() { + var err error + customActions, standardActions, err = r.listActionsForNamespaces(ctx, derivedActionNamespaces(derived)) + if err != nil { + return nil, err + } + existing.CustomActions = customActions + existing.StandardActions = standardActions + } + + if scopes.requiresSubjectConditionSets() { + subjectConditionSets, err := r.listSubjectConditionSetsForNamespaces(ctx, derivedSubjectConditionSetNamespaces(derived)) + if err != nil { + return nil, err + } + existing.SubjectConditionSets = subjectConditionSets + } + + if scopes.has(ScopeSubjectMappings) { + subjectMappings, err := r.listSubjectMappingsForNamespaces(ctx, derivedSubjectMappingNamespaces(derived)) + if err != nil { + return nil, err + } + existing.SubjectMappings = subjectMappings + } + + if scopes.has(ScopeRegisteredResources) { + registeredResources, err := r.listRegisteredResourcesForNamespaces(ctx, derivedRegisteredResourceNamespaces(derived)) + if err != nil { + return nil, err + } + existing.RegisteredResources = registeredResources + } + + if scopes.has(ScopeObligationTriggers) { + obligationTriggers, err := r.listObligationTriggersForNamespaces( + ctx, + derivedObligationTriggerNamespaces(derived), + actionIDsByNamespace(derivedActionNamespaces(derived), customActions, standardActions), + ) + if err != nil { + return nil, err + } + existing.ObligationTriggers = obligationTriggers + } + + return existing, nil +} + +func (r *Retriever) retrieveSubjectMappings(ctx context.Context) ([]*policy.SubjectMapping, error) { + var ( + candidates []*policy.SubjectMapping + offset int32 + ) + + for { + resp, err := r.handler.ListSubjectMappings(ctx, r.pageSize, offset, "", handlers.SortOption{}) + if err != nil { + return nil, fmt.Errorf("list subject mappings: %w", err) + } + + items := resp.GetSubjectMappings() + if len(items) == 0 { + break + } + + for _, mapping := range items { + if mapping.GetId() == "" || !isLegacyNamespace(mapping.GetNamespace()) || hasObject(candidates, mapping.GetId()) { + continue + } + candidates = append(candidates, mapping) + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list subject mappings: %w", err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + + return candidates, nil +} + +func (r *Retriever) retrieveSubjectConditionSets(ctx context.Context) ([]*policy.SubjectConditionSet, error) { + var candidates []*policy.SubjectConditionSet + var offset int32 + + for { + resp, err := r.handler.ListSubjectConditionSets(ctx, r.pageSize, offset, "", handlers.SortOption{}) + if err != nil { + return nil, fmt.Errorf("list subject condition sets: %w", err) + } + + items := resp.GetSubjectConditionSets() + if len(items) == 0 { + break + } + + for _, scs := range items { + if scs.GetId() == "" { + continue + } + if isLegacyNamespace(scs.GetNamespace()) && !hasObject(candidates, scs.GetId()) { + candidates = append(candidates, scs) + } + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list subject condition sets: %w", err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + + return candidates, nil +} + +func (r *Retriever) retrieveRegisteredResources(ctx context.Context) ([]*policy.RegisteredResource, error) { + var ( + candidates []*policy.RegisteredResource + offset int32 + ) + + for { + resp, err := r.handler.ListRegisteredResources(ctx, r.pageSize, offset, "", handlers.SortOption{}) + if err != nil { + return nil, fmt.Errorf("list registered resources: %w", err) + } + + items := resp.GetResources() + if len(items) == 0 { + break + } + + for _, resource := range items { + if resource.GetId() == "" || !isLegacyNamespace(resource.GetNamespace()) || hasObject(candidates, resource.GetId()) { + continue + } + + hydrated, err := r.hydrateRegisteredResource(ctx, resource) + if err != nil { + return nil, fmt.Errorf("list registered resource values for resource %s: %w", resource.GetId(), err) + } + candidates = append(candidates, hydrated) + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list registered resources: %w", err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + + return candidates, nil +} + +func (r *Retriever) retrieveActions(ctx context.Context) ([]*policy.Action, error) { + var candidates []*policy.Action + var offset int32 + + for { + resp, err := r.handler.ListActions(ctx, r.pageSize, offset, "") + if err != nil { + return nil, fmt.Errorf("list actions: %w", err) + } + + if len(resp.GetActionsStandard()) == 0 && len(resp.GetActionsCustom()) == 0 { + break + } + + for _, action := range resp.GetActionsStandard() { + if action.GetId() == "" { + continue + } + if isLegacyNamespace(action.GetNamespace()) { + if !hasObject(candidates, action.GetId()) { + candidates = append(candidates, action) + } + continue + } + } + + for _, action := range resp.GetActionsCustom() { + if action.GetId() == "" { + continue + } + if isLegacyNamespace(action.GetNamespace()) { + if !hasObject(candidates, action.GetId()) { + candidates = append(candidates, action) + } + continue + } + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list actions: %w", err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + + return candidates, nil +} + +func (r *Retriever) retrieveObligationTriggers(ctx context.Context, legacyActionIDs map[string]struct{}) ([]*policy.ObligationTrigger, error) { + var ( + candidates []*policy.ObligationTrigger + offset int32 + ) + + for { + resp, err := r.handler.ListObligationTriggers(ctx, "", r.pageSize, offset) + if err != nil { + return nil, fmt.Errorf("list obligation triggers: %w", err) + } + + items := resp.GetTriggers() + if len(items) == 0 { + break + } + + for _, trigger := range items { + if trigger.GetId() == "" || trigger.GetAction().GetId() == "" || hasObject(candidates, trigger.GetId()) { + continue + } + // ! If the trigger action id is not within the candidate set, it is not a legacy obligation trigger. + if _, ok := legacyActionIDs[trigger.GetAction().GetId()]; !ok { + continue + } + candidates = append(candidates, trigger) + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list obligation triggers: %w", err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + + return candidates, nil +} + +func actionIDsByNamespace(namespaces []*policy.Namespace, customByNamespace, standardByNamespace map[string][]*policy.Action) map[string]map[string]struct{} { + idsByNamespace := make(map[string]map[string]struct{}, len(namespaces)) + for _, namespace := range dedupeTargetNamespaces(namespaces) { + idsByNamespace[namespace.GetId()] = make(map[string]struct{}) + } + add := func(namespaceID string, actions []*policy.Action) { + if namespaceID == "" { + return + } + if idsByNamespace[namespaceID] == nil { + idsByNamespace[namespaceID] = make(map[string]struct{}, len(actions)) + } + for _, action := range actions { + if action == nil || action.GetId() == "" { + continue + } + idsByNamespace[namespaceID][action.GetId()] = struct{}{} + } + } + + for namespaceID, actions := range customByNamespace { + add(namespaceID, actions) + } + for namespaceID, actions := range standardByNamespace { + add(namespaceID, actions) + } + + return idsByNamespace +} + +func (r *Retriever) listActionsForNamespaces(ctx context.Context, namespaces []*policy.Namespace) (map[string][]*policy.Action, map[string][]*policy.Action, error) { + customByNamespace := make(map[string][]*policy.Action) + standardByNamespace := make(map[string][]*policy.Action) + + for _, namespace := range dedupeTargetNamespaces(namespaces) { + var offset int32 + for { + resp, err := r.handler.ListActions(ctx, r.pageSize, offset, namespace.GetId()) + if err != nil { + return nil, nil, fmt.Errorf("list actions for namespace %s: %w", namespace.GetId(), err) + } + + for _, action := range resp.GetActionsCustom() { + if action.GetId() == "" || !sameNamespace(action.GetNamespace(), namespace) || hasObject(customByNamespace[namespace.GetId()], action.GetId()) { + continue + } + customByNamespace[namespace.GetId()] = append(customByNamespace[namespace.GetId()], action) + } + for _, action := range resp.GetActionsStandard() { + if action.GetId() == "" || !sameNamespace(action.GetNamespace(), namespace) || hasObject(standardByNamespace[namespace.GetId()], action.GetId()) { + continue + } + standardByNamespace[namespace.GetId()] = append(standardByNamespace[namespace.GetId()], action) + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, nil, fmt.Errorf("list actions for namespace %s: %w", namespace.GetId(), err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + } + + return customByNamespace, standardByNamespace, nil +} + +func (r *Retriever) listSubjectConditionSetsForNamespaces(ctx context.Context, namespaces []*policy.Namespace) (map[string][]*policy.SubjectConditionSet, error) { + byNamespace := make(map[string][]*policy.SubjectConditionSet) + + for _, namespace := range dedupeTargetNamespaces(namespaces) { + var offset int32 + for { + resp, err := r.handler.ListSubjectConditionSets(ctx, r.pageSize, offset, namespace.GetId(), handlers.SortOption{}) + if err != nil { + return nil, fmt.Errorf("list subject condition sets for namespace %s: %w", namespace.GetId(), err) + } + + for _, scs := range resp.GetSubjectConditionSets() { + if scs.GetId() == "" || hasObject(byNamespace[namespace.GetId()], scs.GetId()) { + continue + } + byNamespace[namespace.GetId()] = append(byNamespace[namespace.GetId()], scs) + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list subject condition sets for namespace %s: %w", namespace.GetId(), err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + } + + return byNamespace, nil +} + +func (r *Retriever) listSubjectMappingsForNamespaces(ctx context.Context, namespaces []*policy.Namespace) (map[string][]*policy.SubjectMapping, error) { + byNamespace := make(map[string][]*policy.SubjectMapping) + + for _, namespace := range dedupeTargetNamespaces(namespaces) { + var offset int32 + for { + resp, err := r.handler.ListSubjectMappings(ctx, r.pageSize, offset, namespace.GetId(), handlers.SortOption{}) + if err != nil { + return nil, fmt.Errorf("list subject mappings for namespace %s: %w", namespace.GetId(), err) + } + + for _, mapping := range resp.GetSubjectMappings() { + if mapping.GetId() == "" || hasObject(byNamespace[namespace.GetId()], mapping.GetId()) { + continue + } + byNamespace[namespace.GetId()] = append(byNamespace[namespace.GetId()], mapping) + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list subject mappings for namespace %s: %w", namespace.GetId(), err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + } + + return byNamespace, nil +} + +func (r *Retriever) listRegisteredResourcesForNamespaces(ctx context.Context, namespaces []*policy.Namespace) (map[string][]*policy.RegisteredResource, error) { + byNamespace := make(map[string][]*policy.RegisteredResource) + + for _, namespace := range dedupeTargetNamespaces(namespaces) { + var offset int32 + for { + resp, err := r.handler.ListRegisteredResources(ctx, r.pageSize, offset, namespace.GetId(), handlers.SortOption{}) + if err != nil { + return nil, fmt.Errorf("list registered resources for namespace %s: %w", namespace.GetId(), err) + } + + for _, resource := range resp.GetResources() { + if resource.GetId() == "" || hasObject(byNamespace[namespace.GetId()], resource.GetId()) { + continue + } + + hydrated, err := r.hydrateRegisteredResource(ctx, resource) + if err != nil { + return nil, fmt.Errorf("list registered resource values for resource %s in namespace %s: %w", resource.GetId(), namespace.GetId(), err) + } + byNamespace[namespace.GetId()] = append(byNamespace[namespace.GetId()], hydrated) + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list registered resources for namespace %s: %w", namespace.GetId(), err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + } + + return byNamespace, nil +} + +func (r *Retriever) hydrateRegisteredResource(ctx context.Context, resource *policy.RegisteredResource) (*policy.RegisteredResource, error) { + if resource == nil || resource.GetId() == "" { + return resource, nil + } + + values, err := r.listRegisteredResourceValues(ctx, resource.GetId()) + if err != nil { + return nil, err + } + + hydrated, ok := proto.Clone(resource).(*policy.RegisteredResource) + if !ok { + return nil, errors.New("clone registered resource: unexpected type") + } + hydrated.Values = values + return hydrated, nil +} + +func (r *Retriever) listRegisteredResourceValues(ctx context.Context, resourceID string) ([]*policy.RegisteredResourceValue, error) { + if resourceID == "" { + return nil, nil + } + + var ( + values []*policy.RegisteredResourceValue + offset int32 + ) + + for { + resp, err := r.handler.ListRegisteredResourceValues(ctx, resourceID, r.pageSize, offset) + if err != nil { + return nil, err + } + + values = append(values, resp.GetValues()...) + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, err + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + + return values, nil +} + +// * Getting an existing trigger means to retrieve all triggers for a set of namespaces +// * where the obligation trigger has an action that has a namespace. +// * +// * ListRPCs do not return namespace information for non-target objects (i.e. actions for triggers) +// * so we must lookup the action from ListActionsExisting to discern whether or not the action tied +// * to the Obligation Trigger is legacy or not. +func (r *Retriever) listObligationTriggersForNamespaces(ctx context.Context, namespaces []*policy.Namespace, actionIDsByNamespace map[string]map[string]struct{}) (map[string][]*policy.ObligationTrigger, error) { + byNamespace := make(map[string][]*policy.ObligationTrigger) + + for _, namespace := range dedupeTargetNamespaces(namespaces) { + allowedActionIDs, hasActionNamespace := actionIDsByNamespace[namespace.GetId()] + // ! Actions should always include the derived obligation trigger namespaces. + if !hasActionNamespace { + return nil, fmt.Errorf("obligation trigger existing-target lookup for namespace %q is missing action candidates", namespace.GetId()) + } + var offset int32 + for { + resp, err := r.handler.ListObligationTriggers(ctx, namespace.GetId(), r.pageSize, offset) + if err != nil { + return nil, fmt.Errorf("list obligation triggers for namespace %s: %w", namespace.GetId(), err) + } + + for _, trigger := range resp.GetTriggers() { + if trigger.GetId() == "" || trigger.GetAction().GetId() == "" || hasObject(byNamespace[namespace.GetId()], trigger.GetId()) { + continue + } + // ! Check that the trigger action is one with a namespace. + if _, actionAllowed := allowedActionIDs[trigger.GetAction().GetId()]; !actionAllowed { + continue + } + byNamespace[namespace.GetId()] = append(byNamespace[namespace.GetId()], trigger) + } + + nextOffset, err := nextOffsetFromPage(resp) + if err != nil { + return nil, fmt.Errorf("list obligation triggers for namespace %s: %w", namespace.GetId(), err) + } + if nextOffset <= 0 { + break + } + offset = nextOffset + } + } + + return byNamespace, nil +} + +func dedupeTargetNamespaces(namespaces []*policy.Namespace) []*policy.Namespace { + deduped := make([]*policy.Namespace, 0, len(namespaces)) + seen := make(map[string]struct{}, len(namespaces)) + + for _, namespace := range namespaces { + if namespace == nil || namespace.GetId() == "" { + continue + } + if _, ok := seen[namespace.GetId()]; ok { + continue + } + seen[namespace.GetId()] = struct{}{} + deduped = append(deduped, namespace) + } + + return deduped +} + +func isLegacyNamespace(namespace *policy.Namespace) bool { + return namespace == nil || (namespace.GetId() == "" && namespace.GetFqn() == "") +} + +func namespaceRefKey(namespace *policy.Namespace) string { + if namespace == nil { + return "" + } + if id := namespace.GetId(); id != "" { + return "id:" + id + } + if fqn := namespace.GetFqn(); fqn != "" { + return "fqn:" + fqn + } + return "" +} + +func nextOffsetFromPage(resp pagedResponse) (int32, error) { + page := resp.GetPagination() + if page == nil { + return 0, errors.New("missing pagination in response") + } + + return page.GetNextOffset(), nil +} diff --git a/otdfctl/migrations/namespacedpolicy/retrieve_test.go b/otdfctl/migrations/namespacedpolicy/retrieve_test.go new file mode 100644 index 0000000000..8a04757e9a --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/retrieve_test.go @@ -0,0 +1,473 @@ +package namespacedpolicy + +import ( + "context" + "testing" + + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/obligations" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRetrieverRetrieveActionsFiltersLegacyAndDedupes(t *testing.T) { + t.Parallel() + + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + "": { + ActionsStandard: []*policy.Action{ + {Id: "action-dup", Name: "read"}, + { + Id: "action-namespaced", + Name: "write", + Namespace: &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"}, + }, + }, + ActionsCustom: []*policy.Action{ + {Id: "action-dup", Name: "read"}, + {Id: "action-custom", Name: "decrypt"}, + }, + Pagination: emptyPageResponse(), + }, + }, + } + + actions, err := newRetriever(handler, 25).retrieveActions(t.Context()) + require.NoError(t, err) + + assert.ElementsMatch(t, []string{"action-dup", "action-custom"}, policyObjectIDs(actions)) + assert.Equal(t, []string{""}, handler.actionCalls) +} + +func TestRetrieverListActionsForNamespacesDropsGlobalFallbackStandardActions(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + namespace.GetId(): { + ActionsCustom: []*policy.Action{ + { + Id: "custom-namespaced", + Name: "decrypt", + Namespace: namespace, + }, + }, + ActionsStandard: []*policy.Action{ + { + Id: "standard-global-fallback", + Name: "read", + }, + { + Id: "standard-namespaced", + Name: "read", + Namespace: namespace, + }, + }, + Pagination: emptyPageResponse(), + }, + }, + } + + customByNamespace, standardByNamespace, err := newRetriever(handler, 25).listActionsForNamespaces( + t.Context(), + []*policy.Namespace{namespace}, + ) + require.NoError(t, err) + + require.Contains(t, customByNamespace, namespace.GetId()) + assert.Equal(t, []string{"custom-namespaced"}, policyObjectIDs(customByNamespace[namespace.GetId()])) + + require.Contains(t, standardByNamespace, namespace.GetId()) + assert.Equal(t, []string{"standard-namespaced"}, policyObjectIDs(standardByNamespace[namespace.GetId()])) + assert.Equal(t, []string{namespace.GetId()}, handler.actionCalls) +} + +func TestRetrieverListRegisteredResourcesForNamespacesDedupesNamespacesAndHydratesValues(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + inlineValue := &policy.RegisteredResourceValue{Value: "inline-value"} + value := &policy.RegisteredResourceValue{ + Id: "value-1", + Value: "prod", + Metadata: &common.Metadata{ + Labels: map[string]string{ + "owner": "platform", + }, + }, + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + testActionAttributeValue( + "action-1", + "decrypt", + testAttributeValue("https://example.com/attr/classification/value/secret", nil), + ), + }, + } + resource := &policy.RegisteredResource{ + Id: "resource-1", + Name: "documents", + Values: []*policy.RegisteredResourceValue{inlineValue}, + } + handler := &plannerTestHandler{ + registeredResourcesByNamespace: map[string]*registeredresources.ListRegisteredResourcesResponse{ + namespace.GetId(): { + Resources: []*policy.RegisteredResource{resource}, + Pagination: emptyPageResponse(), + }, + }, + registeredResourceValuesByResourceID: map[string]*registeredresources.ListRegisteredResourceValuesResponse{ + resource.GetId(): { + Values: []*policy.RegisteredResourceValue{value}, + Pagination: emptyPageResponse(), + }, + }, + } + + resources, err := newRetriever(handler, 25).listRegisteredResourcesForNamespaces( + context.Background(), + []*policy.Namespace{namespace, namespace}, + ) + require.NoError(t, err) + + require.Contains(t, resources, namespace.GetId()) + require.Len(t, resources[namespace.GetId()], 1) + assert.Equal(t, resource.GetId(), resources[namespace.GetId()][0].GetId()) + require.Len(t, resources[namespace.GetId()][0].GetValues(), 1) + assert.Equal(t, "prod", resources[namespace.GetId()][0].GetValues()[0].GetValue()) + assert.Equal(t, map[string]string{"owner": "platform"}, resources[namespace.GetId()][0].GetValues()[0].GetMetadata().GetLabels()) + assert.Equal(t, []string{namespace.GetId()}, handler.registeredResourceCalls) + assert.Equal(t, []string{resource.GetId()}, handler.registeredResourceValueCalls) +} + +func TestRetrieverRetrieveSubjectMappingsDedupesAcrossPages(t *testing.T) { + t.Parallel() + + handler := &pagedRetrieveTestHandler{ + subjectMappingPages: map[int32]*subjectmapping.ListSubjectMappingsResponse{ + 0: { + SubjectMappings: []*policy.SubjectMapping{ + {Id: "mapping-dup"}, + { + Id: "mapping-namespaced", + Namespace: &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"}, + }, + }, + Pagination: pageResponse(1), + }, + 1: { + SubjectMappings: []*policy.SubjectMapping{ + {Id: "mapping-dup"}, + {Id: "mapping-new"}, + {Id: ""}, + }, + Pagination: emptyPageResponse(), + }, + }, + } + + mappings, err := newRetriever(handler, 25).retrieveSubjectMappings(t.Context()) + require.NoError(t, err) + + assert.Equal(t, []string{"mapping-dup", "mapping-new"}, policyObjectIDs(mappings)) +} + +func TestRetrieverRetrieveRegisteredResourcesDedupesAcrossPages(t *testing.T) { + t.Parallel() + + valueDup := &policy.RegisteredResourceValue{ + Id: "resource-dup-value-1", + Value: "prod", + Metadata: &common.Metadata{ + Labels: map[string]string{ + "owner": "policy-team", + }, + }, + } + handler := &pagedRetrieveTestHandler{ + registeredResourcePages: map[int32]*registeredresources.ListRegisteredResourcesResponse{ + 0: { + Resources: []*policy.RegisteredResource{ + {Id: "resource-dup", Name: "documents", Values: []*policy.RegisteredResourceValue{{Value: "inline"}}}, + { + Id: "resource-namespaced", + Name: "contracts", + Namespace: &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"}, + }, + }, + Pagination: pageResponse(1), + }, + 1: { + Resources: []*policy.RegisteredResource{ + {Id: "resource-dup", Name: "documents"}, + {Id: "resource-new", Name: "reports"}, + {Id: "", Name: "missing-id"}, + }, + Pagination: emptyPageResponse(), + }, + }, + registeredResourceValuePages: map[string]map[int32]*registeredresources.ListRegisteredResourceValuesResponse{ + "resource-dup": { + 0: { + Values: []*policy.RegisteredResourceValue{valueDup}, + Pagination: emptyPageResponse(), + }, + }, + "resource-new": { + 0: { + Values: []*policy.RegisteredResourceValue{{Id: "resource-new-value-1", Value: "reports"}}, + Pagination: emptyPageResponse(), + }, + }, + }, + } + + resources, err := newRetriever(handler, 25).retrieveRegisteredResources(t.Context()) + require.NoError(t, err) + + assert.Equal(t, []string{"resource-dup", "resource-new"}, policyObjectIDs(resources)) + require.Len(t, resources[0].GetValues(), 1) + assert.Equal(t, "prod", resources[0].GetValues()[0].GetValue()) + assert.Equal(t, map[string]string{"owner": "policy-team"}, resources[0].GetValues()[0].GetMetadata().GetLabels()) + assert.Equal(t, []string{"resource-dup", "resource-new"}, handler.registeredResourceValueCalls) +} + +func TestRetrieverListExistingTargetsFiltersObligationTriggersByNamespacedActionIDs(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + handler := &plannerTestHandler{ + actionsByNamespace: map[string]*actions.ListActionsResponse{ + namespace.GetId(): { + ActionsCustom: []*policy.Action{ + { + Id: "action-target", + Name: "decrypt", + Namespace: namespace, + }, + }, + Pagination: emptyPageResponse(), + }, + }, + obligationTriggersByNamespace: map[string]*obligations.ListObligationTriggersResponse{ + namespace.GetId(): { + Triggers: []*policy.ObligationTrigger{ + { + Id: "trigger-legacy-action", + Action: &policy.Action{ + Id: "action-legacy", + Name: "decrypt-2", + }, + }, + { + Id: "trigger-target-action", + Action: &policy.Action{ + Id: "action-target", + Name: "decrypt", + }, + }, + }, + Pagination: emptyPageResponse(), + }, + }, + } + + scopes, err := normalizeScopes([]Scope{ScopeActions, ScopeObligationTriggers}) + require.NoError(t, err) + + existing, err := newRetriever(handler, 25).listExistingTargets(t.Context(), scopes, &DerivedTargets{ + Actions: []*DerivedAction{ + {Targets: []*policy.Namespace{namespace}}, + }, + ObligationTriggers: []*DerivedObligationTrigger{ + {Target: namespace}, + }, + }) + require.NoError(t, err) + + require.Contains(t, existing.ObligationTriggers, namespace.GetId()) + assert.Equal(t, []string{"trigger-target-action"}, policyObjectIDs(existing.ObligationTriggers[namespace.GetId()])) + assert.Equal(t, []string{namespace.GetId()}, handler.actionCalls) + assert.Equal(t, []string{namespace.GetId()}, handler.obligationTriggerCalls) +} + +func TestRetrieverListObligationTriggersForNamespacesFailsWhenNamespaceMissingFromActionMap(t *testing.T) { + t.Parallel() + + namespace := &policy.Namespace{ + Id: "ns-1", + Fqn: "https://example.com", + } + + _, err := newRetriever(&pagedRetrieveTestHandler{}, 25).listObligationTriggersForNamespaces( + context.Background(), + []*policy.Namespace{namespace}, + map[string]map[string]struct{}{}, + ) + require.Error(t, err) + assert.EqualError(t, err, `obligation trigger existing-target lookup for namespace "ns-1" is missing action candidates`) +} + +func TestRetrieverRetrieveObligationTriggersUsesListActionsToFilterLegacyActionIDs(t *testing.T) { + t.Parallel() + + handler := &pagedRetrieveTestHandler{ + actionPages: map[int32]*actions.ListActionsResponse{ + 0: { + ActionsCustom: []*policy.Action{ + {Id: "action-1", Name: "decrypt"}, + {Id: "action-3", Name: "encrypt"}, + }, + ActionsStandard: []*policy.Action{ + { + Id: "action-2", + Name: "decrypt", + Namespace: &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"}, + }, + }, + Pagination: emptyPageResponse(), + }, + }, + obligationTriggerPages: map[int32]*obligations.ListObligationTriggersResponse{ + 0: { + Triggers: []*policy.ObligationTrigger{ + { + Id: "trigger-dup", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + }, + { + Id: "trigger-non-legacy", + Action: &policy.Action{Id: "action-2", Name: "decrypt"}, + }, + }, + Pagination: pageResponse(1), + }, + 1: { + Triggers: []*policy.ObligationTrigger{ + { + Id: "trigger-dup", + Action: &policy.Action{Id: "action-1", Name: "decrypt"}, + }, + { + Id: "trigger-new", + Action: &policy.Action{Id: "action-3", Name: "encrypt"}, + }, + { + Id: "", + Action: &policy.Action{Id: "action-4", Name: "read"}, + }, + }, + Pagination: emptyPageResponse(), + }, + }, + } + + scopes, err := normalizeScopes([]Scope{ScopeObligationTriggers}) + require.NoError(t, err) + + retrieved, err := newRetriever(handler, 25).retrieve(t.Context(), scopes) + require.NoError(t, err) + + assert.Equal(t, []string{"action-1", "action-3"}, policyObjectIDs(retrieved.Candidates.Actions)) + assert.Equal(t, []string{"trigger-dup", "trigger-new"}, policyObjectIDs(retrieved.Candidates.ObligationTriggers)) +} + +func policyObjectIDs[T interface{ GetId() string }](items []T) []string { + ids := make([]string, 0, len(items)) + for _, item := range items { + if item.GetId() == "" { + continue + } + ids = append(ids, item.GetId()) + } + + return ids +} + +type pagedRetrieveTestHandler struct { + actionPages map[int32]*actions.ListActionsResponse + subjectMappingPages map[int32]*subjectmapping.ListSubjectMappingsResponse + registeredResourcePages map[int32]*registeredresources.ListRegisteredResourcesResponse + registeredResourceValuePages map[string]map[int32]*registeredresources.ListRegisteredResourceValuesResponse + obligationTriggerPages map[int32]*obligations.ListObligationTriggersResponse + registeredResourceValueCalls []string +} + +func (h *pagedRetrieveTestHandler) ListActions(_ context.Context, limit, offset int32, namespace string) (*actions.ListActionsResponse, error) { + if resp, ok := h.actionPages[offset]; ok { + return resp, nil + } + return &actions.ListActionsResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *pagedRetrieveTestHandler) ListSubjectConditionSets(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectConditionSetsResponse, error) { + return &subjectmapping.ListSubjectConditionSetsResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *pagedRetrieveTestHandler) ListSubjectMappings(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectMappingsResponse, error) { + if resp, ok := h.subjectMappingPages[offset]; ok { + return resp, nil + } + return &subjectmapping.ListSubjectMappingsResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *pagedRetrieveTestHandler) ListRegisteredResources(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*registeredresources.ListRegisteredResourcesResponse, error) { + if resp, ok := h.registeredResourcePages[offset]; ok { + return resp, nil + } + return ®isteredresources.ListRegisteredResourcesResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *pagedRetrieveTestHandler) ListRegisteredResourceValues(_ context.Context, resourceID string, limit, offset int32) (*registeredresources.ListRegisteredResourceValuesResponse, error) { + h.registeredResourceValueCalls = append(h.registeredResourceValueCalls, resourceID) + if pages, ok := h.registeredResourceValuePages[resourceID]; ok { + if resp, exists := pages[offset]; exists { + return resp, nil + } + } + + for _, resp := range h.registeredResourcePages { + for _, resource := range resp.GetResources() { + if resource.GetId() != resourceID { + continue + } + return ®isteredresources.ListRegisteredResourceValuesResponse{ + Values: resource.GetValues(), + Pagination: emptyPageResponse(), + }, nil + } + } + + return ®isteredresources.ListRegisteredResourceValuesResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *pagedRetrieveTestHandler) ListObligationTriggers(_ context.Context, namespace string, limit, offset int32) (*obligations.ListObligationTriggersResponse, error) { + if resp, ok := h.obligationTriggerPages[offset]; ok { + return resp, nil + } + return &obligations.ListObligationTriggersResponse{Pagination: emptyPageResponse()}, nil +} + +func (h *pagedRetrieveTestHandler) ListNamespaces(_ context.Context, state common.ActiveStateEnum, limit, offset int32, sort handlers.SortOption) (*namespaces.ListNamespacesResponse, error) { + return &namespaces.ListNamespacesResponse{Pagination: emptyPageResponse()}, nil +} + +func pageResponse(nextOffset int32) *policy.PageResponse { + return &policy.PageResponse{NextOffset: nextOffset} +} diff --git a/otdfctl/migrations/namespacedpolicy/scopes.go b/otdfctl/migrations/namespacedpolicy/scopes.go new file mode 100644 index 0000000000..198aa14c1f --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/scopes.go @@ -0,0 +1,143 @@ +package namespacedpolicy + +import ( + "errors" + "fmt" + "strings" +) + +var ( + ErrEmptyPlannerScope = errors.New("at least one migration scope is required") + ErrInvalidScope = errors.New("invalid migration scope") +) + +type Scope string + +const ( + ScopeActions Scope = "actions" + ScopeSubjectConditionSets Scope = "subject-condition-sets" + ScopeSubjectMappings Scope = "subject-mappings" + ScopeRegisteredResources Scope = "registered-resources" + ScopeObligationTriggers Scope = "obligation-triggers" +) + +var supportedScopes = []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeRegisteredResources, + ScopeObligationTriggers, +} + +func ParseScopes(csv string) ([]Scope, error) { + if strings.TrimSpace(csv) == "" { + return nil, ErrEmptyPlannerScope + } + + scopes, err := normalizeScopes(splitScopes(csv)) + if err != nil { + return nil, err + } + + return scopes.ordered(), nil +} + +func splitScopes(csv string) []Scope { + rawScopes := strings.Split(csv, ",") + scopes := make([]Scope, 0, len(rawScopes)) + for _, raw := range rawScopes { + scopes = append(scopes, Scope(strings.TrimSpace(raw))) + } + + return scopes +} + +func normalizeScopes(scopes []Scope) (scopeSet, error) { + if len(scopes) == 0 { + return nil, ErrEmptyPlannerScope + } + + requested := make(scopeSet, len(supportedScopes)) + for _, scope := range scopes { + if scope == "" { + return nil, ErrEmptyPlannerScope + } + if !isSupportedScope(scope) { + return nil, fmt.Errorf("%w: %s", ErrInvalidScope, scope) + } + requested[scope] = struct{}{} + } + + return requested, nil +} + +func expandScopes(scopes scopeSet) scopeSet { + if len(scopes) == 0 { + return scopes + } + + expanded := make(scopeSet, len(supportedScopes)) + for scope := range scopes { + expanded[scope] = struct{}{} + } + + if expanded.has(ScopeSubjectMappings) { + expanded[ScopeActions] = struct{}{} + expanded[ScopeSubjectConditionSets] = struct{}{} + } + if expanded.has(ScopeRegisteredResources) { + expanded[ScopeActions] = struct{}{} + } + if expanded.has(ScopeObligationTriggers) { + expanded[ScopeActions] = struct{}{} + } + + return expanded +} + +type scopeSet map[Scope]struct{} + +func (s scopeSet) ordered() []Scope { + ordered := make([]Scope, 0, len(s)) + for _, scope := range supportedScopes { + if s.has(scope) { + ordered = append(ordered, scope) + } + } + + return ordered +} + +func (s scopeSet) has(scope Scope) bool { + _, ok := s[scope] + return ok +} + +func (s scopeSet) requiresActions() bool { + return s.has(ScopeActions) || s.has(ScopeSubjectMappings) || s.has(ScopeRegisteredResources) || s.has(ScopeObligationTriggers) +} + +func (s scopeSet) requiresSubjectConditionSets() bool { + return s.has(ScopeSubjectConditionSets) || s.has(ScopeSubjectMappings) +} + +func (s scopeSet) requiresSubjectMappings() bool { + return s.has(ScopeActions) || s.has(ScopeSubjectConditionSets) || s.has(ScopeSubjectMappings) +} + +func (s scopeSet) requiresRegisteredResources() bool { + return s.has(ScopeActions) || s.has(ScopeRegisteredResources) +} + +func (s scopeSet) requiresObligationTriggers() bool { + return s.has(ScopeActions) || s.has(ScopeObligationTriggers) +} + +func isSupportedScope(scope Scope) bool { + for _, supported := range supportedScopes { + if scope == supported { + return true + } + } + return false +} diff --git a/otdfctl/migrations/namespacedpolicy/scopes_test.go b/otdfctl/migrations/namespacedpolicy/scopes_test.go new file mode 100644 index 0000000000..aae0163c15 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/scopes_test.go @@ -0,0 +1,71 @@ +package namespacedpolicy + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseScopesNormalizesAndOrders(t *testing.T) { + t.Parallel() + + scopes, err := ParseScopes(" registered-resources, actions, subject-mappings, actions ") + require.NoError(t, err) + + assert.Equal(t, []Scope{ + ScopeActions, + ScopeSubjectMappings, + ScopeRegisteredResources, + }, scopes) +} + +func TestParseScopesRejectsInvalidInput(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + err error + }{ + { + name: "empty csv", + input: " ", + err: ErrEmptyPlannerScope, + }, + { + name: "empty entry", + input: "actions,", + err: ErrEmptyPlannerScope, + }, + { + name: "invalid scope", + input: "actions,widgets", + err: ErrInvalidScope, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, err := ParseScopes(tt.input) + require.ErrorIs(t, err, tt.err) + }) + } +} + +func TestExpandScopesAddsRequiredDependencies(t *testing.T) { + t.Parallel() + + requested, err := normalizeScopes([]Scope{ScopeSubjectMappings, ScopeObligationTriggers}) + require.NoError(t, err) + + assert.Equal(t, []Scope{ + ScopeActions, + ScopeSubjectConditionSets, + ScopeSubjectMappings, + ScopeObligationTriggers, + }, expandScopes(requested).ordered()) +} diff --git a/otdfctl/migrations/namespacedpolicy/subject_condition_sets_execute.go b/otdfctl/migrations/namespacedpolicy/subject_condition_sets_execute.go new file mode 100644 index 0000000000..3b02fddef4 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/subject_condition_sets_execute.go @@ -0,0 +1,132 @@ +package namespacedpolicy + +import ( + "context" + "fmt" + + "github.com/opentdf/platform/protocol/go/policy" +) + +func (e *MigrationExecutor) rememberSubjectConditionSetTarget(sourceID string, target *SubjectConditionSetTargetPlan) { + if e == nil || sourceID == "" || target == nil { + return + } + + namespaceKey := namespaceRefKey(target.Namespace) + if namespaceKey == "" { + return + } + + if e.subjectConditionSets == nil { + e.subjectConditionSets = make(map[string]map[string]*SubjectConditionSetTargetPlan) + } + if e.subjectConditionSets[sourceID] == nil { + e.subjectConditionSets[sourceID] = make(map[string]*SubjectConditionSetTargetPlan) + } + + e.subjectConditionSets[sourceID][namespaceKey] = target +} + +func (e *MigrationExecutor) cachedScsTargetID(sourceID string, namespace *policy.Namespace) string { + if e == nil || sourceID == "" { + return "" + } + + namespaceKey := namespaceRefKey(namespace) + if namespaceKey == "" { + return "" + } + + targets := e.subjectConditionSets[sourceID] + if targets == nil { + return "" + } + + target := targets[namespaceKey] + if target == nil { + return "" + } + + return target.TargetID() +} + +func (e *MigrationExecutor) executeSubjectConditionSets(ctx context.Context, plans []*SubjectConditionSetPlan) error { + if len(plans) == 0 { + return nil + } + + for _, scsPlan := range plans { + if scsPlan == nil || scsPlan.Source == nil { + continue + } + + for _, target := range scsPlan.Targets { + if target == nil { + continue + } + + if err := e.executeSubjectConditionSetTarget(ctx, scsPlan, target); err != nil { + return err + } + } + } + + return nil +} + +func (e *MigrationExecutor) executeSubjectConditionSetTarget(ctx context.Context, scsPlan *SubjectConditionSetPlan, target *SubjectConditionSetTargetPlan) error { + //nolint:exhaustive // SCS execution only handles create and already-migrated explicitly; all other statuses are unsupported. + switch target.Status { + case TargetStatusAlreadyMigrated: + if target.TargetID() == "" { + return fmt.Errorf("%w: subject condition set %q target %q", ErrMissingMigratedTarget, scsPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + e.rememberSubjectConditionSetTarget(scsPlan.Source.GetId(), target) + return nil + case TargetStatusSkipped: + return nil + case TargetStatusCreate: + return e.createSubjectConditionSetTarget(ctx, scsPlan, target) + case TargetStatusUnresolved: + return nil + default: + return fmt.Errorf("%w: subject condition set %q target %q has unsupported status %q", ErrUnsupportedStatus, scsPlan.Source.GetId(), namespaceLabel(target.Namespace), target.Status) + } +} + +func (e *MigrationExecutor) createSubjectConditionSetTarget(ctx context.Context, scsPlan *SubjectConditionSetPlan, target *SubjectConditionSetTargetPlan) error { + namespace := namespaceIdentifier(target.Namespace) + if namespace == "" { + return fmt.Errorf("%w: subject condition set %q", ErrTargetNamespaceRequired, scsPlan.Source.GetId()) + } + + created, err := e.handler.CreateSubjectConditionSet( + ctx, + scsPlan.Source.GetSubjectSets(), + metadataForCreate( + scsPlan.Source.GetId(), + metadataLabels(scsPlan.Source.GetMetadata()), + ), + namespace, + ) + if err != nil { + target.Execution = &ExecutionResult{ + Failure: err.Error(), + } + return fmt.Errorf("create subject condition set %q in namespace %q: %w", scsPlan.Source.GetId(), namespaceLabel(target.Namespace), err) + } + if created.GetId() == "" { + target.Execution = &ExecutionResult{ + Failure: ErrMissingCreatedTargetID.Error(), + } + return fmt.Errorf("%w: subject condition set %q target %q", ErrMissingCreatedTargetID, scsPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + target.Execution = &ExecutionResult{ + Applied: true, + CreatedTargetID: created.GetId(), + } + e.rememberSubjectConditionSetTarget(scsPlan.Source.GetId(), target) + + return nil +} diff --git a/otdfctl/migrations/namespacedpolicy/subject_condition_sets_execute_test.go b/otdfctl/migrations/namespacedpolicy/subject_condition_sets_execute_test.go new file mode 100644 index 0000000000..c5d967d597 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/subject_condition_sets_execute_test.go @@ -0,0 +1,308 @@ +package namespacedpolicy + +import ( + "errors" + "testing" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExecuteSubjectConditionSets(t *testing.T) { + t.Parallel() + + namespace1 := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + namespace2 := &policy.Namespace{Id: "ns-2", Fqn: "https://example.net"} + errBoom := errors.New("boom") + subjectSets := []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: "https://example.com/selector/role", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"admin"}, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + plan *MigrationPlan + handler *mockExecutorHandler + wantErr *expectedError + assert func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) + }{ + { + name: "handles created and already migrated subject condition set targets", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{ + Id: "scs-1", + SubjectSets: subjectSets, + Metadata: &common.Metadata{ + Labels: map[string]string{ + "owner": "policy-team", + "env": "dev", + }, + }, + }, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + { + Namespace: namespace2, + Status: TargetStatusAlreadyMigrated, + ExistingID: "migrated-scs-1", + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + subjectConditionSetResult: map[string]map[string]*policy.SubjectConditionSet{ + "scs-1": { + "ns-1": {Id: "created-scs-1"}, + }, + }, + }, + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + require.Contains(t, handler.createdSubjectConditions, "scs-1") + require.Contains(t, handler.createdSubjectConditions["scs-1"], "ns-1") + assert.Len(t, handler.createdSubjectConditions["scs-1"], 1) + assert.Equal(t, subjectSets, handler.createdSubjectConditions["scs-1"]["ns-1"].SubjectSets) + assert.Equal(t, "ns-1", handler.createdSubjectConditions["scs-1"]["ns-1"].Namespace) + assert.Equal(t, map[string]string{ + "owner": "policy-team", + "env": "dev", + migrationLabelMigratedFrom: "scs-1", + }, handler.createdSubjectConditions["scs-1"]["ns-1"].Metadata.GetLabels()) + + createdTarget := plan.SubjectConditionSets[0].Targets[0] + assert.Equal(t, TargetStatusCreate, createdTarget.Status) + assert.Empty(t, createdTarget.ExistingID) + require.NotNil(t, createdTarget.Execution) + assert.True(t, createdTarget.Execution.Applied) + assert.Equal(t, "created-scs-1", createdTarget.Execution.CreatedTargetID) + assert.Equal(t, "created-scs-1", createdTarget.TargetID()) + + migratedTarget := plan.SubjectConditionSets[0].Targets[1] + assert.Equal(t, "migrated-scs-1", migratedTarget.TargetID()) + assert.Nil(t, migratedTarget.Execution) + assert.Equal(t, "created-scs-1", executor.cachedScsTargetID("scs-1", namespace1)) + assert.Equal(t, "migrated-scs-1", executor.cachedScsTargetID("scs-1", namespace2)) + assert.Empty(t, executor.cachedScsTargetID("scs-2", namespace1)) + }, + }, + { + name: "ignores unresolved target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusUnresolved, + Reason: "missing target namespace mapping", + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Empty(t, handler.createdSubjectConditions) + }, + }, + { + name: "returns error for missing already migrated target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrMissingMigratedTarget, `subject condition set %q target %q`, "scs-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdSubjectConditions) + }, + }, + { + name: "returns error for missing target namespace", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Status: TargetStatusCreate, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrTargetNamespaceRequired, `subject condition set %q`, "scs-1"), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdSubjectConditions) + }, + }, + { + name: "returns error for missing created target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + subjectConditionSetResult: map[string]map[string]*policy.SubjectConditionSet{ + "scs-1": { + "ns-1": {}, + }, + }, + }, + wantErr: wantError(ErrMissingCreatedTargetID, `subject condition set %q target %q`, "scs-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.createdSubjectConditions, "scs-1") + require.NotNil(t, plan.SubjectConditionSets[0].Targets[0].Execution) + assert.Equal(t, ErrMissingCreatedTargetID.Error(), plan.SubjectConditionSets[0].Targets[0].Execution.Failure) + assert.Empty(t, executor.cachedScsTargetID("scs-1", namespace1)) + }, + }, + { + name: "returns error for unsupported target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatus("bogus"), + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError( + ErrUnsupportedStatus, + `subject condition set %q target %q has unsupported status %q`, + "scs-1", + namespace1.GetFqn(), + TargetStatus("bogus"), + ), + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdSubjectConditions) + }, + }, + { + name: "records create failures on the target", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectConditionSets}, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1", SubjectSets: subjectSets}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + }, + handler: &mockExecutorHandler{ + subjectConditionSetErrs: map[string]map[string]error{ + "scs-1": { + "ns-1": errBoom, + }, + }, + }, + wantErr: &expectedError{ + is: errBoom, + message: `create subject condition set "scs-1" in namespace "https://example.com": boom`, + }, + assert: func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.NotNil(t, plan.SubjectConditionSets[0].Targets[0].Execution) + assert.Equal(t, "boom", plan.SubjectConditionSets[0].Targets[0].Execution.Failure) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + executor, err := NewMigrationExecutor(tt.handler) + require.NoError(t, err) + + err = executor.ExecuteMigration(t.Context(), tt.plan) + switch { + case tt.wantErr != nil: + require.Error(t, err) + require.ErrorIs(t, err, tt.wantErr.is) + require.EqualError(t, err, tt.wantErr.message) + default: + require.NoError(t, err) + } + + tt.assert(t, err, executor, tt.handler, tt.plan) + }) + } +} diff --git a/otdfctl/migrations/namespacedpolicy/subject_mappings_execute.go b/otdfctl/migrations/namespacedpolicy/subject_mappings_execute.go new file mode 100644 index 0000000000..30b7a5aa5e --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/subject_mappings_execute.go @@ -0,0 +1,134 @@ +package namespacedpolicy + +import ( + "context" + "fmt" + + "github.com/opentdf/platform/protocol/go/policy" +) + +func (e *MigrationExecutor) executeSubjectMappings(ctx context.Context, plans []*SubjectMappingPlan) error { + if len(plans) == 0 { + return nil + } + + for _, mappingPlan := range plans { + if mappingPlan == nil || mappingPlan.Source == nil { + continue + } + + if mappingPlan.Target == nil { + continue + } + + if err := e.executeSubjectMappingTarget(ctx, mappingPlan, mappingPlan.Target); err != nil { + return err + } + } + + return nil +} + +func (e *MigrationExecutor) executeSubjectMappingTarget(ctx context.Context, mappingPlan *SubjectMappingPlan, target *SubjectMappingTargetPlan) error { + //nolint:exhaustive // Subject mapping execution only handles create and already-migrated explicitly; all other statuses are unsupported. + switch target.Status { + case TargetStatusAlreadyMigrated: + if target.TargetID() == "" { + return fmt.Errorf("%w: subject mapping %q target %q", ErrMissingMigratedTarget, mappingPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + return nil + case TargetStatusSkipped: + return nil + case TargetStatusCreate: + return e.createSubjectMappingTarget(ctx, mappingPlan, target) + case TargetStatusUnresolved: + return nil + default: + return fmt.Errorf("%w: subject mapping %q target %q has unsupported status %q", ErrUnsupportedStatus, mappingPlan.Source.GetId(), namespaceLabel(target.Namespace), target.Status) + } +} + +func (e *MigrationExecutor) createSubjectMappingTarget(ctx context.Context, mappingPlan *SubjectMappingPlan, target *SubjectMappingTargetPlan) error { + namespace := namespaceIdentifier(target.Namespace) + if namespace == "" { + return fmt.Errorf("%w: subject mapping %q", ErrTargetNamespaceRequired, mappingPlan.Source.GetId()) + } + + actions, err := e.resolveSubjectMappingActions(mappingPlan, target) + if err != nil { + return err + } + + subjectConditionSetID, err := e.resolveSubjectMappingSubjectConditionSet(mappingPlan, target) + if err != nil { + return err + } + + attributeValueID := mappingPlan.Source.GetAttributeValue().GetId() + if attributeValueID == "" { + return fmt.Errorf("subject mapping %q missing attribute value id", mappingPlan.Source.GetId()) + } + + created, err := e.handler.CreateNewSubjectMapping( + ctx, + attributeValueID, + actions, + subjectConditionSetID, + nil, + metadataForCreate( + mappingPlan.Source.GetId(), + metadataLabels(mappingPlan.Source.GetMetadata()), + ), + namespace, + ) + if err != nil { + target.Execution = &ExecutionResult{ + Failure: err.Error(), + } + return fmt.Errorf("create subject mapping %q in namespace %q: %w", mappingPlan.Source.GetId(), namespaceLabel(target.Namespace), err) + } + if created.GetId() == "" { + target.Execution = &ExecutionResult{ + Failure: ErrMissingCreatedTargetID.Error(), + } + return fmt.Errorf("%w: subject mapping %q target %q", ErrMissingCreatedTargetID, mappingPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + target.Execution = &ExecutionResult{ + Applied: true, + CreatedTargetID: created.GetId(), + } + + return nil +} + +func (e *MigrationExecutor) resolveSubjectMappingActions(mappingPlan *SubjectMappingPlan, target *SubjectMappingTargetPlan) ([]*policy.Action, error) { + actions := make([]*policy.Action, 0, len(target.ActionSourceIDs)) + for _, sourceID := range target.ActionSourceIDs { + if sourceID == "" { + return nil, fmt.Errorf("%w: subject mapping %q target %q", ErrMissingActionTarget, mappingPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + targetID := e.cachedActionTargetID(sourceID, target.Namespace) + if targetID == "" { + return nil, fmt.Errorf("%w: subject mapping %q action %q target %q", ErrMissingActionTarget, mappingPlan.Source.GetId(), sourceID, namespaceLabel(target.Namespace)) + } + + actions = append(actions, &policy.Action{Id: targetID}) + } + + return actions, nil +} + +func (e *MigrationExecutor) resolveSubjectMappingSubjectConditionSet(mappingPlan *SubjectMappingPlan, target *SubjectMappingTargetPlan) (string, error) { + if target.SubjectConditionSetSourceID == "" { + return "", fmt.Errorf("%w: subject mapping %q target %q", ErrMissingSubjectConditionSetTarget, mappingPlan.Source.GetId(), namespaceLabel(target.Namespace)) + } + + targetID := e.cachedScsTargetID(target.SubjectConditionSetSourceID, target.Namespace) + if targetID == "" { + return "", fmt.Errorf("%w: subject mapping %q subject condition set %q target %q", ErrMissingSubjectConditionSetTarget, mappingPlan.Source.GetId(), target.SubjectConditionSetSourceID, namespaceLabel(target.Namespace)) + } + + return targetID, nil +} diff --git a/otdfctl/migrations/namespacedpolicy/subject_mappings_execute_test.go b/otdfctl/migrations/namespacedpolicy/subject_mappings_execute_test.go new file mode 100644 index 0000000000..a1718e734f --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/subject_mappings_execute_test.go @@ -0,0 +1,485 @@ +package namespacedpolicy + +import ( + "errors" + "testing" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExecuteSubjectMappings(t *testing.T) { + t.Parallel() + + namespace1 := &policy.Namespace{Id: "ns-1", Fqn: "https://example.com"} + errBoom := errors.New("boom") + + tests := []struct { + name string + plan *MigrationPlan + handler *mockExecutorHandler + wantErr *expectedError + assert func(t *testing.T, err error, executor *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) + }{ + { + name: "creates subject mappings with migrated action and scs ids", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeSubjectConditionSets, ScopeSubjectMappings}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{ + Id: "scs-1", + }, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusCreate, + }, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Id: "av-1", + }, + Metadata: &common.Metadata{ + Labels: map[string]string{ + "owner": "policy-team", + "env": "dev", + }, + }, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceIDs: []string{"action-1"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{ + results: map[string]map[string]*policy.Action{ + "decrypt": { + "ns-1": {Id: "action-target-1"}, + }, + }, + subjectConditionSetResult: map[string]map[string]*policy.SubjectConditionSet{ + "scs-1": { + "ns-1": {Id: "scs-target-1"}, + }, + }, + subjectMappingResults: map[string]map[string]*policy.SubjectMapping{ + "mapping-1": { + "ns-1": {Id: "mapping-target-1"}, + }, + }, + }, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + require.Contains(t, handler.createdSubjectMappings, "mapping-1") + require.Contains(t, handler.createdSubjectMappings["mapping-1"], "ns-1") + call := handler.createdSubjectMappings["mapping-1"]["ns-1"] + assert.Equal(t, "av-1", call.AttributeValueID) + require.Len(t, call.Actions, 1) + assert.Equal(t, "action-target-1", call.Actions[0].GetId()) + assert.Equal(t, "scs-target-1", call.ExistingSubjectConditionSet) + assert.Nil(t, call.NewSubjectConditionSet) + assert.Equal(t, "ns-1", call.Namespace) + assert.Equal(t, map[string]string{ + "owner": "policy-team", + "env": "dev", + migrationLabelMigratedFrom: "mapping-1", + }, call.Metadata.GetLabels()) + + target := plan.SubjectMappings[0].Target + assert.Equal(t, TargetStatusCreate, target.Status) + assert.Empty(t, target.ExistingID) + require.NotNil(t, target.Execution) + assert.True(t, target.Execution.Applied) + assert.Equal(t, "mapping-target-1", target.Execution.CreatedTargetID) + assert.Equal(t, "mapping-target-1", target.TargetID()) + }, + }, + { + name: "skips already migrated subject mapping targets", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{Id: "mapping-1"}, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + ExistingID: "mapping-target-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Empty(t, handler.createdSubjectMappings) + assert.Equal(t, "mapping-target-1", plan.SubjectMappings[0].Target.TargetID()) + assert.Nil(t, plan.SubjectMappings[0].Target.Execution) + }, + }, + { + name: "ignores unresolved target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{Id: "mapping-1"}, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatusUnresolved, + Reason: "missing target namespace mapping", + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.NoError(t, err) + assert.Empty(t, handler.createdSubjectMappings) + }, + }, + { + name: "returns error for missing already migrated target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{Id: "mapping-1"}, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrMissingMigratedTarget, `subject mapping %q target %q`, "mapping-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdSubjectMappings) + }, + }, + { + name: "returns error for missing action target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Id: "av-1", + }, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceIDs: []string{"action-1"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrMissingActionTarget, `subject mapping %q action %q target %q`, "mapping-1", "action-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdSubjectMappings) + }, + }, + { + name: "returns error for missing scs target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeSubjectMappings}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + ExistingID: "action-target-1", + }, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Id: "av-1", + }, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceIDs: []string{"action-1"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrMissingSubjectConditionSetTarget, `subject mapping %q subject condition set %q target %q`, "mapping-1", "scs-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdSubjectMappings) + }, + }, + { + name: "returns error for missing target namespace", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Id: "av-1", + }, + }, + Target: &SubjectMappingTargetPlan{ + Status: TargetStatusCreate, + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError(ErrTargetNamespaceRequired, `subject mapping %q`, "mapping-1"), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdSubjectMappings) + }, + }, + { + name: "returns error for missing created target id", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeSubjectConditionSets, ScopeSubjectMappings}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + ExistingID: "action-target-1", + }, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + ExistingID: "scs-target-1", + }, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Id: "av-1", + }, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceIDs: []string{"action-1"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{ + subjectMappingResults: map[string]map[string]*policy.SubjectMapping{ + "mapping-1": { + "ns-1": {}, + }, + }, + }, + wantErr: wantError(ErrMissingCreatedTargetID, `subject mapping %q target %q`, "mapping-1", namespace1.GetFqn()), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.createdSubjectMappings, "mapping-1") + require.NotNil(t, plan.SubjectMappings[0].Target.Execution) + assert.Equal(t, ErrMissingCreatedTargetID.Error(), plan.SubjectMappings[0].Target.Execution.Failure) + }, + }, + { + name: "returns error for unsupported target status", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeSubjectMappings}, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{Id: "mapping-1"}, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatus("bogus"), + }, + }, + }, + }, + handler: &mockExecutorHandler{}, + wantErr: wantError( + ErrUnsupportedStatus, + `subject mapping %q target %q has unsupported status %q`, + "mapping-1", + namespace1.GetFqn(), + TargetStatus("bogus"), + ), + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, _ *MigrationPlan) { + t.Helper() + + require.Error(t, err) + assert.Empty(t, handler.createdSubjectMappings) + }, + }, + { + name: "records create failures on the target", + plan: &MigrationPlan{ + Scopes: []Scope{ScopeActions, ScopeSubjectConditionSets, ScopeSubjectMappings}, + Actions: []*ActionPlan{ + { + Source: &policy.Action{Id: "action-1", Name: "decrypt"}, + Targets: []*ActionTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + ExistingID: "action-target-1", + }, + }, + }, + }, + SubjectConditionSets: []*SubjectConditionSetPlan{ + { + Source: &policy.SubjectConditionSet{Id: "scs-1"}, + Targets: []*SubjectConditionSetTargetPlan{ + { + Namespace: namespace1, + Status: TargetStatusAlreadyMigrated, + ExistingID: "scs-target-1", + }, + }, + }, + }, + SubjectMappings: []*SubjectMappingPlan{ + { + Source: &policy.SubjectMapping{ + Id: "mapping-1", + AttributeValue: &policy.Value{ + Id: "av-1", + }, + }, + Target: &SubjectMappingTargetPlan{ + Namespace: namespace1, + Status: TargetStatusCreate, + ActionSourceIDs: []string{"action-1"}, + SubjectConditionSetSourceID: "scs-1", + }, + }, + }, + }, + handler: &mockExecutorHandler{ + subjectMappingErrs: map[string]map[string]error{ + "mapping-1": { + "ns-1": errBoom, + }, + }, + }, + wantErr: &expectedError{ + is: errBoom, + message: `create subject mapping "mapping-1" in namespace "https://example.com": boom`, + }, + assert: func(t *testing.T, err error, _ *MigrationExecutor, handler *mockExecutorHandler, plan *MigrationPlan) { + t.Helper() + + require.Error(t, err) + require.Contains(t, handler.createdSubjectMappings, "mapping-1") + require.NotNil(t, plan.SubjectMappings[0].Target.Execution) + assert.Equal(t, "boom", plan.SubjectMappings[0].Target.Execution.Failure) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + executor, err := NewMigrationExecutor(tt.handler) + require.NoError(t, err) + + err = executor.ExecuteMigration(t.Context(), tt.plan) + switch { + case tt.wantErr != nil: + require.Error(t, err) + require.ErrorIs(t, err, tt.wantErr.is) + require.EqualError(t, err, tt.wantErr.message) + default: + require.NoError(t, err) + } + + tt.assert(t, err, executor, tt.handler, tt.plan) + }) + } +} + +func TestSubjectMappingTargetIDPrefersExecutionResult(t *testing.T) { + t.Parallel() + + target := &SubjectMappingTargetPlan{ + Namespace: &policy.Namespace{Id: "ns-2", Fqn: "https://example.net"}, + ExistingID: "existing-target", + Execution: &ExecutionResult{ + CreatedTargetID: "created-target", + }, + } + + assert.Equal(t, "created-target", target.TargetID()) +} diff --git a/otdfctl/migrations/namespacedpolicy/summary_renderer.go b/otdfctl/migrations/namespacedpolicy/summary_renderer.go new file mode 100644 index 0000000000..8ef65f2859 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/summary_renderer.go @@ -0,0 +1,187 @@ +package namespacedpolicy + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/opentdf/platform/otdfctl/migrations" +) + +type summaryCounts struct { + applied int + pending int + existingStandard int + alreadyMigrated int + skipped int + blocked int + failed int + unresolved int +} + +type summaryOperation string + +const ( + summaryOperationMigration summaryOperation = "migration" + summaryOperationPrune summaryOperation = "prune" +) + +type constructSummary struct { + label string + include bool + counts summaryCounts + applied []string + pending []string + failed []string + skipped []string + blocked []string + unresolved []string +} + +type operationExecutionState string + +const ( + operationExecutionStateApplied operationExecutionState = "applied" + operationExecutionStatePending operationExecutionState = "pending" + operationExecutionStateFailed operationExecutionState = "failed" +) + +type summaryDocument struct { + plannedTitle string + committedTitle string + operation summaryOperation + scopes []Scope + commit bool + result string + summaries []constructSummary +} + +func renderSummaryDocument(styles *migrations.DisplayStyles, doc summaryDocument) string { + var b strings.Builder + if doc.commit { + b.WriteString(styles.Title().Render(doc.committedTitle)) + } else { + b.WriteString(styles.Title().Render(doc.plannedTitle)) + } + b.WriteByte('\n') + b.WriteString(styles.Separator().Render(styles.SeparatorText())) + b.WriteByte('\n') + fmt.Fprintf(&b, "%s %s\n", styles.Info().Render("Scopes:"), styles.Info().Render(joinScopeLabels(doc.scopes))) + fmt.Fprintf(&b, "%s %t\n", styles.Info().Render("Commit:"), doc.commit) + b.WriteString(styles.Info().Render("Result: " + strings.TrimSpace(doc.result))) + b.WriteByte('\n') + + for _, summary := range doc.summaries { + if !summary.include { + continue + } + b.WriteByte('\n') + b.WriteString(styles.Title().Render(summary.label)) + b.WriteByte('\n') + b.WriteString(styles.Separator().Render(styles.SeparatorText())) + b.WriteByte('\n') + fmt.Fprintf(&b, "%s %s\n", styles.Info().Render("Counts:"), formatConstructSummaryCounts(summary.counts, doc.operation, doc.commit)) + + appendSummarySection(&b, appliedSummarySection(doc.operation), styles.Action(), summary.applied) + appendSummarySection(&b, pendingSummarySection(doc.operation), styles.Action(), summary.pending) + appendSummarySection(&b, "Failed", styles.Warning(), summary.failed) + appendSummarySection(&b, "Skipped", styles.Warning(), summary.skipped) + appendSummarySection(&b, "Blocked", styles.Warning(), summary.blocked) + appendSummarySection(&b, "Unresolved", styles.Warning(), summary.unresolved) + } + + return strings.TrimRight(b.String(), "\n") +} + +func appendSummarySection(b *strings.Builder, label string, style lipgloss.Style, lines []string) { + if len(lines) == 0 { + return + } + b.WriteByte('\n') + b.WriteString(style.Render(label)) + b.WriteByte('\n') + for _, line := range lines { + b.WriteString(" - ") + b.WriteString(line) + b.WriteByte('\n') + } +} + +func includesScope(scopes []Scope, scope Scope) bool { + for _, candidate := range scopes { + if candidate == scope { + return true + } + } + return false +} + +func joinScopeLabels(scopes []Scope) string { + if len(scopes) == 0 { + return "(none)" + } + + labels := make([]string, 0, len(scopes)) + for _, scope := range scopes { + labels = append(labels, string(scope)) + } + + return strings.Join(labels, ", ") +} + +func formatConstructSummaryCounts(counts summaryCounts, operation summaryOperation, commit bool) string { + var parts []string + if commit { + parts = append(parts, fmt.Sprintf("%s=%d", appliedSummaryCount(operation), counts.applied)) + } + if !commit || counts.pending > 0 { + parts = append(parts, fmt.Sprintf("%s=%d", pendingSummaryCount(operation), counts.pending)) + } + if operation == summaryOperationMigration { + parts = appendMigrationSummaryCountParts(parts, counts) + } + parts = append(parts, fmt.Sprintf("skipped=%d", counts.skipped)) + if operation == summaryOperationPrune { + parts = appendPruneSummaryCountParts(parts, counts) + } + parts = append(parts, + fmt.Sprintf("failed=%d", counts.failed), + fmt.Sprintf("unresolved=%d", counts.unresolved), + ) + return strings.Join(parts, " ") +} + +func appliedSummaryCount(operation summaryOperation) string { + if operation == summaryOperationPrune { + return pruneAppliedCountLabel + } + return migrationAppliedCountLabel +} + +func pendingSummaryCount(operation summaryOperation) string { + if operation == summaryOperationPrune { + return prunePendingCountLabel + } + return migrationPendingCountLabel +} + +func appliedSummarySection(operation summaryOperation) string { + if operation == summaryOperationPrune { + return pruneAppliedSectionLabel + } + return migrationAppliedSectionLabel +} + +func pendingSummarySection(operation summaryOperation) string { + if operation == summaryOperationPrune { + return prunePendingSectionLabel + } + return migrationPendingSectionLabel +} + +func executionFailure(execution *ExecutionResult) string { + if execution == nil { + return "" + } + return execution.Failure +} diff --git a/otdfctl/migrations/namespacedpolicy/summary_test_helpers_test.go b/otdfctl/migrations/namespacedpolicy/summary_test_helpers_test.go new file mode 100644 index 0000000000..f232e402f1 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/summary_test_helpers_test.go @@ -0,0 +1,8 @@ +package namespacedpolicy + +import "regexp" + +func stripANSI(value string) string { + tidyWhitespace := regexp.MustCompile(`\x1b\[[0-9;]*m`) + return tidyWhitespace.ReplaceAllString(value, "") +} diff --git a/otdfctl/migrations/namespacedpolicy/test_helpers_test.go b/otdfctl/migrations/namespacedpolicy/test_helpers_test.go new file mode 100644 index 0000000000..815f661cb6 --- /dev/null +++ b/otdfctl/migrations/namespacedpolicy/test_helpers_test.go @@ -0,0 +1,43 @@ +package namespacedpolicy + +import "github.com/opentdf/platform/protocol/go/policy" + +func testNamespace(fqn string) *policy.Namespace { + return &policy.Namespace{ + Fqn: fqn, + } +} + +func testAttributeValue(fqn string, namespace *policy.Namespace) *policy.Value { + return &policy.Value{ + Fqn: fqn, + Attribute: &policy.Attribute{ + Namespace: namespace, + }, + } +} + +func testActionAttributeValue(actionID, actionName string, attributeValue *policy.Value) *policy.RegisteredResourceValue_ActionAttributeValue { + return &policy.RegisteredResourceValue_ActionAttributeValue{ + Action: &policy.Action{ + Id: actionID, + Name: actionName, + }, + AttributeValue: attributeValue, + } +} + +func testRegisteredResourceValue(value string, aavs ...*policy.RegisteredResourceValue_ActionAttributeValue) *policy.RegisteredResourceValue { + return &policy.RegisteredResourceValue{ + Value: value, + ActionAttributeValues: aavs, + } +} + +func testRegisteredResource(id, name string, values ...*policy.RegisteredResourceValue) *policy.RegisteredResource { + return &policy.RegisteredResource{ + Id: id, + Name: name, + Values: values, + } +} diff --git a/otdfctl/migrations/styles.go b/otdfctl/migrations/styles.go new file mode 100644 index 0000000000..4459c614cb --- /dev/null +++ b/otdfctl/migrations/styles.go @@ -0,0 +1,79 @@ +package migrations + +import "github.com/charmbracelet/lipgloss" + +// DisplayStyles holds all lipgloss styles for migration output. +type DisplayStyles struct { + styleTitle lipgloss.Style + styleResourceID lipgloss.Style + styleNamespace lipgloss.Style + styleName lipgloss.Style + styleValue lipgloss.Style + styleID lipgloss.Style + styleWarning lipgloss.Style + styleInfo lipgloss.Style + styleSeparator lipgloss.Style + styleAction lipgloss.Style + separatorText string +} + +// NewDisplayStyles initializes and returns migration display styles. +func NewDisplayStyles() *DisplayStyles { + return &DisplayStyles{ + styleTitle: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")), + styleResourceID: lipgloss.NewStyle().Foreground(lipgloss.Color("10")), + styleNamespace: lipgloss.NewStyle().Foreground(lipgloss.Color("11")), + styleName: lipgloss.NewStyle().Foreground(lipgloss.Color("13")), + styleValue: lipgloss.NewStyle().Foreground(lipgloss.Color("14")), + styleID: lipgloss.NewStyle().Foreground(lipgloss.Color("15")), + styleWarning: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9")), + styleInfo: lipgloss.NewStyle(), + styleSeparator: lipgloss.NewStyle().Faint(true), + styleAction: lipgloss.NewStyle().Foreground(lipgloss.Color("6")), + separatorText: "----------------------------------------------------------------------------------------------------", + } +} + +func (s *DisplayStyles) Title() lipgloss.Style { + return s.styleTitle +} + +func (s *DisplayStyles) ResourceID() lipgloss.Style { + return s.styleResourceID +} + +func (s *DisplayStyles) Namespace() lipgloss.Style { + return s.styleNamespace +} + +func (s *DisplayStyles) Name() lipgloss.Style { + return s.styleName +} + +func (s *DisplayStyles) Value() lipgloss.Style { + return s.styleValue +} + +func (s *DisplayStyles) ID() lipgloss.Style { + return s.styleID +} + +func (s *DisplayStyles) Warning() lipgloss.Style { + return s.styleWarning +} + +func (s *DisplayStyles) Info() lipgloss.Style { + return s.styleInfo +} + +func (s *DisplayStyles) Separator() lipgloss.Style { + return s.styleSeparator +} + +func (s *DisplayStyles) Action() lipgloss.Style { + return s.styleAction +} + +func (s *DisplayStyles) SeparatorText() string { + return s.separatorText +} diff --git a/otdfctl/pkg/auth/auth.go b/otdfctl/pkg/auth/auth.go new file mode 100644 index 0000000000..bd505d88ab --- /dev/null +++ b/otdfctl/pkg/auth/auth.go @@ -0,0 +1,372 @@ +package auth + +import ( + "context" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "net" + "net/url" + "os" + "strconv" + "strings" + "time" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/google/uuid" + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "github.com/opentdf/platform/sdk" + oidcrp "github.com/zitadel/oidc/v3/pkg/client/rp" + oidcCLI "github.com/zitadel/oidc/v3/pkg/client/rp/cli" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/oauth2" +) + +const authCallbackPath = "/callback" + +type ClientCredentials struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + Scopes []string `json:"scopes,omitempty"` +} + +type platformConfiguration struct { + issuer string + authzEndpoint string + tokenEndpoint string +} + +type oidcClientCredentials struct { + clientID string + clientSecret string + isPublic bool +} + +type JWTClaims struct { + Expiration int64 `json:"exp"` +} + +func NormalizeScopes(scopes []string) []string { + if len(scopes) == 0 { + return nil + } + normalized := make([]string, 0, len(scopes)) + for _, scope := range scopes { + normalized = append(normalized, strings.Fields(scope)...) + } + if len(normalized) == 0 { + return nil + } + return normalized +} + +func normalizeClientCredScopes(creds *ClientCredentials) { + if creds == nil { + return + } + creds.Scopes = NormalizeScopes(creds.Scopes) +} + +// Retrieves credentials by reading specified file +func GetClientCredsFromFile(filepath string) (ClientCredentials, error) { + creds := ClientCredentials{} + f, err := os.Open(filepath) + if err != nil { + return creds, errors.Join(errors.New("failed to open creds file"), err) + } + defer f.Close() + + if err := json.NewDecoder(f).Decode(&creds); err != nil { + return creds, errors.Join(errors.New("failed to decode creds file"), err) + } + normalizeClientCredScopes(&creds) + + return creds, nil +} + +// Parse the JSON and return the client ID and secret +func GetClientCredsFromJSON(credsJSON []byte) (ClientCredentials, error) { + creds := ClientCredentials{} + if err := json.Unmarshal(credsJSON, &creds); err != nil { + return creds, errors.Join(errors.New("failed to decode creds JSON"), err) + } + normalizeClientCredScopes(&creds) + + return creds, nil +} + +func getPlatformConfiguration(endpoint string, tlsNoVerify bool) (platformConfiguration, error) { + c := platformConfiguration{} + + normalized, err := utils.NormalizeEndpoint(endpoint) + if err != nil { + return c, err + } + + opts := []sdk.Option{sdk.WithConnectionValidation()} + if tlsNoVerify { + opts = append(opts, sdk.WithInsecureSkipVerifyConn()) + } + + if normalized.Scheme == "http" { + opts = append(opts, sdk.WithInsecurePlaintextConn()) + } + + s, err := sdk.New(normalized.String(), opts...) + if err != nil { + return c, err + } + + var e error + c.issuer, e = s.PlatformConfiguration.Issuer() + if e != nil { + err = errors.Join(err, sdk.ErrPlatformIssuerNotFound) + } + + c.authzEndpoint, e = s.PlatformConfiguration.AuthzEndpoint() + if e != nil { + err = errors.Join(err, sdk.ErrPlatformAuthzEndpointNotFound) + } + + c.tokenEndpoint, e = s.PlatformConfiguration.TokenEndpoint() + if e != nil { + err = errors.Join(err, sdk.ErrPlatformTokenEndpointNotFound) + } + + if err != nil { + return c, errors.Join(err, ErrProfileCredentialsNotFound) + } + + return c, nil +} + +func buildToken(c *profiles.AuthCredentials) *oauth2.Token { + return &oauth2.Token{ + AccessToken: c.AccessToken.AccessToken, + Expiry: time.Unix(c.AccessToken.Expiration, 0), + RefreshToken: c.AccessToken.RefreshToken, + } +} + +func ParseClaimsJWT(accessToken string) (JWTClaims, error) { + c := JWTClaims{} + jwt, err := jwt.ParseSigned(accessToken) + if err != nil { + return c, errors.Join(ErrParsingAccessToken, err) + } + if err := jwt.UnsafeClaimsWithoutVerification(&c); err != nil { + return c, errors.Join(ErrParsingAccessToken, err) + } + return c, nil +} + +func GetSDKAuthOptionFromProfile(profile *profiles.OtdfctlProfileStore) (sdk.Option, error) { + c := profile.GetAuthCredentials() + + switch c.AuthType { + case profiles.AuthTypeClientCredentials: + return sdk.WithClientCredentials(c.ClientID, c.ClientSecret, NormalizeScopes(c.Scopes)), nil + case profiles.AuthTypeAccessToken: + tokenSource := oauth2.StaticTokenSource(buildToken(&c)) + return sdk.WithOAuthAccessTokenSource(tokenSource), nil + default: + return nil, ErrInvalidAuthType + } +} + +func ValidateProfileAuthCredentials(ctx context.Context, profile *profiles.OtdfctlProfileStore) error { + c := profile.GetAuthCredentials() + + switch c.AuthType { + case "": + return ErrProfileCredentialsNotFound + case profiles.AuthTypeClientCredentials: + _, err := GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes) + if err != nil { + return err + } + return nil + case profiles.AuthTypeAccessToken: + if !buildToken(&c).Valid() { + return ErrAccessTokenExpired + } + default: + return ErrInvalidAuthType + } + return nil +} + +func GetTokenWithProfile(ctx context.Context, profile *profiles.OtdfctlProfileStore) (*oauth2.Token, error) { + c := profile.GetAuthCredentials() + + switch c.AuthType { + case profiles.AuthTypeClientCredentials: + return GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes) + case profiles.AuthTypeAccessToken: + return buildToken(&c), nil + default: + return nil, ErrInvalidAuthType + } +} + +// Uses the OAuth2 client credentials flow to obtain a token. +func GetTokenWithClientCreds(ctx context.Context, endpoint string, clientID string, clientSecret string, tlsNoVerify bool, scopes []string) (*oauth2.Token, error) { + rp, err := newOidcRelyingParty(ctx, endpoint, tlsNoVerify, oidcClientCredentials{ + clientID: clientID, + clientSecret: clientSecret, + }) + if err != nil { + return nil, err + } + params := url.Values{} + if normalized := NormalizeScopes(scopes); len(normalized) > 0 { + params.Set("scope", strings.Join(normalized, " ")) + } + return oidcrp.ClientCredentials(ctx, rp, params) +} + +const ( + keyLength = 16 + fiveSecDuration = 5 * time.Second +) + +// GetFreePort returns an available TCP port on localhost. +// The function works by asking the operating system to assign +// a free port (by using port 0), then returns that assigned port. +func GetFreePort(ctx context.Context) (int, error) { + // Create a listener on localhost with port 0 (OS will assign a free port) + cfg := &net.ListenConfig{} + listener, err := cfg.Listen(ctx, "tcp", "localhost:0") + if err != nil { + return 0, fmt.Errorf("failed to find available port: %w", err) + } + + // Make sure we release the port when done + defer listener.Close() + + // Get the address information from the listener + addr, ok := listener.Addr().(*net.TCPAddr) + if !ok { + return 0, errors.New("failed to get TCP address from listener") + } + + // Return the port that was assigned + return addr.Port, nil +} + +// Facilitates an auth code PKCE flow to obtain OIDC tokens. +// Spawns a local server to handle the callback and opens a browser window in each respective OS. +func Login(ctx context.Context, platformEndpoint, tokenURL, authURL, publicClientID, authCodeFlowPort string) (*oauth2.Token, error) { + // Generate random hash and encryption keys for cookie handling + hashKey := make([]byte, keyLength) + encryptKey := make([]byte, keyLength) + + _, err := rand.Read(hashKey) + if err != nil { + return nil, err + } + + _, err = rand.Read(encryptKey) + if err != nil { + return nil, err + } + + if strings.TrimSpace(authCodeFlowPort) == "" { + port, err := GetFreePort(ctx) + if err != nil { + return nil, fmt.Errorf("failed to find available port for auth code flow: %w", err) + } + authCodeFlowPort = strconv.Itoa(port) + } + + conf := &oauth2.Config{ + ClientID: publicClientID, + Scopes: []string{"openid", "profile", "email"}, + RedirectURL: fmt.Sprintf("http://localhost:%s%s", authCodeFlowPort, authCallbackPath), + Endpoint: oauth2.Endpoint{ + AuthURL: authURL, + TokenURL: tokenURL, + }, + } + + cookiehandler := httphelper.NewCookieHandler(hashKey, encryptKey) + + relyingParty, err := oidcrp.NewRelyingPartyOAuth(conf, + // allow cookie handling for PKCE + oidcrp.WithCookieHandler(cookiehandler), + // use PKCE + oidcrp.WithPKCE(cookiehandler), + // allow IAT claim offset of 5 seconds + oidcrp.WithVerifierOpts(oidcrp.WithIssuedAtOffset(fiveSecDuration)), + ) + if err != nil { + return nil, fmt.Errorf("failed to create relying party: %w", err) + } + stateProvider := func() string { + return uuid.New().String() + } + tok := oidcCLI.CodeFlow[*oidc.IDTokenClaims](ctx, relyingParty, authCallbackPath, authCodeFlowPort, stateProvider) + return &oauth2.Token{ + AccessToken: tok.AccessToken, + TokenType: tok.TokenType, + RefreshToken: tok.RefreshToken, + Expiry: tok.Expiry, + }, nil +} + +// Logs in using the auth code PKCE flow driven by the platform well-known idP OIDC configuration. +func LoginWithPKCE(ctx context.Context, host, clientID string, tlsNoVerify bool, port string) (*oauth2.Token, error) { + pc, err := getPlatformConfiguration(host, tlsNoVerify) + if err != nil { + return nil, fmt.Errorf("failed to get platform configuration: %w", err) + } + + tok, err := Login(ctx, host, pc.tokenEndpoint, pc.authzEndpoint, clientID, port) + if err != nil { + return nil, fmt.Errorf("failed to login: %w", err) + } + + return tok, nil +} + +// Revokes the access token +func RevokeAccessToken(ctx context.Context, endpoint, clientID, refreshToken string, tlsNoVerify bool) error { + rp, err := newOidcRelyingParty(ctx, endpoint, tlsNoVerify, oidcClientCredentials{ + clientID: clientID, + isPublic: true, + }) + if err != nil { + return err + } + return oidcrp.RevokeToken(ctx, rp, refreshToken, "refresh_token") +} + +func newOidcRelyingParty(ctx context.Context, endpoint string, tlsNoVerify bool, clientCreds oidcClientCredentials) (oidcrp.RelyingParty, error) { + if clientCreds.clientID == "" { + return nil, errors.New("client ID is required") + } + if clientCreds.clientSecret == "" && !clientCreds.isPublic { + return nil, errors.New("client secret is required") + } + if clientCreds.clientSecret != "" && clientCreds.isPublic { + return nil, errors.New("client secret must be empty for public clients") + } + + pc, err := getPlatformConfiguration(endpoint, tlsNoVerify) + if err != nil { + return nil, err + } + + return oidcrp.NewRelyingPartyOIDC( + ctx, + pc.issuer, + clientCreds.clientID, + clientCreds.clientSecret, + "", + nil, + oidcrp.WithHTTPClient(utils.NewHTTPClient(tlsNoVerify)), + ) +} diff --git a/otdfctl/pkg/auth/errors.go b/otdfctl/pkg/auth/errors.go new file mode 100644 index 0000000000..b074779795 --- /dev/null +++ b/otdfctl/pkg/auth/errors.go @@ -0,0 +1,15 @@ +package auth + +import "errors" + +var ( + ErrAccessTokenExpired = errors.New("access token expired") + ErrAccessTokenNotFound = errors.New("no access token found") + ErrClientCredentialsNotFound = errors.New("client credentials not found") + ErrInvalidAuthType = errors.New("invalid auth type") + ErrUnauthenticated = errors.New("not logged in") + ErrParsingAccessToken = errors.New("failed to parse access token") + ErrProfileCredentialsNotFound = errors.New("profile missing credentials") + ErrNoRefreshToken = errors.New("no refresh token available") + ErrRefreshFailed = errors.New("token refresh failed") +) diff --git a/otdfctl/pkg/auth/refresh.go b/otdfctl/pkg/auth/refresh.go new file mode 100644 index 0000000000..8965dbcb4a --- /dev/null +++ b/otdfctl/pkg/auth/refresh.go @@ -0,0 +1,145 @@ +package auth + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "golang.org/x/oauth2" +) + +const ( + DefaultPublicClientID = "cli-client" + // expiryBuffer is added to the current time to account for token expiry occurring during + // subprocess startup and network latency between the expiry check and the actual API call. + expiryBuffer = 30 * time.Second +) + +// tokenEndpointResolver looks up the OAuth2 token endpoint for a given +// platform endpoint. Production code uses getTokenEndpoint; tests inject +// a stub to avoid real gRPC calls. +type tokenEndpointResolver func(endpoint string, tlsNoVerify bool) (string, error) + +// RefreshAccessToken refreshes the access token using the stored refresh token +// and updates the profile with the new tokens. +func RefreshAccessToken(ctx context.Context, profile *profiles.OtdfctlProfileStore) error { + return refreshAccessToken(ctx, profile, getTokenEndpoint) +} + +func refreshAccessToken(ctx context.Context, profile *profiles.OtdfctlProfileStore, resolveEndpoint tokenEndpointResolver) error { + if profile == nil { + return errors.New("profile is required") + } + + creds := profile.GetAuthCredentials() + + if creds.AuthType != profiles.AuthTypeAccessToken { + return fmt.Errorf("%w: auth type is %s, not access-token", ErrInvalidAuthType, creds.AuthType) + } + + if creds.AccessToken.RefreshToken == "" { + return ErrNoRefreshToken + } + + endpoint := profile.GetEndpoint() + tlsNoVerify := profile.GetTLSNoVerify() + + normalized, err := utils.NormalizeEndpoint(endpoint) + if err != nil { + return fmt.Errorf("failed to normalize endpoint: %w", err) + } + + tokenEndpoint, err := resolveEndpoint(normalized.String(), tlsNoVerify) + if err != nil { + return fmt.Errorf("failed to get token endpoint: %w", err) + } + + clientID := creds.AccessToken.ClientID + if clientID == "" { + clientID = DefaultPublicClientID + } + + oauth2Config := &oauth2.Config{ + ClientID: clientID, + Endpoint: oauth2.Endpoint{ + TokenURL: tokenEndpoint, + }, + } + + oldToken := &oauth2.Token{ + RefreshToken: creds.AccessToken.RefreshToken, + } + + if tlsNoVerify { + httpClient := utils.NewHTTPClient(tlsNoVerify) + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + } + + tokenSource := oauth2Config.TokenSource(ctx, oldToken) + newToken, err := tokenSource.Token() + if err != nil { + return fmt.Errorf("%w: %w", ErrRefreshFailed, err) + } + + slog.Debug("successfully refreshed access token") + + expiration := newToken.Expiry.Unix() + if newToken.Expiry.IsZero() { + expiration = time.Now().Add(time.Hour).Unix() + slog.Warn("token response missing expires_in, assuming 1 hour") + } + + newCreds := profiles.AuthCredentials{ + AuthType: profiles.AuthTypeAccessToken, + AccessToken: profiles.AuthCredentialsAccessToken{ + ClientID: clientID, + AccessToken: newToken.AccessToken, + RefreshToken: newToken.RefreshToken, + Expiration: expiration, + }, + } + + if err := profile.SetAuthCredentials(newCreds); err != nil { + return fmt.Errorf("failed to save refreshed credentials: %w", err) + } + + slog.Info("access token refreshed and saved") + return nil +} + +// IsTokenExpired checks if the access token in the profile is expired. +// Returns false for non-access-token auth types since refresh only applies there. +func IsTokenExpired(profile *profiles.OtdfctlProfileStore) bool { + if profile == nil { + return true + } + creds := profile.GetAuthCredentials() + if creds.AuthType != profiles.AuthTypeAccessToken { + return false + } + expiry := time.Unix(creds.AccessToken.Expiration, 0) + // We are checking if the current time plus the buffer is after the true token expiry time. + // If it is, we refresh the token. The purpose of the buffer is to avoid expiry between calls. + return time.Now().Add(expiryBuffer).After(expiry) +} + +// HasRefreshToken checks if the profile has a refresh token. +func HasRefreshToken(profile *profiles.OtdfctlProfileStore) bool { + if profile == nil { + return false + } + creds := profile.GetAuthCredentials() + return creds.AuthType == profiles.AuthTypeAccessToken && creds.AccessToken.RefreshToken != "" +} + +func getTokenEndpoint(endpoint string, tlsNoVerify bool) (string, error) { + pc, err := getPlatformConfiguration(endpoint, tlsNoVerify) + if err != nil { + return "", fmt.Errorf("failed to get platform configuration: %w", err) + } + return pc.tokenEndpoint, nil +} diff --git a/otdfctl/pkg/auth/refresh_test.go b/otdfctl/pkg/auth/refresh_test.go new file mode 100644 index 0000000000..b7e4c9c0af --- /dev/null +++ b/otdfctl/pkg/auth/refresh_test.go @@ -0,0 +1,315 @@ +package auth + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestProfile(t *testing.T, authType string, accessToken, refreshToken string, expiration int64) *profiles.OtdfctlProfileStore { + t.Helper() + cfg := &profiles.ProfileConfig{ + Name: "test", + Endpoint: "https://example.com", + TLSNoVerify: false, + } + store, err := profiles.NewOtdfctlProfileStore(profiles.ProfileDriverMemory, cfg, false) + require.NoError(t, err) + err = store.SetAuthCredentials(profiles.AuthCredentials{ + AuthType: authType, + AccessToken: profiles.AuthCredentialsAccessToken{ + ClientID: "cli-client", + AccessToken: accessToken, + RefreshToken: refreshToken, + Expiration: expiration, + }, + }) + require.NoError(t, err) + return store +} + +func TestIsTokenExpired(t *testing.T) { + tests := []struct { + name string + authType string + exp int64 + want bool + }{ + { + name: "expired token", + authType: profiles.AuthTypeAccessToken, + exp: time.Now().Add(-time.Hour).Unix(), + want: true, + }, + { + name: "valid token", + authType: profiles.AuthTypeAccessToken, + exp: time.Now().Add(time.Hour).Unix(), + want: false, + }, + { + name: "token within expiry buffer", + authType: profiles.AuthTypeAccessToken, + exp: time.Now().Add(10 * time.Second).Unix(), + want: true, + }, + { + name: "non-access-token auth type", + authType: profiles.AuthTypeClientCredentials, + exp: time.Now().Add(-time.Hour).Unix(), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + profile := newTestProfile(t, tt.authType, "tok", "refresh", tt.exp) + got := IsTokenExpired(profile) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestHasRefreshToken(t *testing.T) { + tests := []struct { + name string + authType string + refreshToken string + want bool + }{ + { + name: "has refresh token", + authType: profiles.AuthTypeAccessToken, + refreshToken: "refresh-tok", + want: true, + }, + { + name: "no refresh token", + authType: profiles.AuthTypeAccessToken, + refreshToken: "", + want: false, + }, + { + name: "wrong auth type", + authType: profiles.AuthTypeClientCredentials, + refreshToken: "refresh-tok", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + profile := newTestProfile(t, tt.authType, "tok", tt.refreshToken, time.Now().Add(time.Hour).Unix()) + got := HasRefreshToken(profile) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRefreshAccessTokenWrongAuthType(t *testing.T) { + profile := newTestProfile(t, profiles.AuthTypeClientCredentials, "tok", "refresh", time.Now().Add(time.Hour).Unix()) + err := RefreshAccessToken(t.Context(), profile) + require.ErrorIs(t, err, ErrInvalidAuthType) +} + +func TestRefreshAccessTokenNoRefreshToken(t *testing.T) { + profile := newTestProfile(t, profiles.AuthTypeAccessToken, "tok", "", time.Now().Add(-time.Hour).Unix()) + err := RefreshAccessToken(t.Context(), profile) + require.ErrorIs(t, err, ErrNoRefreshToken) +} + +func TestRefreshAccessTokenSuccess(t *testing.T) { + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(map[string]any{ + "access_token": "new-access-token", + "refresh_token": "new-refresh-token", + "token_type": "Bearer", + "expires_in": 3600, + }) + assert.NoError(t, err) + })) + defer tokenServer.Close() + + resolver := func(string, bool) (string, error) { + return tokenServer.URL, nil + } + + profile := newTestProfile(t, profiles.AuthTypeAccessToken, "old-token", "old-refresh", time.Now().Add(-time.Hour).Unix()) + + err := refreshAccessToken(t.Context(), profile, resolver) + require.NoError(t, err) + + creds := profile.GetAuthCredentials() + assert.Equal(t, "new-access-token", creds.AccessToken.AccessToken) + assert.Equal(t, "new-refresh-token", creds.AccessToken.RefreshToken) + assert.Greater(t, creds.AccessToken.Expiration, time.Now().Unix(), + "expiration should be updated to a future timestamp") +} + +func TestRefreshAccessTokenZeroExpiry(t *testing.T) { + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(map[string]any{ + "access_token": "new-token", + "refresh_token": "new-refresh", + "token_type": "Bearer", + }) + assert.NoError(t, err) + })) + defer tokenServer.Close() + + resolver := func(string, bool) (string, error) { + return tokenServer.URL, nil + } + + profile := newTestProfile(t, profiles.AuthTypeAccessToken, "old", "refresh", time.Now().Add(-time.Hour).Unix()) + err := refreshAccessToken(t.Context(), profile, resolver) + require.NoError(t, err) + + creds := profile.GetAuthCredentials() + assert.Greater(t, creds.AccessToken.Expiration, time.Now().Unix(), + "zero expiry should default to ~1 hour from now") +} + +func TestRefreshAccessTokenEndpointError(t *testing.T) { + resolver := func(string, bool) (string, error) { + return "", errors.New("gRPC connection failed") + } + + profile := newTestProfile(t, profiles.AuthTypeAccessToken, "tok", "refresh", time.Now().Add(-time.Hour).Unix()) + err := refreshAccessToken(t.Context(), profile, resolver) + require.ErrorContains(t, err, "failed to get token endpoint") +} + +func TestRefreshAccessTokenRefreshFails(t *testing.T) { + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + err := json.NewEncoder(w).Encode(map[string]string{"error": "invalid_grant"}) + assert.NoError(t, err) + })) + defer tokenServer.Close() + + resolver := func(string, bool) (string, error) { + return tokenServer.URL, nil + } + + profile := newTestProfile(t, profiles.AuthTypeAccessToken, "tok", "refresh", time.Now().Add(-time.Hour).Unix()) + err := refreshAccessToken(t.Context(), profile, resolver) + require.Error(t, err) + require.ErrorIs(t, err, ErrRefreshFailed) +} + +func TestRefreshAccessTokenEmptyClientID(t *testing.T) { + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(map[string]any{ + "access_token": "new-token", + "refresh_token": "new-refresh", + "token_type": "Bearer", + "expires_in": 3600, + }) + assert.NoError(t, err) + })) + defer tokenServer.Close() + + resolver := func(string, bool) (string, error) { + return tokenServer.URL, nil + } + + cfg := &profiles.ProfileConfig{ + Name: "test", + Endpoint: "https://example.com", + } + profile, err := profiles.NewOtdfctlProfileStore(profiles.ProfileDriverMemory, cfg, false) + require.NoError(t, err) + err = profile.SetAuthCredentials(profiles.AuthCredentials{ + AuthType: profiles.AuthTypeAccessToken, + AccessToken: profiles.AuthCredentialsAccessToken{ + ClientID: "", + AccessToken: "old-token", + RefreshToken: "old-refresh", + Expiration: time.Now().Add(-time.Hour).Unix(), + }, + }) + require.NoError(t, err) + + err = refreshAccessToken(t.Context(), profile, resolver) + require.NoError(t, err) + + creds := profile.GetAuthCredentials() + assert.Equal(t, DefaultPublicClientID, creds.AccessToken.ClientID) +} + +func TestRefreshAccessTokenTLSNoVerify(t *testing.T) { + tokenServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(map[string]any{ + "access_token": "tls-token", + "refresh_token": "tls-refresh", + "token_type": "Bearer", + "expires_in": 3600, + }) + assert.NoError(t, err) + })) + defer tokenServer.Close() + + resolver := func(string, bool) (string, error) { + return tokenServer.URL, nil + } + + cfg := &profiles.ProfileConfig{ + Name: "test", + Endpoint: "https://example.com", + TLSNoVerify: true, + } + profile, err := profiles.NewOtdfctlProfileStore(profiles.ProfileDriverMemory, cfg, false) + require.NoError(t, err) + err = profile.SetAuthCredentials(profiles.AuthCredentials{ + AuthType: profiles.AuthTypeAccessToken, + AccessToken: profiles.AuthCredentialsAccessToken{ + ClientID: "cli-client", + AccessToken: "old-token", + RefreshToken: "old-refresh", + Expiration: time.Now().Add(-time.Hour).Unix(), + }, + }) + require.NoError(t, err) + + err = refreshAccessToken(t.Context(), profile, resolver) + require.NoError(t, err) + + creds := profile.GetAuthCredentials() + assert.Equal(t, "tls-token", creds.AccessToken.AccessToken) +} + +func TestGetTokenEndpointBadEndpoint(t *testing.T) { + _, err := getTokenEndpoint("https://localhost:1", false) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get platform configuration") +} + +func TestGetTokenEndpointEmptyEndpoint(t *testing.T) { + _, err := getTokenEndpoint("", false) + require.Error(t, err) +} + +func TestRefreshAccessTokenNilProfile(t *testing.T) { + err := RefreshAccessToken(t.Context(), nil) + require.Error(t, err) +} + +func TestIsTokenExpiredNilProfile(t *testing.T) { + assert.True(t, IsTokenExpired(nil)) +} + +func TestHasRefreshTokenNilProfile(t *testing.T) { + assert.False(t, HasRefreshToken(nil)) +} diff --git a/otdfctl/pkg/cli/cli.go b/otdfctl/pkg/cli/cli.go new file mode 100644 index 0000000000..e6ffcdb3a9 --- /dev/null +++ b/otdfctl/pkg/cli/cli.go @@ -0,0 +1,52 @@ +package cli + +import ( + "context" + + "github.com/spf13/cobra" +) + +type Cli struct { + cmd *cobra.Command + args []string + + // Helpers + Flags *flagHelper + FlagHelper *flagHelper + printer *Printer +} + +// New creates a new Cli object +func New(cmd *cobra.Command, args []string, options ...cliVariadicOption) *Cli { + opts := cliOptions{ + printerJSON: false, + } + for _, opt := range options { + opts = opt(opts) + } + + cli := &Cli{ + cmd: cmd, + args: args, + } + + if cmd == nil { + ExitWithError("cli expects a command", ErrPrinterExpectsCommand) + } + + cli.Flags = newFlagHelper(cmd) + // Temp wrapper for FlagHelper until we can remove it + cli.FlagHelper = cli.Flags + + cli.printer = newPrinter(opts.printerJSON || cli.Flags.GetOptionalBool("json")) + + return cli +} + +func (c *Cli) Cmd() *cobra.Command { + return c.cmd +} + +func (c *Cli) Context() context.Context { + return c.cmd.Context() +} diff --git a/otdfctl/pkg/cli/clioptions.go b/otdfctl/pkg/cli/clioptions.go new file mode 100644 index 0000000000..6aaa589d7c --- /dev/null +++ b/otdfctl/pkg/cli/clioptions.go @@ -0,0 +1,15 @@ +package cli + +type cliOptions struct { + printerJSON bool +} + +type cliVariadicOption func(cliOptions) cliOptions + +// WithPrintJSON is a variadic option that enforces JSON output for the printer +func WithPrintJSON() cliVariadicOption { + return func(o cliOptions) cliOptions { + o.printerJSON = true + return o + } +} diff --git a/otdfctl/pkg/cli/confirm.go b/otdfctl/pkg/cli/confirm.go new file mode 100644 index 0000000000..36dc46048a --- /dev/null +++ b/otdfctl/pkg/cli/confirm.go @@ -0,0 +1,110 @@ +package cli + +import ( + "fmt" + + "github.com/charmbracelet/huh" +) + +const ( + // top level actions + ActionGet = "get" + ActionList = "list" + ActionCreate = "create" + ActionUpdate = "update" + ActionUpdateUnsafe = "unsafely update" + ActionDeactivate = "deactivate" + ActionReactivate = "reactivate" + ActionDelete = "delete" + + // text input names + InputNameFQN = "fully qualified name (FQN)" + InputNameFQNUpdated = "deprecated fully qualified name (FQN) being altered" +) + +func ConfirmActionSubtext(action, resource, id, subtext string, force bool) { + if force { + return + } + var confirm bool + title := fmt.Sprintf("Are you sure you want to %s %s:\n\n\t%s", action, resource, id) + if subtext != "" { + // since we don't return an error to stay consistent with the original function, + // only append the subtext if populated + title += "\n\n" + subtext + } + err := huh.NewConfirm(). + Title(title). + Affirmative("yes"). + Negative("no"). + Value(&confirm). + Run() + if err != nil { + ExitWithError("Confirmation prompt failed", err) + } + + if !confirm { + ExitWithError("Aborted", nil) + } +} + +func ConfirmAction(action, resource, id string, force bool) { + if force { + return + } + var confirm bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Are you sure you want to %s %s:\n\n\t%s", action, resource, id)). + Affirmative("yes"). + Negative("no"). + Value(&confirm). + Run() + if err != nil { + ExitWithError("Confirmation prompt failed", err) + } + + if !confirm { + ExitWithError("Aborted", nil) + } +} + +func ConfirmTextInput(action, resource, inputName, shouldMatchValue string) { + var input string + err := huh.NewInput(). + Title(fmt.Sprintf("To confirm you want to %s this %s and accept any side effects, please enter the %s to proceed: %s", action, resource, inputName, shouldMatchValue)). + Value(&input). + Validate(func(s string) error { + if s != shouldMatchValue { + return fmt.Errorf("entered FQN [%s] does not match required %s: %s", s, inputName, shouldMatchValue) + } + return nil + }).Run() + if err != nil { + ExitWithError("Confirmation prompt failed", err) + } +} + +func AskForInput(message string) string { + var input string + err := huh.NewInput(). + Value(&input). + Title(message). + Run() + if err != nil { + ExitWithError("Prompt for input failed", err) + } + return input +} + +func AskForSecret(message string) string { + var secret string + err := huh.NewInput(). + Value(&secret). + Title(message). + EchoMode(huh.EchoModePassword). + Run() + if err != nil { + ExitWithError("Prompt for secret failed", err) + } + return secret +} diff --git a/otdfctl/pkg/cli/errors.go b/otdfctl/pkg/cli/errors.go new file mode 100644 index 0000000000..4d79d9cc4e --- /dev/null +++ b/otdfctl/pkg/cli/errors.go @@ -0,0 +1,96 @@ +package cli + +import ( + "io" + "os" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + ExitCodeSuccess = 0 + ExitCodeError = 1 +) + +func ExitWithError(errMsg string, err error) { + // This is temporary until we can refactor the code to use the Cli struct + (&Cli{printer: defaultPrinter()}).ExitWithError(errMsg, err) +} + +func ExitWithNotFoundError(errMsg string, err error) { + // This is temporary until we can refactor the code to use the Cli struct + (&Cli{printer: defaultPrinter()}).ExitWithNotFoundError(errMsg, err) +} + +func ExitWithWarning(warnMsg string) { + // This is temporary until we can refactor the code to use the Cli struct + (&Cli{printer: defaultPrinter()}).ExitWithWarning(warnMsg) +} + +// ExitWithError prints an error message and exits with a non-zero status code. +func (c *Cli) ExitWithError(errMsg string, err error) { + c.ExitWithNotFoundError(errMsg, err) + c.ExitWith(ErrorMessage(errMsg, err), ErrorJSON(errMsg, err), ExitCodeError, os.Stderr) +} + +// ExitWithNotFoundError prints an error message and exits with a non-zero status code if the error is a NotFound error. +func (c *Cli) ExitWithNotFoundError(errMsg string, err error) { + if err != nil { + if e, ok := status.FromError(err); ok && e.Code() == codes.NotFound { + c.ExitWith( + ErrorMessage(errMsg+": not found", nil), + MessageJSON("ERROR", errMsg+": not found"), + ExitCodeError, + os.Stderr, + ) + } + } +} + +func (c *Cli) ExitWithWarning(warnMsg string) { + c.ExitWith(WarningMessage(warnMsg), WarningJSON(warnMsg), ExitCodeError, os.Stderr) +} + +func (c *Cli) ExitWithSuccess(msg string) { + c.ExitWith(SuccessMessage(msg), SuccessJSON(msg), ExitCodeSuccess, os.Stdout) +} + +func (c *Cli) ExitWithMessage(msg string, code int) { + w := os.Stdout + if code != ExitCodeSuccess { + w = os.Stderr + } + if c.printer.json { + c.printJSON(MessageJSON(statusForExitCode(code), strings.TrimSpace(msg)), w) + } else { + c.println(w, msg) + } + os.Exit(code) +} + +func (c *Cli) ExitWithJSON(v interface{}, code int) { + if c.printer.json { + c.printJSON(v, os.Stdout) + os.Exit(code) + } +} + +// exitWith is the core exit function that handles both JSON and styled output +// It writes to the appropriate stream (stdout for success, stderr for errors/warnings) +func (c *Cli) ExitWith(styledMsg string, jsonMsg interface{}, code int, w io.Writer) { + if c.printer.json { + c.printJSON(jsonMsg, w) + } else { + c.println(w, styledMsg) + } + os.Exit(code) +} + +func statusForExitCode(code int) string { + if code == ExitCodeSuccess { + return "SUCCESS" + } + return "ERROR" +} diff --git a/otdfctl/pkg/cli/flagValues.go b/otdfctl/pkg/cli/flagValues.go new file mode 100644 index 0000000000..5f5c5f76a9 --- /dev/null +++ b/otdfctl/pkg/cli/flagValues.go @@ -0,0 +1,150 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/google/uuid" + "github.com/opentdf/platform/protocol/go/common" + "github.com/spf13/cobra" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +type FlagsStringSliceOptions struct { + Min int + Max int +} + +type flagHelper struct { + cmd *cobra.Command +} + +func newFlagHelper(cmd *cobra.Command) *flagHelper { + return &flagHelper{cmd: cmd} +} + +func (f flagHelper) GetRequiredString(flag string) string { + v := f.cmd.Flag(flag).Value.String() + if v == "" { + ExitWithError("Flag '--"+flag+"' is required", nil) + } + return v +} + +func (f flagHelper) GetRequiredID(idFlag string) string { + v := f.GetRequiredString(idFlag) + id, err := uuid.Parse(v) + if err != nil { + ExitWithError(fmt.Sprintf("Flag '--%s' received value '%s' must be a valid UUID", idFlag, v), nil) + } + return id.String() +} + +func (f flagHelper) GetOptionalID(idFlag string) string { + p := f.GetOptionalString(idFlag) + if p == "" { + return "" + } + id, err := uuid.Parse(p) + if err != nil { + ExitWithError(fmt.Sprintf("Optional flag '--%s' received value '%s' and must be a valid UUID if used", idFlag, p), nil) + } + return id.String() +} + +func (f flagHelper) GetOptionalString(flag string) string { + p := f.cmd.Flag(flag) + if p == nil { + return "" + } + return p.Value.String() +} + +func (f flagHelper) GetStringSlice(flag string, v []string, opts FlagsStringSliceOptions) []string { + if len(v) < opts.Min { + ExitWithError(fmt.Sprintf("Flag '--%s' must have at least %d non-empty values", flag, opts.Min), nil) + } + if opts.Max > 0 && len(v) > opts.Max { + ExitWithError(fmt.Sprintf("Flag '--%s' must have at most %d non-empty values", flag, opts.Max), nil) + } + return v +} + +func (f flagHelper) GetRequiredInt32(flag string) int32 { + v, e := f.cmd.Flags().GetInt32(flag) + if e != nil { + ExitWithError("Flag '--"+flag+"' is required", nil) + } + // if v == 0 { + // fmt.Println(ErrorMessage("Flag "+flag+" must be greater than 0", nil)) + // os.Exit(1) + // } + return v +} + +func (f flagHelper) GetOptionalInt32(flag string) int32 { + v, _ := f.cmd.Flags().GetInt32(flag) + return v +} + +func (f flagHelper) GetOptionalBool(flag string) bool { + v, _ := f.cmd.Flags().GetBool(flag) + return v +} + +// Returns nil when the flag is not explicitly set. +func (f flagHelper) GetOptionalBoolWrapper(flag string) *wrapperspb.BoolValue { + if !f.cmd.Flags().Changed(flag) { + return nil + } + v, _ := f.cmd.Flags().GetBool(flag) + return wrapperspb.Bool(v) +} + +func (f flagHelper) GetRequiredBool(flag string) bool { + v, e := f.cmd.Flags().GetBool(flag) + if e != nil { + ExitWithError("Flag '--"+flag+"' is required", nil) + } + return v +} + +// Transforms into enum value and defaults to active state +func GetState(cmd *cobra.Command) common.ActiveStateEnum { + state := common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE + stateFlag := strings.ToUpper(cmd.Flag("state").Value.String()) + if stateFlag != "" { + switch stateFlag { + case "INACTIVE": + state = common.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE + case "ANY": + state = common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY + } + } + return state +} + +// func (f flagHelper) GetStructSlice(flag string, v []StructFlag[T], opts flagHelperStringSliceOptions) ([]StructFlag[T], err) { +// if len(v) < opts.Min { +// fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at least %d non-empty values", flag, opts.Min), nil)) +// os.Exit(1) +// } +// if opts.Max > 0 && len(v) > opts.Max { +// fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at most %d non-empty values", flag, opts.Max), nil)) +// os.Exit(1) +// } +// return v +// } + +// type StructFlag[T any] struct { +// Val T +// } + +// func (this StructFlag[T]) String() string { +// b, _ := json.Marshal(this) +// return string(b) +// } + +// func (this StructFlag[T]) Set(s string) error { +// return json.Unmarshal([]byte(s), this) +// } diff --git a/otdfctl/pkg/cli/messages.go b/otdfctl/pkg/cli/messages.go new file mode 100644 index 0000000000..ce2b733a0b --- /dev/null +++ b/otdfctl/pkg/cli/messages.go @@ -0,0 +1,64 @@ +package cli + +import ( + "github.com/charmbracelet/lipgloss" +) + +func SuccessMessage(msg string) string { + return lipgloss.JoinHorizontal(lipgloss.Left, styleSuccessStatusBar.Render("SUCCESS"), msg) +} + +func SuccessJSON(msg string) interface{} { + return MessageJSON("SUCCESS", msg) +} + +func FooterMessage(msg string) string { + if msg == "" { + return "" + } + w := lipgloss.Width + note := footerLabelStyle.Render("NOTE") + footer := footerTextStyle.Width(TermWidth() - w(note)).Render(msg) + return lipgloss.JoinHorizontal( + lipgloss.Left, + note, + footer, + ) +} + +func DebugMessage(msg string) string { + return lipgloss.JoinHorizontal(lipgloss.Left, styleDebugStatusBar.Render("DEBUG"), msg) +} + +func DebugJSON(msg string) interface{} { + return MessageJSON("DEBUG", msg) +} + +func ErrorMessage(msg string, err error) string { + if err != nil { + msg += ": " + err.Error() + } + return lipgloss.JoinHorizontal(lipgloss.Left, styleErrorStatusBar.Render("ERROR"), msg) +} + +func ErrorJSON(msg string, err error) interface{} { + if err != nil { + msg += ": " + err.Error() + } + return MessageJSON("ERROR", msg) +} + +func WarningMessage(msg string) string { + return lipgloss.JoinHorizontal(lipgloss.Left, styleWarningStatusBar.Render("WARNING"), msg) +} + +func WarningJSON(msg string) interface{} { + return MessageJSON("WARNING", msg) +} + +func MessageJSON(status string, msg string) interface{} { + return map[string]interface{}{ + "status": status, + "message": msg, + } +} diff --git a/otdfctl/pkg/cli/pipe.go b/otdfctl/pkg/cli/pipe.go new file mode 100644 index 0000000000..561c8ff19f --- /dev/null +++ b/otdfctl/pkg/cli/pipe.go @@ -0,0 +1,45 @@ +package cli + +import ( + "io" + "os" +) + +func ReadFromArgsOrPipe(args []string, pipe *os.File) []byte { + if len(args) > 0 { + return ReadFromFile(args[0]) + } + if pipe == nil { + pipe = os.Stdin + } + return ReadFromPipe(pipe) +} + +func ReadFromPipe(in *os.File) []byte { + stat, err := in.Stat() + if err != nil { + ExitWithError("failed to read stat from stdin", err) + } + if (stat.Mode() & os.ModeCharDevice) == 0 { + buf, err := io.ReadAll(in) + if err != nil { + ExitWithError("failed to scan bytes from stdin", err) + } + return buf + } + return nil +} + +func ReadFromFile(filePath string) []byte { + fileToEncrypt, err := os.Open(filePath) + if err != nil { + ExitWithError("Failed to git open file at path: "+filePath, err) + } + defer fileToEncrypt.Close() + + bytes, err := io.ReadAll(fileToEncrypt) + if err != nil { + ExitWithError("Failed to read bytes from file at path: "+filePath, err) + } + return bytes +} diff --git a/otdfctl/pkg/cli/printer.go b/otdfctl/pkg/cli/printer.go new file mode 100644 index 0000000000..a4b89c0c7b --- /dev/null +++ b/otdfctl/pkg/cli/printer.go @@ -0,0 +1,70 @@ +package cli + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "sync/atomic" +) + +var ErrPrinterExpectsCommand = errors.New("printer expects a command") + +var defaultJSONOutput atomic.Bool + +type Printer struct { + enabled bool + json bool + debug bool +} + +func newPrinter(json bool) *Printer { + p := &Printer{ + enabled: true, + json: false, + debug: false, + } + + // if json output is enabled, disable the printer + defaultJSONOutput.Store(json) + p.setJSON(json) + + return p +} + +func (p *Printer) setJSON(json bool) { + p.json = json + p.enabled = !json +} + +// PrintJSON prints the given value as json +// ignores the printer enabled flag +func (c *Cli) printJSON(v interface{}, w io.Writer) { + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + encoder.SetEscapeHTML(false) + if err := encoder.Encode(v); err != nil { + ExitWithError("failed to encode json", err) + } +} + +func (c *Cli) println(w io.Writer, args ...interface{}) { + if c.printer.enabled { + fmt.Fprintln(w, args...) + } +} + +func (c *Cli) SetJSONOutput(enabled bool) { + if c.printer == nil { + return + } + c.printer.setJSON(enabled) +} + +func defaultPrinter() *Printer { + json := defaultJSONOutput.Load() + return &Printer{ + enabled: !json, + json: json, + } +} diff --git a/otdfctl/pkg/cli/sdkHelpers.go b/otdfctl/pkg/cli/sdkHelpers.go new file mode 100644 index 0000000000..943fbbc71e --- /dev/null +++ b/otdfctl/pkg/cli/sdkHelpers.go @@ -0,0 +1,187 @@ +package cli + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" +) + +type SimpleAttribute struct { + ID string + Name string + Rule string + Values []string + Namespace string + Active string + AllowTraversal string + Metadata map[string]string +} + +type SimpleAttributeValue struct { + ID string + FQN string + Active string + Metadata map[string]string +} + +func ConstructMetadata(m *common.Metadata) map[string]string { + var metadata map[string]string + if m == nil { + return metadata + } + metadata = map[string]string{ + "Created At": m.GetCreatedAt().AsTime().Format(time.UnixDate), + "Updated At": m.GetUpdatedAt().AsTime().Format(time.UnixDate), + } + + labels := []string{} + if m.Labels != nil { + for k, v := range m.GetLabels() { + labels = append(labels, k+": "+v) + } + } + metadata["Labels"] = CommaSeparated(labels) + return metadata +} + +func GetSimpleAttribute(a *policy.Attribute) SimpleAttribute { + values := []string{} + for _, v := range a.GetValues() { + values = append(values, v.GetValue()) + } + + return SimpleAttribute{ + ID: a.GetId(), + Name: a.GetName(), + Rule: handlers.GetAttributeRuleFromAttributeType(a.GetRule()), + Values: values, + Namespace: a.GetNamespace().GetName(), + Active: strconv.FormatBool(a.GetActive().GetValue()), + AllowTraversal: strconv.FormatBool(a.GetAllowTraversal().GetValue()), + Metadata: ConstructMetadata(a.GetMetadata()), + } +} + +func GetSimpleAttributeValue(v *policy.Value) SimpleAttributeValue { + return SimpleAttributeValue{ + ID: v.GetId(), + FQN: v.GetFqn(), + Active: strconv.FormatBool(v.GetActive().GetValue()), + Metadata: ConstructMetadata(v.GetMetadata()), + } +} + +func GetSimpleObligationValues(v []*policy.ObligationValue) []string { + values := make([]string, len(v)) + for i, val := range v { + values[i] = val.GetValue() + } + return values +} + +func GetSimpleRegisteredResourceValues(v []*policy.RegisteredResourceValue) []string { + values := make([]string, len(v)) + for i, val := range v { + values[i] = val.GetValue() + } + return values +} + +func GetSimpleRegisteredResourceActionAttributeValues(v []*policy.RegisteredResourceValue_ActionAttributeValue) []string { + values := make([]string, len(v)) + sb := new(strings.Builder) + + for i, val := range v { + action := val.GetAction() + attrVal := val.GetAttributeValue() + + sb.WriteString(action.GetName()) + sb.WriteString(" -> ") + sb.WriteString(attrVal.GetFqn()) + + values[i] = sb.String() + sb.Reset() + } + + return values +} + +func KeyAlgToEnum(alg string) (policy.Algorithm, error) { + switch strings.ToLower(alg) { + case "rsa:2048": + return policy.Algorithm_ALGORITHM_RSA_2048, nil + case "rsa:4096": + return policy.Algorithm_ALGORITHM_RSA_4096, nil + case "ec:secp256r1": + return policy.Algorithm_ALGORITHM_EC_P256, nil + case "ec:secp384r1": + return policy.Algorithm_ALGORITHM_EC_P384, nil + case "ec:secp521r1": + return policy.Algorithm_ALGORITHM_EC_P521, nil + case "hpqt:xwing": + return policy.Algorithm_ALGORITHM_HPQT_XWING, nil + case "hpqt:secp256r1-mlkem768": + return policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, nil + case "hpqt:secp384r1-mlkem1024": + return policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, nil + default: + return policy.Algorithm_ALGORITHM_UNSPECIFIED, errors.New("invalid algorithm") + } +} + +func KeyEnumToAlg(enum policy.Algorithm) (string, error) { + switch enum { //nolint:exhaustive // UNSPECIFIED is not needed here + case policy.Algorithm_ALGORITHM_RSA_2048: + return "rsa:2048", nil + case policy.Algorithm_ALGORITHM_RSA_4096: + return "rsa:4096", nil + case policy.Algorithm_ALGORITHM_EC_P256: + return "ec:secp256r1", nil + case policy.Algorithm_ALGORITHM_EC_P384: + return "ec:secp384r1", nil + case policy.Algorithm_ALGORITHM_EC_P521: + return "ec:secp521r1", nil + case policy.Algorithm_ALGORITHM_HPQT_XWING: + return "hpqt:xwing", nil + case policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768: + return "hpqt:secp256r1-mlkem768", nil + case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: + return "hpqt:secp384r1-mlkem1024", nil + default: + return "", errors.New("invalid enum algorithm") + } +} + +func AggregateClientIDs(reqCtx []*policy.RequestContext) []string { + ids := []string{} + seen := map[string]bool{} + for _, r := range reqCtx { + id := r.GetPep().GetClientId() + if id != "" && !seen[id] { + ids = append(ids, id) + seen[id] = true + } + } + return ids +} + +// Gets JSON from either a file path or a JSON string +func GetJSONInput(data string) (string, error) { + if _, err := os.Stat(data); err == nil { + // It's a file path, read the content + fileContent, err := os.ReadFile(data) + if err != nil { + return "", fmt.Errorf("failed to read file %s: %w", data, err) + } + return string(fileContent), nil + } + + return data, nil +} diff --git a/otdfctl/pkg/cli/sdkHelpers_test.go b/otdfctl/pkg/cli/sdkHelpers_test.go new file mode 100644 index 0000000000..52d3c64fef --- /dev/null +++ b/otdfctl/pkg/cli/sdkHelpers_test.go @@ -0,0 +1,46 @@ +package cli + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/require" +) + +func TestKeyAlgToEnum_RoundTrip(t *testing.T) { + tests := []struct { + alg string + enum policy.Algorithm + }{ + {"rsa:2048", policy.Algorithm_ALGORITHM_RSA_2048}, + {"rsa:4096", policy.Algorithm_ALGORITHM_RSA_4096}, + {"ec:secp256r1", policy.Algorithm_ALGORITHM_EC_P256}, + {"ec:secp384r1", policy.Algorithm_ALGORITHM_EC_P384}, + {"ec:secp521r1", policy.Algorithm_ALGORITHM_EC_P521}, + {"hpqt:xwing", policy.Algorithm_ALGORITHM_HPQT_XWING}, + {"hpqt:secp256r1-mlkem768", policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768}, + {"hpqt:secp384r1-mlkem1024", policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024}, + } + + for _, tt := range tests { + t.Run(tt.alg, func(t *testing.T) { + got, err := KeyAlgToEnum(tt.alg) + require.NoError(t, err) + require.Equal(t, tt.enum, got) + + back, err := KeyEnumToAlg(got) + require.NoError(t, err) + require.Equal(t, tt.alg, back) + }) + } +} + +func TestKeyAlgToEnum_Invalid(t *testing.T) { + _, err := KeyAlgToEnum("not-a-real-alg") + require.Error(t, err) +} + +func TestKeyEnumToAlg_Invalid(t *testing.T) { + _, err := KeyEnumToAlg(policy.Algorithm_ALGORITHM_UNSPECIFIED) + require.Error(t, err) +} diff --git a/otdfctl/pkg/cli/style.go b/otdfctl/pkg/cli/style.go new file mode 100644 index 0000000000..07ca3aa7dc --- /dev/null +++ b/otdfctl/pkg/cli/style.go @@ -0,0 +1,372 @@ +//nolint:mnd // styling is magic +package cli + +import "github.com/charmbracelet/lipgloss" + +type Color struct { + Foreground lipgloss.CompleteAdaptiveColor + Background lipgloss.CompleteAdaptiveColor +} + +var colorRed = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FF0000", + ANSI256: "9", + ANSI: "1", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#FF0000", + ANSI256: "9", + ANSI: "1", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FFD2D2", + ANSI256: "224", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#da6b81", + ANSI256: "52", + ANSI: "4", + }, + }, +} + +var colorOrange = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FFA500", + ANSI256: "214", + ANSI: "3", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#FFA500", + ANSI256: "214", + ANSI: "3", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FFEBCC", + ANSI256: "230", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#663300", + ANSI256: "94", + ANSI: "4", + }, + }, +} + +//lint:ignore U1000 // not used yet +var colorYellow = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FFFF00", + ANSI256: "11", + ANSI: "3", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#FFFF00", + ANSI256: "11", + ANSI: "3", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FFFFE0", + ANSI256: "229", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#666600", + ANSI256: "100", + ANSI: "4", + }, + }, +} + +func ColorYellow() Color { + return colorYellow +} + +var colorGreen = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#008000", + ANSI256: "28", + ANSI: "2", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#008000", + ANSI256: "28", + ANSI: "2", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#D2FFD2", + ANSI256: "157", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#29cf68", + ANSI256: "22", + ANSI: "4", + }, + }, +} + +var colorBlue = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#0000FF", + ANSI256: "21", + ANSI: "4", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#3355d3", + ANSI256: "21", + ANSI: "4", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#7d8ad1", + ANSI256: "189", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#85a2d0", + ANSI256: "17", + ANSI: "4", + }, + }, +} + +var colorIndigo = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#4B0082", + ANSI256: "57", + ANSI: "5", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#4B0082", + ANSI256: "57", + ANSI: "5", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#E6E6FA", + ANSI256: "225", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#2A0033", + ANSI256: "55", + ANSI: "4", + }, + }, +} + +//lint:ignore U1000 // not used yet +var colorViolet = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#EE82EE", + ANSI256: "13", + ANSI: "5", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#EE82EE", + ANSI256: "13", + ANSI: "5", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#F5E6FF", + ANSI256: "189", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#550055", + ANSI256: "90", + ANSI: "4", + }, + }, +} + +var colorGray = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#808080", + ANSI256: "244", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#808080", + ANSI256: "244", + ANSI: "7", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#F2F2F2", + ANSI256: "231", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#333333", + ANSI256: "235", + ANSI: "0", + }, + }, +} + +var colorWhite = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FFFFFF", + ANSI256: "15", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#FFFFFF", + ANSI256: "15", + ANSI: "7", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#F5F5F5", + ANSI256: "231", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#333333", + ANSI256: "235", + ANSI: "0", + }, + }, +} + +var colorBlack = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#000000", + ANSI256: "0", + ANSI: "0", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#000000", + ANSI256: "0", + ANSI: "0", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#E0E0E0", + ANSI256: "248", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#121212", + ANSI256: "235", + ANSI: "0", + }, + }, +} + +//////// + +var statusBarStyle = lipgloss.NewStyle(). + Padding(1). + MarginRight(1) + +var styleSuccessStatusBar = lipgloss.NewStyle(). + Inherit(statusBarStyle). + Foreground(colorBlack.Foreground). + Background(colorGreen.Background). + Padding(0, 2). + MarginRight(1). + MarginBottom(1) + +var styleErrorStatusBar = lipgloss.NewStyle(). + Inherit(statusBarStyle). + Foreground(colorBlack.Foreground). + Background(colorRed.Background). + Padding(0, 3). + PaddingRight(3). + MarginRight(1) + +//lint:ignore U1000 // not used yet +var styleNoteStatusBar = lipgloss.NewStyle(). + Inherit(statusBarStyle). + Foreground(colorYellow.Foreground). + Background(colorYellow.Background) + +var styleDebugStatusBar = lipgloss.NewStyle(). + Inherit(statusBarStyle). + Foreground(colorBlack.Foreground). + Background(colorIndigo.Background). + PaddingRight(3) + +var styleWarningStatusBar = lipgloss.NewStyle(). + Inherit(statusBarStyle). + Foreground(colorOrange.Foreground). + Background(colorOrange.Background). + Padding(0, 2). + MarginRight(1) + +var footerLabelStyle = lipgloss.NewStyle(). + Inherit(statusBarStyle). + Foreground(colorWhite.Foreground). + Background(colorBlue.Background). + Padding(0, 2) + +var footerTextStyle = lipgloss. + NewStyle(). + Background(colorGray.Background). + PaddingLeft(1). + Inherit(statusBarStyle) + +// Table + +var styleTableBorder = lipgloss.CompleteAdaptiveColor{ + Light: colorIndigo.Background.Dark, + Dark: colorIndigo.Background.Light, +} + +var styleTable = lipgloss. + NewStyle(). + Foreground(lipgloss.CompleteAdaptiveColor{ + Light: colorBlack.Foreground.Light, + Dark: colorWhite.Foreground.Dark, + }). + BorderForeground(styleTableBorder) + +// Text + +//lint:ignore U1000 // not used yet +var styleText = lipgloss. + NewStyle(). + Foreground(lipgloss.CompleteAdaptiveColor{ + Light: colorBlack.Foreground.Light, + Dark: colorWhite.Foreground.Dark, + }) + +//lint:ignore U1000 // not used yet +var styleTextBold = lipgloss. + NewStyle(). + Foreground(lipgloss.CompleteAdaptiveColor{ + Light: colorBlack.Foreground.Light, + Dark: colorWhite.Foreground.Dark, + }). + Bold(true) diff --git a/otdfctl/pkg/cli/table.go b/otdfctl/pkg/cli/table.go new file mode 100644 index 0000000000..7564bf9ac0 --- /dev/null +++ b/otdfctl/pkg/cli/table.go @@ -0,0 +1,46 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/evertras/bubble-table/table" + "github.com/opentdf/platform/protocol/go/policy" +) + +const ( + FlexColumnWidthOne = 1 + FlexColumnWidthTwo = 2 + FlexColumnWidthThree = 3 + FlexColumnWidthFour = 4 + FlexColumnWidthFive = 5 +) + +func NewTable(cols ...table.Column) table.Model { + return table.New(cols). + BorderRounded(). + WithBaseStyle(styleTable). + WithNoPagination(). + WithTargetWidth(TermWidth()) +} + +func NewUUIDColumn() table.Column { + return table.NewFlexColumn("id", "ID", FlexColumnWidthFive) +} + +// Adds the page information to the table footer +func WithListPaginationFooter(t table.Model, p *policy.PageResponse) table.Model { + info := []string{ + fmt.Sprintf("Total: %d", p.GetTotal()), + fmt.Sprintf("Current Offset: %d", p.GetCurrentOffset()), + } + if p.GetNextOffset() > 0 { + info = append(info, fmt.Sprintf("Next Offset: %d", p.GetNextOffset())) + } + + content := strings.Join(info, " | ") + + leftAligned := lipgloss.NewStyle().Align(lipgloss.Left) + return t.WithStaticFooter(content).WithBaseStyle(leftAligned) +} diff --git a/otdfctl/pkg/cli/tabular.go b/otdfctl/pkg/cli/tabular.go new file mode 100644 index 0000000000..9a9343c64d --- /dev/null +++ b/otdfctl/pkg/cli/tabular.go @@ -0,0 +1,98 @@ +//nolint:forbidigo // should be able to print tables as needed +package cli + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/evertras/bubble-table/table" + "github.com/spf13/cobra" +) + +func NewTabular(rows ...[]string) table.Model { + columnKeyProperty := "Property" + columnKeyValue := "Value" + t := NewTable( + table.NewFlexColumn(columnKeyProperty, columnKeyProperty, FlexColumnWidthOne), + table.NewFlexColumn(columnKeyValue, columnKeyValue, FlexColumnWidthTwo), + ) + + tr := []table.Row{} + if len(rows) == 0 { + tr = append(tr, table.NewRow(table.RowData{ + columnKeyProperty: "No properties found", + columnKeyValue: "", + })) + } + for _, r := range rows { + p := r[0] + v := "" + if len(r) > 1 { + v = r[1] + } + tr = append(tr, table.NewRow(table.RowData{ + columnKeyProperty: p, + columnKeyValue: v, + })) + } + + t = t.WithTargetWidth(TermWidth()) + + t = t.WithRows(tr) + return t +} + +func getJSONHelper(command string) string { + return fmt.Sprintf("Use '%s --json' to see all properties", command) +} + +func PrintSuccessTable(cmd *cobra.Command, id string, t table.Model) { + parent := cmd.Parent() + resourceShort := parent.Use + resource := parent.Use + for parent.Parent() != nil { + resource = parent.Parent().Use + " " + resource + parent = parent.Parent() + } + + var msg struct { + verb string + helper string + } + switch cmd.Use { + case ActionGet: + msg.verb = fmt.Sprintf("Found %s: %s", resourceShort, id) + msg.helper = getJSONHelper(resource + " get --id=" + id) + case ActionCreate: + msg.verb = fmt.Sprintf("Created %s: %s", resourceShort, id) + msg.helper = getJSONHelper(resource + " get --id=" + id) + case ActionUpdate: + msg.verb = fmt.Sprintf("Updated %s: %s", resourceShort, id) + msg.helper = getJSONHelper(resource + " get --id=" + id) + case ActionDelete: + msg.verb = fmt.Sprintf("Deleted %s: %s", resourceShort, id) + // strip off unsafe subcommand if found to get proper path to the list command + msg.helper = getJSONHelper(strings.ReplaceAll(resource, " unsafe", "") + " list") + case ActionDeactivate: + msg.verb = fmt.Sprintf("Deactivated %s: %s", resourceShort, id) + msg.helper = getJSONHelper(resource + " list") // TODO: make sure the filters are provided here to get ACTIVE/INACTIVE/ANY + case ActionList: + msg.verb = fmt.Sprintf("Found %s list", resourceShort) + msg.helper = getJSONHelper(resource + " get --id=") + default: + msg.verb = "" + msg.helper = "" + } + + successMessage := SuccessMessage(msg.verb) + jsonDirections := FooterMessage(msg.helper) + + ts := t.View() + if ts == "" { + fmt.Println(lipgloss.JoinVertical(lipgloss.Top, successMessage, jsonDirections)) + return + } + + fmt.Println(lipgloss.JoinVertical(lipgloss.Top, successMessage, ts, jsonDirections)) +} diff --git a/otdfctl/pkg/cli/utils.go b/otdfctl/pkg/cli/utils.go new file mode 100644 index 0000000000..ff7317e2a9 --- /dev/null +++ b/otdfctl/pkg/cli/utils.go @@ -0,0 +1,50 @@ +package cli + +import ( + "os" + "strconv" + "strings" + + "github.com/opentdf/platform/otdfctl/pkg/config" + "golang.org/x/term" +) + +func CommaSeparated(values []string) string { + return "[" + strings.Join(values, ", ") + "]" +} + +var defaultWidth = 80 + +// Returns the terminal width (overridden by env var for testing) +func TermWidth() int { + var ( + w int + err error + ) + testSize := os.Getenv(config.TestTerminalWidth) + if testSize == "" { + w, _, err = term.GetSize(0) + if err != nil { + return defaultWidth + } + return w + } + if w, err = strconv.Atoi(testSize); err != nil { + return defaultWidth + } + return w +} + +func PrettyList(values []string) string { + var b strings.Builder + for i, v := range values { + if i == len(values)-1 { + b.WriteString("or ") + b.WriteString(v) + } else { + b.WriteString(v) + b.WriteString(", ") + } + } + return b.String() +} diff --git a/otdfctl/pkg/config/config.go b/otdfctl/pkg/config/config.go new file mode 100644 index 0000000000..340a53af48 --- /dev/null +++ b/otdfctl/pkg/config/config.go @@ -0,0 +1,21 @@ +package config + +var ( + // Name of the publisher (PascalCase for when it is used in the UI and file system directory names) + ServicePublisher = "VirtruCorporation" + // AppName is the name of the application + // Note: use caution when renaming as it is used in various places within the CLI including for + // config file naming and in the profile store + AppName = "otdfctl" + + Version = "0.32.0" // x-release-please-version + BuildTime = "1970-01-01T00:00:00Z" + CommitSha = "0000000" + + // Test mode is used to determine if the application is running in test mode + // "true" = running in test mode + TestMode = "" + + // Test terminal size is a runtime env var to allow for testing of terminal output + TestTerminalWidth = "TEST_TERMINAL_WIDTH" +) diff --git a/otdfctl/pkg/handlers/actions.go b/otdfctl/pkg/handlers/actions.go new file mode 100644 index 0000000000..7f8db58dee --- /dev/null +++ b/otdfctl/pkg/handlers/actions.go @@ -0,0 +1,78 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" +) + +func (h Handler) GetAction(ctx context.Context, id string, name string, namespace string) (*policy.Action, error) { + req := &actions.GetActionRequest{} + if id != "" { + req.Identifier = &actions.GetActionRequest_Id{ + Id: id, + } + } else { + req.Identifier = &actions.GetActionRequest_Name{ + Name: name, + } + } + + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + + resp, err := h.sdk.Actions.GetAction(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetAction(), nil +} + +func (h Handler) ListActions(ctx context.Context, limit, offset int32, namespace string) (*actions.ListActionsResponse, error) { + req := &actions.ListActionsRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + + return h.sdk.Actions.ListActions(ctx, req) +} + +func (h Handler) CreateAction(ctx context.Context, name string, namespace string, metadata *common.MetadataMutable) (*policy.Action, error) { + req := &actions.CreateActionRequest{ + Name: name, + Metadata: metadata, + } + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + + resp, err := h.sdk.Actions.CreateAction(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetAction(), nil +} + +func (h Handler) UpdateAction(ctx context.Context, id, name string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.Action, error) { + _, err := h.sdk.Actions.UpdateAction(ctx, &actions.UpdateActionRequest{ + Id: id, + Metadata: metadata, + Name: name, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + return h.GetAction(ctx, id, "", "") +} + +func (h Handler) DeleteAction(ctx context.Context, id string) error { + _, err := h.sdk.Actions.DeleteAction(ctx, &actions.DeleteActionRequest{ + Id: id, + }) + return err +} diff --git a/otdfctl/pkg/handlers/attribute.go b/otdfctl/pkg/handlers/attribute.go new file mode 100644 index 0000000000..dd31f0a2d7 --- /dev/null +++ b/otdfctl/pkg/handlers/attribute.go @@ -0,0 +1,265 @@ +package handlers + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/unsafe" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// TODO: Might be useful to map out the attribute rule definitions for help text in the CLI and TUI + +const ( + AttributeRuleAllOf = "ALL_OF" + AttributeRuleAnyOf = "ANY_OF" + AttributeRuleHierarchy = "HIERARCHY" +) + +type CreateAttributeError struct { + ValueErrors map[string]error + + Err error +} + +func (e *CreateAttributeError) Error() string { + if e.ValueErrors != nil { + return "Error creating attribute with values" + fmt.Sprintf("%v", e.ValueErrors) + } + + return "Error creating attribute" +} + +func (h Handler) GetAttribute(ctx context.Context, identifier string) (*policy.Attribute, error) { + req := &attributes.GetAttributeRequest{ + Identifier: &attributes.GetAttributeRequest_AttributeId{ + AttributeId: identifier, + }, + } + if _, err := uuid.Parse(identifier); err != nil { + req.Identifier = &attributes.GetAttributeRequest_Fqn{ + Fqn: identifier, + } + } + + resp, err := h.sdk.Attributes.GetAttribute(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get attribute [%s]: %w", identifier, err) + } + + return resp.GetAttribute(), nil +} + +func (h Handler) ListAttributes(ctx context.Context, state common.ActiveStateEnum, limit, offset int32, sort SortOption) (*attributes.ListAttributesResponse, error) { + req := &attributes.ListAttributesRequest{ + State: state, + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + if !sort.IsZero() { + allowedFields := map[string]attributes.SortAttributesType{ + "name": attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME, + "created_at": attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, + "updated_at": attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UPDATED_AT, + } + field, err := sortField("attributes", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*attributes.AttributesSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.Attributes.ListAttributes(ctx, req) +} + +// Creates and returns the created attribute +func (h Handler) CreateAttribute(ctx context.Context, name string, rule string, namespace string, values []string, metadata *common.MetadataMutable, allowTraversal *wrapperspb.BoolValue) (*policy.Attribute, error) { + r, err := GetAttributeRuleFromReadableString(rule) + if err != nil { + return nil, err + } + + attrReq := &attributes.CreateAttributeRequest{ + NamespaceId: namespace, + Name: name, + Rule: r, + Metadata: metadata, + Values: values, + } + if allowTraversal != nil { + attrReq.AllowTraversal = allowTraversal + } + + resp, err := h.sdk.Attributes.CreateAttribute(ctx, attrReq) + if err != nil { + return nil, err + } + + return h.GetAttribute(ctx, resp.GetAttribute().GetId()) +} + +// Updates and returns updated attribute +func (h *Handler) UpdateAttribute( + ctx context.Context, + id string, + metadata *common.MetadataMutable, + behavior common.MetadataUpdateEnum, +) (*policy.Attribute, error) { + _, err := h.sdk.Attributes.UpdateAttribute(ctx, &attributes.UpdateAttributeRequest{ + Id: id, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + return h.GetAttribute(ctx, id) +} + +// Deactivates and returns deactivated attribute +func (h Handler) DeactivateAttribute(ctx context.Context, id string) (*policy.Attribute, error) { + _, err := h.sdk.Attributes.DeactivateAttribute(ctx, &attributes.DeactivateAttributeRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + return h.GetAttribute(ctx, id) +} + +// Reactivates and returns reactivated attribute +func (h Handler) UnsafeReactivateAttribute(ctx context.Context, id string) (*policy.Attribute, error) { + _, err := h.sdk.Unsafe.UnsafeReactivateAttribute(ctx, &unsafe.UnsafeReactivateAttributeRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + return h.GetAttribute(ctx, id) +} + +// Deletes and returns error if deletion failed +func (h Handler) UnsafeDeleteAttribute(ctx context.Context, id, fqn string) error { + _, err := h.sdk.Unsafe.UnsafeDeleteAttribute(ctx, &unsafe.UnsafeDeleteAttributeRequest{ + Id: id, + Fqn: fqn, + }) + return err +} + +// Deletes and returns error if deletion failed +func (h Handler) UnsafeUpdateAttribute(ctx context.Context, id, name, rule string, valuesOrder []string, allowTraversal *wrapperspb.BoolValue) (*policy.Attribute, error) { + req := &unsafe.UnsafeUpdateAttributeRequest{ + Id: id, + Name: name, + } + + if rule != "" { + r, err := GetAttributeRuleFromReadableString(rule) + if err != nil { + return nil, fmt.Errorf("invalid attribute rule: %s", rule) + } + req.Rule = r + } + if len(valuesOrder) > 0 { + req.ValuesOrder = valuesOrder + } + if allowTraversal != nil { + req.AllowTraversal = allowTraversal + } + + _, err := h.sdk.Unsafe.UnsafeUpdateAttribute(ctx, req) + if err != nil { + return nil, err + } + return h.GetAttribute(ctx, id) +} + +func (h Handler) AssignKeyToAttribute(ctx context.Context, attr, keyID string) (*attributes.AttributeKey, error) { + attrKey := &attributes.AttributeKey{ + KeyId: keyID, + AttributeId: attr, + } + if _, err := uuid.Parse(attr); err != nil { + attr, err := h.GetAttribute(ctx, attr) + if err != nil { + return nil, err + } + attrKey.AttributeId = attr.GetId() + } + resp, err := h.sdk.Attributes.AssignPublicKeyToAttribute(ctx, &attributes.AssignPublicKeyToAttributeRequest{ + AttributeKey: attrKey, + }) + if err != nil { + return nil, err + } + + return resp.GetAttributeKey(), nil +} + +func (h Handler) RemoveKeyFromAttribute(ctx context.Context, attr, keyID string) error { + attrKey := &attributes.AttributeKey{ + KeyId: keyID, + AttributeId: attr, + } + if _, err := uuid.Parse(attr); err != nil { + attr, err := h.GetAttribute(ctx, attr) + if err != nil { + return err + } + attrKey.AttributeId = attr.GetId() + } + _, err := h.sdk.Attributes.RemovePublicKeyFromAttribute(ctx, &attributes.RemovePublicKeyFromAttributeRequest{ + AttributeKey: attrKey, + }) + if err != nil { + return err + } + + return nil +} + +func GetAttributeFqn(namespace string, name string) string { + return fmt.Sprintf("https://%s/attr/%s", namespace, name) +} + +func GetAttributeRuleOptions() []string { + return []string{ + AttributeRuleAllOf, + AttributeRuleAnyOf, + AttributeRuleHierarchy, + } +} + +// Provides the un-prefixed human-readable attribute rule +func GetAttributeRuleFromAttributeType(rule policy.AttributeRuleTypeEnum) string { + //nolint:exhaustive // should not consider UNSPECIFIED + switch rule { + case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF: + return AttributeRuleAllOf + case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF: + return AttributeRuleAnyOf + case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY: + return AttributeRuleHierarchy + default: + return "" + } +} + +func GetAttributeRuleFromReadableString(rule string) (policy.AttributeRuleTypeEnum, error) { + // should not consider UNSPECIFIED + switch rule { + case AttributeRuleAllOf: + return policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, nil + case AttributeRuleAnyOf: + return policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, nil + case AttributeRuleHierarchy: + return policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, nil + } + return 0, fmt.Errorf("invalid attribute rule: %s, must be one of [%s, %s, %s]", rule, AttributeRuleAllOf, AttributeRuleAnyOf, AttributeRuleHierarchy) +} diff --git a/otdfctl/pkg/handlers/attributeValues.go b/otdfctl/pkg/handlers/attributeValues.go new file mode 100644 index 0000000000..836aa243f0 --- /dev/null +++ b/otdfctl/pkg/handlers/attributeValues.go @@ -0,0 +1,156 @@ +package handlers + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/unsafe" +) + +// ListAttributeValues fetches all values via GetAttribute; client-side filtering replaces the deprecated ListAttributeValues RPC. +func (h *Handler) ListAttributeValues(ctx context.Context, attributeID string) ([]*policy.Value, error) { + attr, err := h.GetAttribute(ctx, attributeID) + if err != nil { + return nil, err + } + return attr.GetValues(), nil +} + +// Creates and returns the created value +func (h *Handler) CreateAttributeValue(ctx context.Context, attributeID string, value string, metadata *common.MetadataMutable) (*policy.Value, error) { + resp, err := h.sdk.Attributes.CreateAttributeValue(ctx, &attributes.CreateAttributeValueRequest{ + AttributeId: attributeID, + Value: value, + Metadata: metadata, + }) + if err != nil { + return nil, err + } + + return h.GetAttributeValue(ctx, resp.GetValue().GetId()) +} + +func (h *Handler) GetAttributeValue(ctx context.Context, identifier string) (*policy.Value, error) { + req := &attributes.GetAttributeValueRequest{ + Identifier: &attributes.GetAttributeValueRequest_ValueId{ + ValueId: identifier, + }, + } + if _, err := uuid.Parse(identifier); err != nil { + req.Identifier = &attributes.GetAttributeValueRequest_Fqn{ + Fqn: identifier, + } + } + resp, err := h.sdk.Attributes.GetAttributeValue(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get attribute value [%s]: %w", identifier, err) + } + + return resp.GetValue(), nil +} + +// Updates and returns updated value +func (h *Handler) UpdateAttributeValue(ctx context.Context, id string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.Value, error) { + resp, err := h.sdk.Attributes.UpdateAttributeValue(ctx, &attributes.UpdateAttributeValueRequest{ + Id: id, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return h.GetAttributeValue(ctx, resp.GetValue().GetId()) +} + +// Deactivates and returns deactivated value +func (h *Handler) DeactivateAttributeValue(ctx context.Context, id string) (*policy.Value, error) { + _, err := h.sdk.Attributes.DeactivateAttributeValue(ctx, &attributes.DeactivateAttributeValueRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + return h.GetAttributeValue(ctx, id) +} + +// Reactivates and returns reactivated attribute +func (h Handler) UnsafeReactivateAttributeValue(ctx context.Context, id string) (*policy.Value, error) { + _, err := h.sdk.Unsafe.UnsafeReactivateAttributeValue(ctx, &unsafe.UnsafeReactivateAttributeValueRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + return h.GetAttributeValue(ctx, id) +} + +// Deletes and returns error if deletion failed +func (h Handler) UnsafeDeleteAttributeValue(ctx context.Context, id, fqn string) error { + _, err := h.sdk.Unsafe.UnsafeDeleteAttributeValue(ctx, &unsafe.UnsafeDeleteAttributeValueRequest{ + Id: id, + Fqn: fqn, + }) + return err +} + +// Deletes and returns error if deletion failed +func (h Handler) UnsafeUpdateAttributeValue(ctx context.Context, id, value string) error { + req := &unsafe.UnsafeUpdateAttributeValueRequest{ + Id: id, + Value: value, + } + + _, err := h.sdk.Unsafe.UnsafeUpdateAttributeValue(ctx, req) + return err +} + +// AssignKeyToAttributeValue assigns a KAS key to an attribute value +func (h *Handler) AssignKeyToAttributeValue(ctx context.Context, value, keyID string) (*attributes.ValueKey, error) { + valueKey := &attributes.ValueKey{ + KeyId: keyID, + ValueId: value, + } + + if _, err := uuid.Parse(value); err != nil { + attrValue, err := h.GetAttributeValue(ctx, value) + if err != nil { + return nil, err + } + valueKey.ValueId = attrValue.GetId() + } + + resp, err := h.sdk.Attributes.AssignPublicKeyToValue(ctx, &attributes.AssignPublicKeyToValueRequest{ + ValueKey: valueKey, + }) + if err != nil { + return nil, err + } + + return resp.GetValueKey(), nil +} + +// RemoveKeyFromAttributeValue removes a KAS key from an attribute value +func (h *Handler) RemoveKeyFromAttributeValue(ctx context.Context, value, keyID string) error { + valueKey := &attributes.ValueKey{ + KeyId: keyID, + ValueId: value, + } + + if _, err := uuid.Parse(value); err != nil { + attrValue, err := h.GetAttributeValue(ctx, value) + if err != nil { + return err + } + valueKey.ValueId = attrValue.GetId() + } + + _, err := h.sdk.Attributes.RemovePublicKeyFromValue(ctx, &attributes.RemovePublicKeyFromValueRequest{ + ValueKey: valueKey, + }) + return err +} diff --git a/otdfctl/pkg/handlers/base-keys.go b/otdfctl/pkg/handlers/base-keys.go new file mode 100644 index 0000000000..dd8e8d0976 --- /dev/null +++ b/otdfctl/pkg/handlers/base-keys.go @@ -0,0 +1,35 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" +) + +// GetBaseKey retrieves a base key from the KAS registry. +// This is a stub function and needs to be implemented. +func (h Handler) GetBaseKey(ctx context.Context) (*policy.SimpleKasKey, error) { + resp, err := h.sdk.KeyAccessServerRegistry.GetBaseKey(ctx, &kasregistry.GetBaseKeyRequest{}) + if err != nil { + return nil, err + } + + return resp.GetBaseKey(), nil +} + +func (h Handler) SetBaseKey(ctx context.Context, id string, key *kasregistry.KasKeyIdentifier) (*kasregistry.SetBaseKeyResponse, error) { + req := kasregistry.SetBaseKeyRequest{} + + if id != "" && key == nil { + req.ActiveKey = &kasregistry.SetBaseKeyRequest_Id{ + Id: id, + } + } else if key != nil { + req.ActiveKey = &kasregistry.SetBaseKeyRequest_Key{ + Key: key, + } + } + + return h.sdk.KeyAccessServerRegistry.SetBaseKey(ctx, &req) +} diff --git a/otdfctl/pkg/handlers/kas-grants.go b/otdfctl/pkg/handlers/kas-grants.go new file mode 100644 index 0000000000..0b09bb994a --- /dev/null +++ b/otdfctl/pkg/handlers/kas-grants.go @@ -0,0 +1,72 @@ +//nolint:staticcheck // deprecated KAS grant functions are still supported while migrating +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/opentdf/platform/protocol/go/policy/namespaces" +) + +func (h Handler) DeleteKasGrantFromAttribute(ctx context.Context, attrID string, kasID string) (*attributes.AttributeKeyAccessServer, error) { + kas := &attributes.AttributeKeyAccessServer{ + AttributeId: attrID, + KeyAccessServerId: kasID, + } + resp, err := h.sdk.Attributes.RemoveKeyAccessServerFromAttribute(ctx, &attributes.RemoveKeyAccessServerFromAttributeRequest{ + AttributeKeyAccessServer: kas, + }) + if err != nil { + return nil, err + } + + return resp.GetAttributeKeyAccessServer(), nil +} + +func (h Handler) DeleteKasGrantFromValue(ctx context.Context, valID string, kasID string) (*attributes.ValueKeyAccessServer, error) { + kas := &attributes.ValueKeyAccessServer{ + ValueId: valID, + KeyAccessServerId: kasID, + } + resp, err := h.sdk.Attributes.RemoveKeyAccessServerFromValue(ctx, &attributes.RemoveKeyAccessServerFromValueRequest{ + ValueKeyAccessServer: kas, + }) + if err != nil { + return nil, err + } + + return resp.GetValueKeyAccessServer(), nil +} + +func (h Handler) DeleteKasGrantFromNamespace(ctx context.Context, nsID string, kasID string) (*namespaces.NamespaceKeyAccessServer, error) { + kas := &namespaces.NamespaceKeyAccessServer{ + NamespaceId: nsID, + KeyAccessServerId: kasID, + } + resp, err := h.sdk.Namespaces.RemoveKeyAccessServerFromNamespace(ctx, &namespaces.RemoveKeyAccessServerFromNamespaceRequest{ + NamespaceKeyAccessServer: kas, + }) + if err != nil { + return nil, err + } + + return resp.GetNamespaceKeyAccessServer(), nil +} + +func (h Handler) ListKasGrants(ctx context.Context, kasID, kasURI string, limit, offset int32) ([]*kasregistry.KeyAccessServerGrants, *policy.PageResponse, error) { + resp, err := h.sdk.KeyAccessServerRegistry.ListKeyAccessServerGrants(ctx, &kasregistry.ListKeyAccessServerGrantsRequest{ + KasId: kasID, + KasUri: kasURI, + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + }) + if err != nil { + return nil, nil, err + } + //nolint:staticcheck // deprecated but not removed while public keys work is experimental + return resp.GetGrants(), resp.GetPagination(), nil +} diff --git a/otdfctl/pkg/handlers/kas-keys.go b/otdfctl/pkg/handlers/kas-keys.go new file mode 100644 index 0000000000..d664115734 --- /dev/null +++ b/otdfctl/pkg/handlers/kas-keys.go @@ -0,0 +1,208 @@ +package handlers + +import ( + "context" + "errors" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/opentdf/platform/protocol/go/policy/unsafe" +) + +type RotateKeyResult struct { + KasKey *policy.KasKey `json:"kas_key"` + RotatedResources *kasregistry.RotatedResources `json:"rotated_resources"` +} + +func (h Handler) CreateKasKey( + ctx context.Context, + kasID string, + keyID string, + alg policy.Algorithm, + mode policy.KeyMode, + pubKeyCtx *policy.PublicKeyCtx, + privKeyCtx *policy.PrivateKeyCtx, + providerConfigID string, + metadata *common.MetadataMutable, + legacy bool, +) (*policy.KasKey, error) { + req := kasregistry.CreateKeyRequest{ + KasId: kasID, + KeyId: keyID, + KeyAlgorithm: alg, + KeyMode: mode, + PublicKeyCtx: pubKeyCtx, + PrivateKeyCtx: privKeyCtx, + ProviderConfigId: providerConfigID, + Metadata: metadata, + Legacy: legacy, + } + + resp, err := h.sdk.KeyAccessServerRegistry.CreateKey(ctx, &req) + if err != nil { + return nil, err + } + + return resp.GetKasKey(), nil +} + +func (h Handler) GetKasKey(ctx context.Context, id string, key *kasregistry.KasKeyIdentifier) (*policy.KasKey, error) { + req := kasregistry.GetKeyRequest{} + switch { + case id != "" && key == nil: + req.Identifier = &kasregistry.GetKeyRequest_Id{ + Id: id, + } + case key != nil: + req.Identifier = &kasregistry.GetKeyRequest_Key{ + Key: key, + } + default: + return nil, errors.New("id or key must be provided") + } + + resp, err := h.sdk.KeyAccessServerRegistry.GetKey(ctx, &req) + if err != nil { + return nil, err + } + + return resp.GetKasKey(), nil +} + +func (h Handler) UpdateKasKey(ctx context.Context, id string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.KasKey, error) { + req := kasregistry.UpdateKeyRequest{ + Id: id, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + } + + resp, err := h.sdk.KeyAccessServerRegistry.UpdateKey(ctx, &req) + if err != nil { + return nil, err + } + + return resp.GetKasKey(), nil +} + +func (h Handler) ListKasKeys( + ctx context.Context, + limit, offset int32, + algorithm policy.Algorithm, + identifier KasIdentifier, + legacy *bool, + sort SortOption, +) (*kasregistry.ListKeysResponse, error) { + req := kasregistry.ListKeysRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + KeyAlgorithm: algorithm, + } + + switch { + case identifier.ID != "": + req.KasFilter = &kasregistry.ListKeysRequest_KasId{ + KasId: identifier.ID, + } + case identifier.Name != "": + req.KasFilter = &kasregistry.ListKeysRequest_KasName{ + KasName: identifier.Name, + } + case identifier.URI != "": + req.KasFilter = &kasregistry.ListKeysRequest_KasUri{ + KasUri: identifier.URI, + } + } + req.Legacy = legacy + if !sort.IsZero() { + allowedFields := map[string]kasregistry.SortKasKeysType{ + "key_id": kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID, + "created_at": kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, + "updated_at": kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UPDATED_AT, + } + field, err := sortField("KAS keys", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*kasregistry.KasKeysSort{{Field: field, Direction: sort.Direction}} + } + + return h.sdk.KeyAccessServerRegistry.ListKeys(ctx, &req) +} + +func (h Handler) ListKeyMappings( + ctx context.Context, + limit, offset int32, + keySystemID string, + keyUserIdentifier *kasregistry.KasKeyIdentifier, +) (*kasregistry.ListKeyMappingsResponse, error) { + req := kasregistry.ListKeyMappingsRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + + switch { + case keySystemID != "": + req.Identifier = &kasregistry.ListKeyMappingsRequest_Id{ + Id: keySystemID, + } + case keyUserIdentifier != nil: + req.Identifier = &kasregistry.ListKeyMappingsRequest_Key{ + Key: keyUserIdentifier, + } + } + + resp, err := h.sdk.KeyAccessServerRegistry.ListKeyMappings(ctx, &req) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (h Handler) RotateKasKey( + ctx context.Context, + oldKeyID string, + key *kasregistry.KasKeyIdentifier, + newKey *kasregistry.RotateKeyRequest_NewKey, +) (*RotateKeyResult, error) { + req := kasregistry.RotateKeyRequest{ + NewKey: newKey, + } + + switch { + case oldKeyID != "" && key == nil: + req.ActiveKey = &kasregistry.RotateKeyRequest_Id{ + Id: oldKeyID, + } + case key != nil: + req.ActiveKey = &kasregistry.RotateKeyRequest_Key{ + Key: key, + } + default: + return nil, errors.New("old key id or key must be provided") + } + + resp, err := h.sdk.KeyAccessServerRegistry.RotateKey(ctx, &req) + if err != nil { + return nil, err + } + + return &RotateKeyResult{ + KasKey: resp.GetKasKey(), + RotatedResources: resp.GetRotatedResources(), + }, nil +} + +func (h Handler) UnsafeDeleteKasKey(ctx context.Context, id, kid, kasURI string) (*policy.KasKey, error) { + resp, err := h.sdk.Unsafe.UnsafeDeleteKasKey(ctx, &unsafe.UnsafeDeleteKasKeyRequest{ + Id: id, + Kid: kid, + KasUri: kasURI, + }) + return resp.GetKey(), err +} diff --git a/otdfctl/pkg/handlers/kas-registry.go b/otdfctl/pkg/handlers/kas-registry.go new file mode 100644 index 0000000000..a0d52a84dc --- /dev/null +++ b/otdfctl/pkg/handlers/kas-registry.go @@ -0,0 +1,116 @@ +package handlers + +import ( + "context" + "errors" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" +) + +type KasIdentifier struct { + ID string + Name string + URI string +} + +func (h Handler) GetKasRegistryEntry(ctx context.Context, identifer KasIdentifier) (*policy.KeyAccessServer, error) { + req := &kasregistry.GetKeyAccessServerRequest{} + switch { + case identifer.ID != "": + req.Identifier = &kasregistry.GetKeyAccessServerRequest_KasId{ + KasId: identifer.ID, + } + case identifer.Name != "": + req.Identifier = &kasregistry.GetKeyAccessServerRequest_Name{ + Name: identifer.Name, + } + case identifer.URI != "": + req.Identifier = &kasregistry.GetKeyAccessServerRequest_Uri{ + Uri: identifer.URI, + } + default: + return nil, errors.New("id, name or uri must be provided") + } + + resp, err := h.sdk.KeyAccessServerRegistry.GetKeyAccessServer(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetKeyAccessServer(), nil +} + +func (h Handler) ListKasRegistryEntries(ctx context.Context, limit, offset int32, sort SortOption) (*kasregistry.ListKeyAccessServersResponse, error) { + req := &kasregistry.ListKeyAccessServersRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + if !sort.IsZero() { + allowedFields := map[string]kasregistry.SortKeyAccessServersType{ + "name": kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME, + "uri": kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_URI, + "created_at": kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, + "updated_at": kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT, + } + field, err := sortField("KAS registry entries", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*kasregistry.KeyAccessServersSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.KeyAccessServerRegistry.ListKeyAccessServers(ctx, req) +} + +// Creates the KAS registry and then returns the KAS +func (h Handler) CreateKasRegistryEntry(ctx context.Context, uri string, name string, metadata *common.MetadataMutable) (*policy.KeyAccessServer, error) { + req := &kasregistry.CreateKeyAccessServerRequest{ + Uri: uri, + Name: name, + Metadata: metadata, + } + + resp, err := h.sdk.KeyAccessServerRegistry.CreateKeyAccessServer(ctx, req) + if err != nil { + return nil, err + } + + return h.GetKasRegistryEntry(ctx, KasIdentifier{ + ID: resp.GetKeyAccessServer().GetId(), + }) +} + +// Updates the KAS registry and then returns the KAS +func (h Handler) UpdateKasRegistryEntry(ctx context.Context, id, uri, name string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.KeyAccessServer, error) { + _, err := h.sdk.KeyAccessServerRegistry.UpdateKeyAccessServer(ctx, &kasregistry.UpdateKeyAccessServerRequest{ + Id: id, + Uri: uri, + Name: name, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return h.GetKasRegistryEntry(ctx, KasIdentifier{ + ID: id, + }) +} + +// Deletes the KAS registry and returns the deleted KAS +func (h Handler) DeleteKasRegistryEntry(ctx context.Context, id string) (*policy.KeyAccessServer, error) { + req := &kasregistry.DeleteKeyAccessServerRequest{ + Id: id, + } + + resp, err := h.sdk.KeyAccessServerRegistry.DeleteKeyAccessServer(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetKeyAccessServer(), nil +} diff --git a/otdfctl/pkg/handlers/namespaces.go b/otdfctl/pkg/handlers/namespaces.go new file mode 100644 index 0000000000..b7ebbdb0bd --- /dev/null +++ b/otdfctl/pkg/handlers/namespaces.go @@ -0,0 +1,181 @@ +package handlers + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/unsafe" +) + +func getNamespaceIDAndFQN(namespace string) (string, string) { + if _, err := uuid.Parse(namespace); err != nil { + return "", namespace + } + return namespace, "" +} + +func (h Handler) GetNamespace(ctx context.Context, identifier string) (*policy.Namespace, error) { + req := &namespaces.GetNamespaceRequest{ + Identifier: &namespaces.GetNamespaceRequest_NamespaceId{ + NamespaceId: identifier, + }, + } + if _, err := uuid.Parse(identifier); err != nil { + req.Identifier = &namespaces.GetNamespaceRequest_Fqn{ + Fqn: identifier, + } + } + + resp, err := h.sdk.Namespaces.GetNamespace(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get namespace [%s]: %w", identifier, err) + } + + return resp.GetNamespace(), nil +} + +func (h Handler) ListNamespaces(ctx context.Context, state common.ActiveStateEnum, limit, offset int32, sort SortOption) (*namespaces.ListNamespacesResponse, error) { + req := &namespaces.ListNamespacesRequest{ + State: state, + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + if !sort.IsZero() { + allowedFields := map[string]namespaces.SortNamespacesType{ + "name": namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_NAME, + "fqn": namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_FQN, + "created_at": namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, + "updated_at": namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UPDATED_AT, + } + field, err := sortField("namespaces", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*namespaces.NamespacesSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.Namespaces.ListNamespaces(ctx, req) +} + +// Creates and returns the created n +func (h Handler) CreateNamespace(ctx context.Context, name string, metadata *common.MetadataMutable) (*policy.Namespace, error) { + resp, err := h.sdk.Namespaces.CreateNamespace(ctx, &namespaces.CreateNamespaceRequest{ + Name: name, + Metadata: metadata, + }) + if err != nil { + return nil, err + } + + return h.GetNamespace(ctx, resp.GetNamespace().GetId()) +} + +// Updates and returns the updated namespace +func (h Handler) UpdateNamespace(ctx context.Context, id string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.Namespace, error) { + _, err := h.sdk.Namespaces.UpdateNamespace(ctx, &namespaces.UpdateNamespaceRequest{ + Id: id, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + return h.GetNamespace(ctx, id) +} + +// Deactivates and returns the deactivated namespace +func (h Handler) DeactivateNamespace(ctx context.Context, id string) (*policy.Namespace, error) { + _, err := h.sdk.Namespaces.DeactivateNamespace(ctx, &namespaces.DeactivateNamespaceRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + + return h.GetNamespace(ctx, id) +} + +// Reactivates and returns the reactivated namespace +func (h Handler) UnsafeReactivateNamespace(ctx context.Context, id string) (*policy.Namespace, error) { + _, err := h.sdk.Unsafe.UnsafeReactivateNamespace(ctx, &unsafe.UnsafeReactivateNamespaceRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + + return h.GetNamespace(ctx, id) +} + +// Deletes and returns the deleted namespace +func (h Handler) UnsafeDeleteNamespace(ctx context.Context, id string, fqn string) error { + _, err := h.sdk.Unsafe.UnsafeDeleteNamespace(ctx, &unsafe.UnsafeDeleteNamespaceRequest{ + Id: id, + Fqn: fqn, + }) + return err +} + +// Unsafely updates the namespace and returns the renamed namespace +func (h Handler) UnsafeUpdateNamespace(ctx context.Context, id, name string) (*policy.Namespace, error) { + _, err := h.sdk.Unsafe.UnsafeUpdateNamespace(ctx, &unsafe.UnsafeUpdateNamespaceRequest{ + Id: id, + Name: name, + }) + if err != nil { + return nil, err + } + + return h.GetNamespace(ctx, id) +} + +// AssignKeyToAttributeNamespace assigns a KAS key to an attribute namespace +func (h *Handler) AssignKeyToAttributeNamespace(ctx context.Context, namespace, keyID string) (*namespaces.NamespaceKey, error) { + namespaceKey := &namespaces.NamespaceKey{ + KeyId: keyID, + NamespaceId: namespace, + } + + if _, err := uuid.Parse(namespace); err != nil { + ns, err := h.GetNamespace(ctx, namespace) + if err != nil { + return nil, err + } + namespaceKey.NamespaceId = ns.GetId() + } + + resp, err := h.sdk.Namespaces.AssignPublicKeyToNamespace(ctx, &namespaces.AssignPublicKeyToNamespaceRequest{ + NamespaceKey: namespaceKey, + }) + if err != nil { + return nil, err + } + + return resp.GetNamespaceKey(), nil +} + +// RemoveKeyFromAttributeNamespace removes a KAS key from an attribute namespace +func (h *Handler) RemoveKeyFromAttributeNamespace(ctx context.Context, namespace, keyID string) error { + namespaceKey := &namespaces.NamespaceKey{ + KeyId: keyID, + NamespaceId: namespace, + } + + if _, err := uuid.Parse(namespace); err != nil { + ns, err := h.GetNamespace(ctx, namespace) + if err != nil { + return err + } + namespaceKey.NamespaceId = ns.GetId() + } + + _, err := h.sdk.Namespaces.RemovePublicKeyFromNamespace(ctx, &namespaces.RemovePublicKeyFromNamespaceRequest{ + NamespaceKey: namespaceKey, + }) + return err +} diff --git a/otdfctl/pkg/handlers/obligations.go b/otdfctl/pkg/handlers/obligations.go new file mode 100644 index 0000000000..7aae3b761e --- /dev/null +++ b/otdfctl/pkg/handlers/obligations.go @@ -0,0 +1,246 @@ +package handlers + +import ( + "context" + + "github.com/google/uuid" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/obligations" +) + +// ParseToIDFqnIdentifier creates an IdFqnIdentifier based on whether the input is a UUID or FQN +func ParseToIDFqnIdentifier(value string) *common.IdFqnIdentifier { + _, err := uuid.Parse(value) + if err != nil { + return &common.IdFqnIdentifier{Fqn: value} + } + return &common.IdFqnIdentifier{Id: value} +} + +// ParseToIDNameIdentifier creates an IdNameIdentifier based on whether the input is a UUID or name +func ParseToIDNameIdentifier(value string) *common.IdNameIdentifier { + _, err := uuid.Parse(value) + if err != nil { + return &common.IdNameIdentifier{Name: value} + } + return &common.IdNameIdentifier{Id: value} +} + +// +// Obligations +// + +func (h Handler) CreateObligation(ctx context.Context, namespace, name string, values []string, metadata *common.MetadataMutable) (*policy.Obligation, error) { + req := &obligations.CreateObligationRequest{ + Name: name, + Values: values, + Metadata: metadata, + } + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + + resp, err := h.sdk.Obligations.CreateObligation(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetObligation(), nil +} + +func (h Handler) GetObligation(ctx context.Context, id, fqn string) (*policy.Obligation, error) { + req := &obligations.GetObligationRequest{} + if id != "" { + req.Id = id + } else { + req.Fqn = fqn + } + + resp, err := h.sdk.Obligations.GetObligation(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetObligation(), nil +} + +func (h Handler) ListObligations(ctx context.Context, limit, offset int32, namespace string, sort SortOption) (*obligations.ListObligationsResponse, error) { + req := &obligations.ListObligationsRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + if namespace != "" { + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + } + if !sort.IsZero() { + allowedFields := map[string]obligations.SortObligationsType{ + "name": obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME, + "fqn": obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN, + "created_at": obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, + "updated_at": obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UPDATED_AT, + } + field, err := sortField("obligations", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*obligations.ObligationsSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.Obligations.ListObligations(ctx, req) +} + +func (h Handler) UpdateObligation(ctx context.Context, id, name string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.Obligation, error) { + res, err := h.sdk.Obligations.UpdateObligation(ctx, &obligations.UpdateObligationRequest{ + Id: id, + Name: name, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return res.GetObligation(), nil +} + +func (h Handler) DeleteObligation(ctx context.Context, id, fqn string) error { + req := &obligations.DeleteObligationRequest{} + if id != "" { + req.Id = id + } else { + req.Fqn = fqn + } + _, err := h.sdk.Obligations.DeleteObligation(ctx, req) + if err != nil { + return err + } + + return nil +} + +// +// Obligation Values +// + +func (h Handler) CreateObligationValue(ctx context.Context, obligation, value string, triggers []*obligations.ValueTriggerRequest, metadata *common.MetadataMutable) (*policy.ObligationValue, error) { + req := &obligations.CreateObligationValueRequest{ + Value: value, + Triggers: triggers, + Metadata: metadata, + } + + _, err := uuid.Parse(obligation) + if err != nil { + req.ObligationFqn = obligation + } else { + req.ObligationId = obligation + } + + resp, err := h.sdk.Obligations.CreateObligationValue(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetValue(), nil +} + +func (h Handler) GetObligationValue(ctx context.Context, id, fqn string) (*policy.ObligationValue, error) { + req := &obligations.GetObligationValueRequest{} + if id != "" { + req.Id = id + } else { + req.Fqn = fqn + } + + resp, err := h.sdk.Obligations.GetObligationValue(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetValue(), nil +} + +func (h Handler) UpdateObligationValue(ctx context.Context, id, value string, triggers []*obligations.ValueTriggerRequest, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.ObligationValue, error) { + res, err := h.sdk.Obligations.UpdateObligationValue(ctx, &obligations.UpdateObligationValueRequest{ + Id: id, + Value: value, + Triggers: triggers, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return res.GetValue(), nil +} + +func (h Handler) DeleteObligationValue(ctx context.Context, id, fqn string) error { + req := &obligations.DeleteObligationValueRequest{} + if id != "" { + req.Id = id + } else { + req.Fqn = fqn + } + _, err := h.sdk.Obligations.DeleteObligationValue(ctx, req) + if err != nil { + return err + } + + return nil +} + +// ****** +// Obligation Triggers +// ****** +func (h Handler) CreateObligationTrigger(ctx context.Context, attributeValue, action, obligationValue, clientID string, metadata *common.MetadataMutable) (*policy.ObligationTrigger, error) { + req := &obligations.AddObligationTriggerRequest{ + Metadata: metadata, + } + + req.AttributeValue = ParseToIDFqnIdentifier(attributeValue) + req.Action = ParseToIDNameIdentifier(action) + req.ObligationValue = ParseToIDFqnIdentifier(obligationValue) + + if clientID != "" { + req.Context = &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: clientID, + }, + } + } + + resp, err := h.sdk.Obligations.AddObligationTrigger(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetTrigger(), nil +} + +func (h Handler) DeleteObligationTrigger(ctx context.Context, id string) (*policy.ObligationTrigger, error) { + req := &obligations.RemoveObligationTriggerRequest{ + Id: id, + } + resp, err := h.sdk.Obligations.RemoveObligationTrigger(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetTrigger(), nil +} + +func (h Handler) ListObligationTriggers(ctx context.Context, namespace string, limit, offset int32) (*obligations.ListObligationTriggersResponse, error) { + req := &obligations.ListObligationTriggersRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + + if namespace != "" { + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + } + + return h.sdk.Obligations.ListObligationTriggers(ctx, req) +} diff --git a/otdfctl/pkg/handlers/provider-config.go b/otdfctl/pkg/handlers/provider-config.go new file mode 100644 index 0000000000..be25e3d4ed --- /dev/null +++ b/otdfctl/pkg/handlers/provider-config.go @@ -0,0 +1,95 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/keymanagement" +) + +func (h Handler) CreateProviderConfig( + ctx context.Context, + name, manager string, + config []byte, + metadata *common.MetadataMutable, +) (*policy.KeyProviderConfig, error) { + req := keymanagement.CreateProviderConfigRequest{ + Name: name, + Manager: manager, + ConfigJson: config, + Metadata: metadata, + } + + resp, err := h.sdk.KeyManagement.CreateProviderConfig(ctx, &req) + if err != nil { + return nil, err + } + + return resp.GetProviderConfig(), nil +} + +func (h Handler) GetProviderConfig(ctx context.Context, id, name string) (*policy.KeyProviderConfig, error) { + req := keymanagement.GetProviderConfigRequest{} + if id != "" { + req.Identifier = &keymanagement.GetProviderConfigRequest_Id{ + Id: id, + } + } else if name != "" { + req.Identifier = &keymanagement.GetProviderConfigRequest_Name{ + Name: name, + } + } + + resp, err := h.sdk.KeyManagement.GetProviderConfig(ctx, &req) + if err != nil { + return nil, err + } + + return resp.GetProviderConfig(), nil +} + +func (h Handler) UpdateProviderConfig( + ctx context.Context, + id, name, manager string, + config []byte, + metadata *common.MetadataMutable, + behavior common.MetadataUpdateEnum, +) (*policy.KeyProviderConfig, error) { + req := keymanagement.UpdateProviderConfigRequest{ + Id: id, + Name: name, + Manager: manager, + ConfigJson: config, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + } + + resp, err := h.sdk.KeyManagement.UpdateProviderConfig(ctx, &req) + if err != nil { + return nil, err + } + + return resp.GetProviderConfig(), nil +} + +func (h Handler) ListProviderConfigs(ctx context.Context, limit, offset int32) (*keymanagement.ListProviderConfigsResponse, error) { + req := keymanagement.ListProviderConfigsRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + + return h.sdk.KeyManagement.ListProviderConfigs(ctx, &req) +} + +func (h *Handler) DeleteProviderConfig(ctx context.Context, id string) error { + _, err := h.sdk.KeyManagement.DeleteProviderConfig(ctx, &keymanagement.DeleteProviderConfigRequest{ + Id: id, + }) + if err != nil { + return err + } + return nil +} diff --git a/otdfctl/pkg/handlers/registeredResources.go b/otdfctl/pkg/handlers/registeredResources.go new file mode 100644 index 0000000000..1672a93adb --- /dev/null +++ b/otdfctl/pkg/handlers/registeredResources.go @@ -0,0 +1,171 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" +) + +// +// Registered Resources +// + +func (h Handler) CreateRegisteredResource(ctx context.Context, namespace, name string, values []string, metadata *common.MetadataMutable) (*policy.RegisteredResource, error) { + req := ®isteredresources.CreateRegisteredResourceRequest{ + Name: name, + Values: values, + Metadata: metadata, + } + + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + + resp, err := h.sdk.RegisteredResources.CreateRegisteredResource(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetResource(), nil +} + +func (h Handler) GetRegisteredResource(ctx context.Context, id, name, namespace string) (*policy.RegisteredResource, error) { + req := ®isteredresources.GetRegisteredResourceRequest{} + if id != "" { + req.Identifier = ®isteredresources.GetRegisteredResourceRequest_Id{ + Id: id, + } + } else { + req.Identifier = ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: name, + } + } + if namespace != "" { + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + } + + resp, err := h.sdk.RegisteredResources.GetRegisteredResource(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetResource(), nil +} + +func (h Handler) ListRegisteredResources(ctx context.Context, limit, offset int32, namespace string, sort SortOption) (*registeredresources.ListRegisteredResourcesResponse, error) { + req := ®isteredresources.ListRegisteredResourcesRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + if namespace != "" { + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + } + if !sort.IsZero() { + allowedFields := map[string]registeredresources.SortRegisteredResourcesType{ + "name": registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME, + "created_at": registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, + "updated_at": registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT, + } + field, err := sortField("registered resources", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*registeredresources.RegisteredResourcesSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.RegisteredResources.ListRegisteredResources(ctx, req) +} + +func (h Handler) UpdateRegisteredResource(ctx context.Context, id, name string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.RegisteredResource, error) { + _, err := h.sdk.RegisteredResources.UpdateRegisteredResource(ctx, ®isteredresources.UpdateRegisteredResourceRequest{ + Id: id, + Name: name, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return h.GetRegisteredResource(ctx, id, "", "") +} + +func (h Handler) DeleteRegisteredResource(ctx context.Context, id string) error { + _, err := h.sdk.RegisteredResources.DeleteRegisteredResource(ctx, ®isteredresources.DeleteRegisteredResourceRequest{ + Id: id, + }) + + return err +} + +// +// Registered Resource Values +// + +func (h Handler) CreateRegisteredResourceValue(ctx context.Context, resourceID string, value string, actionAttributeValues []*registeredresources.ActionAttributeValue, metadata *common.MetadataMutable) (*policy.RegisteredResourceValue, error) { + resp, err := h.sdk.RegisteredResources.CreateRegisteredResourceValue(ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: resourceID, + Value: value, + ActionAttributeValues: actionAttributeValues, + Metadata: metadata, + }) + if err != nil { + return nil, err + } + + return resp.GetValue(), nil +} + +func (h Handler) GetRegisteredResourceValue(ctx context.Context, id, fqn string) (*policy.RegisteredResourceValue, error) { + req := ®isteredresources.GetRegisteredResourceValueRequest{} + if id != "" { + req.Identifier = ®isteredresources.GetRegisteredResourceValueRequest_Id{ + Id: id, + } + } else { + req.Identifier = ®isteredresources.GetRegisteredResourceValueRequest_Fqn{ + Fqn: fqn, + } + } + + resp, err := h.sdk.RegisteredResources.GetRegisteredResourceValue(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetValue(), nil +} + +func (h Handler) ListRegisteredResourceValues(ctx context.Context, resourceID string, limit, offset int32) (*registeredresources.ListRegisteredResourceValuesResponse, error) { + return h.sdk.RegisteredResources.ListRegisteredResourceValues(ctx, ®isteredresources.ListRegisteredResourceValuesRequest{ + ResourceId: resourceID, + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + }) +} + +func (h Handler) UpdateRegisteredResourceValue(ctx context.Context, id, value string, actionAttributeValues []*registeredresources.ActionAttributeValue, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.RegisteredResourceValue, error) { + _, err := h.sdk.RegisteredResources.UpdateRegisteredResourceValue(ctx, ®isteredresources.UpdateRegisteredResourceValueRequest{ + Id: id, + Value: value, + ActionAttributeValues: actionAttributeValues, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return h.GetRegisteredResourceValue(ctx, id, "") +} + +func (h Handler) DeleteRegisteredResourceValue(ctx context.Context, id string) error { + _, err := h.sdk.RegisteredResources.DeleteRegisteredResourceValue(ctx, ®isteredresources.DeleteRegisteredResourceValueRequest{ + Id: id, + }) + + return err +} diff --git a/otdfctl/pkg/handlers/resourceMappingGroups.go b/otdfctl/pkg/handlers/resourceMappingGroups.go new file mode 100644 index 0000000000..499df96a00 --- /dev/null +++ b/otdfctl/pkg/handlers/resourceMappingGroups.go @@ -0,0 +1,71 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/resourcemapping" +) + +// Creates and returns the created resource mapping +func (h *Handler) CreateResourceMappingGroup(ctx context.Context, namespaceID string, name string, metadata *common.MetadataMutable) (*policy.ResourceMappingGroup, error) { + res, err := h.sdk.ResourceMapping.CreateResourceMappingGroup(ctx, &resourcemapping.CreateResourceMappingGroupRequest{ + NamespaceId: namespaceID, + Name: name, + Metadata: metadata, + }) + if err != nil { + return nil, err + } + + return h.GetResourceMappingGroup(ctx, res.GetResourceMappingGroup().GetId()) +} + +func (h *Handler) GetResourceMappingGroup(ctx context.Context, id string) (*policy.ResourceMappingGroup, error) { + res, err := h.sdk.ResourceMapping.GetResourceMappingGroup(ctx, &resourcemapping.GetResourceMappingGroupRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + + return res.GetResourceMappingGroup(), nil +} + +func (h *Handler) ListResourceMappingGroups(ctx context.Context, limit, offset int32) (*resourcemapping.ListResourceMappingGroupsResponse, error) { + return h.sdk.ResourceMapping.ListResourceMappingGroups(ctx, &resourcemapping.ListResourceMappingGroupsRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + }) +} + +// TODO: verify updation behavior +// Updates and returns the updated resource mapping +func (h *Handler) UpdateResourceMappingGroup(ctx context.Context, id string, namespaceID string, name string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.ResourceMappingGroup, error) { + _, err := h.sdk.ResourceMapping.UpdateResourceMappingGroup(ctx, &resourcemapping.UpdateResourceMappingGroupRequest{ + Id: id, + NamespaceId: namespaceID, + Name: name, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return h.GetResourceMappingGroup(ctx, id) +} + +func (h *Handler) DeleteResourceMappingGroup(ctx context.Context, id string) (*policy.ResourceMappingGroup, error) { + resp, err := h.sdk.ResourceMapping.DeleteResourceMappingGroup(ctx, &resourcemapping.DeleteResourceMappingGroupRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + + return resp.GetResourceMappingGroup(), nil +} diff --git a/otdfctl/pkg/handlers/resourceMappings.go b/otdfctl/pkg/handlers/resourceMappings.go new file mode 100644 index 0000000000..ccb80d72bf --- /dev/null +++ b/otdfctl/pkg/handlers/resourceMappings.go @@ -0,0 +1,73 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/resourcemapping" +) + +// Creates and returns the created resource mapping +func (h *Handler) CreateResourceMapping(attributeID string, terms []string, grpID string, metadata *common.MetadataMutable) (*policy.ResourceMapping, error) { + res, err := h.sdk.ResourceMapping.CreateResourceMapping(context.Background(), &resourcemapping.CreateResourceMappingRequest{ + AttributeValueId: attributeID, + GroupId: grpID, + Terms: terms, + Metadata: metadata, + }) + if err != nil { + return nil, err + } + + return h.GetResourceMapping(res.GetResourceMapping().GetId()) +} + +func (h *Handler) GetResourceMapping(id string) (*policy.ResourceMapping, error) { + res, err := h.sdk.ResourceMapping.GetResourceMapping(context.Background(), &resourcemapping.GetResourceMappingRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + + return res.GetResourceMapping(), nil +} + +func (h *Handler) ListResourceMappings(ctx context.Context, limit, offset int32) (*resourcemapping.ListResourceMappingsResponse, error) { + return h.sdk.ResourceMapping.ListResourceMappings(ctx, &resourcemapping.ListResourceMappingsRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + }) +} + +// TODO: verify updation behavior +// Updates and returns the updated resource mapping +func (h *Handler) UpdateResourceMapping(id string, attrValueID string, grpID string, terms []string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.ResourceMapping, error) { + _, err := h.sdk.ResourceMapping.UpdateResourceMapping(context.Background(), &resourcemapping.UpdateResourceMappingRequest{ + Id: id, + AttributeValueId: attrValueID, + Terms: terms, + GroupId: grpID, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return h.GetResourceMapping(id) +} + +func (h *Handler) DeleteResourceMapping(id string) (*policy.ResourceMapping, error) { + resp, err := h.sdk.ResourceMapping.DeleteResourceMapping(context.Background(), &resourcemapping.DeleteResourceMappingRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + + return resp.GetResourceMapping(), nil +} diff --git a/otdfctl/pkg/handlers/sdk.go b/otdfctl/pkg/handlers/sdk.go new file mode 100644 index 0000000000..2c1e0f7871 --- /dev/null +++ b/otdfctl/pkg/handlers/sdk.go @@ -0,0 +1,152 @@ +package handlers + +import ( + "errors" + "log/slog" + + "github.com/opentdf/platform/otdfctl/pkg/auth" + "github.com/opentdf/platform/otdfctl/pkg/profiles" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/sdk" +) + +var ( + SDK *sdk.SDK + + ErrUnauthenticated = errors.New("unauthenticated") +) + +type Handler struct { + sdk *sdk.SDK + platformEndpoint string +} + +type handlerOpts struct { + endpoint string + TLSNoVerify bool + + profile *profiles.OtdfctlProfileStore + + sdkOpts []sdk.Option +} + +type handlerOptsFunc func(handlerOpts) handlerOpts + +func WithEndpoint(endpoint string, tlsNoVerify bool) handlerOptsFunc { + return func(c handlerOpts) handlerOpts { + c.endpoint = endpoint + c.TLSNoVerify = tlsNoVerify + return c + } +} + +func WithProfile(profile *profiles.OtdfctlProfileStore) handlerOptsFunc { + return func(c handlerOpts) handlerOpts { + c.profile = profile + c.endpoint = profile.GetEndpoint() + c.TLSNoVerify = profile.GetTLSNoVerify() + + // get sdk opts + opts, err := auth.GetSDKAuthOptionFromProfile(profile) + if err != nil { + return c + } + c.sdkOpts = append(c.sdkOpts, opts) + + return c + } +} + +func WithSDKOpts(opts ...sdk.Option) handlerOptsFunc { + return func(c handlerOpts) handlerOpts { + c.sdkOpts = opts + return c + } +} + +// Creates a new handler wrapping the SDK, which is authenticated through the cached client-credentials flow tokens +func New(opts ...handlerOptsFunc) (Handler, error) { + var o handlerOpts + for _, f := range opts { + o = f(o) + } + + u, err := utils.NormalizeEndpoint(o.endpoint) + if err != nil { + return Handler{}, err + } + + // get auth + authSDKOpt, err := auth.GetSDKAuthOptionFromProfile(o.profile) + if err != nil { + return Handler{}, err + } + + defaultSDKOpts := []sdk.Option{ + authSDKOpt, + sdk.WithConnectionValidation(), + sdk.WithLogger(slog.Default()), + } + if o.TLSNoVerify { + defaultSDKOpts = append(defaultSDKOpts, sdk.WithInsecureSkipVerifyConn()) + } + + if u.Scheme == "http" { + defaultSDKOpts = append(defaultSDKOpts, sdk.WithInsecurePlaintextConn()) + } + o.sdkOpts = append(defaultSDKOpts, o.sdkOpts...) + + s, err := sdk.New(u.String(), o.sdkOpts...) + if err != nil { + return Handler{}, err + } + + return Handler{ + sdk: s, + platformEndpoint: o.endpoint, + }, nil +} + +func (h Handler) Close() error { + return h.sdk.Close() +} + +func (h Handler) Direct() *sdk.SDK { + return h.sdk +} + +// Replace all labels in the metadata +func (h Handler) WithReplaceLabelsMetadata(metadata *common.MetadataMutable, labels map[string]string) func(*common.MetadataMutable) *common.MetadataMutable { + return func(*common.MetadataMutable) *common.MetadataMutable { + nextMetadata := &common.MetadataMutable{ + Labels: labels, + } + return nextMetadata + } +} + +// Append a label to the metadata +func (h Handler) WithLabelMetadata(metadata *common.MetadataMutable, key, value string) func(*common.MetadataMutable) *common.MetadataMutable { + return func(*common.MetadataMutable) *common.MetadataMutable { + labels := metadata.GetLabels() + labels[key] = value + nextMetadata := &common.MetadataMutable{ + Labels: labels, + } + return nextMetadata + } +} + +// func buildMetadata(metadata *common.MetadataMutable, fns ...func(*common.MetadataMutable) *common.MetadataMutable) *common.MetadataMutable { +// if metadata == nil { +// metadata = &common.MetadataMutable{} +// } +// if len(fns) == 0 { +// return metadata +// } +// for _, fn := range fns { +// metadata = fn(metadata) +// } +// return metadata +// } diff --git a/otdfctl/pkg/handlers/selectors.go b/otdfctl/pkg/handlers/selectors.go new file mode 100644 index 0000000000..70f3871eb6 --- /dev/null +++ b/otdfctl/pkg/handlers/selectors.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/golang-jwt/jwt/v5" + flat "github.com/opentdf/platform/lib/flattening" +) + +func ParseSubjectString(subject string) (map[string]interface{}, error) { + var value map[string]interface{} + //nolint:errcheck // if fails to unmarshal, may be a JWT, so swallow the error + json.Unmarshal([]byte(subject), &value) + + if value == nil { + token, _, err := new(jwt.Parser).ParseUnverified(subject, jwt.MapClaims{}) + if err != nil { + return nil, fmt.Errorf("failed to flatten subject [%v]: %w", subject, err) + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + value = claims + } else { + return nil, errors.New("failed to get claims from subject JWT token") + } + } + + if value == nil { + return nil, errors.New("invalid subject context type. Must be of type: [json, jwt]") + } + return value, nil +} + +func FlattenSubjectContext(subject string) ([]flat.Item, error) { + value, err := ParseSubjectString(subject) + if err != nil { + return nil, fmt.Errorf("failed to parse subject string into JSON or JWT [%s]: %w", subject, err) + } + + flattened, err := flat.Flatten(value) + if err != nil { + return nil, fmt.Errorf("failed to flatten subject [%v]: %w", subject, err) + } + + return flattened.Items, nil +} diff --git a/otdfctl/pkg/handlers/sort.go b/otdfctl/pkg/handlers/sort.go new file mode 100644 index 0000000000..c1bd2cc604 --- /dev/null +++ b/otdfctl/pkg/handlers/sort.go @@ -0,0 +1,87 @@ +package handlers + +import ( + "errors" + "fmt" + "sort" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" +) + +type SortOption struct { + Field string + Direction policy.SortDirection +} + +const ( + sortDirectionAsc = "asc" + sortDirectionDesc = "desc" +) + +var ( + ErrInvalidSortDirection = errors.New("invalid sort direction") + ErrInvalidSortField = errors.New("invalid sort field") +) + +func NewSortOption(field, order string) (SortOption, error) { + field = strings.ToLower(strings.TrimSpace(field)) + direction, err := ParseSortOrder(order) + if err != nil { + return SortOption{}, err + } + + return SortOption{ + Field: field, + Direction: direction, + }, nil +} + +func ParseSortOrder(value string) (policy.SortDirection, error) { + value = strings.TrimSpace(value) + if value == "" { + return policy.SortDirection_SORT_DIRECTION_UNSPECIFIED, nil + } + + switch strings.ToLower(value) { + case sortDirectionAsc: + return policy.SortDirection_SORT_DIRECTION_ASC, nil + case sortDirectionDesc: + return policy.SortDirection_SORT_DIRECTION_DESC, nil + default: + return policy.SortDirection_SORT_DIRECTION_UNSPECIFIED, errors.Join( + ErrInvalidSortDirection, + fmt.Errorf("%q must be asc or desc", value), + ) + } +} + +func (s SortOption) IsZero() bool { + return s.Field == "" && s.Direction == policy.SortDirection_SORT_DIRECTION_UNSPECIFIED +} + +func sortField[T any](resource string, option SortOption, allowed map[string]T) (T, error) { + var zero T + if option.Field == "" { + return zero, nil + } + + field, ok := allowed[option.Field] + if !ok { + return zero, invalidSortFieldError(resource, option.Field, allowed) + } + + return field, nil +} + +func invalidSortFieldError[T any](resource, field string, allowed map[string]T) error { + fields := make([]string, 0, len(allowed)) + for f := range allowed { + fields = append(fields, f) + } + sort.Strings(fields) + return errors.Join( + ErrInvalidSortField, + fmt.Errorf("%q is not a valid sort field for %s; valid fields: %s", field, resource, strings.Join(fields, ", ")), + ) +} diff --git a/otdfctl/pkg/handlers/sort_test.go b/otdfctl/pkg/handlers/sort_test.go new file mode 100644 index 0000000000..ad906f982a --- /dev/null +++ b/otdfctl/pkg/handlers/sort_test.go @@ -0,0 +1,103 @@ +package handlers + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewSortOption(t *testing.T) { + tests := []struct { + name string + field string + order string + expected SortOption + wantError error + }{ + { + name: "empty", + }, + { + name: "field only", + field: "name", + expected: SortOption{ + Field: "name", + Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED, + }, + }, + { + name: "ascending", + field: "created_at", + order: "asc", + expected: SortOption{ + Field: "created_at", + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + { + name: "descending with whitespace", + field: " updated_at ", + order: " DESC ", + expected: SortOption{ + Field: "updated_at", + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + { + name: "direction only", + order: "desc", + expected: SortOption{ + Field: "", + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + { + name: "invalid direction", + field: "name", + order: "up", + wantError: ErrInvalidSortDirection, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := NewSortOption(tt.field, tt.order) + if tt.wantError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.wantError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestSortField(t *testing.T) { + allowed := map[string]int{ + "name": 1, + "created_at": 2, + } + + t.Run("omitted field returns zero value", func(t *testing.T) { + field, err := sortField("test resources", SortOption{}, allowed) + require.NoError(t, err) + assert.Equal(t, 0, field) + }) + + t.Run("known field returns mapped value", func(t *testing.T) { + field, err := sortField("test resources", SortOption{Field: "name"}, allowed) + require.NoError(t, err) + assert.Equal(t, 1, field) + }) + + t.Run("unknown field returns valid fields", func(t *testing.T) { + field, err := sortField("test resources", SortOption{Field: "updated_at"}, allowed) + require.Error(t, err) + assert.Equal(t, 0, field) + require.ErrorIs(t, err, ErrInvalidSortField) + assert.EqualError(t, err, "invalid sort field\n\"updated_at\" is not a valid sort field for test resources; valid fields: created_at, name") + }) +} diff --git a/otdfctl/pkg/handlers/subjectConditionSets.go b/otdfctl/pkg/handlers/subjectConditionSets.go new file mode 100644 index 0000000000..847d517ab4 --- /dev/null +++ b/otdfctl/pkg/handlers/subjectConditionSets.go @@ -0,0 +1,87 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" +) + +func (h Handler) GetSubjectConditionSet(ctx context.Context, id string) (*policy.SubjectConditionSet, error) { + resp, err := h.sdk.SubjectMapping.GetSubjectConditionSet(ctx, &subjectmapping.GetSubjectConditionSetRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + + return resp.GetSubjectConditionSet(), nil +} + +func (h Handler) ListSubjectConditionSets(ctx context.Context, limit, offset int32, namespace string, sort SortOption) (*subjectmapping.ListSubjectConditionSetsResponse, error) { + req := &subjectmapping.ListSubjectConditionSetsRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + if !sort.IsZero() { + allowedFields := map[string]subjectmapping.SortSubjectConditionSetsType{ + "created_at": subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, + "updated_at": subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT, + } + field, err := sortField("subject condition sets", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*subjectmapping.SubjectConditionSetsSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.SubjectMapping.ListSubjectConditionSets(ctx, req) +} + +// Creates and returns the created subject condition set +func (h Handler) CreateSubjectConditionSet(ctx context.Context, ss []*policy.SubjectSet, metadata *common.MetadataMutable, namespace string) (*policy.SubjectConditionSet, error) { + req := &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: ss, + Metadata: metadata, + }, + } + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + resp, err := h.sdk.SubjectMapping.CreateSubjectConditionSet(ctx, req) + if err != nil { + return nil, err + } + return h.GetSubjectConditionSet(ctx, resp.GetSubjectConditionSet().GetId()) +} + +// Updates and returns the updated subject condition set +func (h Handler) UpdateSubjectConditionSet(ctx context.Context, id string, ss []*policy.SubjectSet, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.SubjectConditionSet, error) { + _, err := h.sdk.SubjectMapping.UpdateSubjectConditionSet(ctx, &subjectmapping.UpdateSubjectConditionSetRequest{ + Id: id, + SubjectSets: ss, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + return h.GetSubjectConditionSet(ctx, id) +} + +func (h Handler) DeleteSubjectConditionSet(ctx context.Context, id string) error { + _, err := h.sdk.SubjectMapping.DeleteSubjectConditionSet(ctx, &subjectmapping.DeleteSubjectConditionSetRequest{ + Id: id, + }) + return err +} + +func (h Handler) PruneSubjectConditionSets(ctx context.Context) ([]*policy.SubjectConditionSet, error) { + rsp, err := h.sdk.SubjectMapping.DeleteAllUnmappedSubjectConditionSets(ctx, &subjectmapping.DeleteAllUnmappedSubjectConditionSetsRequest{}) + if err != nil { + return nil, err + } + return rsp.GetSubjectConditionSets(), nil +} diff --git a/otdfctl/pkg/handlers/subjectmappings.go b/otdfctl/pkg/handlers/subjectmappings.go new file mode 100644 index 0000000000..6412b7b3df --- /dev/null +++ b/otdfctl/pkg/handlers/subjectmappings.go @@ -0,0 +1,129 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" +) + +const ( + SubjectMappingOperatorIn = "IN" + SubjectMappingOperatorNotIn = "NOT_IN" + SubjectMappingOperatorInContains = "IN_CONTAINS" + SubjectMappingOperatorUnspecified = "UNSPECIFIED" +) + +var SubjectMappingOperatorEnumChoices = []string{SubjectMappingOperatorIn, SubjectMappingOperatorNotIn, SubjectMappingOperatorUnspecified} + +func (h Handler) GetSubjectMapping(ctx context.Context, id string) (*policy.SubjectMapping, error) { + resp, err := h.sdk.SubjectMapping.GetSubjectMapping(ctx, &subjectmapping.GetSubjectMappingRequest{ + Id: id, + }) + return resp.GetSubjectMapping(), err +} + +func (h Handler) ListSubjectMappings(ctx context.Context, limit, offset int32, namespace string, sort SortOption) (*subjectmapping.ListSubjectMappingsResponse, error) { + req := &subjectmapping.ListSubjectMappingsRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + } + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + if !sort.IsZero() { + allowedFields := map[string]subjectmapping.SortSubjectMappingsType{ + "created_at": subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, + "updated_at": subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT, + } + field, err := sortField("subject mappings", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*subjectmapping.SubjectMappingsSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.SubjectMapping.ListSubjectMappings(ctx, req) +} + +// Creates and returns the created subject mapping +func (h Handler) CreateNewSubjectMapping(ctx context.Context, attrValID string, actions []*policy.Action, existingSCSId string, newScs *subjectmapping.SubjectConditionSetCreate, m *common.MetadataMutable, namespace string) (*policy.SubjectMapping, error) { + req := &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: actions, + ExistingSubjectConditionSetId: existingSCSId, + NewSubjectConditionSet: newScs, + Metadata: m, + } + req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + resp, err := h.sdk.SubjectMapping.CreateSubjectMapping(ctx, req) + if err != nil { + return nil, err + } + return h.GetSubjectMapping(ctx, resp.GetSubjectMapping().GetId()) +} + +// Updates and returns the updated subject mapping +func (h Handler) UpdateSubjectMapping(ctx context.Context, id string, updatedSCSId string, updatedActions []*policy.Action, metadata *common.MetadataMutable, metadataBehavior common.MetadataUpdateEnum) (*policy.SubjectMapping, error) { + _, err := h.sdk.SubjectMapping.UpdateSubjectMapping(ctx, &subjectmapping.UpdateSubjectMappingRequest{ + Id: id, + SubjectConditionSetId: updatedSCSId, + Actions: updatedActions, + MetadataUpdateBehavior: metadataBehavior, + Metadata: metadata, + }) + if err != nil { + return nil, err + } + return h.GetSubjectMapping(ctx, id) +} + +func (h Handler) DeleteSubjectMapping(ctx context.Context, id string) (*policy.SubjectMapping, error) { + resp, err := h.sdk.SubjectMapping.DeleteSubjectMapping(ctx, &subjectmapping.DeleteSubjectMappingRequest{ + Id: id, + }) + return resp.GetSubjectMapping(), err +} + +func (h Handler) MatchSubjectMappings(ctx context.Context, selectors []string) ([]*policy.SubjectMapping, error) { + subjectProperties := make([]*policy.SubjectProperty, len(selectors)) + for i, selector := range selectors { + subjectProperties[i] = &policy.SubjectProperty{ + ExternalSelectorValue: selector, + } + } + resp, err := h.sdk.SubjectMapping.MatchSubjectMappings(ctx, &subjectmapping.MatchSubjectMappingsRequest{ + SubjectProperties: subjectProperties, + }) + return resp.GetSubjectMappings(), err +} + +func GetSubjectMappingOperatorFromChoice(readable string) policy.SubjectMappingOperatorEnum { + switch readable { + case SubjectMappingOperatorIn: + return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN + case SubjectMappingOperatorNotIn: + return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + case SubjectMappingOperatorInContains: + return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS + case SubjectMappingOperatorUnspecified: + return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + default: + return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED + } +} + +func GetSubjectMappingOperatorChoiceFromEnum(enum policy.SubjectMappingOperatorEnum) string { + switch enum { + case policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN: + return SubjectMappingOperatorIn + case policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN: + return SubjectMappingOperatorNotIn + case policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS: + return SubjectMappingOperatorInContains + case policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED: + return SubjectMappingOperatorUnspecified + default: + return SubjectMappingOperatorUnspecified + } +} diff --git a/otdfctl/pkg/handlers/tdf.go b/otdfctl/pkg/handlers/tdf.go new file mode 100644 index 0000000000..d8a5b0d69f --- /dev/null +++ b/otdfctl/pkg/handlers/tdf.go @@ -0,0 +1,278 @@ +package handlers + +import ( + "bytes" + "context" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io" + "log/slog" + "strings" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/otdfctl/pkg/tdf" + "github.com/opentdf/platform/otdfctl/pkg/utils" + "github.com/opentdf/platform/sdk" +) + +var ( + ErrTDFInspectFailNotValidTDF = errors.New("file or input is not a valid TDF") + ErrTDFInspectFailNotInspectable = errors.New("file or input is not inspectable") + ErrTDFUnableToReadAttributes = errors.New("unable to read attributes from TDF") + ErrTDFUnableToReadUnencryptedMetadata = errors.New("unable to read unencrypted metadata from TDF") + ErrTDFUnableToReadAssertions = errors.New("unable to read assertions") + ErrTDFUnableToReadAssertionVerificationKeys = errors.New("unable to read assertion verification keys") +) + +const ( + MaxAssertionsFileSize = int64(5 * 1024 * 1024) // 5MB +) + +type TDFInspect struct { + ZTDFManifest *sdk.Manifest + Attributes []string + UnencryptedMetadata []byte +} + +func (h Handler) EncryptBytes( + tdfType string, + unencrypted []byte, + attrValues []string, + mimeType string, + kasURLPath string, + assertions string, + wrappingKeyAlgorithm ocrypto.KeyType, + targetMode string, +) (*bytes.Buffer, error) { + var encrypted []byte + enc := bytes.NewBuffer(encrypted) + + switch tdfType { + // Encrypt the data as a ZTDF + case "", tdf.TypeTDF3, tdf.TypeZTDF: + opts := []sdk.TDFOption{ + sdk.WithDataAttributes(attrValues...), + sdk.WithKasInformation(sdk.KASInfo{ + URL: h.platformEndpoint + kasURLPath, + }), + sdk.WithMimeType(mimeType), + sdk.WithWrappingKeyAlg(wrappingKeyAlgorithm), //nolint:staticcheck // SDK option is deprecated but no replacement is available in this SDK version. + } + + var assertionConfigs []sdk.AssertionConfig + //nolint:nestif // nested its mainly for error catching and handling case of string vs file + if assertions != "" { + err := json.Unmarshal([]byte(assertions), &assertionConfigs) + if err != nil { + // if unable to marshal to json, interpret as file string and try to read from file + assertionBytes, err := utils.ReadBytesFromFile(assertions, MaxAssertionsFileSize) + if err != nil { + return nil, fmt.Errorf("unable to read assertions file: %w", err) + } + err = json.Unmarshal(assertionBytes, &assertionConfigs) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal assertions json: %w", err) + } + } + for i, config := range assertionConfigs { + if !config.SigningKey.IsEmpty() { + correctedKey, err := correctKeyType(config.SigningKey, false) + if err != nil { + return nil, fmt.Errorf("error with assertion signing key: %w", err) + } + assertionConfigs[i].SigningKey.Key = correctedKey + } + } + opts = append(opts, sdk.WithAssertions(assertionConfigs...)) + } + + if targetMode != "" { + opts = append(opts, sdk.WithTargetMode(targetMode)) + } + + _, err := h.sdk.CreateTDF(enc, bytes.NewReader(unencrypted), opts...) + return enc, err + default: + return nil, errors.New("unknown TDF type") + } +} + +func (h Handler) DecryptBytes( + ctx context.Context, + toDecrypt []byte, + assertionVerificationKeysFile string, + disableAssertionCheck bool, + sessionKeyAlgorithm ocrypto.KeyType, + kasAllowList []string, + ignoreAllowlist bool, + fulfillableObligations []string, +) (*bytes.Buffer, error) { + out := &bytes.Buffer{} + pt := io.Writer(out) + ec := bytes.NewReader(toDecrypt) + switch sdk.GetTdfType(ec) { + case sdk.Standard: + opts := []sdk.TDFReaderOption{ + sdk.WithDisableAssertionVerification(disableAssertionCheck), + sdk.WithSessionKeyType(sessionKeyAlgorithm), + sdk.WithIgnoreAllowlist(ignoreAllowlist), + sdk.WithTDFFulfillableObligationFQNs(fulfillableObligations), + } + if kasAllowList != nil { + opts = append(opts, sdk.WithKasAllowlist(kasAllowList)) + } + var assertionVerificationKeys sdk.AssertionVerificationKeys + if assertionVerificationKeysFile != "" { + // read the file + assertionVerificationBytes, err := utils.ReadBytesFromFile(assertionVerificationKeysFile, MaxAssertionsFileSize) + if err != nil { + return nil, fmt.Errorf("unable to read assertions verification keys file: %w", err) + } + err = json.Unmarshal(assertionVerificationBytes, &assertionVerificationKeys) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal assertion verification keys json: %w", err) + } + for assertionName, key := range assertionVerificationKeys.Keys { + correctedKey, err := correctKeyType(key, true) + if err != nil { + return nil, fmt.Errorf("error with assertion signing key: %w", err) + } + assertionVerificationKeys.Keys[assertionName] = sdk.AssertionKey{Alg: key.Alg, Key: correctedKey} + } + opts = append(opts, sdk.WithAssertionVerificationKeys(assertionVerificationKeys)) + } + r, err := h.sdk.LoadTDF(ec, opts...) + if err != nil { + return nil, err + } + //nolint:errorlint // callers intended to test error equality directly + if _, err = io.Copy(pt, r); err != nil && err != io.EOF { + return nil, formatDecryptError(ctx, r.Obligations, err) + } + case sdk.Invalid: + return nil, errors.New("invalid TDF") + default: + return nil, errors.New("unknown TDF type") + } + return out, nil +} + +func (h Handler) InspectTDF(toInspect []byte) (TDFInspect, []error) { + b := bytes.NewReader(toInspect) + switch sdk.GetTdfType(b) { + case sdk.Standard: + // grouping errors so we don't impact the piping of the data + errs := []error{} + + tdfreader, err := h.sdk.LoadTDF(bytes.NewReader(toInspect)) + if err != nil { + if strings.Contains(err.Error(), "zip: not a valid zip file") { + return TDFInspect{}, []error{ErrTDFInspectFailNotInspectable} + } + return TDFInspect{}, []error{errors.Join(ErrTDFInspectFailNotValidTDF, err)} + } + + attributes, err := tdfreader.DataAttributes() + if err != nil { + errs = append(errs, errors.Join(ErrTDFUnableToReadAttributes, err)) + } + + unencryptedMetadata, err := tdfreader.UnencryptedMetadata() + if err != nil { + errs = append(errs, errors.Join(ErrTDFUnableToReadUnencryptedMetadata, err)) + } + + m := tdfreader.Manifest() + return TDFInspect{ + ZTDFManifest: &m, + Attributes: attributes, + UnencryptedMetadata: unencryptedMetadata, + }, errs + case sdk.Invalid: + return TDFInspect{}, []error{ErrTDFInspectFailNotValidTDF} + default: + return TDFInspect{}, []error{errors.New("tdf format unrecognized")} + } +} + +func correctKeyType(assertionKey sdk.AssertionKey, public bool) (interface{}, error) { + strKey, ok := assertionKey.Key.(string) + if !ok { + return nil, errors.New("unable to convert assertion key to string") + } + + switch assertionKey.Alg { + case sdk.AssertionKeyAlgHS256: + // convert the hs256 key to []byte + return []byte(strKey), nil + case sdk.AssertionKeyAlgRS256: + // Decode the PEM block + block, _ := pem.Decode([]byte(strKey)) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + + // Check the block type and parse accordingly + var privateKey *rsa.PrivateKey + var publicKey *rsa.PublicKey + var err error + switch block.Type { + case "RSA PRIVATE KEY": + privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + publicKey = &privateKey.PublicKey + case "PRIVATE KEY": + parsedKey, parseErr := x509.ParsePKCS8PrivateKey(block.Bytes) + if parseErr != nil { + return nil, fmt.Errorf("failed to parse PKCS#8 private key: %w", parseErr) + } + privateKey, ok = parsedKey.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("parsed key is not an RSA private key") + } + publicKey = &privateKey.PublicKey + case "RSA PUBLIC KEY": + publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes) + case "PUBLIC KEY": + parsedKey, parseErr := x509.ParsePKIXPublicKey(block.Bytes) + if parseErr != nil { + return nil, fmt.Errorf("failed to parse PKIX public key: %w", parseErr) + } + publicKey, ok = parsedKey.(*rsa.PublicKey) + if !ok { + return nil, errors.New("parsed key is not an RSA public key") + } + default: + return nil, fmt.Errorf("unsupported key type: %s", block.Type) + } + + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + if public { + return publicKey, nil + } + return privateKey, nil + } + return nil, fmt.Errorf("unsupported signing key alg: %v", assertionKey.Alg) +} + +func formatDecryptError(ctx context.Context, getObligations func(ctx context.Context) (sdk.RequiredObligations, error), err error) error { + // Avoid calling Rewrap again, if the error is a 500 error from KAS + if errors.Is(err, sdk.ErrRewrapForbidden) { + obligations, oblErr := getObligations(ctx) + if oblErr != nil { + slog.DebugContext(ctx, "failed to get obligations after decrypt, obligations must not be cached", + slog.Any("error", oblErr), + ) + } + + if len(obligations.FQNs) > 0 { + err = errors.Join(err, fmt.Errorf("\nrequired obligations: %v", obligations.FQNs)) + } + } + return err +} diff --git a/otdfctl/pkg/man/docflags.go b/otdfctl/pkg/man/docflags.go new file mode 100644 index 0000000000..1b1b5b6b09 --- /dev/null +++ b/otdfctl/pkg/man/docflags.go @@ -0,0 +1,50 @@ +package man + +import ( + "fmt" + + "github.com/opentdf/platform/otdfctl/pkg/cli" +) + +// SensitiveAnnotationKey is the pflag annotation key used to mark flags whose +// values contain secrets (cryptographic keys, tokens, etc.) and must not appear +// in logs or process listings. +const SensitiveAnnotationKey = "sensitive" + +type DocFlag struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Shorthand string `yaml:"shorthand"` + Default string `yaml:"default"` + Enum []string `yaml:"enum"` + Sensitive bool `yaml:"sensitive"` +} + +func (d *Doc) GetDocFlag(name string) DocFlag { + for _, f := range d.DocFlags { + if f.Name == name { + if len(f.Enum) > 0 { + f.Description = fmt.Sprintf("%s %s", f.Description, cli.CommaSeparated(f.Enum)) + } + return f + } + } + panic(fmt.Sprintf("No doc flag found for name, %s for command %s", name, d.Use)) +} + +func (f DocFlag) DefaultAsBool() bool { + return f.Default == "true" +} + +// MarkSensitiveFlags sets pflag annotations on all flags in the command's +// FlagSet that are marked sensitive in the doc metadata. Call after all +// flags have been registered. +func (d *Doc) MarkSensitiveFlags() { + for _, df := range d.DocFlags { + if df.Sensitive { + if err := d.Flags().SetAnnotation(df.Name, SensitiveAnnotationKey, []string{"true"}); err != nil { + panic(fmt.Sprintf("failed to mark flag %q as sensitive for command %q: %v", df.Name, d.Use, err)) + } + } + } +} diff --git a/otdfctl/pkg/man/docflags_test.go b/otdfctl/pkg/man/docflags_test.go new file mode 100644 index 0000000000..0605817961 --- /dev/null +++ b/otdfctl/pkg/man/docflags_test.go @@ -0,0 +1,78 @@ +package man + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDocFlagSensitiveParsing(t *testing.T) { + doc, err := ProcessDoc(`--- +title: Test Command +command: + name: test + flags: + - name: wrapping-key + sensitive: true + description: A sensitive flag + - name: algorithm + description: A non-sensitive flag +--- + +Test doc body. +`) + require.NoError(t, err) + require.Len(t, doc.DocFlags, 2) + + wk := doc.GetDocFlag("wrapping-key") + assert.True(t, wk.Sensitive) + + alg := doc.GetDocFlag("algorithm") + assert.False(t, alg.Sensitive) +} + +func TestMarkSensitiveFlags(t *testing.T) { + doc, err := ProcessDoc(`--- +title: Test Command +command: + name: test + flags: + - name: wrapping-key + sensitive: true + description: Sensitive + - name: name + description: Not sensitive +--- + +Body. +`) + require.NoError(t, err) + + doc.Flags().String("wrapping-key", "", "Sensitive") + doc.Flags().String("name", "", "Not sensitive") + + doc.MarkSensitiveFlags() + + wkFlag := doc.Flags().Lookup("wrapping-key") + require.NotNil(t, wkFlag) + assert.Equal(t, []string{"true"}, wkFlag.Annotations[SensitiveAnnotationKey]) + + nameFlag := doc.Flags().Lookup("name") + require.NotNil(t, nameFlag) + assert.Nil(t, nameFlag.Annotations) +} + +func TestMarkSensitiveFlagsPanicsOnUnregistered(t *testing.T) { + doc := &Doc{ + Command: cobra.Command{Use: "test"}, + DocFlags: []DocFlag{ + {Name: "missing-flag", Sensitive: true}, + }, + } + + assert.Panics(t, func() { + doc.MarkSensitiveFlags() + }) +} diff --git a/otdfctl/pkg/man/man.go b/otdfctl/pkg/man/man.go new file mode 100644 index 0000000000..0039c4b216 --- /dev/null +++ b/otdfctl/pkg/man/man.go @@ -0,0 +1,274 @@ +package man + +import ( + "embed" + "errors" + "fmt" + "io/fs" + "log/slog" + "strings" + + "github.com/adrg/frontmatter" + docsEmbed "github.com/opentdf/platform/otdfctl/docs" + "github.com/spf13/cobra" +) + +var Docs Manual + +type CommandOpts func(d *Doc) + +type Doc struct { + cobra.Command + DocFlags []DocFlag + DocSubcommands []*Doc +} + +// deprecated +func (d *Doc) GetShort(subCmds []string) string { + return fmt.Sprintf("%s [%s]", d.Short, strings.Join(subCmds, ", ")) +} + +func (d *Doc) AddSubcommands(subCmds ...*Doc) { + cmds := make([]string, 0) + for _, c := range subCmds { + cmds = append(cmds, c.Use) + d.DocSubcommands = append(d.DocSubcommands, c) + d.AddCommand(&c.Command) + } + d.Short = d.GetShort(cmds) +} + +func WithSubcommands(subCmds ...*Doc) CommandOpts { + return func(d *Doc) { + for _, c := range subCmds { + d.DocSubcommands = append(d.DocSubcommands, c) + d.AddCommand(&c.Command) + } + } +} + +func WithRun(f func(cmd *cobra.Command, args []string)) CommandOpts { + return func(d *Doc) { + d.Run = f + } +} + +// Hide any global or persisent flags from parent commands on the given command +func WithHiddenFlags(flags ...string) CommandOpts { + return func(d *Doc) { + // to hide root global flags, must set a custom help func that hides then calls the parent help func + d.SetHelpFunc(func(command *cobra.Command, strings []string) { + for _, f := range flags { + //nolint:errcheck // hidden flag err is not a concern + command.Flags().MarkHidden(f) + } + d.Parent().HelpFunc()(command, strings) + }) + } +} + +type Manual struct { + lang string + Docs map[string]*Doc + En map[string]*Doc + Fr map[string]*Doc +} + +func (m *Manual) SetLang(l string) { + switch l { + case "en", "fr": + m.lang = l + default: + panic("Unknown language: " + l) + } +} + +func (m Manual) GetDoc(cmd string) *Doc { + if m.lang != "en" { + //nolint:gocritic // other languages may be supported + switch m.lang { + case "fr": + if _, ok := m.Fr[cmd]; ok { + return m.Fr[cmd] + } + // if no doc found in french, fallback to english + slog.Debug("no doc found for cmd, falling back to english", + slog.String("cmd", cmd), + slog.String("lang", m.lang), + ) + } + } + + if _, ok := m.En[cmd]; !ok { + panic("No doc found for cmd, " + cmd) + } + + return m.En[cmd] +} + +func (m Manual) GetCommand(cmd string, opts ...CommandOpts) *Doc { + d := m.GetDoc(cmd) + + for _, opt := range opts { + opt(d) + } + + if len(d.DocSubcommands) > 0 { + s := make([]string, 0) + for _, c := range d.DocSubcommands { + s = append(s, c.Use) + } + d.Short = d.GetShort(s) + } + + return d +} + +//nolint:mnd,gocritic // allow file separator counts to be hardcoded +func ProcessEmbeddedDocs(manFiles embed.FS) { + err := fs.WalkDir(manFiles, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + + // extract language from filename + p := strings.Split(d.Name(), ".") + cmd := p[0] + lang := "en" + + // check if file is a markdown file + if p[len(p)-1] != "md" { + return nil + } else if len(p) < 2 || len(p) > 3 { + return nil + } else if len(p) == 3 { + lang = p[1] + } + + // remove extension and extract command from path + p = strings.Split(path, "/") + // remove leading and trailing slashes + p = p[1 : len(p)-1] + // if the last element is not _index, it is a subcommand + if cmd != "_index" { + p = append(p, cmd) + } + cmd = strings.Join(p, "/") + + if cmd == "" { + cmd = "" + } + + slog.Debug("found doc", + slog.String("cmd", cmd), + slog.String("lang", lang), + ) + c, err := manFiles.ReadFile(path) + if err != nil { + return fmt.Errorf("could not read file, %s: %s ", path, err.Error()) + } + + doc, err := ProcessDoc(string(c)) + if err != nil { + return fmt.Errorf("could not process doc, %s: %s", path, err.Error()) + } + + slog.Debug("adding doc", + slog.String("cmd", cmd), + slog.String("lang", lang), + ) + switch lang { + case "fr": + Docs.Fr[cmd] = doc + case "en": + Docs.En[cmd] = doc + default: + + return fmt.Errorf("unknown language [%s]", lang) + } + return nil + }) + if err != nil { + panic("Could not read embedded files: " + err.Error()) + } +} + +func init() { + slog.Debug("loading docs from embed") + Docs = Manual{ + Docs: make(map[string]*Doc), + En: make(map[string]*Doc), + Fr: make(map[string]*Doc), + } + + ProcessEmbeddedDocs(docsEmbed.ManFiles) +} + +func ProcessDoc(doc string) (*Doc, error) { + if len(doc) == 0 { + return nil, errors.New("empty document") + } + var matter struct { + Title string `yaml:"title"` + Command struct { + Name string `yaml:"name"` + Args []string `yaml:"arguments"` + ArbitraryArgs []string `yaml:"arbitraryArgs"` + Hidden bool `yaml:"hidden"` + Aliases []string `yaml:"aliases"` + Flags []DocFlag `yaml:"flags"` + } `yaml:"command"` + } + rest, err := frontmatter.Parse(strings.NewReader(doc), &matter) + if err != nil { + return nil, err + } + + c := matter.Command + + if c.Name == "" { + return nil, errors.New("required 'command' property") + } + + long := "# " + matter.Title + "\n\n" + strings.TrimSpace(string(rest)) + + var args cobra.PositionalArgs + switch { + case len(c.Args) > 0 && len(c.ArbitraryArgs) > 0: + args = cobra.MinimumNArgs(len(c.Args)) + case len(c.Args) > 0: + args = cobra.ExactArgs(len(c.Args)) + case len(c.ArbitraryArgs) > 0: + args = cobra.ArbitraryArgs + } + + d := Doc{ + cobra.Command{ + Use: buildUseString(c.Name, c.Args, c.ArbitraryArgs), + Args: args, + Hidden: c.Hidden, + Aliases: c.Aliases, + Short: matter.Title, + Long: styleDoc(long), + }, + c.Flags, + nil, + } + + return &d, nil +} + +func buildUseString(name string, args, arbitraryArgs []string) string { + parts := make([]string, 0, 1+len(args)+len(arbitraryArgs)) + parts = append(parts, name) + for _, a := range args { + parts = append(parts, "<"+a+">") + } + for _, a := range arbitraryArgs { + parts = append(parts, "["+a+"]") + } + return strings.Join(parts, " ") +} diff --git a/otdfctl/pkg/man/man_test.go b/otdfctl/pkg/man/man_test.go new file mode 100644 index 0000000000..023b04fbe0 --- /dev/null +++ b/otdfctl/pkg/man/man_test.go @@ -0,0 +1,135 @@ +package man + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProcessDocNoArgs(t *testing.T) { + doc, err := ProcessDoc(`--- +title: List namespaces +command: + name: list +--- + +List all namespaces. +`) + require.NoError(t, err) + assert.Equal(t, "list", doc.Use) +} + +func TestProcessDocWithArgs(t *testing.T) { + doc, err := ProcessDoc(`--- +title: Get a resource +command: + name: get + arguments: + - resource-id +--- + +Get a resource by ID. +`) + require.NoError(t, err) + assert.Equal(t, "get ", doc.Use) +} + +func TestProcessDocWithArbitraryArgs(t *testing.T) { + doc, err := ProcessDoc(`--- +title: Do something +command: + name: do + arbitraryArgs: + - optional-arg +--- + +Do something optionally. +`) + require.NoError(t, err) + assert.Equal(t, "do [optional-arg]", doc.Use) +} + +func TestProcessDocWithBothArgTypes(t *testing.T) { + doc, err := ProcessDoc(`--- +title: Authenticate with client credentials +command: + name: client-credentials + arguments: + - client-id + arbitraryArgs: + - client-secret +--- + +Authenticate via client credentials flow. +`) + require.NoError(t, err) + assert.Equal(t, "client-credentials [client-secret]", doc.Use) +} + +func TestProcessDocWithBothArgTypesValidator(t *testing.T) { + doc, err := ProcessDoc(`--- +title: Authenticate with client credentials +command: + name: client-credentials + arguments: + - client-id + arbitraryArgs: + - client-secret +--- + +Authenticate via client credentials flow. +`) + require.NoError(t, err) + require.NoError(t, doc.Args(&doc.Command, []string{"id"})) + require.NoError(t, doc.Args(&doc.Command, []string{"id", "secret"})) + require.Error(t, doc.Args(&doc.Command, []string{})) +} + +func TestBuildUseString(t *testing.T) { + tests := []struct { + name string + cmdName string + args []string + arbitraryArgs []string + want string + }{ + { + name: "name only", + cmdName: "list", + want: "list", + }, + { + name: "with required args", + cmdName: "get", + args: []string{"id"}, + want: "get ", + }, + { + name: "with optional args", + cmdName: "run", + arbitraryArgs: []string{"extra"}, + want: "run [extra]", + }, + { + name: "with both", + cmdName: "auth", + args: []string{"client-id"}, + arbitraryArgs: []string{"client-secret"}, + want: "auth [client-secret]", + }, + { + name: "multiple required", + cmdName: "copy", + args: []string{"src", "dst"}, + want: "copy ", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildUseString(tt.cmdName, tt.args, tt.arbitraryArgs) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/otdfctl/pkg/man/style.go b/otdfctl/pkg/man/style.go new file mode 100644 index 0000000000..fcf35eaafe --- /dev/null +++ b/otdfctl/pkg/man/style.go @@ -0,0 +1,50 @@ +//nolint:mnd // styling is magic +package man + +import ( + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/glamour/ansi" + "github.com/charmbracelet/glamour/styles" + "golang.org/x/term" +) + +var ( + termWidthDefault = 80 + termWidthWide = 120 +) + +func styleDoc(doc string) string { + w, _, err := term.GetSize(0) + if err != nil { + w = termWidthDefault + } + if w > termWidthWide { + w = termWidthWide + } + // Set up a new glamour instance + // with some options + ds := styles.DarkStyleConfig + // ls := glamour.DefaultStyles["light"] + + ds.Document.Margin = uintPtr(0) + ds.Paragraph.Margin = uintPtr(2) + // Capitalize headers + ds.H1.StylePrimitive = ansi.StylePrimitive{ + Color: stringPtr("#F1F1F1"), + Format: "# {{.text}}", + } + r, _ := glamour.NewTermRenderer( + // glamour.WithAutoStyle(), + glamour.WithStyles(ds), + glamour.WithWordWrap(w), + glamour.WithPreservedNewLines(), + ) + + // Render the content + out, _ := r.Render(doc) + + return out +} + +func stringPtr(s string) *string { return &s } +func uintPtr(u uint) *uint { return &u } diff --git a/otdfctl/pkg/profiles/errors.go b/otdfctl/pkg/profiles/errors.go new file mode 100644 index 0000000000..8bbc90bad8 --- /dev/null +++ b/otdfctl/pkg/profiles/errors.go @@ -0,0 +1,14 @@ +package profiles + +import "errors" + +var ( + ErrDeletingDefaultProfile = errors.New("cannot delete the default profile") + ErrProfileIsEmpty = errors.New("error profile is empty") + ErrProfileIncorrectType = errors.New("error profile is not of type ProfileConfig") + ErrCreatingPlatform = errors.New("error when creating platform") + ErrCreatingNewProfile = errors.New("error creating profile") + ErrUnknownProfileDriverType = errors.New("error unknown profile driver type") + ErrCleaningUpProfiles = errors.New("error occurred when cleaning up profiles") + ErrProfileConfigEmpty = errors.New("error profile configuration cannot be empty") +) diff --git a/otdfctl/pkg/profiles/profile.go b/otdfctl/pkg/profiles/profile.go new file mode 100644 index 0000000000..8fa06cc0b9 --- /dev/null +++ b/otdfctl/pkg/profiles/profile.go @@ -0,0 +1,133 @@ +package profiles + +import ( + "errors" + "log/slog" + "runtime" + "strings" + + osprofiles "github.com/jrschumacher/go-osprofiles" + osplatform "github.com/jrschumacher/go-osprofiles/pkg/platform" + "github.com/opentdf/platform/otdfctl/pkg/config" +) + +type ProfileDriver string + +const ( + ProfileDriverKeyring ProfileDriver = "keyring" + ProfileDriverMemory ProfileDriver = "in-memory" + ProfileDriverFileSystem ProfileDriver = "filesystem" + ProfileDriverUnknown ProfileDriver = "unknown" + ProfileDriverDefault = ProfileDriverFileSystem +) + +func newFileStoreProfiler() (*osprofiles.Profiler, error) { + platform, err := osplatform.NewPlatform(config.ServicePublisher, config.AppName, runtime.GOOS) + if err != nil { + return nil, errors.Join(ErrCreatingPlatform, err) + } + profiler, err := osprofiles.New(config.AppName, osprofiles.WithFileStore(platform.UserAppConfigDirectory())) + if err != nil { + return nil, errors.Join(ErrCreatingNewProfile, err) + } + return profiler, nil +} + +func NewProfiler(store string) (*osprofiles.Profiler, error) { + driverType, err := ToProfileDriver(store) + if err != nil { + return nil, err + } + + return CreateProfiler(driverType) +} + +func ToProfileDriver(driverType string) (ProfileDriver, error) { + normalizedType := strings.ToLower(strings.TrimSpace(driverType)) + switch normalizedType { + case string(ProfileDriverMemory): + return ProfileDriverMemory, nil + case string(ProfileDriverKeyring): + return ProfileDriverKeyring, nil + case string(ProfileDriverFileSystem): + return ProfileDriverFileSystem, nil + case string(ProfileDriverUnknown): + fallthrough + default: + return ProfileDriverUnknown, ErrUnknownProfileDriverType + } +} + +func CreateProfiler(driverType ProfileDriver) (*osprofiles.Profiler, error) { + switch driverType { + case ProfileDriverMemory: + return osprofiles.New(config.AppName, osprofiles.WithInMemoryStore()) + case ProfileDriverKeyring: + return osprofiles.New(config.AppName, osprofiles.WithKeyringStore()) + case ProfileDriverFileSystem: + return newFileStoreProfiler() + case ProfileDriverUnknown: + fallthrough + default: + return nil, ErrUnknownProfileDriverType + } +} + +func Migrate(to ProfileDriver, from ProfileDriver) error { + fromProfiler, err := CreateProfiler(from) + if err != nil { + return err + } + + toProfiler, err := CreateProfiler(to) + if err != nil { + return err + } + + profilesToMigrate := osprofiles.ListProfiles(fromProfiler) + if len(profilesToMigrate) == 0 { + return nil + } + + defaultProfileBeingMigrated := osprofiles.GetGlobalConfig(fromProfiler).GetDefaultProfile() + + slog.Debug("migrating profiles", + slog.Any("count", len(profilesToMigrate)), + slog.Any("from", string(from)), + slog.Any("to", string(to)), + ) + + for _, profileName := range profilesToMigrate { + store, err := osprofiles.GetProfile[*ProfileConfig](fromProfiler, profileName) + if err != nil { + return err + } + + p, ok := store.Profile.(*ProfileConfig) + if !ok || p == nil { + return ErrProfileIncorrectType + } + + setDefault := profileName == defaultProfileBeingMigrated + + if err := toProfiler.AddProfile(p, setDefault); err != nil { + return err + } + + slog.Debug("migrated profile", + slog.String("profile", profileName), + slog.Bool("set_default", setDefault), + ) + } + + slog.Debug("removing profiles", + slog.String("from", string(from)), + slog.Any("count", len(profilesToMigrate)), + ) + if err = fromProfiler.Cleanup(false); err != nil { + return errors.Join(ErrCleaningUpProfiles, err) + } + + slog.Debug("migration complete") + return nil +} diff --git a/otdfctl/pkg/profiles/profileAuthCreds.go b/otdfctl/pkg/profiles/profileAuthCreds.go new file mode 100644 index 0000000000..10db5b9827 --- /dev/null +++ b/otdfctl/pkg/profiles/profileAuthCreds.go @@ -0,0 +1,31 @@ +package profiles + +const ( + AuthTypeClientCredentials = "client-credentials" + AuthTypeAccessToken = "access-token" +) + +type AuthCredentials struct { + AuthType string `json:"authType"` + ClientID string `json:"clientId"` + // Used for client credentials + ClientSecret string `json:"clientSecret,omitempty"` + Scopes []string `json:"scopes,omitempty"` + AccessToken AuthCredentialsAccessToken `json:"accessToken,omitempty"` +} + +type AuthCredentialsAccessToken struct { + ClientID string `json:"clientId"` + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + Expiration int64 `json:"expiration"` +} + +func (p *OtdfctlProfileStore) GetAuthCredentials() AuthCredentials { + return p.config.AuthCredentials +} + +func (p *OtdfctlProfileStore) SetAuthCredentials(authCredentials AuthCredentials) error { + p.config.AuthCredentials = authCredentials + return p.store.Save() +} diff --git a/otdfctl/pkg/profiles/profileConfig.go b/otdfctl/pkg/profiles/profileConfig.go new file mode 100644 index 0000000000..c07e959383 --- /dev/null +++ b/otdfctl/pkg/profiles/profileConfig.go @@ -0,0 +1,167 @@ +package profiles + +import ( + "errors" + "strings" + + osprofiles "github.com/jrschumacher/go-osprofiles" + "github.com/opentdf/platform/otdfctl/pkg/utils" +) + +const ( + OutputJSON = "json" + OutputStyled = "styled" +) + +type OtdfctlProfileStore struct { + store osprofiles.ProfileStore + config *ProfileConfig // Pointer to the store.Profile field + profiler *osprofiles.Profiler +} + +type ProfileConfig struct { + Name string `json:"profile"` + Endpoint string `json:"endpoint"` + TLSNoVerify bool `json:"tlsNoVerify"` + OutputFormat string `json:"outputFormat,omitempty"` + AuthCredentials AuthCredentials `json:"authCredentials"` +} + +func (pc *ProfileConfig) GetName() string { + return pc.Name +} + +func NewOtdfctlProfileStore(storeType ProfileDriver, cfg *ProfileConfig, setDefault bool) (*OtdfctlProfileStore, error) { + if cfg == nil { + return nil, ErrProfileConfigEmpty + } + + profiler, err := CreateProfiler(storeType) + if err != nil { + return nil, err + } + + u, err := utils.NormalizeEndpoint(cfg.Endpoint) + if err != nil { + return nil, err + } + + p := &ProfileConfig{ + Name: cfg.Name, + Endpoint: u.String(), + TLSNoVerify: cfg.TLSNoVerify, + OutputFormat: NormalizeOutputFormat(cfg.OutputFormat), + } + err = profiler.AddProfile(p, setDefault) + if err != nil { + return nil, err + } + + store, err := osprofiles.UseProfile[*ProfileConfig](profiler, cfg.Name) + if err != nil { + return nil, err + } + + // Cast Profile to ProfileConfig + pc, ok := store.Profile.(*ProfileConfig) + if !ok { + return nil, errors.Join(ErrProfileIncorrectType, err) + } + + return newProfileStore(profiler, store, pc), nil +} + +func LoadOtdfctlProfileStore(storeType ProfileDriver, profileName string) (*OtdfctlProfileStore, error) { + profiler, err := CreateProfiler(storeType) + if err != nil { + return nil, err + } + + store, err := osprofiles.GetProfile[*ProfileConfig](profiler, profileName) + if err != nil { + return nil, err + } + + pc, ok := store.Profile.(*ProfileConfig) + if !ok { + return nil, errors.Join(ErrProfileIncorrectType, err) + } + + return newProfileStore(profiler, store, pc), nil +} + +func newProfileStore(profiler *osprofiles.Profiler, store *osprofiles.ProfileStore, pc *ProfileConfig) *OtdfctlProfileStore { + ensureProfileDefaults(pc) + return &OtdfctlProfileStore{ + store: *store, + config: pc, + profiler: profiler, + } +} + +func ensureProfileDefaults(pc *ProfileConfig) { + if pc == nil { + return + } + pc.OutputFormat = NormalizeOutputFormat(pc.OutputFormat) +} + +func (p *OtdfctlProfileStore) GetEndpoint() string { + return p.config.Endpoint +} + +func (p *OtdfctlProfileStore) SetEndpoint(endpoint string) error { + u, err := utils.NormalizeEndpoint(endpoint) + if err != nil { + return err + } + + p.config.Endpoint = u.String() + return p.store.Save() +} + +func (p *OtdfctlProfileStore) GetTLSNoVerify() bool { + return p.config.TLSNoVerify +} + +func (p *OtdfctlProfileStore) SetTLSNoVerify(tlsNoVerify bool) error { + p.config.TLSNoVerify = tlsNoVerify + return p.store.Save() +} + +func (p *OtdfctlProfileStore) GetOutputFormat() string { + return NormalizeOutputFormat(p.config.OutputFormat) +} + +func (p *OtdfctlProfileStore) SetOutputFormat(format string) error { + p.config.OutputFormat = NormalizeOutputFormat(format) + return p.store.Save() +} + +func (p *OtdfctlProfileStore) Name() string { + return p.config.Name +} + +func (p *OtdfctlProfileStore) IsDefault() bool { + return p.Name() == osprofiles.GetGlobalConfig(p.profiler).GetDefaultProfile() +} + +// NormalizeOutputFormat returns a supported output format. Any unknown value defaults to styled output. +func NormalizeOutputFormat(format string) string { + switch strings.ToLower(strings.TrimSpace(format)) { + case OutputJSON: + return OutputJSON + default: + return OutputStyled + } +} + +// IsValidOutputFormat reports whether the provided format string is supported. +func IsValidOutputFormat(format string) bool { + switch strings.ToLower(strings.TrimSpace(format)) { + case OutputJSON, OutputStyled: + return true + default: + return false + } +} diff --git a/otdfctl/pkg/tdf/tdf.go b/otdfctl/pkg/tdf/tdf.go new file mode 100644 index 0000000000..3f0bf65ccf --- /dev/null +++ b/otdfctl/pkg/tdf/tdf.go @@ -0,0 +1,6 @@ +package tdf + +const ( + TypeZTDF = "ztdf" + TypeTDF3 = "tdf3" // alias for TDF +) diff --git a/otdfctl/pkg/utils/http.go b/otdfctl/pkg/utils/http.go new file mode 100644 index 0000000000..e73e2cccef --- /dev/null +++ b/otdfctl/pkg/utils/http.go @@ -0,0 +1,17 @@ +package utils + +import ( + "crypto/tls" + "net/http" +) + +func NewHTTPClient(tlsNoVerify bool) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + //nolint:gosec // skip tls verification allowed if requested + InsecureSkipVerify: tlsNoVerify, + }, + }, + } +} diff --git a/otdfctl/pkg/utils/identifier.go b/otdfctl/pkg/utils/identifier.go new file mode 100644 index 0000000000..a7e371bb9f --- /dev/null +++ b/otdfctl/pkg/utils/identifier.go @@ -0,0 +1,73 @@ +// pkg/utils/identifier.go +package utils + +import ( + "net/url" + "strings" + + "github.com/google/uuid" +) + +// IdentifierStringType defines the type of string identified. +type IdentifierStringType int + +const ( + // StringTypeUnknown indicates the string type could not be determined or is empty. + StringTypeUnknown IdentifierStringType = iota + // StringTypeUUID indicates the string is a valid UUID. + StringTypeUUID + // StringTypeURI indicates the string is a valid absolute URI. + StringTypeURI + // StringTypeGeneric indicates the string is not a UUID or URI, and can be treated as a generic identifier (e.g., a name). + StringTypeGeneric +) + +// String returns a string representation of the IdentifierStringType. +func (it IdentifierStringType) String() string { + switch it { + case StringTypeUUID: + return "UUID" + case StringTypeURI: + return "URI" + case StringTypeGeneric: + return "Generic" + case StringTypeUnknown: + fallthrough + default: + return "Unknown" + } +} + +// ClassifyString attempts to determine if the input string is a UUID, an absolute URI, or a generic string. +// It prioritizes UUID, then URI, then defaults to Generic. +func ClassifyString(input string) IdentifierStringType { + trimmedInput := strings.TrimSpace(input) + if trimmedInput == "" { + return StringTypeUnknown // Or StringTypeGeneric if empty strings should be treated as such + } + + // Check for UUID + // uuid.Parse is strict and will return an error if the string is not a valid UUID. + if _, err := uuid.Parse(trimmedInput); err == nil { + return StringTypeUUID + } + + // Check for an absolute URI + // url.ParseRequestURI requires the URL to be absolute. + // We also check for a scheme and host to ensure it's a usable network URI. + if parsedURL, err := url.ParseRequestURI(trimmedInput); err == nil { + if parsedURL.Scheme != "" && parsedURL.Host != "" { + return StringTypeURI + } + } + // A slightly more lenient check that also catches schemeless URLs if needed, + // but for KAS identifiers, absolute URIs are usually expected. + // if parsedURL, err := url.Parse(trimmedInput); err == nil { + // if parsedURL.Scheme != "" && parsedURL.Host != "" { + // return StringTypeURI + // } + // } + + // If not a UUID and not a well-formed absolute URI, treat as generic + return StringTypeGeneric +} diff --git a/otdfctl/pkg/utils/identifier_test.go b/otdfctl/pkg/utils/identifier_test.go new file mode 100644 index 0000000000..532f283072 --- /dev/null +++ b/otdfctl/pkg/utils/identifier_test.go @@ -0,0 +1,92 @@ +package utils + +import ( + "testing" +) + +func TestClassifyString(t *testing.T) { + tests := []struct { + name string + input string + expected IdentifierStringType + }{ + { + name: "Valid UUID", + input: "123e4567-e89b-12d3-a456-426614174000", + expected: StringTypeUUID, + }, + { + name: "Valid UUID with spaces", + input: " 123e4567-e89b-12d3-a456-426614174000 ", + expected: StringTypeUUID, + }, + { + name: "Valid URI - https", + input: "https://example.com/path?query=value", + expected: StringTypeURI, + }, + { + name: "Valid URI - http", + input: "http://localhost:8080", + expected: StringTypeURI, + }, + { + name: "Valid URI with spaces", + input: " https://example.com/path ", + expected: StringTypeURI, + }, + { + name: "Generic string - name", + input: "my-kas-server", + expected: StringTypeGeneric, + }, + { + name: "Generic string - simple word", + input: "kas1", + expected: StringTypeGeneric, + }, + { + name: "Generic string with spaces", + input: " My KAS Name ", + expected: StringTypeGeneric, + }, + { + name: "Empty string", + input: "", + expected: StringTypeUnknown, + }, + { + name: "String with only spaces", + input: " ", + expected: StringTypeUnknown, + }, + { + name: "Invalid UUID - too short", + input: "123e4567-e89b-12d3-a456-42661417400", + expected: StringTypeGeneric, // Falls back to generic + }, + { + name: "Invalid URI - no scheme", + input: "example.com/path", + expected: StringTypeGeneric, // Falls back to generic + }, + { + name: "Invalid URI - no host", + input: "https:///path", + expected: StringTypeGeneric, // Falls back to generic + }, + { + name: "String that looks like URI but isn't absolute", + input: "/just/a/path", + expected: StringTypeGeneric, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ClassifyString(tt.input); got != tt.expected { + t.Errorf("ClassifyString() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/otdfctl/pkg/utils/pemvalidate.go b/otdfctl/pkg/utils/pemvalidate.go new file mode 100644 index 0000000000..29fc17f8ef --- /dev/null +++ b/otdfctl/pkg/utils/pemvalidate.go @@ -0,0 +1,63 @@ +package utils + +import ( + "errors" + "fmt" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/protocol/go/policy" +) + +// ValidatePublicKeyPEM validates a PEM-encoded public key block and ensures it +// matches the expected algorithm. The input should be raw PEM bytes (not base64). +func ValidatePublicKeyPEM(pemBytes []byte, expected policy.Algorithm) error { + if len(pemBytes) == 0 { + return errors.New("empty pem input") + } + + enc, err := ocrypto.FromPublicPEM(string(pemBytes)) + if err != nil { + return fmt.Errorf("invalid public key pem: %w", err) + } + + switch expected { + case policy.Algorithm_ALGORITHM_RSA_2048: + if enc.KeyType() != ocrypto.RSA2048Key { + return errors.New("algorithm mismatch: expected RSA 2048") + } + case policy.Algorithm_ALGORITHM_RSA_4096: + if enc.KeyType() != ocrypto.RSA4096Key { + return errors.New("algorithm mismatch: expected RSA 4096") + } + case policy.Algorithm_ALGORITHM_EC_P256: + if enc.KeyType() != ocrypto.EC256Key { + return errors.New("algorithm mismatch: expected EC P-256") + } + case policy.Algorithm_ALGORITHM_EC_P384: + if enc.KeyType() != ocrypto.EC384Key { + return errors.New("algorithm mismatch: expected EC P-384") + } + case policy.Algorithm_ALGORITHM_EC_P521: + if enc.KeyType() != ocrypto.EC521Key { + return errors.New("algorithm mismatch: expected EC P-521") + } + case policy.Algorithm_ALGORITHM_HPQT_XWING: + if enc.KeyType() != ocrypto.HybridXWingKey { + return errors.New("algorithm mismatch: expected hybrid X-Wing (X25519 + ML-KEM-768)") + } + case policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768: + if enc.KeyType() != ocrypto.HybridSecp256r1MLKEM768Key { + return errors.New("algorithm mismatch: expected hybrid NIST P-256 + ML-KEM-768") + } + case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: + if enc.KeyType() != ocrypto.HybridSecp384r1MLKEM1024Key { + return errors.New("algorithm mismatch: expected hybrid NIST P-384 + ML-KEM-1024") + } + case policy.Algorithm_ALGORITHM_UNSPECIFIED: + fallthrough + default: + return errors.New("unsupported or unspecified algorithm") + } + + return nil +} diff --git a/otdfctl/pkg/utils/pemvalidate_test.go b/otdfctl/pkg/utils/pemvalidate_test.go new file mode 100644 index 0000000000..b2b33fa59d --- /dev/null +++ b/otdfctl/pkg/utils/pemvalidate_test.go @@ -0,0 +1,163 @@ +package utils + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/require" +) + +func pemBlockForKey(t *testing.T, pub interface{}) []byte { + t.Helper() + var der []byte + var err error + switch k := pub.(type) { + case *rsa.PublicKey: + der, err = x509.MarshalPKIXPublicKey(k) + require.NoError(t, err) + case *ecdsa.PublicKey: + der, err = x509.MarshalPKIXPublicKey(k) + require.NoError(t, err) + default: + t.Fatalf("unsupported key type") + } + return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der}) +} + +func TestValidatePublicKeyPEM_RSA2048_OK(t *testing.T) { + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + pub := &k.PublicKey + pemBytes := pemBlockForKey(t, pub) + + err = ValidatePublicKeyPEM(pemBytes, policy.Algorithm_ALGORITHM_RSA_2048) + require.NoError(t, err) +} + +func TestValidatePublicKeyPEM_RSA_SizeMismatch(t *testing.T) { + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + pemBytes := pemBlockForKey(t, &k.PublicKey) + + err = ValidatePublicKeyPEM(pemBytes, policy.Algorithm_ALGORITHM_RSA_4096) + require.Error(t, err) + require.Contains(t, err.Error(), "algorithm mismatch") +} + +func TestValidatePublicKeyPEM_RSA4096_OK(t *testing.T) { + k, err := rsa.GenerateKey(rand.Reader, 4096) + require.NoError(t, err) + pub := &k.PublicKey + pemBytes := pemBlockForKey(t, pub) + + err = ValidatePublicKeyPEM(pemBytes, policy.Algorithm_ALGORITHM_RSA_4096) + require.NoError(t, err) +} + +func TestValidatePublicKeyPEM_EC_P256_OK(t *testing.T) { + k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + pemBytes := pemBlockForKey(t, &k.PublicKey) + + err = ValidatePublicKeyPEM(pemBytes, policy.Algorithm_ALGORITHM_EC_P256) + require.NoError(t, err) +} + +func TestValidatePublicKeyPEM_EC_P384_OK(t *testing.T) { + k, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + pemBytes := pemBlockForKey(t, &k.PublicKey) + + err = ValidatePublicKeyPEM(pemBytes, policy.Algorithm_ALGORITHM_EC_P384) + require.NoError(t, err) +} + +func TestValidatePublicKeyPEM_EC_P521_OK(t *testing.T) { + k, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + require.NoError(t, err) + pemBytes := pemBlockForKey(t, &k.PublicKey) + + err = ValidatePublicKeyPEM(pemBytes, policy.Algorithm_ALGORITHM_EC_P521) + require.NoError(t, err) +} + +func TestValidatePublicKeyPEM_EC_Mismatch(t *testing.T) { + k, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + pemBytes := pemBlockForKey(t, &k.PublicKey) + + err = ValidatePublicKeyPEM(pemBytes, policy.Algorithm_ALGORITHM_EC_P256) + require.Error(t, err) + require.Contains(t, err.Error(), "algorithm mismatch") +} + +func TestValidatePublicKeyPEM_InvalidPEM(t *testing.T) { + err := ValidatePublicKeyPEM([]byte("not a pem"), policy.Algorithm_ALGORITHM_RSA_2048) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid public key pem") +} + +func TestValidatePublicKeyPEM_EmptyPEM(t *testing.T) { + err := ValidatePublicKeyPEM([]byte(""), policy.Algorithm_ALGORITHM_RSA_2048) + require.Error(t, err) + require.Contains(t, err.Error(), "empty pem input") +} + +func TestValidatePublicKeyPEM_UnsupportedAlgorithm(t *testing.T) { + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + pemBytes := pemBlockForKey(t, &k.PublicKey) + + err = ValidatePublicKeyPEM(pemBytes, policy.Algorithm_ALGORITHM_UNSPECIFIED) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported or unspecified algorithm") +} + +func TestValidatePublicKeyPEM_HybridXWing_OK(t *testing.T) { + kp, err := ocrypto.NewKeyPair(ocrypto.HybridXWingKey) + require.NoError(t, err) + pubPem, err := kp.PublicKeyInPemFormat() + require.NoError(t, err) + + err = ValidatePublicKeyPEM([]byte(pubPem), policy.Algorithm_ALGORITHM_HPQT_XWING) + require.NoError(t, err) +} + +func TestValidatePublicKeyPEM_HybridP256MLKEM768_OK(t *testing.T) { + kp, err := ocrypto.NewKeyPair(ocrypto.HybridSecp256r1MLKEM768Key) + require.NoError(t, err) + pubPem, err := kp.PublicKeyInPemFormat() + require.NoError(t, err) + + err = ValidatePublicKeyPEM([]byte(pubPem), policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768) + require.NoError(t, err) +} + +func TestValidatePublicKeyPEM_HybridP384MLKEM1024_OK(t *testing.T) { + kp, err := ocrypto.NewKeyPair(ocrypto.HybridSecp384r1MLKEM1024Key) + require.NoError(t, err) + pubPem, err := kp.PublicKeyInPemFormat() + require.NoError(t, err) + + err = ValidatePublicKeyPEM([]byte(pubPem), policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024) + require.NoError(t, err) +} + +func TestValidatePublicKeyPEM_HybridMismatch(t *testing.T) { + // Generate an X-Wing key but validate against a different hybrid algorithm + kp, err := ocrypto.NewKeyPair(ocrypto.HybridXWingKey) + require.NoError(t, err) + pubPem, err := kp.PublicKeyInPemFormat() + require.NoError(t, err) + + err = ValidatePublicKeyPEM([]byte(pubPem), policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768) + require.Error(t, err) + require.Contains(t, err.Error(), "algorithm mismatch") +} diff --git a/otdfctl/pkg/utils/read.go b/otdfctl/pkg/utils/read.go new file mode 100644 index 0000000000..093a3192b9 --- /dev/null +++ b/otdfctl/pkg/utils/read.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "io" + "os" +) + +func ReadBytesFromFile(filePath string, maxBytes int64) ([]byte, error) { + fileInfo, err := os.Stat(filePath) + if err != nil { + return nil, fmt.Errorf("failed to stat file at path %s: %w", filePath, err) + } + + // Check if the file size exceeds the limit + if fileInfo.Size() > maxBytes { + return nil, fmt.Errorf("file size exceeds the limit of %d bytes", maxBytes) + } + + fileToEncrypt, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open file at path %s: %w", filePath, err) + } + defer fileToEncrypt.Close() + + // Limit the reader to the specified maximum number of bytes + limitedReader := io.LimitReader(fileToEncrypt, maxBytes) + bytes, err := io.ReadAll(limitedReader) + if err != nil { + return nil, fmt.Errorf("failed to read bytes from file at path %s: %w", filePath, err) + } + + return bytes, nil +} diff --git a/otdfctl/pkg/utils/validators.go b/otdfctl/pkg/utils/validators.go new file mode 100644 index 0000000000..8569aaf9c6 --- /dev/null +++ b/otdfctl/pkg/utils/validators.go @@ -0,0 +1,33 @@ +package utils + +import ( + "errors" + "net/url" + "strings" +) + +func NormalizeEndpoint(endpoint string) (*url.URL, error) { + if endpoint == "" { + return nil, errors.New("endpoint is required") + } + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + switch u.Scheme { + case "http": + if u.Port() == "" { + u.Host += ":80" + } + case "https": + if u.Port() == "" { + u.Host += ":443" + } + default: + return nil, errors.New("invalid scheme") + } + for strings.HasSuffix(u.Path, "/") { + u.Path = strings.TrimSuffix(u.Path, "/") + } + return u, nil +} diff --git a/otdfctl/scripts/verify-checksums.sh b/otdfctl/scripts/verify-checksums.sh new file mode 100755 index 0000000000..6ae3a89199 --- /dev/null +++ b/otdfctl/scripts/verify-checksums.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Check if the required arguments are provided +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Verifying checksums..." +# Location of the checksum file +checksumFile=$1/$2 +outputDir=$1 + +echo "Looking for checksum file: $checksumFile" +test -f "$checksumFile" || { echo "ERROR: Checksum file not found!"; exit 1; } + +# Iterate over each line in the checksum file +while read -r line; do + # Extract the expected checksum and filename from each line + read -ra ADDR <<< "$line" # Read the line into an array + expectedChecksum="${ADDR[0]}" + fileName="${ADDR[2]}" + + # Calculate the actual checksum of the file + actualChecksum=$(shasum -a 256 "$outputDir/$fileName" | awk '{print $1}') + + # Compare the expected checksum with the actual checksum + if [ "$expectedChecksum" == "$actualChecksum" ]; then + echo "SUCCESS: Checksum for $fileName is valid." + else + echo "ERROR: Checksum for $fileName does not match." + fi +done < "$checksumFile" diff --git a/otdfctl/scripts/zip-builds.sh b/otdfctl/scripts/zip-builds.sh new file mode 100755 index 0000000000..36eaa47d91 --- /dev/null +++ b/otdfctl/scripts/zip-builds.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Check if the required arguments are provided +if [ $# -ne 3 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign the arguments to variables +build_semver="$1" +binary_dir="$2" +output_dir="$3" + +# Create the output directory if it doesn't exist +mkdir -p "$output_dir" + +# Create a checksums file +checksums_file="$output_dir/${build_semver}_checksums.txt" +touch $checksums_file + +# Iterate over each binary file +for binary_file in "$binary_dir"/*; do + compressed="" + if [[ $binary_file == *.exe ]]; then + # If the file is a Windows binary, zip it + filename=$(basename "$binary_file") + compressed="${filename%.exe}.zip" + zip -j "$output_dir/$compressed" "$binary_file" + else + # For other binaries, tar and gzip them + filename=$(basename "$binary_file") + compressed="${filename}.tar.gz" + tar -czf "$output_dir/$compressed" "$binary_file" + fi + + # Append checksums to the file + echo "$(cat "$output_dir/$compressed" | shasum -a 256) $compressed" >> $checksums_file +done diff --git a/otdfctl/tui/appMenu.go b/otdfctl/tui/appMenu.go new file mode 100644 index 0000000000..5907e9a979 --- /dev/null +++ b/otdfctl/tui/appMenu.go @@ -0,0 +1,105 @@ +//nolint:gocritic // still in development +package tui + +import ( + "context" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/tui/constants" +) + +const ( + mainMenu menuState = iota + namespaceMenu + attributeMenu + entitlementMenu + resourceEncodingMenu + subjectEncodingMenu +) + +type menuState int + +type AppMenuItem struct { + id menuState + title string + description string +} + +func (m AppMenuItem) FilterValue() string { + return m.title +} + +func (m AppMenuItem) Title() string { + return m.title +} + +func (m AppMenuItem) Description() string { + return m.description +} + +type AppMenu struct { + list list.Model + view tea.Model + sdk handlers.Handler +} + +func InitAppMenu(h handlers.Handler) (AppMenu, tea.Cmd) { + m := AppMenu{ + view: nil, + sdk: h, + } + //nolint:mnd // styling is magic + m.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 8, 8) + m.list.Title = "OpenTDF" + m.list.SetItems([]list.Item{ + // AppMenuItem{title: "Namespaces", description: "Manage namespaces", id: namespaceMenu}, + AppMenuItem{title: "Attributes", description: "Manage attributes", id: attributeMenu}, + // AppMenuItem{title: "Entitlements", description: "Manage entitlements", id: entitlementMenu}, + // AppMenuItem{title: "Resource Encodings", description: "Manage resource encodings", id: resourceEncodingMenu}, + // AppMenuItem{title: "Subject Encodings", description: "Manage subject encodings", id: subjectEncodingMenu}, + }) + return m, func() tea.Msg { return nil } +} + +func (m AppMenu) Init() tea.Cmd { + return nil +} + +func (m AppMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + ctx := context.Background() + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + constants.WindowSize = msg + m.list.SetSize(msg.Width, msg.Height) + return m, nil + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "ctrl+d": + return m, nil + case "enter": + switch m.list.SelectedItem().(AppMenuItem).id { + // case namespaceMenu: + // // get namespaces + // nl, cmd := InitNamespaceList([]list.Item{}, 0) + // return nl, cmd + case attributeMenu: + // list attributes + al, cmd := InitAttributeList(ctx, "", m.sdk) + return al, cmd + } + } + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m AppMenu) View() string { + return ViewList(m.list) +} diff --git a/otdfctl/tui/attributeCreateView.go b/otdfctl/tui/attributeCreateView.go new file mode 100644 index 0000000000..5960eb006e --- /dev/null +++ b/otdfctl/tui/attributeCreateView.go @@ -0,0 +1,59 @@ +package tui + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/opentdf/platform/otdfctl/tui/constants" +) + +type AttributeCreateModel struct { + form *huh.Form +} + +func InitAttributeCreateModel() (tea.Model, tea.Cmd) { + namespace := "" + m := AttributeCreateModel{} + m.form = huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Namespace"). + Options( + huh.NewOption("demo.com", "demo.com"), + huh.NewOption("demo.net", "demo.net"), + ). + Validate(func(str string) error { + // Check if namespace exists + fmt.Println(str) + return nil + }). + Value(&namespace), + ), + ) + + return m, nil +} + +func (m AttributeCreateModel) Init() tea.Cmd { + return nil +} + +func (m AttributeCreateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + constants.WindowSize = msg + return m, nil + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + } + } + + return m, nil +} + +func (m AttributeCreateModel) View() string { + return "" +} diff --git a/otdfctl/tui/attributeList.go b/otdfctl/tui/attributeList.go new file mode 100644 index 0000000000..ec3787888e --- /dev/null +++ b/otdfctl/tui/attributeList.go @@ -0,0 +1,150 @@ +package tui + +import ( + "context" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/tui/constants" + "github.com/opentdf/platform/protocol/go/common" +) + +type AttributeList struct { + list list.Model + h handlers.Handler +} + +type AttributeItem struct { + id string + name string +} + +func (m AttributeItem) FilterValue() string { + return m.name +} + +func (m AttributeItem) Title() string { + return m.name +} + +func (m AttributeItem) Description() string { + return m.id +} + +func InitAttributeList(ctx context.Context, id string, h handlers.Handler) (tea.Model, tea.Cmd) { + l := list.New([]list.Item{}, list.NewDefaultDelegate(), constants.WindowSize.Width, constants.WindowSize.Height) + // TODO: handle and return error view and use real command flags limit/offset + var ( + limit int32 = 100 + offset int32 = 0 + ) + res, _ := h.ListAttributes(ctx, common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, limit, offset, handlers.SortOption{}) + var attrs []list.Item + selectIdx := 0 + for i, attr := range res.GetAttributes() { + var vals []string + for _, val := range attr.GetValues() { + // TODO: do something with values here + //lint:ignore SA4010 // still in-progress + vals = append(vals, val.GetValue()) + } + if attr.GetId() == id { + selectIdx = i + } + item := AttributeItem{ + id: attr.GetId(), + name: attr.GetName(), + } + attrs = append(attrs, item) + } + l.Title = "Attributes" + l.SetItems(attrs) + l.Select(selectIdx) + m := AttributeList{h: h, list: l} + return m.Update(WindowMsg()) +} + +func (m AttributeList) Init() tea.Cmd { + return nil +} + +func StyleAttr(attr string) string { + return lipgloss.NewStyle(). + Foreground(constants.Magenta). + Render(attr) +} + +func CreateViewFormat(num int) string { + var format string + for i := 0; i < num; i++ { + format += "%s %s\n" + } + return format +} + +func (m AttributeList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + ctx := context.Background() + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + constants.WindowSize = msg + m.list.SetSize(msg.Width, msg.Height) + return m, nil + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "ctrl+[", "backspace": + am, _ := InitAppMenu(m.h) + // make enum for Attributes idx in AppMenu + am.list.Select(0) + return am.Update(WindowMsg()) + // case "c": + // create new attribute + // return InitAttributeView(m.list.Items(), len(m.list.Items())) + case "enter", "e": + return InitAttributeView(ctx, m.list.Items()[m.list.Index()].(AttributeItem).id, m.h) + // case "ctrl+d": + // m.list.RemoveItem(m.list.Index()) + // newIndex := m.list.Index() - 1 + // if newIndex < 0 { + // newIndex = 0 + // } + // m.list.Select(newIndex) + } + } + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m AttributeList) View() string { + return ViewList(m.list) +} + +// func AddAttribute() { +// var namespace string + +// form := huh.NewForm( +// huh.NewGroup( +// huh.NewSelect[string](). +// Title("Namespace"). +// Options( +// huh.NewOption("demo.com", "demo.com"), +// huh.NewOption("demo.net", "demo.net"), +// ). +// Validate(func(str string) error { +// // Check if namespace exists +// fmt.Println(str) +// return nil +// }). +// Value(&namespace), +// ), +// ) + +// if err := form.Run(); err != nil { +// return +// } +// } diff --git a/otdfctl/tui/attributeView.go b/otdfctl/tui/attributeView.go new file mode 100644 index 0000000000..7196966f5d --- /dev/null +++ b/otdfctl/tui/attributeView.go @@ -0,0 +1,106 @@ +package tui + +import ( + "context" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/tui/constants" + "github.com/opentdf/platform/protocol/go/policy" +) + +type AttributeSubItem struct { + title string + description string +} + +func (m AttributeSubItem) FilterValue() string { + return m.title +} + +func (m AttributeSubItem) Title() string { + return m.title +} + +func (m AttributeSubItem) Description() string { + return m.description +} + +type AttributeView struct { + attr *policy.Attribute + read Read + sdk handlers.Handler +} + +func InitAttributeView(ctx context.Context, id string, h handlers.Handler) (AttributeView, tea.Cmd) { + // TODO: handle and return error view + attr, _ := h.GetAttribute(ctx, id) + sa := cli.GetSimpleAttribute(attr) + items := []list.Item{ + AttributeSubItem{title: "ID", description: sa.ID}, + AttributeSubItem{title: "Name", description: sa.Name}, + AttributeSubItem{title: "Rule", description: sa.Rule}, + AttributeSubItem{title: "Values", description: cli.CommaSeparated(sa.Values)}, + AttributeSubItem{title: "Namespace", description: sa.Namespace}, + AttributeSubItem{title: "Active", description: sa.Active}, + AttributeSubItem{title: "Labels", description: sa.Metadata["Labels"]}, + AttributeSubItem{title: "Created At", description: sa.Metadata["Created At"]}, + AttributeSubItem{title: "Updated At", description: sa.Metadata["Updated At"]}, + } + model, _ := InitRead("Read Attribute", items) + + mod, _ := model.(Read) + m := AttributeView{sdk: h, attr: attr, read: mod} + model, msg := m.Update(WindowMsg()) + m = model.(AttributeView) + return m, msg +} + +func (m AttributeView) Init() tea.Cmd { + return nil +} + +func (m AttributeView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + ctx := context.Background() + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + constants.WindowSize = msg + m.read.list.SetSize(msg.Width, msg.Height) + return m, nil + case tea.KeyMsg: + switch msg.String() { + case "backspace": + return InitAttributeList(ctx, m.attr.GetId(), m.sdk) + case "ctrl+c", "q": + return m, tea.Quit + case "ctrl+d": + return m, nil + case "enter": + if m.read.list.SelectedItem().(AttributeSubItem).title == "Labels" { + return InitLabelList(m.attr, m.sdk) + } + // case "enter": + // switch m.list.SelectedItem().(AttributeItem).id { + // // case namespaceMenu: + // // // get namespaces + // // nl, cmd := InitNamespaceList([]list.Item{}, 0) + // // return nl, cmd + // case attributeMenu: + // // list attributes + // al, cmd := InitAttributeList("", m.sdk) + // return al, cmd + // } + } + } + + var cmd tea.Cmd + m.read.list, cmd = m.read.list.Update(msg) + return m, cmd +} + +func (m AttributeView) View() string { + return m.read.View() +} diff --git a/otdfctl/tui/common.go b/otdfctl/tui/common.go new file mode 100644 index 0000000000..2a334e35f0 --- /dev/null +++ b/otdfctl/tui/common.go @@ -0,0 +1,45 @@ +package tui + +import ( + "log" + "os" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/tui/constants" +) + +// StartTea the entry point for the UI. Initializes the model. +func StartTea(h handlers.Handler) error { + if f, err := tea.LogToFile("debug.log", "help"); err != nil { + cli.ExitWithError("Couldn't open a file for logging:", err) + os.Exit(1) + } else { + defer func() { + err = f.Close() + if err != nil { + log.Fatal(err) + } + }() + } + + m, _ := InitAppMenu(h) + constants.P = tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) + if _, err := constants.P.Run(); err != nil { + cli.ExitWithError("Error running program:", err) + } + return nil +} + +func ViewList(m list.Model) string { + //nolint:mnd // styling is magic + lipgloss.NewStyle().Padding(1, 2, 1, 2) + return lipgloss.JoinVertical(lipgloss.Top, m.View()) +} + +func WindowMsg() tea.WindowSizeMsg { + return tea.WindowSizeMsg{Width: constants.WindowSize.Width, Height: constants.WindowSize.Height} +} diff --git a/otdfctl/tui/constants/consts.go b/otdfctl/tui/constants/consts.go new file mode 100644 index 0000000000..8184f6ec84 --- /dev/null +++ b/otdfctl/tui/constants/consts.go @@ -0,0 +1,17 @@ +package constants + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + P *tea.Program + + WindowSize struct { + Width int + Height int + } + + Magenta = lipgloss.Color("#EE6FF8") +) diff --git a/otdfctl/tui/form/addAttribute.go b/otdfctl/tui/form/addAttribute.go new file mode 100644 index 0000000000..931b2f1aa0 --- /dev/null +++ b/otdfctl/tui/form/addAttribute.go @@ -0,0 +1,100 @@ +package forms + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/opentdf/platform/protocol/go/policy" +) + +type AttributeDefinition struct { + Name string + Namespace string + Description string + Labels map[string]string + Type string + Rule policy.AttributeRuleTypeEnum + Values []string +} + +func AddAttribute() (AttributeDefinition, error) { + attr := AttributeDefinition{} + + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Namespace"). + Description("Select a namespace. To create a namespace go back and select 'Add Namespace'"). + Options( + huh.NewOption("demo.com", "demo.com"), + ). + Value(&attr.Namespace), + + huh.NewInput(). + Title("Attribute Name"). + Value(&attr.Name), + + // Description + huh.NewText(). + Title("Description"). + Value(&attr.Description), + + // Select Rule + huh.NewSelect[policy.AttributeRuleTypeEnum](). + Title("Rule"). + Options( + huh.NewOption("All Of", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF), + huh.NewOption("Any Of", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF), + huh.NewOption("Hierarchical", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY), + ). + Value(&attr.Rule), + ), + ) + + if err := form.Run(); err != nil { + return attr, err + } + + for { + value, another, err := addValue() + if err != nil { + return attr, err + } + + if value == "" { + fmt.Print("Value cannot be empty\n") + continue + } + + attr.Values = append(attr.Values, value) + + if !another { + break + } + } + + return attr, nil +} + +func addValue() (string, bool, error) { + var ( + value string + another bool + err error + ) + valueForm := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Value"). + Value(&value), + + huh.NewConfirm(). + Title("Add Another Value"). + Value(&another), + ), + ) + + err = valueForm.Run() + + return value, another, err +} diff --git a/otdfctl/tui/labelList.go b/otdfctl/tui/labelList.go new file mode 100644 index 0000000000..995b906dc2 --- /dev/null +++ b/otdfctl/tui/labelList.go @@ -0,0 +1,86 @@ +package tui + +import ( + "context" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/otdfctl/tui/constants" + "github.com/opentdf/platform/protocol/go/policy" +) + +type LabelList struct { + attr *policy.Attribute + sdk handlers.Handler + read Read +} + +type LabelItem struct { + title string + description string +} + +func (m LabelItem) FilterValue() string { + return m.title +} + +func (m LabelItem) Title() string { + return m.title +} + +func (m LabelItem) Description() string { + return m.description +} + +func InitLabelList(attr *policy.Attribute, sdk handlers.Handler) (tea.Model, tea.Cmd) { + labels := attr.GetMetadata().GetLabels() + var items []list.Item + for k, v := range labels { + item := LabelItem{ + title: k, + description: v, + } + items = append(items, item) + } + model, _ := InitRead("Read Labels", items) + // TODO: handle and return error view + mod, _ := model.(Read) + return LabelList{attr: attr, sdk: sdk, read: mod}, nil +} + +func (m LabelList) Init() tea.Cmd { + return nil +} + +func (m LabelList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + ctx := context.Background() + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + constants.WindowSize = msg + m.read.list.SetSize(msg.Width, msg.Height) + return m, nil + case tea.KeyMsg: + switch msg.String() { + case "backspace": + return InitAttributeView(ctx, m.attr.GetId(), m.sdk) + case "ctrl+c", "q", "esc": + return m, tea.Quit + case "enter", "e": + return InitLabelUpdate(m.read.list.Items()[m.read.list.Index()].(LabelItem), m.attr, m.sdk), nil + case "c": + return InitLabelUpdate(LabelItem{}, m.attr, m.sdk), nil + case "d": + // delete label + return m, nil + } + } + var cmd tea.Cmd + m.read.list, cmd = m.read.list.Update(msg) + return m, cmd +} + +func (m LabelList) View() string { + return ViewList(m.read.list) +} diff --git a/otdfctl/tui/labelUpdate.go b/otdfctl/tui/labelUpdate.go new file mode 100644 index 0000000000..5059719704 --- /dev/null +++ b/otdfctl/tui/labelUpdate.go @@ -0,0 +1,62 @@ +package tui + +import ( + "context" + + tea "github.com/charmbracelet/bubbletea" + "github.com/opentdf/platform/otdfctl/pkg/handlers" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" +) + +type LabelUpdate struct { + label LabelItem + update Update + attr *policy.Attribute + sdk handlers.Handler +} + +func InitLabelUpdate(label LabelItem, attr *policy.Attribute, sdk handlers.Handler) LabelUpdate { + return LabelUpdate{ + label: label, + update: InitUpdate([]string{"Key", "Value"}, []string{label.title, label.description}), + attr: attr, + sdk: sdk, + } +} + +func (m LabelUpdate) Init() tea.Cmd { + return nil +} + +func (m LabelUpdate) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + ctx := context.Background() + + if msg, ok := msg.(tea.KeyMsg); ok { + switch msg.String() { + case "enter": + if m.update.focusIndex == len(m.update.inputs) { + // update the label + metadata := &common.MetadataMutable{Labels: m.attr.GetMetadata().GetLabels()} + oldKey := m.label.title + newKey := m.update.inputs[0].Value() + newVal := m.update.inputs[1].Value() + if oldKey != newKey { + delete(metadata.GetLabels(), oldKey) + } + metadata.Labels[newKey] = newVal + behavior := common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE + // TODO: handle and return error view + attr, _ := m.sdk.UpdateAttribute(ctx, m.attr.GetId(), metadata, behavior) + return InitLabelList(attr, m.sdk) + } + } + } + update, cmd := m.update.Update(msg) + m.update = update.(Update) + return m, cmd +} + +func (m LabelUpdate) View() string { + return m.update.View() +} diff --git a/otdfctl/tui/read.go b/otdfctl/tui/read.go new file mode 100644 index 0000000000..e53f0676c9 --- /dev/null +++ b/otdfctl/tui/read.go @@ -0,0 +1,44 @@ +package tui + +import ( + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/opentdf/platform/otdfctl/tui/constants" +) + +type Read struct { + list list.Model + width int +} + +func InitRead(title string, items []list.Item) (tea.Model, tea.Cmd) { + m := Read{} + m.list = list.New(items, list.NewDefaultDelegate(), constants.WindowSize.Width, constants.WindowSize.Height) + m.list.Title = title + return m.Update(WindowMsg()) +} + +func (m Read) Init() tea.Cmd { + return nil +} + +func (m Read) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + constants.WindowSize = msg + m.list.SetSize(msg.Width, msg.Height) + m.width = msg.Width + return m, nil + case tea.KeyMsg: + //nolint:exhaustive // only interested in a few key types + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + } + } + return m, nil +} + +func (m Read) View() string { + return ViewList(m.list) +} diff --git a/otdfctl/tui/update.go b/otdfctl/tui/update.go new file mode 100644 index 0000000000..bc45898789 --- /dev/null +++ b/otdfctl/tui/update.go @@ -0,0 +1,163 @@ +package tui + +// A simple example demonstrating the use of multiple text input components +// from the Bubbles component library. + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/cursor" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + cursorStyle = focusedStyle + noStyle = lipgloss.NewStyle() + helpStyle = blurredStyle + cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) + + focusedButton = focusedStyle.Render("[ Submit ]") + blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit")) +) + +type Update struct { + focusIndex int + inputs []textinput.Model + cursorMode cursor.Mode + keys []string +} + +func InitUpdate(keys []string, vals []string) Update { + m := Update{ + inputs: make([]textinput.Model, len(keys)), + keys: keys, + } + + var t textinput.Model + for i := range m.inputs { + t = textinput.New() + t.Cursor.Style = cursorStyle + t.CharLimit = 32 + t.SetValue(vals[i]) + if i == 0 { + t.Focus() + t.PromptStyle = focusedStyle + t.TextStyle = focusedStyle + } + + m.inputs[i] = t + } + + return m +} + +func (m Update) Init() tea.Cmd { + return textinput.Blink +} + +func (m Update) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if msg, ok := msg.(tea.KeyMsg); ok { + switch msg.String() { + case "ctrl+c", "esc": + return m, tea.Quit + + // Change cursor mode + case "ctrl+r": + m.cursorMode++ + if m.cursorMode > cursor.CursorHide { + m.cursorMode = cursor.CursorBlink + } + cmds := make([]tea.Cmd, len(m.inputs)) + for i := range m.inputs { + cmds[i] = m.inputs[i].Cursor.SetMode(m.cursorMode) + } + return m, tea.Batch(cmds...) + + // Set focus to next input + case "tab", "shift+tab", "enter", "up", "down": + s := msg.String() + + // Did the user press enter while the submit button was focused? + // If so, exit. + if s == "enter" && m.focusIndex == len(m.inputs) { + return m, nil + } + + // Cycle indexes + if s == "up" || s == "shift+tab" { + m.focusIndex-- + } else { + m.focusIndex++ + } + + if m.focusIndex > len(m.inputs) { + m.focusIndex = 0 + } else if m.focusIndex < 0 { + m.focusIndex = len(m.inputs) + } + + cmds := make([]tea.Cmd, len(m.inputs)) + for i := 0; i <= len(m.inputs)-1; i++ { + if i == m.focusIndex { + // Set focused state + cmds[i] = m.inputs[i].Focus() + m.inputs[i].PromptStyle = focusedStyle + m.inputs[i].TextStyle = focusedStyle + continue + } + // Remove focused state + m.inputs[i].Blur() + m.inputs[i].PromptStyle = noStyle + m.inputs[i].TextStyle = noStyle + } + + return m, tea.Batch(cmds...) + } + } + + // Handle character input and blinking + cmd := m.updateInputs(msg) + + return m, cmd +} + +func (m *Update) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(m.inputs)) + + // Only text inputs with Focus() set will respond, so it's safe to simply + // update all of them here without any further logic. + for i := range m.inputs { + m.inputs[i], cmds[i] = m.inputs[i].Update(msg) + } + + return tea.Batch(cmds...) +} + +func (m Update) View() string { + var b strings.Builder + + for i := range m.inputs { + b.WriteString(m.keys[i] + "\n") + b.WriteString(m.inputs[i].View()) + if i < len(m.inputs)-1 { + b.WriteRune('\n') + } + } + + button := &blurredButton + if m.focusIndex == len(m.inputs) { + button = &focusedButton + } + fmt.Fprintf(&b, "\n\n%s\n\n", *button) + + b.WriteString(helpStyle.Render("cursor mode is ")) + b.WriteString(cursorModeHelpStyle.Render(m.cursorMode.String())) + b.WriteString(helpStyle.Render(" (ctrl+r to change style)")) + + return b.String() +} diff --git a/protocol/codegen/go.mod b/protocol/codegen/go.mod new file mode 100644 index 0000000000..1c11114085 --- /dev/null +++ b/protocol/codegen/go.mod @@ -0,0 +1,3 @@ +module github.com/opentdf/platform/protocol/codegen + +go 1.25.5 diff --git a/protocol/codegen/go.work b/protocol/codegen/go.work new file mode 100644 index 0000000000..2789a8cc01 --- /dev/null +++ b/protocol/codegen/go.work @@ -0,0 +1,5 @@ +// Isolate this module from the root workspace so `go run .` resolves locally +// without adding protocol/codegen to the root go.work. +go 1.25.5 + +use . diff --git a/protocol/codegen/main.go b/protocol/codegen/main.go new file mode 100644 index 0000000000..fc3da86b35 --- /dev/null +++ b/protocol/codegen/main.go @@ -0,0 +1,163 @@ +// Command codegen copies helper source files from protocol/go/internal/ into their +// corresponding proto package directories with a "Code generated" header prepended. +// Helper source files import proto types explicitly for IDE support; the copier strips +// the self-referencing import and type qualifiers so the output compiles in-package. +// +// See https://github.com/opentdf/platform/pull/3232 for background on the source-file codegen approach. +package main + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" +) + +// helperMapping defines a source directory (relative to protocol/go/internal/) and its +// target directory (relative to protocol/go/) where files will be copied. +type helperMapping struct { + // Source is the subdirectory under internal/ containing the source files. + Source string + // Target is the subdirectory under protocol/go/ where files are copied. + Target string + // ProtoImportPath is the full Go import path of the target proto package. + ProtoImportPath string + // ProtoImportAlias is the import alias used in the source files for the proto package. + ProtoImportAlias string +} + +var mappings = []helperMapping{ + { + Source: "authorization/v2", + Target: "authorization/v2", + ProtoImportPath: "github.com/opentdf/platform/protocol/go/authorization/v2", + ProtoImportAlias: "authorizationv2", + }, + { + Source: "policy", + Target: "policy", + ProtoImportPath: "github.com/opentdf/platform/protocol/go/policy", + ProtoImportAlias: "policy", + }, +} + +const generatedHeader = "// Code generated by protocol/codegen. DO NOT EDIT.\n\n" + +func main() { + baseDir, err := getBaseDir() + if err != nil { + log.Fatal(err) + } + + helpersDir := filepath.Join(baseDir, "internal") + for _, m := range mappings { + srcDir := filepath.Join(helpersDir, m.Source) + dstDir := filepath.Join(baseDir, m.Target) + if err := copyHelpers(srcDir, dstDir, m); err != nil { + log.Fatalf("copying helpers from %s to %s: %v", srcDir, dstDir, err) + } + } +} + +// generatedFile holds a transformed helper ready to write. +type generatedFile struct { + dst string + content []byte + src string +} + +func copyHelpers(srcDir, dstDir string, m helperMapping) error { + entries, err := os.ReadDir(srcDir) + if err != nil { + return fmt.Errorf("reading source directory: %w", err) + } + + // Read and transform all source files before touching the target directory. + var files []generatedFile + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + + src := filepath.Join(srcDir, name) + content, err := os.ReadFile(src) + if err != nil { + return fmt.Errorf("reading %s: %w", src, err) + } + + transformed := rewriteImports(string(content), m) + outName := strings.TrimSuffix(name, ".go") + ".gen.go" + + files = append(files, generatedFile{ + dst: filepath.Join(dstDir, outName), + content: []byte(generatedHeader + transformed), + src: src, + }) + } + + // Only remove stale .gen.go files once all reads succeeded. + if err := removeGenFiles(dstDir); err != nil { + return fmt.Errorf("cleaning target directory: %w", err) + } + + for _, f := range files { + if err := os.WriteFile(f.dst, f.content, 0o644); err != nil { + return fmt.Errorf("writing %s: %w", f.dst, err) + } + fmt.Printf(" %s -> %s\n", f.src, f.dst) + } + return nil +} + +func removeGenFiles(dir string) error { + entries, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("reading directory: %w", err) + } + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".gen.go") { + if err := os.Remove(filepath.Join(dir, entry.Name())); err != nil { + return fmt.Errorf("removing %s: %w", entry.Name(), err) + } + } + } + return nil +} + +// rewriteImports removes the self-referencing proto import and strips the alias qualifier +// from type references so the file compiles inside the proto package. +// +// The qualifier regex also matches inside string literals and comments. This is acceptable +// because we control the source files and don't use the alias in non-code contexts. +func rewriteImports(content string, m helperMapping) string { + // Remove the import line: `authorizationv2 "github.com/.../authorization/v2"` + importLineRe := regexp.MustCompile( + `(?m)^\s*` + regexp.QuoteMeta(m.ProtoImportAlias) + `\s+"` + regexp.QuoteMeta(m.ProtoImportPath) + `"\s*\n`, + ) + content = importLineRe.ReplaceAllString(content, "") + + // Strip the alias qualifier from type references: `authorizationv2.Foo` -> `Foo` + qualifierRe := regexp.MustCompile(regexp.QuoteMeta(m.ProtoImportAlias) + `\.`) + content = qualifierRe.ReplaceAllString(content, "") + + // Clean up empty import blocks left behind when the proto import was the only one. + emptyImportRe := regexp.MustCompile(`\nimport \(\n\)\n`) + content = emptyImportRe.ReplaceAllString(content, "") + + return content +} + +// getBaseDir returns the protocol/go/ directory by navigating from this file's location. +// From protocol/codegen/main.go, go up two levels to protocol/, then into go/. +func getBaseDir() (string, error) { + _, filename, _, ok := runtime.Caller(0) + if !ok { + return "", errors.New("could not determine current file location") + } + return filepath.Join(filepath.Dir(filepath.Dir(filename)), "go"), nil +} diff --git a/protocol/codegen/main_test.go b/protocol/codegen/main_test.go new file mode 100644 index 0000000000..0c9f54e301 --- /dev/null +++ b/protocol/codegen/main_test.go @@ -0,0 +1,251 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRewriteImports(t *testing.T) { + m := helperMapping{ + ProtoImportPath: "github.com/opentdf/platform/protocol/go/authorization/v2", + ProtoImportAlias: "authorizationv2", + } + + tests := []struct { + name string + input string + want string + }{ + { + name: "strips import line and qualifiers", + input: `package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" +) + +func ForClientID(clientID string) *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_EntityChain{}, + } +} +`, + want: `package authorizationv2 + +import ( + "github.com/opentdf/platform/protocol/go/entity" +) + +func ForClientID(clientID string) *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_EntityChain{}, + } +} +`, + }, + { + name: "preserves other imports", + input: `package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func WithRequestToken() *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_WithRequestToken{ + WithRequestToken: wrapperspb.Bool(true), + }, + } +} +`, + want: `package authorizationv2 + +import ( + "github.com/opentdf/platform/protocol/go/entity" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func WithRequestToken() *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_WithRequestToken{ + WithRequestToken: wrapperspb.Bool(true), + }, + } +} +`, + }, + { + name: "no-op when no matching import", + input: "package foo\n\nfunc Bar() {}\n", + want: "package foo\n\nfunc Bar() {}\n", + }, + { + name: "does not strip partial alias matches", + input: `package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" +) + +// authorizationv2helper is not a qualifier reference +var authorizationv2helper = "should stay" +func F() *authorizationv2.EntityIdentifier { return nil } +`, + want: `package authorizationv2 + +// authorizationv2helper is not a qualifier reference +var authorizationv2helper = "should stay" +func F() *EntityIdentifier { return nil } +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := rewriteImports(tt.input, m) + if got != tt.want { + t.Errorf("rewriteImports() mismatch\n--- got ---\n%s\n--- want ---\n%s", got, tt.want) + } + }) + } +} + +func TestRemoveGenFiles(t *testing.T) { + dir := t.TempDir() + + // Create a mix of files: .gen.go (should be removed), .pb.go and .go (should survive) + files := map[string]bool{ + "entity_identifier.gen.go": false, // expect removed + "other_helper.gen.go": false, // expect removed + "authorization.pb.go": true, // expect kept + "authorization_grpc.pb.go": true, // expect kept + "regular.go": true, // expect kept + } + for name := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte("package x"), 0o644); err != nil { + t.Fatal(err) + } + } + + if err := removeGenFiles(dir); err != nil { + t.Fatal(err) + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + + remaining := make(map[string]bool) + for _, e := range entries { + remaining[e.Name()] = true + } + + for name, shouldExist := range files { + if shouldExist && !remaining[name] { + t.Errorf("%s was removed but should have been kept", name) + } + if !shouldExist && remaining[name] { + t.Errorf("%s was kept but should have been removed", name) + } + } +} + +func TestCopyHelpers(t *testing.T) { + m := helperMapping{ + Source: "test-pkg", + Target: "test-pkg", + ProtoImportPath: "github.com/example/proto/test", + ProtoImportAlias: "testpkg", + } + + srcDir := filepath.Join(t.TempDir(), "src") + dstDir := filepath.Join(t.TempDir(), "dst") + if err := os.MkdirAll(srcDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(dstDir, 0o755); err != nil { + t.Fatal(err) + } + + // Source .go file that should be copied and transformed. + helperContent := `package testpkg + +import ( + testpkg "github.com/example/proto/test" +) + +func NewFoo() *testpkg.Foo { + return &testpkg.Foo{} +} +` + if err := os.WriteFile(filepath.Join(srcDir, "helper.go"), []byte(helperContent), 0o644); err != nil { + t.Fatal(err) + } + + // _test.go file that should be skipped. + if err := os.WriteFile(filepath.Join(srcDir, "helper_test.go"), []byte("package testpkg\n"), 0o644); err != nil { + t.Fatal(err) + } + + // Non-Go file that should be skipped. + if err := os.WriteFile(filepath.Join(srcDir, "README.md"), []byte("# readme\n"), 0o644); err != nil { + t.Fatal(err) + } + + // Pre-existing stale .gen.go that should be cleaned up. + staleFile := filepath.Join(dstDir, "old_helper.gen.go") + if err := os.WriteFile(staleFile, []byte("package testpkg\n"), 0o644); err != nil { + t.Fatal(err) + } + + if err := copyHelpers(srcDir, dstDir, m); err != nil { + t.Fatalf("copyHelpers failed: %v", err) + } + + // Verify the transformed file was written with .gen.go suffix. + genFile := filepath.Join(dstDir, "helper.gen.go") + content, err := os.ReadFile(genFile) + if err != nil { + t.Fatalf("expected helper.gen.go to exist: %v", err) + } + + got := string(content) + + // Verify generated header is prepended. + if !strings.HasPrefix(got, generatedHeader) { + t.Errorf("missing generated header, starts with: %q", got[:min(len(got), 60)]) + } + + // Verify import rewriting happened (self-referencing import removed, qualifier stripped). + if strings.Contains(got, `"github.com/example/proto/test"`) { + t.Error("self-referencing import was not stripped") + } + if strings.Contains(got, "testpkg.Foo") { + t.Error("qualifier was not stripped from type references") + } + if !strings.Contains(got, "*Foo") { + t.Error("expected unqualified type reference *Foo") + } + + // Verify _test.go was not copied. + if _, err := os.Stat(filepath.Join(dstDir, "helper_test.gen.go")); err == nil { + t.Error("_test.go file should not be copied") + } + + // Verify non-Go file was not copied. + if _, err := os.Stat(filepath.Join(dstDir, "README.gen.go")); err == nil { + t.Error("non-Go file should not be copied") + } + + // Verify stale .gen.go was removed. + if _, err := os.Stat(staleFile); err == nil { + t.Error("stale old_helper.gen.go should have been removed") + } +} diff --git a/protocol/go/CHANGELOG.md b/protocol/go/CHANGELOG.md index b2e667ba07..52e301777e 100644 --- a/protocol/go/CHANGELOG.md +++ b/protocol/go/CHANGELOG.md @@ -1,5 +1,198 @@ # Changelog +## [0.32.0](https://github.com/opentdf/platform/compare/protocol/go/v0.31.0...protocol/go/v0.32.0) (2026-05-21) + + +### Features + +* **core:** add hybrid NIST EC + ML-KEM key wrapping support ([#3276](https://github.com/opentdf/platform/issues/3276)) ([1209acc](https://github.com/opentdf/platform/commit/1209acc2f8ae24af121f6a2892817c20ebb14d25)) + +## [0.31.0](https://github.com/opentdf/platform/compare/protocol/go/v0.30.0...protocol/go/v0.31.0) (2026-05-19) + + +### Bug Fixes + +* **core:** remove deprecated grpc-gateway ([#3479](https://github.com/opentdf/platform/issues/3479)) ([a4230a2](https://github.com/opentdf/platform/commit/a4230a215db71ff369d49216f0f9f61fdb6c042e)) + +## [0.30.0](https://github.com/opentdf/platform/compare/protocol/go/v0.29.0...protocol/go/v0.30.0) (2026-05-11) + + +### Features + +* **policy:** Add FQN to RegisteredResourceValues ([#3446](https://github.com/opentdf/platform/issues/3446)) ([3199583](https://github.com/opentdf/platform/commit/3199583c4a6454ac7eabe1260a142e5c5ff067ad)) +* **policy:** Add resource mapping group FQNs ([#3447](https://github.com/opentdf/platform/issues/3447)) ([6a0b3c6](https://github.com/opentdf/platform/commit/6a0b3c63795cf79b4d87d561464101c7cd2cf351)) + +## [0.29.0](https://github.com/opentdf/platform/compare/protocol/go/v0.28.0...protocol/go/v0.29.0) (2026-05-05) + + +### Features + +* **policy:** support inline obligation triggers on attribute value create ([#3432](https://github.com/opentdf/platform/issues/3432)) ([876f512](https://github.com/opentdf/platform/commit/876f512f9ff944cebd3b6d65c7937446a74ace87)) + +## [0.28.0](https://github.com/opentdf/platform/compare/protocol/go/v0.27.0...protocol/go/v0.28.0) (2026-04-28) + + +### Features + +* **sdk:** add shorthand enum constants for policy types ([#3408](https://github.com/opentdf/platform/issues/3408)) ([c6f18cb](https://github.com/opentdf/platform/commit/c6f18cbcacc7fa285d834f504a6ce43b7363295d)) + +## [0.27.0](https://github.com/opentdf/platform/compare/protocol/go/v0.26.0...protocol/go/v0.27.0) (2026-04-23) + + +### Features + +* **policy:** add sort support to listkaskeys ([#3344](https://github.com/opentdf/platform/issues/3344)) ([de1fe92](https://github.com/opentdf/platform/commit/de1fe926e306a15ff50fa0042b4fee988b3be1e6)) + +## [0.26.0](https://github.com/opentdf/platform/compare/protocol/go/v0.25.0...protocol/go/v0.26.0) (2026-04-22) + + +### Features + +* **sdk:** add ergonomic Resource constructors for authorization ([#3337](https://github.com/opentdf/platform/issues/3337)) ([4a786ca](https://github.com/opentdf/platform/commit/4a786cab530a9518086f8114f819442efad09b78)) + + +### Bug Fixes + +* **sdk:** require at least one FQN in ForAttributeValues ([#3355](https://github.com/opentdf/platform/issues/3355)) ([2529e11](https://github.com/opentdf/platform/commit/2529e117c5a0c60839fc2d50db6f69358034c700)) + +## [0.25.0](https://github.com/opentdf/platform/compare/protocol/go/v0.24.0...protocol/go/v0.25.0) (2026-04-20) + + +### Features + +* **policy:** Add sort support listregisteredresources api ([#3312](https://github.com/opentdf/platform/issues/3312)) ([91a3ff3](https://github.com/opentdf/platform/commit/91a3ff3686512353669e35e4884fde807d73d9b0)) + +## [0.24.0](https://github.com/opentdf/platform/compare/protocol/go/v0.23.0...protocol/go/v0.24.0) (2026-04-17) + + +### Features + +* **policy:** add GetObligationTrigger RPC ([#3318](https://github.com/opentdf/platform/issues/3318)) ([d68e39d](https://github.com/opentdf/platform/commit/d68e39d950d94dcbb98a2f16982ea57f28d9c550)) +* **policy:** add sort ListSubjectMappings API ([#3255](https://github.com/opentdf/platform/issues/3255)) ([9d5d757](https://github.com/opentdf/platform/commit/9d5d7570e22c6227409b01292f03c0d0624c1ce7)) +* **policy:** add sort support to ListKeyAccessServer ([#3287](https://github.com/opentdf/platform/issues/3287)) ([7fae2d7](https://github.com/opentdf/platform/commit/7fae2d701f3967b5ea743d4dc5ce0d41eb4d5413)) +* **policy:** add sort support to listobligations api ([#3300](https://github.com/opentdf/platform/issues/3300)) ([9221cac](https://github.com/opentdf/platform/commit/9221cac2f0a0c82847f0e7973b044f78a30450d8)) +* **policy:** add sort support to ListSubjectConditionSets API ([#3272](https://github.com/opentdf/platform/issues/3272)) ([9010f12](https://github.com/opentdf/platform/commit/9010f125eef244be2ac34906c59e68319d3b8f95)) + +## [0.23.0](https://github.com/opentdf/platform/compare/protocol/go/v0.22.0...protocol/go/v0.23.0) (2026-04-07) + + +### Features + +* **policy:** add sort support to ListAttributes API ([#3223](https://github.com/opentdf/platform/issues/3223)) ([ec3312f](https://github.com/opentdf/platform/commit/ec3312f622dec7ed18ffa6033c86b248b47a420a)) +* **sdk:** source-file codegen for EntityIdentifier helpers ([#3232](https://github.com/opentdf/platform/issues/3232)) ([ee8177c](https://github.com/opentdf/platform/commit/ee8177c98bda4e7483fa26be736fe4965c00bf46)) + +## [0.22.0](https://github.com/opentdf/platform/compare/protocol/go/v0.21.0...protocol/go/v0.22.0) (2026-04-01) + + +### Features + +* **policy:** Add sort support to ListNamespaces API ([#3192](https://github.com/opentdf/platform/issues/3192)) ([aac86cd](https://github.com/opentdf/platform/commit/aac86cdfbfc422149b62f85bbd752260b3a3dcd0)) +* **policy:** add SortField proto and update PageRequest for sort support ([#3187](https://github.com/opentdf/platform/issues/3187)) ([6cf1862](https://github.com/opentdf/platform/commit/6cf1862438c7e62fa676aa74160cfa533a1f6315)) + +## [0.21.0](https://github.com/opentdf/platform/compare/protocol/go/v0.20.0...protocol/go/v0.21.0) (2026-03-26) + + +### Features + +* **policy:** optional namespace for RRs ([#3165](https://github.com/opentdf/platform/issues/3165)) ([8948018](https://github.com/opentdf/platform/commit/89480186006085d2f59ebaeca6be6582db0e67d9)) + + +### Bug Fixes + +* **deps:** bump google.golang.org/grpc from 1.67.1 to 1.79.3 in /protocol/go ([#3173](https://github.com/opentdf/platform/issues/3173)) ([447ece6](https://github.com/opentdf/platform/commit/447ece6d458ecf88c9ca1149d05cce2552a0f883)) + +## [0.20.0](https://github.com/opentdf/platform/compare/protocol/go/v0.19.0...protocol/go/v0.20.0) (2026-03-18) + + +### ⚠ BREAKING CHANGES + +* **policy:** Namespace subject mappings and subject condition sets. ([#3143](https://github.com/opentdf/platform/issues/3143)) +* **policy:** Optional namespace on actions protos, NamespacedPolicy feature flag ([#3155](https://github.com/opentdf/platform/issues/3155)) + +### Features + +* **policy:** Namespace subject mappings and subject condition sets. ([#3143](https://github.com/opentdf/platform/issues/3143)) ([3006780](https://github.com/opentdf/platform/commit/3006780fea56f85b36223c134ae63a8afe109908)) + + +### Bug Fixes + +* **policy:** Optional namespace on actions protos, NamespacedPolicy feature flag ([#3155](https://github.com/opentdf/platform/issues/3155)) ([c20f039](https://github.com/opentdf/platform/commit/c20f039c6dc72bb7627075cf3cb330a6f03f2fec)) + +## [0.19.0](https://github.com/opentdf/platform/compare/protocol/go/v0.18.0...protocol/go/v0.19.0) (2026-03-12) + + +### ⚠ BREAKING CHANGES + +* **policy:** only require namespace on GetAction if no id provided ([#3144](https://github.com/opentdf/platform/issues/3144)) + +### Bug Fixes + +* **policy:** only require namespace on GetAction if no id provided ([#3144](https://github.com/opentdf/platform/issues/3144)) ([10d0c0f](https://github.com/opentdf/platform/commit/10d0c0f88cd7eff3620011bd75b6c2389aa4dfb8)) + +## [0.18.0](https://github.com/opentdf/platform/compare/protocol/go/v0.17.0...protocol/go/v0.18.0) (2026-03-12) + + +### ⚠ BREAKING CHANGES + +* **policy:** add namespace field to Actions proto ([#3130](https://github.com/opentdf/platform/issues/3130)) +* **policy:** namespace Registered Resources ([#3111](https://github.com/opentdf/platform/issues/3111)) + +### Features + +* **policy:** add namespace field to Actions proto ([#3130](https://github.com/opentdf/platform/issues/3130)) ([bedc9b3](https://github.com/opentdf/platform/commit/bedc9b35366104460c5fa5965819578232a3cb01)) +* **policy:** namespace Registered Resources ([#3111](https://github.com/opentdf/platform/issues/3111)) ([6db1883](https://github.com/opentdf/platform/commit/6db188380d3c44f578b6170f123cb9cb1597f4d8)) + + +### Bug Fixes + +* **ci:** Upgrade toolchain version to 1.25.8 ([#3116](https://github.com/opentdf/platform/issues/3116)) ([e1b7882](https://github.com/opentdf/platform/commit/e1b78822c0380a106e6eec05af78dc1fc9e5701f)) +* **policy:** deprecate ListAttributeValues in favor of existing GetAttribute ([#3108](https://github.com/opentdf/platform/issues/3108)) ([7e17c2d](https://github.com/opentdf/platform/commit/7e17c2d5ade62fb3b13265d17d663f928ced2df5)) + +## [0.17.0](https://github.com/opentdf/platform/compare/protocol/go/v0.16.0...protocol/go/v0.17.0) (2026-03-05) + + +### ⚠ BREAKING CHANGES + +* **policy:** add namespace field to RegisteredResource proto ([#3110](https://github.com/opentdf/platform/issues/3110)) + +### Features + +* **policy:** add namespace field to RegisteredResource proto ([#3110](https://github.com/opentdf/platform/issues/3110)) ([04fd85d](https://github.com/opentdf/platform/commit/04fd85d4b69b320f4dad9d21905864fba6708956)) + +## [0.16.0](https://github.com/opentdf/platform/compare/protocol/go/v0.15.0...protocol/go/v0.16.0) (2026-02-17) + + +### ⚠ BREAKING CHANGES + +* **policy:** remove namespace certificate feature ([#3051](https://github.com/opentdf/platform/issues/3051)) + +### Bug Fixes + +* Go 1.25 ([#3053](https://github.com/opentdf/platform/issues/3053)) ([65eb7c3](https://github.com/opentdf/platform/commit/65eb7c3d5fe1892de1e4fabb9b3b7894742c3f02)) + + +### Code Refactoring + +* **policy:** remove namespace certificate feature ([#3051](https://github.com/opentdf/platform/issues/3051)) ([48abb81](https://github.com/opentdf/platform/commit/48abb813ae7accbfcaa6e6ad4bb7071e3476716d)) + +## [0.15.0](https://github.com/opentdf/platform/compare/protocol/go/v0.14.0...protocol/go/v0.15.0) (2026-01-26) + + +### ⚠ BREAKING CHANGES + +* remove nanotdf support ([#3013](https://github.com/opentdf/platform/issues/3013)) + +### Features + +* **core:** add direct entitlement support ([#2630](https://github.com/opentdf/platform/issues/2630)) ([cc8337a](https://github.com/opentdf/platform/commit/cc8337a4d4b6be4cb1f4117711109c2d8d599cb9)) +* **policy:** add allow_traversal to attribute definitions ([#3014](https://github.com/opentdf/platform/issues/3014)) ([bbbe21b](https://github.com/opentdf/platform/commit/bbbe21bb671f5ffedd116a08ff15779ce7034fcb)) + + +### Bug Fixes + +* Connect RPC v1.19.1 ([#3009](https://github.com/opentdf/platform/issues/3009)) ([c354fd3](https://github.com/opentdf/platform/commit/c354fd387f2e17f764feacf302488d9afdbac5f0)) +* remove nanotdf support ([#3013](https://github.com/opentdf/platform/issues/3013)) ([90ff7ce](https://github.com/opentdf/platform/commit/90ff7ce50754a1f37ba1cc530507c1f6e15930a0)) + ## [0.14.0](https://github.com/opentdf/platform/compare/protocol/go/v0.13.0...protocol/go/v0.14.0) (2025-12-19) diff --git a/protocol/go/authorization/authorization.pb.go b/protocol/go/authorization/authorization.pb.go index 210f7213fe..85577910f8 100644 --- a/protocol/go/authorization/authorization.pb.go +++ b/protocol/go/authorization/authorization.pb.go @@ -8,7 +8,6 @@ package authorization import ( policy "github.com/opentdf/platform/protocol/go/policy" - _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" anypb "google.golang.org/protobuf/types/known/anypb" @@ -1229,199 +1228,192 @@ var file_authorization_authorization_proto_rawDesc = []byte{ 0x0a, 0x21, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, - 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x29, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x77, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6a, 0x77, 0x74, 0x22, 0xc9, 0x03, 0x0a, - 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x25, 0x0a, 0x0d, 0x65, 0x6d, 0x61, 0x69, 0x6c, - 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, - 0x52, 0x0c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1d, - 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, - 0x11, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x55, 0x72, 0x6c, 0x12, 0x14, 0x0a, 0x04, 0x75, - 0x75, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x75, 0x75, 0x69, - 0x64, 0x12, 0x2e, 0x0a, 0x06, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6c, 0x61, 0x69, 0x6d, - 0x73, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x00, - 0x52, 0x06, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x12, 0x1d, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x3a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, - 0x6f, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, - 0x6f, 0x72, 0x79, 0x22, 0x54, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, - 0x18, 0x0a, 0x14, 0x43, 0x41, 0x54, 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x41, 0x54, - 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x01, 0x12, - 0x18, 0x0a, 0x14, 0x43, 0x41, 0x54, 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x45, 0x4e, 0x56, 0x49, - 0x52, 0x4f, 0x4e, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x02, 0x42, 0x0d, 0x0a, 0x0b, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x42, 0x0a, 0x0c, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x12, 0x32, 0x0a, 0x09, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, - 0x79, 0x52, 0x09, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x50, 0x0a, 0x0b, - 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x08, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0xcf, - 0x01, 0x0a, 0x0f, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3f, 0x0a, 0x0d, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, - 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x51, 0x0a, - 0x13, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x12, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x22, 0xce, 0x02, 0x0a, 0x10, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, - 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, - 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x44, 0x0a, 0x08, 0x64, - 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, - 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, - 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x08, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x18, 0x0a, 0x14, 0x44, 0x45, 0x43, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x45, 0x43, - 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x4e, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, - 0x44, 0x45, 0x43, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x45, 0x52, 0x4d, 0x49, 0x54, 0x10, - 0x02, 0x22, 0x62, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4b, 0x0a, 0x11, 0x64, 0x65, 0x63, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x52, 0x10, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x73, 0x22, 0x66, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, - 0x12, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x11, 0x64, 0x65, 0x63, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x22, 0xfa, 0x01, - 0x0a, 0x16, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x05, 0x73, - 0x63, 0x6f, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, - 0x73, 0x63, 0x6f, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x1c, 0x77, 0x69, 0x74, 0x68, - 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x68, 0x65, 0x6e, 0x73, 0x69, 0x76, 0x65, 0x5f, 0x68, - 0x69, 0x65, 0x72, 0x61, 0x72, 0x63, 0x68, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, - 0x52, 0x1a, 0x77, 0x69, 0x74, 0x68, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x68, 0x65, 0x6e, 0x73, - 0x69, 0x76, 0x65, 0x48, 0x69, 0x65, 0x72, 0x61, 0x72, 0x63, 0x68, 0x79, 0x88, 0x01, 0x01, 0x42, - 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x42, 0x1f, 0x0a, 0x1d, 0x5f, 0x77, 0x69, - 0x74, 0x68, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x68, 0x65, 0x6e, 0x73, 0x69, 0x76, 0x65, - 0x5f, 0x68, 0x69, 0x65, 0x72, 0x61, 0x72, 0x63, 0x68, 0x79, 0x22, 0x63, 0x0a, 0x12, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x30, 0x0a, - 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x5f, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x71, 0x6e, 0x73, 0x22, - 0x7b, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x71, - 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x71, 0x6e, 0x73, 0x22, 0x60, 0x0a, 0x17, - 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xc1, - 0x01, 0x0a, 0x14, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x2c, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, + 0x6f, 0x6e, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x29, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, + 0x6a, 0x77, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6a, 0x77, 0x74, 0x22, 0xc9, + 0x03, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x25, 0x0a, 0x0d, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x0c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x1d, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x2c, 0x0a, 0x11, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, + 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x55, 0x72, 0x6c, 0x12, 0x14, 0x0a, + 0x04, 0x75, 0x75, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x75, + 0x75, 0x69, 0x64, 0x12, 0x2e, 0x0a, 0x06, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6c, 0x61, + 0x69, 0x6d, 0x73, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x48, 0x00, 0x52, 0x06, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x12, 0x1d, 0x0a, 0x09, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x3a, 0x0a, 0x08, 0x63, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x79, 0x22, 0x54, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, + 0x79, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x41, 0x54, 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, + 0x41, 0x54, 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, + 0x01, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x41, 0x54, 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x45, 0x4e, + 0x56, 0x49, 0x52, 0x4f, 0x4e, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x02, 0x42, 0x0d, 0x0a, 0x0b, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x42, 0x0a, 0x0c, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x12, 0x32, 0x0a, 0x09, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x41, 0x6e, 0x79, 0x52, 0x09, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x50, + 0x0a, 0x0b, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, + 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, + 0x22, 0xcf, 0x01, 0x0a, 0x0f, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3f, + 0x0a, 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, + 0x6e, 0x52, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x51, 0x0a, 0x13, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x22, 0x6e, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x50, 0x0a, 0x11, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x52, 0x10, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x73, 0x22, 0x6d, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x4e, 0x0a, 0x12, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, - 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x11, - 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x73, 0x32, 0x9c, 0x03, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x75, 0x0a, 0x0c, 0x47, 0x65, - 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, - 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, + 0x65, 0x73, 0x22, 0xce, 0x02, 0x0a, 0x10, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, + 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x44, 0x0a, + 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x28, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x08, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x18, 0x0a, 0x14, 0x44, 0x45, 0x43, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x44, + 0x45, 0x43, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x4e, 0x59, 0x10, 0x01, 0x12, 0x13, + 0x0a, 0x0f, 0x44, 0x45, 0x43, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x45, 0x52, 0x4d, 0x49, + 0x54, 0x10, 0x02, 0x22, 0x62, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4b, 0x0a, 0x11, 0x64, 0x65, + 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x10, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x22, 0x66, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x44, 0x65, + 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4e, 0x0a, 0x12, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x11, 0x64, 0x65, + 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x22, + 0xfa, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x3b, 0x0a, + 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x48, 0x00, + 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x1c, 0x77, 0x69, + 0x74, 0x68, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x68, 0x65, 0x6e, 0x73, 0x69, 0x76, 0x65, + 0x5f, 0x68, 0x69, 0x65, 0x72, 0x61, 0x72, 0x63, 0x68, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x48, 0x01, 0x52, 0x1a, 0x77, 0x69, 0x74, 0x68, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x68, 0x65, + 0x6e, 0x73, 0x69, 0x76, 0x65, 0x48, 0x69, 0x65, 0x72, 0x61, 0x72, 0x63, 0x68, 0x79, 0x88, 0x01, + 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x42, 0x1f, 0x0a, 0x1d, 0x5f, + 0x77, 0x69, 0x74, 0x68, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x68, 0x65, 0x6e, 0x73, 0x69, + 0x76, 0x65, 0x5f, 0x68, 0x69, 0x65, 0x72, 0x61, 0x72, 0x63, 0x68, 0x79, 0x22, 0x63, 0x0a, 0x12, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, + 0x30, 0x0a, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x71, 0x6e, + 0x73, 0x22, 0x7b, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, + 0x66, 0x71, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x71, 0x6e, 0x73, 0x22, 0x60, + 0x0a, 0x17, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0c, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x52, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x22, 0xc1, 0x01, 0x0a, 0x14, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x12, 0x51, 0x0a, 0x13, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x52, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x22, 0x6e, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x50, 0x0a, 0x11, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x10, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x73, 0x22, 0x6d, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x12, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x52, 0x11, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x73, 0x32, 0xc5, 0x02, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x59, 0x0a, 0x0c, + 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, + 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x23, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6e, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x65, + 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, - 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x3a, 0x01, 0x2a, 0x22, 0x11, - 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x8d, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x7d, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x3a, 0x01, 0x2a, 0x22, 0x10, - 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x42, 0xb2, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x12, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, - 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xca, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xe2, 0x02, 0x19, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xb2, 0x01, 0x0a, 0x11, + 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x42, 0x12, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, + 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, + 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xa2, 0x02, + 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0xca, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0xe2, 0x02, 0x19, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0xea, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protocol/go/authorization/authorization.pb.gw.go b/protocol/go/authorization/authorization.pb.gw.go deleted file mode 100644 index 036c774561..0000000000 --- a/protocol/go/authorization/authorization.pb.gw.go +++ /dev/null @@ -1,327 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: authorization/authorization.proto - -/* -Package authorization is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package authorization - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -func request_AuthorizationService_GetDecisions_0(ctx context.Context, marshaler runtime.Marshaler, client AuthorizationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetDecisionsRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.GetDecisions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_AuthorizationService_GetDecisions_0(ctx context.Context, marshaler runtime.Marshaler, server AuthorizationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetDecisionsRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.GetDecisions(ctx, &protoReq) - return msg, metadata, err - -} - -var ( - filter_AuthorizationService_GetDecisionsByToken_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} -) - -func request_AuthorizationService_GetDecisionsByToken_0(ctx context.Context, marshaler runtime.Marshaler, client AuthorizationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetDecisionsByTokenRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AuthorizationService_GetDecisionsByToken_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.GetDecisionsByToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_AuthorizationService_GetDecisionsByToken_0(ctx context.Context, marshaler runtime.Marshaler, server AuthorizationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetDecisionsByTokenRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AuthorizationService_GetDecisionsByToken_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.GetDecisionsByToken(ctx, &protoReq) - return msg, metadata, err - -} - -func request_AuthorizationService_GetEntitlements_0(ctx context.Context, marshaler runtime.Marshaler, client AuthorizationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetEntitlementsRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.GetEntitlements(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_AuthorizationService_GetEntitlements_0(ctx context.Context, marshaler runtime.Marshaler, server AuthorizationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetEntitlementsRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.GetEntitlements(ctx, &protoReq) - return msg, metadata, err - -} - -// RegisterAuthorizationServiceHandlerServer registers the http handlers for service AuthorizationService to "mux". -// UnaryRPC :call AuthorizationServiceServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthorizationServiceHandlerFromEndpoint instead. -func RegisterAuthorizationServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthorizationServiceServer) error { - - mux.Handle("POST", pattern_AuthorizationService_GetDecisions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/authorization.AuthorizationService/GetDecisions", runtime.WithHTTPPathPattern("/v1/authorization")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_AuthorizationService_GetDecisions_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AuthorizationService_GetDecisions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_AuthorizationService_GetDecisionsByToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/authorization.AuthorizationService/GetDecisionsByToken", runtime.WithHTTPPathPattern("/v1/token/authorization")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_AuthorizationService_GetDecisionsByToken_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AuthorizationService_GetDecisionsByToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_AuthorizationService_GetEntitlements_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/authorization.AuthorizationService/GetEntitlements", runtime.WithHTTPPathPattern("/v1/entitlements")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_AuthorizationService_GetEntitlements_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AuthorizationService_GetEntitlements_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterAuthorizationServiceHandlerFromEndpoint is same as RegisterAuthorizationServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterAuthorizationServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterAuthorizationServiceHandler(ctx, mux, conn) -} - -// RegisterAuthorizationServiceHandler registers the http handlers for service AuthorizationService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterAuthorizationServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterAuthorizationServiceHandlerClient(ctx, mux, NewAuthorizationServiceClient(conn)) -} - -// RegisterAuthorizationServiceHandlerClient registers the http handlers for service AuthorizationService -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthorizationServiceClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthorizationServiceClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "AuthorizationServiceClient" to call the correct interceptors. -func RegisterAuthorizationServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthorizationServiceClient) error { - - mux.Handle("POST", pattern_AuthorizationService_GetDecisions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/authorization.AuthorizationService/GetDecisions", runtime.WithHTTPPathPattern("/v1/authorization")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_AuthorizationService_GetDecisions_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AuthorizationService_GetDecisions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_AuthorizationService_GetDecisionsByToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/authorization.AuthorizationService/GetDecisionsByToken", runtime.WithHTTPPathPattern("/v1/token/authorization")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_AuthorizationService_GetDecisionsByToken_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AuthorizationService_GetDecisionsByToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_AuthorizationService_GetEntitlements_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/authorization.AuthorizationService/GetEntitlements", runtime.WithHTTPPathPattern("/v1/entitlements")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_AuthorizationService_GetEntitlements_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AuthorizationService_GetEntitlements_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_AuthorizationService_GetDecisions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "authorization"}, "")) - - pattern_AuthorizationService_GetDecisionsByToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "token", "authorization"}, "")) - - pattern_AuthorizationService_GetEntitlements_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "entitlements"}, "")) -) - -var ( - forward_AuthorizationService_GetDecisions_0 = runtime.ForwardResponseMessage - - forward_AuthorizationService_GetDecisionsByToken_0 = runtime.ForwardResponseMessage - - forward_AuthorizationService_GetEntitlements_0 = runtime.ForwardResponseMessage -) diff --git a/protocol/go/authorization/authorizationconnect/authorization.connect.go b/protocol/go/authorization/authorizationconnect/authorization.connect.go index dd5efadb4d..87cb6c1412 100644 --- a/protocol/go/authorization/authorizationconnect/authorization.connect.go +++ b/protocol/go/authorization/authorizationconnect/authorization.connect.go @@ -44,14 +44,6 @@ const ( AuthorizationServiceGetEntitlementsProcedure = "/authorization.AuthorizationService/GetEntitlements" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - authorizationServiceServiceDescriptor = authorization.File_authorization_authorization_proto.Services().ByName("AuthorizationService") - authorizationServiceGetDecisionsMethodDescriptor = authorizationServiceServiceDescriptor.Methods().ByName("GetDecisions") - authorizationServiceGetDecisionsByTokenMethodDescriptor = authorizationServiceServiceDescriptor.Methods().ByName("GetDecisionsByToken") - authorizationServiceGetEntitlementsMethodDescriptor = authorizationServiceServiceDescriptor.Methods().ByName("GetEntitlements") -) - // AuthorizationServiceClient is a client for the authorization.AuthorizationService service. type AuthorizationServiceClient interface { GetDecisions(context.Context, *connect.Request[authorization.GetDecisionsRequest]) (*connect.Response[authorization.GetDecisionsResponse], error) @@ -68,23 +60,24 @@ type AuthorizationServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewAuthorizationServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AuthorizationServiceClient { baseURL = strings.TrimRight(baseURL, "/") + authorizationServiceMethods := authorization.File_authorization_authorization_proto.Services().ByName("AuthorizationService").Methods() return &authorizationServiceClient{ getDecisions: connect.NewClient[authorization.GetDecisionsRequest, authorization.GetDecisionsResponse]( httpClient, baseURL+AuthorizationServiceGetDecisionsProcedure, - connect.WithSchema(authorizationServiceGetDecisionsMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecisions")), connect.WithClientOptions(opts...), ), getDecisionsByToken: connect.NewClient[authorization.GetDecisionsByTokenRequest, authorization.GetDecisionsByTokenResponse]( httpClient, baseURL+AuthorizationServiceGetDecisionsByTokenProcedure, - connect.WithSchema(authorizationServiceGetDecisionsByTokenMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecisionsByToken")), connect.WithClientOptions(opts...), ), getEntitlements: connect.NewClient[authorization.GetEntitlementsRequest, authorization.GetEntitlementsResponse]( httpClient, baseURL+AuthorizationServiceGetEntitlementsProcedure, - connect.WithSchema(authorizationServiceGetEntitlementsMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetEntitlements")), connect.WithClientOptions(opts...), ), } @@ -126,22 +119,23 @@ type AuthorizationServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewAuthorizationServiceHandler(svc AuthorizationServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + authorizationServiceMethods := authorization.File_authorization_authorization_proto.Services().ByName("AuthorizationService").Methods() authorizationServiceGetDecisionsHandler := connect.NewUnaryHandler( AuthorizationServiceGetDecisionsProcedure, svc.GetDecisions, - connect.WithSchema(authorizationServiceGetDecisionsMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecisions")), connect.WithHandlerOptions(opts...), ) authorizationServiceGetDecisionsByTokenHandler := connect.NewUnaryHandler( AuthorizationServiceGetDecisionsByTokenProcedure, svc.GetDecisionsByToken, - connect.WithSchema(authorizationServiceGetDecisionsByTokenMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecisionsByToken")), connect.WithHandlerOptions(opts...), ) authorizationServiceGetEntitlementsHandler := connect.NewUnaryHandler( AuthorizationServiceGetEntitlementsProcedure, svc.GetEntitlements, - connect.WithSchema(authorizationServiceGetEntitlementsMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetEntitlements")), connect.WithHandlerOptions(opts...), ) return "/authorization.AuthorizationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/authorization/v2/authorizationv2connect/authorization.connect.go b/protocol/go/authorization/v2/authorizationv2connect/authorization.connect.go index 43bd8db6de..17a91a8155 100644 --- a/protocol/go/authorization/v2/authorizationv2connect/authorization.connect.go +++ b/protocol/go/authorization/v2/authorizationv2connect/authorization.connect.go @@ -47,15 +47,6 @@ const ( AuthorizationServiceGetEntitlementsProcedure = "/authorization.v2.AuthorizationService/GetEntitlements" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - authorizationServiceServiceDescriptor = v2.File_authorization_v2_authorization_proto.Services().ByName("AuthorizationService") - authorizationServiceGetDecisionMethodDescriptor = authorizationServiceServiceDescriptor.Methods().ByName("GetDecision") - authorizationServiceGetDecisionMultiResourceMethodDescriptor = authorizationServiceServiceDescriptor.Methods().ByName("GetDecisionMultiResource") - authorizationServiceGetDecisionBulkMethodDescriptor = authorizationServiceServiceDescriptor.Methods().ByName("GetDecisionBulk") - authorizationServiceGetEntitlementsMethodDescriptor = authorizationServiceServiceDescriptor.Methods().ByName("GetEntitlements") -) - // AuthorizationServiceClient is a client for the authorization.v2.AuthorizationService service. type AuthorizationServiceClient interface { GetDecision(context.Context, *connect.Request[v2.GetDecisionRequest]) (*connect.Response[v2.GetDecisionResponse], error) @@ -73,29 +64,30 @@ type AuthorizationServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewAuthorizationServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AuthorizationServiceClient { baseURL = strings.TrimRight(baseURL, "/") + authorizationServiceMethods := v2.File_authorization_v2_authorization_proto.Services().ByName("AuthorizationService").Methods() return &authorizationServiceClient{ getDecision: connect.NewClient[v2.GetDecisionRequest, v2.GetDecisionResponse]( httpClient, baseURL+AuthorizationServiceGetDecisionProcedure, - connect.WithSchema(authorizationServiceGetDecisionMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecision")), connect.WithClientOptions(opts...), ), getDecisionMultiResource: connect.NewClient[v2.GetDecisionMultiResourceRequest, v2.GetDecisionMultiResourceResponse]( httpClient, baseURL+AuthorizationServiceGetDecisionMultiResourceProcedure, - connect.WithSchema(authorizationServiceGetDecisionMultiResourceMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecisionMultiResource")), connect.WithClientOptions(opts...), ), getDecisionBulk: connect.NewClient[v2.GetDecisionBulkRequest, v2.GetDecisionBulkResponse]( httpClient, baseURL+AuthorizationServiceGetDecisionBulkProcedure, - connect.WithSchema(authorizationServiceGetDecisionBulkMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecisionBulk")), connect.WithClientOptions(opts...), ), getEntitlements: connect.NewClient[v2.GetEntitlementsRequest, v2.GetEntitlementsResponse]( httpClient, baseURL+AuthorizationServiceGetEntitlementsProcedure, - connect.WithSchema(authorizationServiceGetEntitlementsMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetEntitlements")), connect.WithClientOptions(opts...), ), } @@ -144,28 +136,29 @@ type AuthorizationServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewAuthorizationServiceHandler(svc AuthorizationServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + authorizationServiceMethods := v2.File_authorization_v2_authorization_proto.Services().ByName("AuthorizationService").Methods() authorizationServiceGetDecisionHandler := connect.NewUnaryHandler( AuthorizationServiceGetDecisionProcedure, svc.GetDecision, - connect.WithSchema(authorizationServiceGetDecisionMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecision")), connect.WithHandlerOptions(opts...), ) authorizationServiceGetDecisionMultiResourceHandler := connect.NewUnaryHandler( AuthorizationServiceGetDecisionMultiResourceProcedure, svc.GetDecisionMultiResource, - connect.WithSchema(authorizationServiceGetDecisionMultiResourceMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecisionMultiResource")), connect.WithHandlerOptions(opts...), ) authorizationServiceGetDecisionBulkHandler := connect.NewUnaryHandler( AuthorizationServiceGetDecisionBulkProcedure, svc.GetDecisionBulk, - connect.WithSchema(authorizationServiceGetDecisionBulkMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetDecisionBulk")), connect.WithHandlerOptions(opts...), ) authorizationServiceGetEntitlementsHandler := connect.NewUnaryHandler( AuthorizationServiceGetEntitlementsProcedure, svc.GetEntitlements, - connect.WithSchema(authorizationServiceGetEntitlementsMethodDescriptor), + connect.WithSchema(authorizationServiceMethods.ByName("GetEntitlements")), connect.WithHandlerOptions(opts...), ) return "/authorization.v2.AuthorizationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/authorization/v2/entity_identifier.gen.go b/protocol/go/authorization/v2/entity_identifier.gen.go new file mode 100644 index 0000000000..c226fb5a56 --- /dev/null +++ b/protocol/go/authorization/v2/entity_identifier.gen.go @@ -0,0 +1,64 @@ +// Code generated by protocol/codegen. DO NOT EDIT. + +package authorizationv2 + +import ( + "github.com/opentdf/platform/protocol/go/entity" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// ForToken returns an EntityIdentifier that resolves the entity from the given JWT. +// The authorization service will parse the token to derive the entity chain. +func ForToken(jwt string) *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_Token{ + Token: &entity.Token{ + Jwt: jwt, + }, + }, + } +} + +// WithRequestToken returns an EntityIdentifier that instructs the authorization +// service to derive the entity from the request's Authorization header token. +func WithRequestToken() *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_WithRequestToken{ + WithRequestToken: wrapperspb.Bool(true), + }, + } +} + +// ForClientID returns an EntityIdentifier for a single subject entity identified by client ID. +func ForClientID(clientID string) *EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_ClientId{ClientId: clientID}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +// ForEmail returns an EntityIdentifier for a single subject entity identified by email address. +func ForEmail(email string) *EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_EmailAddress{EmailAddress: email}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +// ForUserName returns an EntityIdentifier for a single subject entity identified by username. +func ForUserName(username string) *EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_UserName{UserName: username}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +func entityIdentifierFromEntity(e *entity.Entity) *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{e}, + }, + }, + } +} diff --git a/protocol/go/authorization/v2/resource.gen.go b/protocol/go/authorization/v2/resource.gen.go new file mode 100644 index 0000000000..14402eb7d4 --- /dev/null +++ b/protocol/go/authorization/v2/resource.gen.go @@ -0,0 +1,30 @@ +// Code generated by protocol/codegen. DO NOT EDIT. + +package authorizationv2 + +// ForAttributeValues returns a Resource containing the given attribute value FQNs. +// This is the most common Resource variant, used when authorizing against +// attribute values attached to data (e.g. those on a TDF). +// At least one FQN is required; calling with zero arguments panics. +func ForAttributeValues(fqns ...string) *Resource { + if len(fqns) == 0 { + panic("ForAttributeValues requires at least one FQN") + } + return &Resource{ + Resource: &Resource_AttributeValues_{ + AttributeValues: &Resource_AttributeValues{ + Fqns: fqns, + }, + }, + } +} + +// ForRegisteredResourceValueFqn returns a Resource that references a single +// registered resource value by its fully qualified name, as stored in platform policy. +func ForRegisteredResourceValueFqn(fqn string) *Resource { + return &Resource{ + Resource: &Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: fqn, + }, + } +} diff --git a/protocol/go/entityresolution/entity_resolution.pb.go b/protocol/go/entityresolution/entity_resolution.pb.go index afc990717c..54c0ebb9ba 100644 --- a/protocol/go/entityresolution/entity_resolution.pb.go +++ b/protocol/go/entityresolution/entity_resolution.pb.go @@ -8,7 +8,6 @@ package entityresolution import ( authorization "github.com/opentdf/platform/protocol/go/authorization" - _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" anypb "google.golang.org/protobuf/types/known/anypb" @@ -402,87 +401,80 @@ var file_entityresolution_entity_resolution_proto_rawDesc = []byte{ 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x21, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, - 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, - 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x4b, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, - 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x31, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x69, 0x65, 0x73, 0x22, 0x7b, 0x0a, 0x14, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x70, - 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x42, 0x0a, 0x10, 0x61, - 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x72, 0x6f, 0x70, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0f, - 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x70, 0x73, 0x12, - 0x1f, 0x0a, 0x0b, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x49, 0x64, - 0x22, 0x78, 0x0a, 0x17, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, 0x16, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x72, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x15, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x70, 0x72, 0x65, - 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x8b, 0x01, 0x0a, 0x13, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, - 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x4f, 0x0a, 0x1f, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x72, 0x6f, - 0x6d, 0x4a, 0x77, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x06, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x63, 0x0a, 0x20, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x72, - 0x6f, 0x6d, 0x4a, 0x77, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, - 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x52, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x32, 0xd6, - 0x02, 0x0a, 0x17, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x8c, 0x01, 0x0a, 0x0f, 0x52, - 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x28, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x3a, 0x01, 0x2a, 0x22, 0x19, - 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, - 0x6e, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x12, 0xab, 0x01, 0x0a, 0x18, 0x43, 0x72, + 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, + 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x4b, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, + 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0x7b, 0x0a, 0x14, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x42, 0x0a, + 0x10, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x72, 0x6f, 0x70, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, + 0x52, 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x70, + 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, + 0x49, 0x64, 0x22, 0x78, 0x0a, 0x17, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, + 0x16, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x72, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x15, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x70, + 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x8b, 0x01, 0x0a, + 0x13, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, + 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x4f, 0x0a, 0x1f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x46, - 0x72, 0x6f, 0x6d, 0x4a, 0x77, 0x74, 0x12, 0x31, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, + 0x72, 0x6f, 0x6d, 0x4a, 0x77, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, + 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x63, 0x0a, 0x20, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, + 0x46, 0x72, 0x6f, 0x6d, 0x4a, 0x77, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x3f, 0x0a, 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, + 0x69, 0x6e, 0x52, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x73, + 0x32, 0x89, 0x02, 0x0a, 0x17, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x6c, + 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x68, 0x0a, 0x0f, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, + 0x28, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x6c, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x83, 0x01, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x72, 0x6f, 0x6d, + 0x4a, 0x77, 0x74, 0x12, 0x31, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, + 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x72, 0x6f, 0x6d, 0x4a, 0x77, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x72, 0x6f, 0x6d, 0x4a, - 0x77, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x72, - 0x6f, 0x6d, 0x4a, 0x77, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x22, 0x3a, 0x01, 0x2a, 0x22, 0x1d, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x42, 0xc7, 0x01, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, - 0x42, 0x15, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, - 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, - 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, - 0x67, 0x6f, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, - 0x69, 0x6f, 0x6e, 0xa2, 0x02, 0x03, 0x45, 0x58, 0x58, 0xaa, 0x02, 0x10, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0xca, 0x02, 0x10, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0xe2, - 0x02, 0x1c, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, - 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x77, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xc7, 0x01, 0x0a, + 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, + 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x15, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x38, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, + 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, + 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0xa2, 0x02, 0x03, 0x45, 0x58, 0x58, 0xaa, 0x02, 0x10, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, - 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0xca, 0x02, 0x10, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0xe2, 0x02, 0x1c, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, + 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x10, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x72, 0x65, 0x73, 0x6f, + 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protocol/go/entityresolution/entity_resolution.pb.gw.go b/protocol/go/entityresolution/entity_resolution.pb.gw.go deleted file mode 100644 index 2f4f3c416a..0000000000 --- a/protocol/go/entityresolution/entity_resolution.pb.gw.go +++ /dev/null @@ -1,240 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: entityresolution/entity_resolution.proto - -/* -Package entityresolution is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package entityresolution - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -func request_EntityResolutionService_ResolveEntities_0(ctx context.Context, marshaler runtime.Marshaler, client EntityResolutionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ResolveEntitiesRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.ResolveEntities(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_EntityResolutionService_ResolveEntities_0(ctx context.Context, marshaler runtime.Marshaler, server EntityResolutionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ResolveEntitiesRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.ResolveEntities(ctx, &protoReq) - return msg, metadata, err - -} - -func request_EntityResolutionService_CreateEntityChainFromJwt_0(ctx context.Context, marshaler runtime.Marshaler, client EntityResolutionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq CreateEntityChainFromJwtRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.CreateEntityChainFromJwt(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_EntityResolutionService_CreateEntityChainFromJwt_0(ctx context.Context, marshaler runtime.Marshaler, server EntityResolutionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq CreateEntityChainFromJwtRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.CreateEntityChainFromJwt(ctx, &protoReq) - return msg, metadata, err - -} - -// RegisterEntityResolutionServiceHandlerServer registers the http handlers for service EntityResolutionService to "mux". -// UnaryRPC :call EntityResolutionServiceServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterEntityResolutionServiceHandlerFromEndpoint instead. -func RegisterEntityResolutionServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server EntityResolutionServiceServer) error { - - mux.Handle("POST", pattern_EntityResolutionService_ResolveEntities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/entityresolution.EntityResolutionService/ResolveEntities", runtime.WithHTTPPathPattern("/entityresolution/resolve")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_EntityResolutionService_ResolveEntities_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_EntityResolutionService_ResolveEntities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_EntityResolutionService_CreateEntityChainFromJwt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/entityresolution.EntityResolutionService/CreateEntityChainFromJwt", runtime.WithHTTPPathPattern("/entityresolution/entitychain")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_EntityResolutionService_CreateEntityChainFromJwt_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_EntityResolutionService_CreateEntityChainFromJwt_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterEntityResolutionServiceHandlerFromEndpoint is same as RegisterEntityResolutionServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterEntityResolutionServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterEntityResolutionServiceHandler(ctx, mux, conn) -} - -// RegisterEntityResolutionServiceHandler registers the http handlers for service EntityResolutionService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterEntityResolutionServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterEntityResolutionServiceHandlerClient(ctx, mux, NewEntityResolutionServiceClient(conn)) -} - -// RegisterEntityResolutionServiceHandlerClient registers the http handlers for service EntityResolutionService -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "EntityResolutionServiceClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "EntityResolutionServiceClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "EntityResolutionServiceClient" to call the correct interceptors. -func RegisterEntityResolutionServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client EntityResolutionServiceClient) error { - - mux.Handle("POST", pattern_EntityResolutionService_ResolveEntities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/entityresolution.EntityResolutionService/ResolveEntities", runtime.WithHTTPPathPattern("/entityresolution/resolve")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_EntityResolutionService_ResolveEntities_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_EntityResolutionService_ResolveEntities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_EntityResolutionService_CreateEntityChainFromJwt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/entityresolution.EntityResolutionService/CreateEntityChainFromJwt", runtime.WithHTTPPathPattern("/entityresolution/entitychain")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_EntityResolutionService_CreateEntityChainFromJwt_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_EntityResolutionService_CreateEntityChainFromJwt_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_EntityResolutionService_ResolveEntities_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"entityresolution", "resolve"}, "")) - - pattern_EntityResolutionService_CreateEntityChainFromJwt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"entityresolution", "entitychain"}, "")) -) - -var ( - forward_EntityResolutionService_ResolveEntities_0 = runtime.ForwardResponseMessage - - forward_EntityResolutionService_CreateEntityChainFromJwt_0 = runtime.ForwardResponseMessage -) diff --git a/protocol/go/entityresolution/entityresolutionconnect/entity_resolution.connect.go b/protocol/go/entityresolution/entityresolutionconnect/entity_resolution.connect.go index 85e82134c5..2f9616c5d3 100644 --- a/protocol/go/entityresolution/entityresolutionconnect/entity_resolution.connect.go +++ b/protocol/go/entityresolution/entityresolutionconnect/entity_resolution.connect.go @@ -41,13 +41,6 @@ const ( EntityResolutionServiceCreateEntityChainFromJwtProcedure = "/entityresolution.EntityResolutionService/CreateEntityChainFromJwt" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - entityResolutionServiceServiceDescriptor = entityresolution.File_entityresolution_entity_resolution_proto.Services().ByName("EntityResolutionService") - entityResolutionServiceResolveEntitiesMethodDescriptor = entityResolutionServiceServiceDescriptor.Methods().ByName("ResolveEntities") - entityResolutionServiceCreateEntityChainFromJwtMethodDescriptor = entityResolutionServiceServiceDescriptor.Methods().ByName("CreateEntityChainFromJwt") -) - // EntityResolutionServiceClient is a client for the entityresolution.EntityResolutionService // service. type EntityResolutionServiceClient interface { @@ -66,17 +59,18 @@ type EntityResolutionServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewEntityResolutionServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) EntityResolutionServiceClient { baseURL = strings.TrimRight(baseURL, "/") + entityResolutionServiceMethods := entityresolution.File_entityresolution_entity_resolution_proto.Services().ByName("EntityResolutionService").Methods() return &entityResolutionServiceClient{ resolveEntities: connect.NewClient[entityresolution.ResolveEntitiesRequest, entityresolution.ResolveEntitiesResponse]( httpClient, baseURL+EntityResolutionServiceResolveEntitiesProcedure, - connect.WithSchema(entityResolutionServiceResolveEntitiesMethodDescriptor), + connect.WithSchema(entityResolutionServiceMethods.ByName("ResolveEntities")), connect.WithClientOptions(opts...), ), createEntityChainFromJwt: connect.NewClient[entityresolution.CreateEntityChainFromJwtRequest, entityresolution.CreateEntityChainFromJwtResponse]( httpClient, baseURL+EntityResolutionServiceCreateEntityChainFromJwtProcedure, - connect.WithSchema(entityResolutionServiceCreateEntityChainFromJwtMethodDescriptor), + connect.WithSchema(entityResolutionServiceMethods.ByName("CreateEntityChainFromJwt")), connect.WithClientOptions(opts...), ), } @@ -113,16 +107,17 @@ type EntityResolutionServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewEntityResolutionServiceHandler(svc EntityResolutionServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + entityResolutionServiceMethods := entityresolution.File_entityresolution_entity_resolution_proto.Services().ByName("EntityResolutionService").Methods() entityResolutionServiceResolveEntitiesHandler := connect.NewUnaryHandler( EntityResolutionServiceResolveEntitiesProcedure, svc.ResolveEntities, - connect.WithSchema(entityResolutionServiceResolveEntitiesMethodDescriptor), + connect.WithSchema(entityResolutionServiceMethods.ByName("ResolveEntities")), connect.WithHandlerOptions(opts...), ) entityResolutionServiceCreateEntityChainFromJwtHandler := connect.NewUnaryHandler( EntityResolutionServiceCreateEntityChainFromJwtProcedure, svc.CreateEntityChainFromJwt, - connect.WithSchema(entityResolutionServiceCreateEntityChainFromJwtMethodDescriptor), + connect.WithSchema(entityResolutionServiceMethods.ByName("CreateEntityChainFromJwt")), connect.WithHandlerOptions(opts...), ) return "/entityresolution.EntityResolutionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/entityresolution/v2/entityresolutionv2connect/entity_resolution.connect.go b/protocol/go/entityresolution/v2/entityresolutionv2connect/entity_resolution.connect.go index 8ceee1587a..c1dc5d14fd 100644 --- a/protocol/go/entityresolution/v2/entityresolutionv2connect/entity_resolution.connect.go +++ b/protocol/go/entityresolution/v2/entityresolutionv2connect/entity_resolution.connect.go @@ -41,13 +41,6 @@ const ( EntityResolutionServiceCreateEntityChainsFromTokensProcedure = "/entityresolution.v2.EntityResolutionService/CreateEntityChainsFromTokens" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - entityResolutionServiceServiceDescriptor = v2.File_entityresolution_v2_entity_resolution_proto.Services().ByName("EntityResolutionService") - entityResolutionServiceResolveEntitiesMethodDescriptor = entityResolutionServiceServiceDescriptor.Methods().ByName("ResolveEntities") - entityResolutionServiceCreateEntityChainsFromTokensMethodDescriptor = entityResolutionServiceServiceDescriptor.Methods().ByName("CreateEntityChainsFromTokens") -) - // EntityResolutionServiceClient is a client for the entityresolution.v2.EntityResolutionService // service. type EntityResolutionServiceClient interface { @@ -65,17 +58,18 @@ type EntityResolutionServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewEntityResolutionServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) EntityResolutionServiceClient { baseURL = strings.TrimRight(baseURL, "/") + entityResolutionServiceMethods := v2.File_entityresolution_v2_entity_resolution_proto.Services().ByName("EntityResolutionService").Methods() return &entityResolutionServiceClient{ resolveEntities: connect.NewClient[v2.ResolveEntitiesRequest, v2.ResolveEntitiesResponse]( httpClient, baseURL+EntityResolutionServiceResolveEntitiesProcedure, - connect.WithSchema(entityResolutionServiceResolveEntitiesMethodDescriptor), + connect.WithSchema(entityResolutionServiceMethods.ByName("ResolveEntities")), connect.WithClientOptions(opts...), ), createEntityChainsFromTokens: connect.NewClient[v2.CreateEntityChainsFromTokensRequest, v2.CreateEntityChainsFromTokensResponse]( httpClient, baseURL+EntityResolutionServiceCreateEntityChainsFromTokensProcedure, - connect.WithSchema(entityResolutionServiceCreateEntityChainsFromTokensMethodDescriptor), + connect.WithSchema(entityResolutionServiceMethods.ByName("CreateEntityChainsFromTokens")), connect.WithClientOptions(opts...), ), } @@ -111,16 +105,17 @@ type EntityResolutionServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewEntityResolutionServiceHandler(svc EntityResolutionServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + entityResolutionServiceMethods := v2.File_entityresolution_v2_entity_resolution_proto.Services().ByName("EntityResolutionService").Methods() entityResolutionServiceResolveEntitiesHandler := connect.NewUnaryHandler( EntityResolutionServiceResolveEntitiesProcedure, svc.ResolveEntities, - connect.WithSchema(entityResolutionServiceResolveEntitiesMethodDescriptor), + connect.WithSchema(entityResolutionServiceMethods.ByName("ResolveEntities")), connect.WithHandlerOptions(opts...), ) entityResolutionServiceCreateEntityChainsFromTokensHandler := connect.NewUnaryHandler( EntityResolutionServiceCreateEntityChainsFromTokensProcedure, svc.CreateEntityChainsFromTokens, - connect.WithSchema(entityResolutionServiceCreateEntityChainsFromTokensMethodDescriptor), + connect.WithSchema(entityResolutionServiceMethods.ByName("CreateEntityChainsFromTokens")), connect.WithHandlerOptions(opts...), ) return "/entityresolution.v2.EntityResolutionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/go.mod b/protocol/go/go.mod index 8c427f5456..fe119565e9 100644 --- a/protocol/go/go.mod +++ b/protocol/go/go.mod @@ -1,21 +1,17 @@ module github.com/opentdf/platform/protocol/go -go 1.24.0 - -toolchain go1.24.11 +go 1.25.0 require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.1-20240508200655-46a4cf4ba109.1 - connectrpc.com/connect v1.17.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 - google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 - google.golang.org/grpc v1.67.1 - google.golang.org/protobuf v1.36.6 + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 + connectrpc.com/connect v1.20.0 + google.golang.org/grpc v1.81.1 + google.golang.org/protobuf v1.36.11 ) require ( - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect ) diff --git a/protocol/go/go.sum b/protocol/go/go.sum index 822d0aedae..bcef5ad6cf 100644 --- a/protocol/go/go.sum +++ b/protocol/go/go.sum @@ -1,27 +1,42 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.1-20240508200655-46a4cf4ba109.1 h1:LEXWFH/xZ5oOWrC3oOtHbUyBdzRWMCPpAQmKC9v05mA= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.1-20240508200655-46a4cf4ba109.1/go.mod h1:XF+P8+RmfdufmIYpGUC+6bF7S+IlmHDEnCrO3OXaUAQ= -connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= -connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= -google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/protocol/go/internal/authorization/v2/entity_identifier.go b/protocol/go/internal/authorization/v2/entity_identifier.go new file mode 100644 index 0000000000..d5b6134236 --- /dev/null +++ b/protocol/go/internal/authorization/v2/entity_identifier.go @@ -0,0 +1,63 @@ +package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// ForToken returns an EntityIdentifier that resolves the entity from the given JWT. +// The authorization service will parse the token to derive the entity chain. +func ForToken(jwt string) *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_Token{ + Token: &entity.Token{ + Jwt: jwt, + }, + }, + } +} + +// WithRequestToken returns an EntityIdentifier that instructs the authorization +// service to derive the entity from the request's Authorization header token. +func WithRequestToken() *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_WithRequestToken{ + WithRequestToken: wrapperspb.Bool(true), + }, + } +} + +// ForClientID returns an EntityIdentifier for a single subject entity identified by client ID. +func ForClientID(clientID string) *authorizationv2.EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_ClientId{ClientId: clientID}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +// ForEmail returns an EntityIdentifier for a single subject entity identified by email address. +func ForEmail(email string) *authorizationv2.EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_EmailAddress{EmailAddress: email}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +// ForUserName returns an EntityIdentifier for a single subject entity identified by username. +func ForUserName(username string) *authorizationv2.EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_UserName{UserName: username}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +func entityIdentifierFromEntity(e *entity.Entity) *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{e}, + }, + }, + } +} diff --git a/protocol/go/internal/authorization/v2/entity_identifier_test.go b/protocol/go/internal/authorization/v2/entity_identifier_test.go new file mode 100644 index 0000000000..f7d0658169 --- /dev/null +++ b/protocol/go/internal/authorization/v2/entity_identifier_test.go @@ -0,0 +1,160 @@ +package authorizationv2 + +import ( + "testing" + + authorizationv2proto "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" +) + +func TestForToken(t *testing.T) { + jwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test" + eid := ForToken(jwt) + + tok, ok := eid.GetIdentifier().(*authorizationv2proto.EntityIdentifier_Token) + if !ok { + t.Fatal("expected Token identifier") + } + if got := tok.Token.GetJwt(); got != jwt { + t.Errorf("jwt = %q, want %q", got, jwt) + } +} + +func TestForToken_EmptyString(t *testing.T) { + eid := ForToken("") + + tok, ok := eid.GetIdentifier().(*authorizationv2proto.EntityIdentifier_Token) + if !ok { + t.Fatal("expected Token identifier") + } + if got := tok.Token.GetJwt(); got != "" { + t.Errorf("jwt = %q, want empty string", got) + } +} + +func TestWithRequestToken(t *testing.T) { + eid := WithRequestToken() + + wrt, ok := eid.GetIdentifier().(*authorizationv2proto.EntityIdentifier_WithRequestToken) + if !ok { + t.Fatal("expected WithRequestToken identifier") + } + if !wrt.WithRequestToken.GetValue() { + t.Error("expected WithRequestToken value to be true") + } +} + +func TestEntityChainConstructors(t *testing.T) { + tests := []struct { + name string + constructor func(string) *authorizationv2proto.EntityIdentifier + input string + checkType func(*entity.Entity) (string, bool) + }{ + { + name: "ForClientID", + constructor: ForClientID, + input: "my-client", + checkType: func(e *entity.Entity) (string, bool) { + cid, ok := e.GetEntityType().(*entity.Entity_ClientId) + if !ok { + return "", false + } + return cid.ClientId, true + }, + }, + { + name: "ForClientID_EmptyString", + constructor: ForClientID, + input: "", + checkType: func(e *entity.Entity) (string, bool) { + cid, ok := e.GetEntityType().(*entity.Entity_ClientId) + if !ok { + return "", false + } + return cid.ClientId, true + }, + }, + { + name: "ForEmail", + constructor: ForEmail, + input: "user@example.com", + checkType: func(e *entity.Entity) (string, bool) { + em, ok := e.GetEntityType().(*entity.Entity_EmailAddress) + if !ok { + return "", false + } + return em.EmailAddress, true + }, + }, + { + name: "ForEmail_EmptyString", + constructor: ForEmail, + input: "", + checkType: func(e *entity.Entity) (string, bool) { + em, ok := e.GetEntityType().(*entity.Entity_EmailAddress) + if !ok { + return "", false + } + return em.EmailAddress, true + }, + }, + { + name: "ForUserName", + constructor: ForUserName, + input: "alice", + checkType: func(e *entity.Entity) (string, bool) { + un, ok := e.GetEntityType().(*entity.Entity_UserName) + if !ok { + return "", false + } + return un.UserName, true + }, + }, + { + name: "ForUserName_EmptyString", + constructor: ForUserName, + input: "", + checkType: func(e *entity.Entity) (string, bool) { + un, ok := e.GetEntityType().(*entity.Entity_UserName) + if !ok { + return "", false + } + return un.UserName, true + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eid := tt.constructor(tt.input) + + chain := extractEntityChain(t, eid) + entities := chain.GetEntities() + if len(entities) != 1 { + t.Fatalf("entities len = %d, want 1", len(entities)) + } + + e := entities[0] + got, ok := tt.checkType(e) + if !ok { + t.Fatalf("unexpected entity type for %s", tt.name) + } + if got != tt.input { + t.Errorf("%s value = %q, want %q", tt.name, got, tt.input) + } + if e.GetCategory() != entity.Entity_CATEGORY_SUBJECT { + t.Errorf("category = %v, want CATEGORY_SUBJECT", e.GetCategory()) + } + }) + } +} + +func extractEntityChain(t *testing.T, eid *authorizationv2proto.EntityIdentifier) *entity.EntityChain { + t.Helper() + ec, ok := eid.GetIdentifier().(*authorizationv2proto.EntityIdentifier_EntityChain) + if !ok { + t.Fatal("expected EntityChain identifier") + } + return ec.EntityChain +} diff --git a/protocol/go/internal/authorization/v2/resource.go b/protocol/go/internal/authorization/v2/resource.go new file mode 100644 index 0000000000..c4fd04b663 --- /dev/null +++ b/protocol/go/internal/authorization/v2/resource.go @@ -0,0 +1,32 @@ +package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" +) + +// ForAttributeValues returns a Resource containing the given attribute value FQNs. +// This is the most common Resource variant, used when authorizing against +// attribute values attached to data (e.g. those on a TDF). +// At least one FQN is required; calling with zero arguments panics. +func ForAttributeValues(fqns ...string) *authorizationv2.Resource { + if len(fqns) == 0 { + panic("ForAttributeValues requires at least one FQN") + } + return &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: fqns, + }, + }, + } +} + +// ForRegisteredResourceValueFqn returns a Resource that references a single +// registered resource value by its fully qualified name, as stored in platform policy. +func ForRegisteredResourceValueFqn(fqn string) *authorizationv2.Resource { + return &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: fqn, + }, + } +} diff --git a/protocol/go/internal/authorization/v2/resource_test.go b/protocol/go/internal/authorization/v2/resource_test.go new file mode 100644 index 0000000000..06ed296b96 --- /dev/null +++ b/protocol/go/internal/authorization/v2/resource_test.go @@ -0,0 +1,80 @@ +package authorizationv2 + +import ( + "testing" + + authorizationv2proto "github.com/opentdf/platform/protocol/go/authorization/v2" +) + +func TestForAttributeValues(t *testing.T) { + fqns := []string{ + "https://example.com/attr/department/value/finance", + "https://example.com/attr/level/value/public", + } + r := ForAttributeValues(fqns...) + + av, ok := r.GetResource().(*authorizationv2proto.Resource_AttributeValues_) + if !ok { + t.Fatal("expected AttributeValues resource") + } + got := av.AttributeValues.GetFqns() + if len(got) != len(fqns) { + t.Fatalf("fqns len = %d, want %d", len(got), len(fqns)) + } + for i, fqn := range fqns { + if got[i] != fqn { + t.Errorf("fqns[%d] = %q, want %q", i, got[i], fqn) + } + } +} + +func TestForAttributeValues_Single(t *testing.T) { + fqn := "https://example.com/attr/department/value/finance" + r := ForAttributeValues(fqn) + + av, ok := r.GetResource().(*authorizationv2proto.Resource_AttributeValues_) + if !ok { + t.Fatal("expected AttributeValues resource") + } + got := av.AttributeValues.GetFqns() + if len(got) != 1 { + t.Fatalf("fqns len = %d, want 1", len(got)) + } + if got[0] != fqn { + t.Errorf("fqns[0] = %q, want %q", got[0], fqn) + } +} + +func TestForAttributeValues_ZeroArgs_Panics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("expected panic for zero FQNs, but did not panic") + } + }() + ForAttributeValues() +} + +func TestForRegisteredResourceValueFqn(t *testing.T) { + fqn := "https://example.com/attr/department/value/finance" + r := ForRegisteredResourceValueFqn(fqn) + + rr, ok := r.GetResource().(*authorizationv2proto.Resource_RegisteredResourceValueFqn) + if !ok { + t.Fatal("expected RegisteredResourceValueFqn resource") + } + if rr.RegisteredResourceValueFqn != fqn { + t.Errorf("fqn = %q, want %q", rr.RegisteredResourceValueFqn, fqn) + } +} + +func TestForRegisteredResourceValueFqn_EmptyString(t *testing.T) { + r := ForRegisteredResourceValueFqn("") + + rr, ok := r.GetResource().(*authorizationv2proto.Resource_RegisteredResourceValueFqn) + if !ok { + t.Fatal("expected RegisteredResourceValueFqn resource") + } + if rr.RegisteredResourceValueFqn != "" { + t.Errorf("fqn = %q, want empty string", rr.RegisteredResourceValueFqn) + } +} diff --git a/protocol/go/internal/policy/enums.go b/protocol/go/internal/policy/enums.go new file mode 100644 index 0000000000..6ed689d857 --- /dev/null +++ b/protocol/go/internal/policy/enums.go @@ -0,0 +1,61 @@ +package policy + +import ( + "github.com/opentdf/platform/protocol/go/common" + policy "github.com/opentdf/platform/protocol/go/policy" +) + +// Shorthand constants for SubjectMappingOperatorEnum. +// +// Example: +// +// condition := &policy.Condition{ +// SubjectExternalSelectorValue: ".email", +// Operator: policy.OperatorInContains, +// SubjectExternalValues: []string{"@example.com"}, +// } +const ( + OperatorIn = policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN + OperatorNotIn = policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + OperatorInContains = policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS +) + +// Shorthand constants for ConditionBooleanTypeEnum. +// +// Example: +// +// group := &policy.ConditionGroup{ +// BooleanOperator: policy.BooleanAnd, +// Conditions: conditions, +// } +const ( + BooleanAnd = policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND + BooleanOr = policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR +) + +// Shorthand constants for AttributeRuleTypeEnum. +// +// Example: +// +// req := &attributes.CreateAttributeRequest{ +// Name: "clearance", +// Rule: policy.RuleHierarchy, +// } +const ( + RuleAllOf = policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + RuleAnyOf = policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + RuleHierarchy = policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY +) + +// Shorthand constants for ActiveStateEnum (from the common package). +// +// Example: +// +// req := &attributes.ListAttributesRequest{ +// State: policy.StateActive, +// } +const ( + StateActive = common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE + StateInactive = common.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE + StateAny = common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY +) diff --git a/protocol/go/internal/policy/enums_test.go b/protocol/go/internal/policy/enums_test.go new file mode 100644 index 0000000000..8fa7e37bd3 --- /dev/null +++ b/protocol/go/internal/policy/enums_test.go @@ -0,0 +1,53 @@ +package policy + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/common" + policyproto "github.com/opentdf/platform/protocol/go/policy" +) + +func TestOperatorConstants(t *testing.T) { + if OperatorIn != policyproto.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN { + t.Errorf("OperatorIn = %d, want %d", OperatorIn, policyproto.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN) + } + if OperatorNotIn != policyproto.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN { + t.Errorf("OperatorNotIn = %d, want %d", OperatorNotIn, policyproto.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN) + } + if OperatorInContains != policyproto.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS { + t.Errorf("OperatorInContains = %d, want %d", OperatorInContains, policyproto.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS) + } +} + +func TestBooleanConstants(t *testing.T) { + if BooleanAnd != policyproto.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND { + t.Errorf("BooleanAnd = %d, want %d", BooleanAnd, policyproto.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND) + } + if BooleanOr != policyproto.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR { + t.Errorf("BooleanOr = %d, want %d", BooleanOr, policyproto.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR) + } +} + +func TestRuleConstants(t *testing.T) { + if RuleAllOf != policyproto.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF { + t.Errorf("RuleAllOf = %d, want %d", RuleAllOf, policyproto.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF) + } + if RuleAnyOf != policyproto.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF { + t.Errorf("RuleAnyOf = %d, want %d", RuleAnyOf, policyproto.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) + } + if RuleHierarchy != policyproto.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY { + t.Errorf("RuleHierarchy = %d, want %d", RuleHierarchy, policyproto.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY) + } +} + +func TestStateConstants(t *testing.T) { + if StateActive != common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE { + t.Errorf("StateActive = %d, want %d", StateActive, common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE) + } + if StateInactive != common.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE { + t.Errorf("StateInactive = %d, want %d", StateInactive, common.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE) + } + if StateAny != common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY { + t.Errorf("StateAny = %d, want %d", StateAny, common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY) + } +} diff --git a/protocol/go/kas/kas.pb.go b/protocol/go/kas/kas.pb.go index 8a3af35cd6..7eb3bfbf5c 100644 --- a/protocol/go/kas/kas.pb.go +++ b/protocol/go/kas/kas.pb.go @@ -7,8 +7,6 @@ package kas import ( - _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" - _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" @@ -244,7 +242,7 @@ type KeyAccess struct { Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` // Type of key wrapping used for the data encryption key // Required: Always - // Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped) + // Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped), 'hybrid-wrapped' (experimental X-Wing-wrapped) KeyType string `protobuf:"bytes,4,opt,name=key_type,json=type,proto3" json:"key_type,omitempty"` // URL of the Key Access Server that can unwrap this key // Optional: May be omitted if KAS URL is known from context @@ -265,14 +263,13 @@ type KeyAccess struct { // Contains the actual DEK encrypted with KAS's public key // This is the core cryptographic material needed for TDF decryption WrappedKey []byte `protobuf:"bytes,8,opt,name=wrapped_key,json=wrappedKey,proto3" json:"wrapped_key,omitempty"` - // Complete NanoTDF header containing all metadata and policy information - // Required: NanoTDF only - // ZTDF: Omitted (policy and metadata are separate) + // Complete header containing all metadata and policy information (for formats that embed it) + // Optional: Not used by ZTDF (policy and metadata are separate) // Contains magic bytes, version, algorithm, policy, and ephemeral key information Header []byte `protobuf:"bytes,9,opt,name=header,proto3" json:"header,omitempty"` // Ephemeral public key for ECDH key derivation (ec-wrapped type only) // Required: When key_type="ec-wrapped" (experimental ECDH-based ZTDF) - // Omitted: When key_type="wrapped" (RSA-based ZTDF) + // Omitted: When key_type="wrapped" or key_type="hybrid-wrapped" // Should be a PEM-encoded PKCS#8 (ASN.1) formatted public key // Used to derive the symmetric key for unwrapping the DEK EphemeralPublicKey string `protobuf:"bytes,10,opt,name=ephemeral_public_key,json=ephemeralPublicKey,proto3" json:"ephemeral_public_key,omitempty"` @@ -856,8 +853,8 @@ type RewrapResponse struct { // Deprecated: Marked as deprecated in kas/kas.proto. EntityWrappedKey []byte `protobuf:"bytes,2,opt,name=entity_wrapped_key,json=entityWrappedKey,proto3" json:"entity_wrapped_key,omitempty"` // KAS's ephemeral session public key in PEM format - // Required: For EC-based operations (NanoTDF and ZTDF with key_type="ec-wrapped") - // Optional: Empty for RSA-based ZTDF (key_type="wrapped") + // Required: For EC-based operations (key_type="ec-wrapped") + // Optional: Empty for RSA-based or X-Wing-based ZTDF (key_type="wrapped" or key_type="hybrid-wrapped") // Used by client to perform ECDH key agreement and decrypt the kas_wrapped_key values SessionPublicKey string `protobuf:"bytes,3,opt,name=session_public_key,json=sessionPublicKey,proto3" json:"session_public_key,omitempty"` // Deprecated: Legacy schema version identifier @@ -1073,15 +1070,14 @@ type UnsignedRewrapRequest_WithPolicyRequest struct { // List of Key Access Objects associated with this policy // Required: Always (at least one) - // NanoTDF: Exactly one KAO per policy - // ZTDF: One or more KAOs per policy + // Some formats require exactly one KAO per policy KeyAccessObjects []*UnsignedRewrapRequest_WithKeyAccessObject `protobuf:"bytes,1,rep,name=key_access_objects,json=keyAccessObjects,proto3" json:"key_access_objects,omitempty"` // Policy information for this group of KAOs // Required: Always Policy *UnsignedRewrapRequest_WithPolicy `protobuf:"bytes,2,opt,name=policy,proto3" json:"policy,omitempty"` // Cryptographic algorithm identifier for the TDF type // Optional: Defaults to rsa:2048 if omitted - // Values: "ec:secp256r1" (NanoTDF), "rsa:2048" (ZTDF), "" (defaults to rsa:2048) + // Values: "ec:secp256r1" (EC-based), "rsa:2048" (RSA-based), "" (defaults to rsa:2048) // Example: "ec:secp256r1" Algorithm string `protobuf:"bytes,3,opt,name=algorithm,proto3" json:"algorithm,omitempty"` } @@ -1143,99 +1139,88 @@ var File_kas_kas_proto protoreflect.FileDescriptor var file_kas_kas_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x6b, 0x61, 0x73, 0x2f, 0x6b, 0x61, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, - 0x03, 0x6b, 0x61, 0x73, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, - 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, - 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0x0d, 0x0a, 0x0b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x28, 0x0a, 0x0c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x36, 0x0a, 0x16, 0x4c, 0x65, 0x67, - 0x61, 0x63, 0x79, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, - 0x6d, 0x22, 0x3b, 0x0a, 0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x69, 0x6e, 0x64, 0x69, - 0x6e, 0x67, 0x12, 0x16, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6c, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, - 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0xd3, - 0x02, 0x0a, 0x09, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x2d, 0x0a, 0x12, - 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, 0x0a, 0x0e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, - 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x12, 0x16, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x07, 0x6b, 0x61, - 0x73, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x69, 0x64, 0x12, 0x15, 0x0a, 0x08, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x72, 0x61, - 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, - 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, - 0x61, 0x64, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, - 0x65, 0x72, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x12, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x22, 0x86, 0x05, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, - 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, - 0x0a, 0x11, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x48, 0x0a, 0x08, 0x72, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6b, - 0x61, 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x73, 0x12, 0x31, 0x0a, 0x0a, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, 0x6b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x1a, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, - 0x72, 0x69, 0x74, 0x68, 0x6d, 0x1a, 0x30, 0x0a, 0x0a, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x82, 0x01, 0x0a, 0x13, 0x57, 0x69, 0x74, 0x68, - 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, - 0x2f, 0x0a, 0x14, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, - 0x12, 0x3a, 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6b, 0x61, - 0x73, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x0f, 0x6b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x1a, 0xce, 0x01, 0x0a, - 0x11, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x5c, 0x0a, 0x12, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, + 0x03, 0x6b, 0x61, 0x73, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0x0d, 0x0a, 0x0b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x28, 0x0a, 0x0c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x36, 0x0a, 0x16, 0x4c, + 0x65, 0x67, 0x61, 0x63, 0x79, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, + 0x74, 0x68, 0x6d, 0x22, 0x3b, 0x0a, 0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x69, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x12, 0x16, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6c, 0x67, 0x12, 0x12, 0x0a, 0x04, + 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, + 0x22, 0xd3, 0x02, 0x0a, 0x09, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x2d, + 0x0a, 0x12, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, 0x0a, + 0x0e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x16, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x07, + 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, + 0x72, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x08, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x77, + 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0a, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x68, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, + 0x6c, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x12, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x86, 0x05, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2a, 0x0a, 0x11, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x48, 0x0a, 0x08, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x77, - 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x10, - 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, - 0x12, 0x3d, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x25, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, + 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x31, 0x0a, 0x0a, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6b, 0x61, 0x73, + 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, + 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x1a, 0x0a, 0x06, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, 0x61, 0x6c, + 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x1a, 0x30, 0x0a, 0x0a, 0x57, 0x69, 0x74, 0x68, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x82, 0x01, 0x0a, 0x13, 0x57, 0x69, + 0x74, 0x68, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x12, 0x2f, 0x0a, 0x14, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, + 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x11, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x49, 0x64, 0x12, 0x3a, 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x6b, 0x61, 0x73, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x0f, 0x6b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x1a, 0xce, + 0x01, 0x0a, 0x11, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x5c, 0x0a, 0x12, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2e, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, - 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, - 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x22, 0xb1, 0x01, - 0x0a, 0x10, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x51, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x33, 0x92, 0x41, 0x30, 0x32, 0x2e, 0x61, 0x6c, 0x67, 0x6f, - 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x72, 0x73, 0x61, 0x3a, 0x3c, - 0x6b, 0x65, 0x79, 0x73, 0x69, 0x7a, 0x65, 0x3e, 0x20, 0x6f, 0x72, 0x20, 0x65, 0x63, 0x3a, 0x3c, - 0x63, 0x75, 0x72, 0x76, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x3e, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, - 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x26, 0x0a, 0x03, 0x66, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x14, 0x92, 0x41, 0x11, 0x32, 0x0f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x03, 0x66, 0x6d, 0x74, 0x12, 0x22, 0x0a, - 0x01, 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x14, 0x92, 0x41, 0x11, 0x32, 0x0f, 0x72, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x01, + 0x68, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x52, 0x10, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x12, 0x3d, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, + 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, + 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x22, + 0x50, 0x0a, 0x10, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, + 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x66, 0x6d, 0x74, 0x12, 0x0c, 0x0a, 0x01, 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x76, 0x22, 0x44, 0x0a, 0x11, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, @@ -1295,43 +1280,28 @@ var file_kas_kas_proto_rawDesc = []byte{ 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0xd1, 0x02, 0x0a, 0x0d, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x69, 0x0a, 0x09, 0x50, 0x75, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0xdb, 0x01, 0x0a, 0x0d, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3f, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2d, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, - 0x30, 0x30, 0x12, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x6b, 0x61, 0x73, - 0x2f, 0x76, 0x32, 0x2f, 0x6b, 0x61, 0x73, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, - 0x65, 0x79, 0x90, 0x02, 0x01, 0x12, 0x7b, 0x0a, 0x0f, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x50, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4c, - 0x65, 0x67, 0x61, 0x63, 0x79, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x22, 0x2d, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, 0x30, 0x30, 0x12, - 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x6b, 0x61, 0x73, 0x2f, 0x6b, 0x61, - 0x73, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x88, 0x02, 0x01, 0x90, - 0x02, 0x01, 0x12, 0x58, 0x0a, 0x06, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x12, 0x12, 0x2e, 0x6b, - 0x61, 0x73, 0x2e, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x13, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, 0x30, - 0x30, 0x12, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x6b, - 0x61, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, 0x42, 0xe2, 0x01, 0x92, - 0x41, 0x73, 0x12, 0x71, 0x0a, 0x1a, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x44, 0x46, 0x20, 0x4b, 0x65, - 0x79, 0x20, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x2a, 0x4c, 0x0a, 0x12, 0x42, 0x53, 0x44, 0x20, 0x33, 0x2d, 0x43, 0x6c, 0x61, 0x75, 0x73, 0x65, - 0x20, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x12, 0x36, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, - 0x64, 0x66, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, - 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, - 0x31, 0x2e, 0x35, 0x2e, 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x2e, 0x6b, 0x61, 0x73, 0x42, 0x08, - 0x4b, 0x61, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, - 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x2f, 0x67, 0x6f, 0x2f, 0x6b, 0x61, 0x73, 0xa2, 0x02, 0x03, 0x4b, 0x58, 0x58, 0xaa, 0x02, 0x03, - 0x4b, 0x61, 0x73, 0xca, 0x02, 0x03, 0x4b, 0x61, 0x73, 0xe2, 0x02, 0x0f, 0x4b, 0x61, 0x73, 0x5c, - 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x03, 0x4b, 0x61, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x54, 0x0a, 0x0f, 0x4c, + 0x65, 0x67, 0x61, 0x63, 0x79, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1b, + 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x06, 0x88, 0x02, 0x01, 0x90, 0x02, + 0x01, 0x12, 0x33, 0x0a, 0x06, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x12, 0x12, 0x2e, 0x6b, 0x61, + 0x73, 0x2e, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x13, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x6c, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x2e, 0x6b, 0x61, + 0x73, 0x42, 0x08, 0x4b, 0x61, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, + 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x6b, 0x61, 0x73, 0xa2, 0x02, 0x03, 0x4b, 0x58, 0x58, + 0xaa, 0x02, 0x03, 0x4b, 0x61, 0x73, 0xca, 0x02, 0x03, 0x4b, 0x61, 0x73, 0xe2, 0x02, 0x0f, 0x4b, + 0x61, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x03, 0x4b, 0x61, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protocol/go/kas/kas.pb.gw.go b/protocol/go/kas/kas.pb.gw.go deleted file mode 100644 index a0073e3be6..0000000000 --- a/protocol/go/kas/kas.pb.gw.go +++ /dev/null @@ -1,337 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: kas/kas.proto - -/* -Package kas is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package kas - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -var ( - filter_AccessService_PublicKey_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} -) - -func request_AccessService_PublicKey_0(ctx context.Context, marshaler runtime.Marshaler, client AccessServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq PublicKeyRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AccessService_PublicKey_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.PublicKey(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_AccessService_PublicKey_0(ctx context.Context, marshaler runtime.Marshaler, server AccessServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq PublicKeyRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AccessService_PublicKey_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.PublicKey(ctx, &protoReq) - return msg, metadata, err - -} - -var ( - filter_AccessService_LegacyPublicKey_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} -) - -func request_AccessService_LegacyPublicKey_0(ctx context.Context, marshaler runtime.Marshaler, client AccessServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq LegacyPublicKeyRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AccessService_LegacyPublicKey_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.LegacyPublicKey(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_AccessService_LegacyPublicKey_0(ctx context.Context, marshaler runtime.Marshaler, server AccessServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq LegacyPublicKeyRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AccessService_LegacyPublicKey_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.LegacyPublicKey(ctx, &protoReq) - return msg, metadata, err - -} - -func request_AccessService_Rewrap_0(ctx context.Context, marshaler runtime.Marshaler, client AccessServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq RewrapRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.Rewrap(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_AccessService_Rewrap_0(ctx context.Context, marshaler runtime.Marshaler, server AccessServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq RewrapRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.Rewrap(ctx, &protoReq) - return msg, metadata, err - -} - -// RegisterAccessServiceHandlerServer registers the http handlers for service AccessService to "mux". -// UnaryRPC :call AccessServiceServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAccessServiceHandlerFromEndpoint instead. -func RegisterAccessServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AccessServiceServer) error { - - mux.Handle("GET", pattern_AccessService_PublicKey_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/kas.AccessService/PublicKey", runtime.WithHTTPPathPattern("/kas/v2/kas_public_key")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_AccessService_PublicKey_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AccessService_PublicKey_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("GET", pattern_AccessService_LegacyPublicKey_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/kas.AccessService/LegacyPublicKey", runtime.WithHTTPPathPattern("/kas/kas_public_key")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_AccessService_LegacyPublicKey_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AccessService_LegacyPublicKey_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_AccessService_Rewrap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/kas.AccessService/Rewrap", runtime.WithHTTPPathPattern("/kas/v2/rewrap")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_AccessService_Rewrap_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AccessService_Rewrap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterAccessServiceHandlerFromEndpoint is same as RegisterAccessServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterAccessServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterAccessServiceHandler(ctx, mux, conn) -} - -// RegisterAccessServiceHandler registers the http handlers for service AccessService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterAccessServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterAccessServiceHandlerClient(ctx, mux, NewAccessServiceClient(conn)) -} - -// RegisterAccessServiceHandlerClient registers the http handlers for service AccessService -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AccessServiceClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AccessServiceClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "AccessServiceClient" to call the correct interceptors. -func RegisterAccessServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AccessServiceClient) error { - - mux.Handle("GET", pattern_AccessService_PublicKey_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/kas.AccessService/PublicKey", runtime.WithHTTPPathPattern("/kas/v2/kas_public_key")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_AccessService_PublicKey_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AccessService_PublicKey_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("GET", pattern_AccessService_LegacyPublicKey_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/kas.AccessService/LegacyPublicKey", runtime.WithHTTPPathPattern("/kas/kas_public_key")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_AccessService_LegacyPublicKey_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AccessService_LegacyPublicKey_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_AccessService_Rewrap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/kas.AccessService/Rewrap", runtime.WithHTTPPathPattern("/kas/v2/rewrap")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_AccessService_Rewrap_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AccessService_Rewrap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_AccessService_PublicKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"kas", "v2", "kas_public_key"}, "")) - - pattern_AccessService_LegacyPublicKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"kas", "kas_public_key"}, "")) - - pattern_AccessService_Rewrap_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"kas", "v2", "rewrap"}, "")) -) - -var ( - forward_AccessService_PublicKey_0 = runtime.ForwardResponseMessage - - forward_AccessService_LegacyPublicKey_0 = runtime.ForwardResponseMessage - - forward_AccessService_Rewrap_0 = runtime.ForwardResponseMessage -) diff --git a/protocol/go/kas/kasconnect/kas.connect.go b/protocol/go/kas/kasconnect/kas.connect.go index b56315d0f8..a0b6a70aa9 100644 --- a/protocol/go/kas/kasconnect/kas.connect.go +++ b/protocol/go/kas/kasconnect/kas.connect.go @@ -43,14 +43,6 @@ const ( AccessServiceRewrapProcedure = "/kas.AccessService/Rewrap" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - accessServiceServiceDescriptor = kas.File_kas_kas_proto.Services().ByName("AccessService") - accessServicePublicKeyMethodDescriptor = accessServiceServiceDescriptor.Methods().ByName("PublicKey") - accessServiceLegacyPublicKeyMethodDescriptor = accessServiceServiceDescriptor.Methods().ByName("LegacyPublicKey") - accessServiceRewrapMethodDescriptor = accessServiceServiceDescriptor.Methods().ByName("Rewrap") -) - // AccessServiceClient is a client for the kas.AccessService service. type AccessServiceClient interface { PublicKey(context.Context, *connect.Request[kas.PublicKeyRequest]) (*connect.Response[kas.PublicKeyResponse], error) @@ -74,25 +66,26 @@ type AccessServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewAccessServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AccessServiceClient { baseURL = strings.TrimRight(baseURL, "/") + accessServiceMethods := kas.File_kas_kas_proto.Services().ByName("AccessService").Methods() return &accessServiceClient{ publicKey: connect.NewClient[kas.PublicKeyRequest, kas.PublicKeyResponse]( httpClient, baseURL+AccessServicePublicKeyProcedure, - connect.WithSchema(accessServicePublicKeyMethodDescriptor), + connect.WithSchema(accessServiceMethods.ByName("PublicKey")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), legacyPublicKey: connect.NewClient[kas.LegacyPublicKeyRequest, wrapperspb.StringValue]( httpClient, baseURL+AccessServiceLegacyPublicKeyProcedure, - connect.WithSchema(accessServiceLegacyPublicKeyMethodDescriptor), + connect.WithSchema(accessServiceMethods.ByName("LegacyPublicKey")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), rewrap: connect.NewClient[kas.RewrapRequest, kas.RewrapResponse]( httpClient, baseURL+AccessServiceRewrapProcedure, - connect.WithSchema(accessServiceRewrapMethodDescriptor), + connect.WithSchema(accessServiceMethods.ByName("Rewrap")), connect.WithClientOptions(opts...), ), } @@ -142,24 +135,25 @@ type AccessServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewAccessServiceHandler(svc AccessServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + accessServiceMethods := kas.File_kas_kas_proto.Services().ByName("AccessService").Methods() accessServicePublicKeyHandler := connect.NewUnaryHandler( AccessServicePublicKeyProcedure, svc.PublicKey, - connect.WithSchema(accessServicePublicKeyMethodDescriptor), + connect.WithSchema(accessServiceMethods.ByName("PublicKey")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) accessServiceLegacyPublicKeyHandler := connect.NewUnaryHandler( AccessServiceLegacyPublicKeyProcedure, svc.LegacyPublicKey, - connect.WithSchema(accessServiceLegacyPublicKeyMethodDescriptor), + connect.WithSchema(accessServiceMethods.ByName("LegacyPublicKey")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) accessServiceRewrapHandler := connect.NewUnaryHandler( AccessServiceRewrapProcedure, svc.Rewrap, - connect.WithSchema(accessServiceRewrapMethodDescriptor), + connect.WithSchema(accessServiceMethods.ByName("Rewrap")), connect.WithHandlerOptions(opts...), ) return "/kas.AccessService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/policy/actions/actions.pb.go b/protocol/go/policy/actions/actions.pb.go index 0ebca0c38b..66e8322dce 100644 --- a/protocol/go/policy/actions/actions.pb.go +++ b/protocol/go/policy/actions/actions.pb.go @@ -35,6 +35,12 @@ type GetActionRequest struct { // *GetActionRequest_Id // *GetActionRequest_Name Identifier isGetActionRequest_Identifier `protobuf_oneof:"identifier"` + // Optional namespace ID to scope name-based lookup. + // If omitted for name-based lookup, action search is limited to legacy (namespace_id = NULL) actions. + NamespaceId string `protobuf:"bytes,3,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + // Optional namespace FQN to scope name-based lookup. + // If omitted for name-based lookup, action search is limited to legacy (namespace_id = NULL) actions. + NamespaceFqn string `protobuf:"bytes,4,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` } func (x *GetActionRequest) Reset() { @@ -90,6 +96,20 @@ func (x *GetActionRequest) GetName() string { return "" } +func (x *GetActionRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *GetActionRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" +} + type isGetActionRequest_Identifier interface { isGetActionRequest_Identifier() } @@ -167,6 +187,10 @@ type ListActionsRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + // ID of the namespace to scope results. If omitted, returns actions across namespaces. + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + // FQN of the namespace to scope results. If omitted, returns actions across namespaces. + NamespaceFqn string `protobuf:"bytes,2,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` } @@ -203,6 +227,20 @@ func (*ListActionsRequest) Descriptor() ([]byte, []int) { return file_policy_actions_actions_proto_rawDescGZIP(), []int{2} } +func (x *ListActionsRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *ListActionsRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" +} + func (x *ListActionsRequest) GetPagination() *policy.PageRequest { if x != nil { return x.Pagination @@ -282,6 +320,12 @@ type CreateActionRequest struct { // Required Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional namespace ID for the custom action. + // If omitted, create targets legacy (namespace_id = NULL) behavior unless enforced by server config. + NamespaceId string `protobuf:"bytes,2,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + // Optional namespace FQN for the custom action. + // If omitted, create targets legacy (namespace_id = NULL) behavior unless enforced by server config. + NamespaceFqn string `protobuf:"bytes,3,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` // Optional Metadata *common.MetadataMutable `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } @@ -325,6 +369,20 @@ func (x *CreateActionRequest) GetName() string { return "" } +func (x *CreateActionRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CreateActionRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" +} + func (x *CreateActionRequest) GetMetadata() *common.MetadataMutable { if x != nil { return x.Metadata @@ -610,8 +668,8 @@ var file_policy_actions_actions_proto_rawDesc = []byte{ 0x6d, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x16, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x73, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xeb, - 0x02, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf5, + 0x03, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0xa5, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x8e, @@ -632,144 +690,169 @@ var file_policy_actions_actions_proto_rawDesc = []byte{ 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x48, - 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x7e, 0x0a, 0x11, - 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x10, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0f, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x49, 0x0a, 0x12, - 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xbd, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x39, 0x0a, 0x10, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x73, 0x74, 0x61, 0x6e, 0x64, - 0x61, 0x72, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x53, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x12, 0x35, 0x0a, 0x0e, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x43, 0x75, 0x73, 0x74, 0x6f, - 0x6d, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xf3, 0x02, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0xa6, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x91, - 0x02, 0xba, 0x48, 0x8d, 0x02, 0xba, 0x01, 0x81, 0x02, 0x0a, 0x12, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xad, 0x01, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, - 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, - 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, - 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, - 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, - 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, - 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, - 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, - 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, - 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, - 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, - 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, - 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3e, 0x0a, - 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xf3, 0x03, - 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, - 0xb6, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xa1, - 0x02, 0xba, 0x48, 0x9d, 0x02, 0xba, 0x01, 0x94, 0x02, 0x0a, 0x12, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xad, 0x01, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, - 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, - 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, - 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, - 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, - 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, - 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, - 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, - 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, - 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x4e, 0x73, - 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, - 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, - 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, - 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, - 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x72, 0x03, 0x18, - 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, - 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, - 0x69, 0x6f, 0x72, 0x22, 0x3e, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, + 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, + 0x48, 0x08, 0xd8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d, + 0xba, 0x48, 0x0a, 0xd8, 0x01, 0x01, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x3a, 0x24, 0xba, 0x48, 0x21, + 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, + 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x10, + 0x00, 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, + 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x7e, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x52, 0x02, 0x69, 0x64, 0x22, 0x3e, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x06, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xd4, 0x03, 0x0a, 0x0d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x0b, 0x4c, 0x69, - 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, + 0x69, 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xcd, 0x01, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, + 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, 0x0a, 0x70, + 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x3a, 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x00, 0x22, 0xbd, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, + 0x0a, 0x10, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, + 0x72, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x53, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x12, 0x35, 0x0a, 0x0e, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xf7, 0x03, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xa6, + 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x91, 0x02, + 0xba, 0x48, 0x8d, 0x02, 0xba, 0x01, 0x81, 0x02, 0x0a, 0x12, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xad, 0x01, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, + 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, + 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, + 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, + 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, + 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, + 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, + 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, + 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, + 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, + 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, + 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, + 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, + 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x24, 0xba, 0x48, 0x21, 0x22, + 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x0a, + 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x00, + 0x22, 0x3e, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0xf3, 0x03, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, + 0x69, 0x64, 0x12, 0xb6, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0xa1, 0x02, 0xba, 0x48, 0x9d, 0x02, 0xba, 0x01, 0x94, 0x02, 0x0a, 0x12, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x12, 0xad, 0x01, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, + 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, + 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, + 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, + 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, + 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x64, 0x20, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, + 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, + 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, + 0x1a, 0x4e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, + 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, + 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, + 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, + 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, + 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, + 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, + 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x3e, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, + 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, + 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x3e, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xd4, 0x03, 0x0a, 0x0d, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x09, 0x47, 0x65, 0x74, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, + 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x5b, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x00, 0x12, 0x5b, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x6e, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, - 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xb3, 0x01, 0x0a, 0x12, - 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x42, 0x0c, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x50, 0x01, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, - 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0xa2, 0x02, 0x03, 0x50, 0x41, 0x58, - 0xaa, 0x02, 0x0e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0xca, 0x02, 0x0e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0xe2, 0x02, 0x1a, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, - 0x02, 0x0f, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xb3, + 0x01, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x0c, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, + 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0xa2, 0x02, 0x03, + 0x50, 0x41, 0x58, 0xaa, 0x02, 0x0e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0xca, 0x02, 0x0e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0xe2, 0x02, 0x1a, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0xea, 0x02, 0x0f, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protocol/go/policy/actions/actionsconnect/actions.connect.go b/protocol/go/policy/actions/actionsconnect/actions.connect.go index 38a80350e3..617a2cacdc 100644 --- a/protocol/go/policy/actions/actionsconnect/actions.connect.go +++ b/protocol/go/policy/actions/actionsconnect/actions.connect.go @@ -49,16 +49,6 @@ const ( ActionServiceDeleteActionProcedure = "/policy.actions.ActionService/DeleteAction" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - actionServiceServiceDescriptor = actions.File_policy_actions_actions_proto.Services().ByName("ActionService") - actionServiceGetActionMethodDescriptor = actionServiceServiceDescriptor.Methods().ByName("GetAction") - actionServiceListActionsMethodDescriptor = actionServiceServiceDescriptor.Methods().ByName("ListActions") - actionServiceCreateActionMethodDescriptor = actionServiceServiceDescriptor.Methods().ByName("CreateAction") - actionServiceUpdateActionMethodDescriptor = actionServiceServiceDescriptor.Methods().ByName("UpdateAction") - actionServiceDeleteActionMethodDescriptor = actionServiceServiceDescriptor.Methods().ByName("DeleteAction") -) - // ActionServiceClient is a client for the policy.actions.ActionService service. type ActionServiceClient interface { GetAction(context.Context, *connect.Request[actions.GetActionRequest]) (*connect.Response[actions.GetActionResponse], error) @@ -77,35 +67,36 @@ type ActionServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewActionServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ActionServiceClient { baseURL = strings.TrimRight(baseURL, "/") + actionServiceMethods := actions.File_policy_actions_actions_proto.Services().ByName("ActionService").Methods() return &actionServiceClient{ getAction: connect.NewClient[actions.GetActionRequest, actions.GetActionResponse]( httpClient, baseURL+ActionServiceGetActionProcedure, - connect.WithSchema(actionServiceGetActionMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("GetAction")), connect.WithClientOptions(opts...), ), listActions: connect.NewClient[actions.ListActionsRequest, actions.ListActionsResponse]( httpClient, baseURL+ActionServiceListActionsProcedure, - connect.WithSchema(actionServiceListActionsMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("ListActions")), connect.WithClientOptions(opts...), ), createAction: connect.NewClient[actions.CreateActionRequest, actions.CreateActionResponse]( httpClient, baseURL+ActionServiceCreateActionProcedure, - connect.WithSchema(actionServiceCreateActionMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("CreateAction")), connect.WithClientOptions(opts...), ), updateAction: connect.NewClient[actions.UpdateActionRequest, actions.UpdateActionResponse]( httpClient, baseURL+ActionServiceUpdateActionProcedure, - connect.WithSchema(actionServiceUpdateActionMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("UpdateAction")), connect.WithClientOptions(opts...), ), deleteAction: connect.NewClient[actions.DeleteActionRequest, actions.DeleteActionResponse]( httpClient, baseURL+ActionServiceDeleteActionProcedure, - connect.WithSchema(actionServiceDeleteActionMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("DeleteAction")), connect.WithClientOptions(opts...), ), } @@ -160,34 +151,35 @@ type ActionServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewActionServiceHandler(svc ActionServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + actionServiceMethods := actions.File_policy_actions_actions_proto.Services().ByName("ActionService").Methods() actionServiceGetActionHandler := connect.NewUnaryHandler( ActionServiceGetActionProcedure, svc.GetAction, - connect.WithSchema(actionServiceGetActionMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("GetAction")), connect.WithHandlerOptions(opts...), ) actionServiceListActionsHandler := connect.NewUnaryHandler( ActionServiceListActionsProcedure, svc.ListActions, - connect.WithSchema(actionServiceListActionsMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("ListActions")), connect.WithHandlerOptions(opts...), ) actionServiceCreateActionHandler := connect.NewUnaryHandler( ActionServiceCreateActionProcedure, svc.CreateAction, - connect.WithSchema(actionServiceCreateActionMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("CreateAction")), connect.WithHandlerOptions(opts...), ) actionServiceUpdateActionHandler := connect.NewUnaryHandler( ActionServiceUpdateActionProcedure, svc.UpdateAction, - connect.WithSchema(actionServiceUpdateActionMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("UpdateAction")), connect.WithHandlerOptions(opts...), ) actionServiceDeleteActionHandler := connect.NewUnaryHandler( ActionServiceDeleteActionProcedure, svc.DeleteAction, - connect.WithSchema(actionServiceDeleteActionMethodDescriptor), + connect.WithSchema(actionServiceMethods.ByName("DeleteAction")), connect.WithHandlerOptions(opts...), ) return "/policy.actions.ActionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/policy/attributes/attributes.pb.go b/protocol/go/policy/attributes/attributes.pb.go index 58f83da6ed..5eea52a0a9 100644 --- a/protocol/go/policy/attributes/attributes.pb.go +++ b/protocol/go/policy/attributes/attributes.pb.go @@ -10,9 +10,9 @@ import ( _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" common "github.com/opentdf/platform/protocol/go/common" policy "github.com/opentdf/platform/protocol/go/policy" - _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" reflect "reflect" sync "sync" ) @@ -24,6 +24,58 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type SortAttributesType int32 + +const ( + SortAttributesType_SORT_ATTRIBUTES_TYPE_UNSPECIFIED SortAttributesType = 0 + SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME SortAttributesType = 1 + SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT SortAttributesType = 2 + SortAttributesType_SORT_ATTRIBUTES_TYPE_UPDATED_AT SortAttributesType = 3 +) + +// Enum value maps for SortAttributesType. +var ( + SortAttributesType_name = map[int32]string{ + 0: "SORT_ATTRIBUTES_TYPE_UNSPECIFIED", + 1: "SORT_ATTRIBUTES_TYPE_NAME", + 2: "SORT_ATTRIBUTES_TYPE_CREATED_AT", + 3: "SORT_ATTRIBUTES_TYPE_UPDATED_AT", + } + SortAttributesType_value = map[string]int32{ + "SORT_ATTRIBUTES_TYPE_UNSPECIFIED": 0, + "SORT_ATTRIBUTES_TYPE_NAME": 1, + "SORT_ATTRIBUTES_TYPE_CREATED_AT": 2, + "SORT_ATTRIBUTES_TYPE_UPDATED_AT": 3, + } +) + +func (x SortAttributesType) Enum() *SortAttributesType { + p := new(SortAttributesType) + *p = x + return p +} + +func (x SortAttributesType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortAttributesType) Descriptor() protoreflect.EnumDescriptor { + return file_policy_attributes_attributes_proto_enumTypes[0].Descriptor() +} + +func (SortAttributesType) Type() protoreflect.EnumType { + return &file_policy_attributes_attributes_proto_enumTypes[0] +} + +func (x SortAttributesType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortAttributesType.Descriptor instead. +func (SortAttributesType) EnumDescriptor() ([]byte, []int) { + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{0} +} + // Deprecated // // Deprecated: Marked as deprecated in policy/attributes/attributes.proto. @@ -256,6 +308,61 @@ func (x *ValueKey) GetKeyId() string { return "" } +type AttributesSort struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field SortAttributesType `protobuf:"varint,1,opt,name=field,proto3,enum=policy.attributes.SortAttributesType" json:"field,omitempty"` + Direction policy.SortDirection `protobuf:"varint,2,opt,name=direction,proto3,enum=policy.SortDirection" json:"direction,omitempty"` +} + +func (x *AttributesSort) Reset() { + *x = AttributesSort{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_attributes_attributes_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AttributesSort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributesSort) ProtoMessage() {} + +func (x *AttributesSort) ProtoReflect() protoreflect.Message { + mi := &file_policy_attributes_attributes_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributesSort.ProtoReflect.Descriptor instead. +func (*AttributesSort) Descriptor() ([]byte, []int) { + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{4} +} + +func (x *AttributesSort) GetField() SortAttributesType { + if x != nil { + return x.Field + } + return SortAttributesType_SORT_ATTRIBUTES_TYPE_UNSPECIFIED +} + +func (x *AttributesSort) GetDirection() policy.SortDirection { + if x != nil { + return x.Direction + } + return policy.SortDirection(0) +} + type ListAttributesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -269,12 +376,18 @@ type ListAttributesRequest struct { Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + Sort []*AttributesSort `protobuf:"bytes,11,rep,name=sort,proto3" json:"sort,omitempty"` } func (x *ListAttributesRequest) Reset() { *x = ListAttributesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[4] + mi := &file_policy_attributes_attributes_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -287,7 +400,7 @@ func (x *ListAttributesRequest) String() string { func (*ListAttributesRequest) ProtoMessage() {} func (x *ListAttributesRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[4] + mi := &file_policy_attributes_attributes_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -300,7 +413,7 @@ func (x *ListAttributesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAttributesRequest.ProtoReflect.Descriptor instead. func (*ListAttributesRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{4} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{5} } func (x *ListAttributesRequest) GetState() common.ActiveStateEnum { @@ -324,6 +437,13 @@ func (x *ListAttributesRequest) GetPagination() *policy.PageRequest { return nil } +func (x *ListAttributesRequest) GetSort() []*AttributesSort { + if x != nil { + return x.Sort + } + return nil +} + type ListAttributesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -336,7 +456,7 @@ type ListAttributesResponse struct { func (x *ListAttributesResponse) Reset() { *x = ListAttributesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[5] + mi := &file_policy_attributes_attributes_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -349,7 +469,7 @@ func (x *ListAttributesResponse) String() string { func (*ListAttributesResponse) ProtoMessage() {} func (x *ListAttributesResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[5] + mi := &file_policy_attributes_attributes_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -362,7 +482,7 @@ func (x *ListAttributesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAttributesResponse.ProtoReflect.Descriptor instead. func (*ListAttributesResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{5} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{6} } func (x *ListAttributesResponse) GetAttributes() []*policy.Attribute { @@ -398,7 +518,7 @@ type GetAttributeRequest struct { func (x *GetAttributeRequest) Reset() { *x = GetAttributeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[6] + mi := &file_policy_attributes_attributes_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -411,7 +531,7 @@ func (x *GetAttributeRequest) String() string { func (*GetAttributeRequest) ProtoMessage() {} func (x *GetAttributeRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[6] + mi := &file_policy_attributes_attributes_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -424,7 +544,7 @@ func (x *GetAttributeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAttributeRequest.ProtoReflect.Descriptor instead. func (*GetAttributeRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{6} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{7} } // Deprecated: Marked as deprecated in policy/attributes/attributes.proto. @@ -484,7 +604,7 @@ type GetAttributeResponse struct { func (x *GetAttributeResponse) Reset() { *x = GetAttributeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[7] + mi := &file_policy_attributes_attributes_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -497,7 +617,7 @@ func (x *GetAttributeResponse) String() string { func (*GetAttributeResponse) ProtoMessage() {} func (x *GetAttributeResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[7] + mi := &file_policy_attributes_attributes_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -510,7 +630,7 @@ func (x *GetAttributeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAttributeResponse.ProtoReflect.Descriptor instead. func (*GetAttributeResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{7} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{8} } func (x *GetAttributeResponse) GetAttribute() *policy.Attribute { @@ -536,13 +656,20 @@ type CreateAttributeRequest struct { // The stored attribute value will be normalized to lower case. Values []string `protobuf:"bytes,4,rep,name=values,proto3" json:"values,omitempty"` // Optional + // Setting allow_traversal=true allows TDF creation to be front-loaded, meaning a customer + // can create encrypted content with an attribute definitions key mapping before + // creating the attribute values needed to decrypt. + // Content will be able to be encrypted with missing attribute values, + // but will not be able to be decrypted until such attribute values exist. + AllowTraversal *wrapperspb.BoolValue `protobuf:"bytes,5,opt,name=allow_traversal,json=allowTraversal,proto3" json:"allow_traversal,omitempty"` + // Optional Metadata *common.MetadataMutable `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *CreateAttributeRequest) Reset() { *x = CreateAttributeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[8] + mi := &file_policy_attributes_attributes_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -555,7 +682,7 @@ func (x *CreateAttributeRequest) String() string { func (*CreateAttributeRequest) ProtoMessage() {} func (x *CreateAttributeRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[8] + mi := &file_policy_attributes_attributes_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -568,7 +695,7 @@ func (x *CreateAttributeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAttributeRequest.ProtoReflect.Descriptor instead. func (*CreateAttributeRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{8} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{9} } func (x *CreateAttributeRequest) GetNamespaceId() string { @@ -599,6 +726,13 @@ func (x *CreateAttributeRequest) GetValues() []string { return nil } +func (x *CreateAttributeRequest) GetAllowTraversal() *wrapperspb.BoolValue { + if x != nil { + return x.AllowTraversal + } + return nil +} + func (x *CreateAttributeRequest) GetMetadata() *common.MetadataMutable { if x != nil { return x.Metadata @@ -617,7 +751,7 @@ type CreateAttributeResponse struct { func (x *CreateAttributeResponse) Reset() { *x = CreateAttributeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[9] + mi := &file_policy_attributes_attributes_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -630,7 +764,7 @@ func (x *CreateAttributeResponse) String() string { func (*CreateAttributeResponse) ProtoMessage() {} func (x *CreateAttributeResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[9] + mi := &file_policy_attributes_attributes_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -643,7 +777,7 @@ func (x *CreateAttributeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAttributeResponse.ProtoReflect.Descriptor instead. func (*CreateAttributeResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{9} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{10} } func (x *CreateAttributeResponse) GetAttribute() *policy.Attribute { @@ -668,7 +802,7 @@ type UpdateAttributeRequest struct { func (x *UpdateAttributeRequest) Reset() { *x = UpdateAttributeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[10] + mi := &file_policy_attributes_attributes_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -681,7 +815,7 @@ func (x *UpdateAttributeRequest) String() string { func (*UpdateAttributeRequest) ProtoMessage() {} func (x *UpdateAttributeRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[10] + mi := &file_policy_attributes_attributes_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -694,7 +828,7 @@ func (x *UpdateAttributeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateAttributeRequest.ProtoReflect.Descriptor instead. func (*UpdateAttributeRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{10} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{11} } func (x *UpdateAttributeRequest) GetId() string { @@ -729,7 +863,7 @@ type UpdateAttributeResponse struct { func (x *UpdateAttributeResponse) Reset() { *x = UpdateAttributeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[11] + mi := &file_policy_attributes_attributes_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -742,7 +876,7 @@ func (x *UpdateAttributeResponse) String() string { func (*UpdateAttributeResponse) ProtoMessage() {} func (x *UpdateAttributeResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[11] + mi := &file_policy_attributes_attributes_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -755,7 +889,7 @@ func (x *UpdateAttributeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateAttributeResponse.ProtoReflect.Descriptor instead. func (*UpdateAttributeResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{11} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{12} } func (x *UpdateAttributeResponse) GetAttribute() *policy.Attribute { @@ -777,7 +911,7 @@ type DeactivateAttributeRequest struct { func (x *DeactivateAttributeRequest) Reset() { *x = DeactivateAttributeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[12] + mi := &file_policy_attributes_attributes_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -790,7 +924,7 @@ func (x *DeactivateAttributeRequest) String() string { func (*DeactivateAttributeRequest) ProtoMessage() {} func (x *DeactivateAttributeRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[12] + mi := &file_policy_attributes_attributes_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -803,7 +937,7 @@ func (x *DeactivateAttributeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeactivateAttributeRequest.ProtoReflect.Descriptor instead. func (*DeactivateAttributeRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{12} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{13} } func (x *DeactivateAttributeRequest) GetId() string { @@ -824,7 +958,7 @@ type DeactivateAttributeResponse struct { func (x *DeactivateAttributeResponse) Reset() { *x = DeactivateAttributeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[13] + mi := &file_policy_attributes_attributes_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -837,7 +971,7 @@ func (x *DeactivateAttributeResponse) String() string { func (*DeactivateAttributeResponse) ProtoMessage() {} func (x *DeactivateAttributeResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[13] + mi := &file_policy_attributes_attributes_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -850,7 +984,7 @@ func (x *DeactivateAttributeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeactivateAttributeResponse.ProtoReflect.Descriptor instead. func (*DeactivateAttributeResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{13} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{14} } func (x *DeactivateAttributeResponse) GetAttribute() *policy.Attribute { @@ -882,7 +1016,7 @@ type GetAttributeValueRequest struct { func (x *GetAttributeValueRequest) Reset() { *x = GetAttributeValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[14] + mi := &file_policy_attributes_attributes_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -895,7 +1029,7 @@ func (x *GetAttributeValueRequest) String() string { func (*GetAttributeValueRequest) ProtoMessage() {} func (x *GetAttributeValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[14] + mi := &file_policy_attributes_attributes_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -908,7 +1042,7 @@ func (x *GetAttributeValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAttributeValueRequest.ProtoReflect.Descriptor instead. func (*GetAttributeValueRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{14} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{15} } // Deprecated: Marked as deprecated in policy/attributes/attributes.proto. @@ -968,7 +1102,7 @@ type GetAttributeValueResponse struct { func (x *GetAttributeValueResponse) Reset() { *x = GetAttributeValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[15] + mi := &file_policy_attributes_attributes_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -981,7 +1115,7 @@ func (x *GetAttributeValueResponse) String() string { func (*GetAttributeValueResponse) ProtoMessage() {} func (x *GetAttributeValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[15] + mi := &file_policy_attributes_attributes_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -994,7 +1128,7 @@ func (x *GetAttributeValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAttributeValueResponse.ProtoReflect.Descriptor instead. func (*GetAttributeValueResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{15} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{16} } func (x *GetAttributeValueResponse) GetValue() *policy.Value { @@ -1021,7 +1155,7 @@ type ListAttributeValuesRequest struct { func (x *ListAttributeValuesRequest) Reset() { *x = ListAttributeValuesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[16] + mi := &file_policy_attributes_attributes_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1034,7 +1168,7 @@ func (x *ListAttributeValuesRequest) String() string { func (*ListAttributeValuesRequest) ProtoMessage() {} func (x *ListAttributeValuesRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[16] + mi := &file_policy_attributes_attributes_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1047,7 +1181,7 @@ func (x *ListAttributeValuesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAttributeValuesRequest.ProtoReflect.Descriptor instead. func (*ListAttributeValuesRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{16} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{17} } func (x *ListAttributeValuesRequest) GetAttributeId() string { @@ -1083,7 +1217,7 @@ type ListAttributeValuesResponse struct { func (x *ListAttributeValuesResponse) Reset() { *x = ListAttributeValuesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[17] + mi := &file_policy_attributes_attributes_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1096,7 +1230,7 @@ func (x *ListAttributeValuesResponse) String() string { func (*ListAttributeValuesResponse) ProtoMessage() {} func (x *ListAttributeValuesResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[17] + mi := &file_policy_attributes_attributes_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1109,7 +1243,7 @@ func (x *ListAttributeValuesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAttributeValuesResponse.ProtoReflect.Descriptor instead. func (*ListAttributeValuesResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{17} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{18} } func (x *ListAttributeValuesResponse) GetValues() []*policy.Value { @@ -1126,6 +1260,81 @@ func (x *ListAttributeValuesResponse) GetPagination() *policy.PageResponse { return nil } +type AttributeValueObligationTriggerRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Required. Existing obligation value to associate with the newly created attribute value. + ObligationValue *common.IdFqnIdentifier `protobuf:"bytes,1,opt,name=obligation_value,json=obligationValue,proto3" json:"obligation_value,omitempty"` + // Required. Action that, together with the newly created attribute value, triggers the obligation value. + Action *common.IdNameIdentifier `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"` + // Optional. Request context for the obligation trigger. + Context *policy.RequestContext `protobuf:"bytes,11,opt,name=context,proto3" json:"context,omitempty"` + // Optional. Common metadata for the obligation trigger. + Metadata *common.MetadataMutable `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` +} + +func (x *AttributeValueObligationTriggerRequest) Reset() { + *x = AttributeValueObligationTriggerRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_attributes_attributes_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AttributeValueObligationTriggerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributeValueObligationTriggerRequest) ProtoMessage() {} + +func (x *AttributeValueObligationTriggerRequest) ProtoReflect() protoreflect.Message { + mi := &file_policy_attributes_attributes_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributeValueObligationTriggerRequest.ProtoReflect.Descriptor instead. +func (*AttributeValueObligationTriggerRequest) Descriptor() ([]byte, []int) { + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{19} +} + +func (x *AttributeValueObligationTriggerRequest) GetObligationValue() *common.IdFqnIdentifier { + if x != nil { + return x.ObligationValue + } + return nil +} + +func (x *AttributeValueObligationTriggerRequest) GetAction() *common.IdNameIdentifier { + if x != nil { + return x.Action + } + return nil +} + +func (x *AttributeValueObligationTriggerRequest) GetContext() *policy.RequestContext { + if x != nil { + return x.Context + } + return nil +} + +func (x *AttributeValueObligationTriggerRequest) GetMetadata() *common.MetadataMutable { + if x != nil { + return x.Metadata + } + return nil +} + type CreateAttributeValueRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1136,6 +1345,9 @@ type CreateAttributeValueRequest struct { // Required Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // Optional + // Existing obligation values to trigger for the newly created attribute value. + ObligationTriggers []*AttributeValueObligationTriggerRequest `protobuf:"bytes,11,rep,name=obligation_triggers,json=obligationTriggers,proto3" json:"obligation_triggers,omitempty"` + // Optional // Common metadata Metadata *common.MetadataMutable `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } @@ -1143,7 +1355,7 @@ type CreateAttributeValueRequest struct { func (x *CreateAttributeValueRequest) Reset() { *x = CreateAttributeValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[18] + mi := &file_policy_attributes_attributes_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1156,7 +1368,7 @@ func (x *CreateAttributeValueRequest) String() string { func (*CreateAttributeValueRequest) ProtoMessage() {} func (x *CreateAttributeValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[18] + mi := &file_policy_attributes_attributes_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1169,7 +1381,7 @@ func (x *CreateAttributeValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAttributeValueRequest.ProtoReflect.Descriptor instead. func (*CreateAttributeValueRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{18} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{20} } func (x *CreateAttributeValueRequest) GetAttributeId() string { @@ -1186,6 +1398,13 @@ func (x *CreateAttributeValueRequest) GetValue() string { return "" } +func (x *CreateAttributeValueRequest) GetObligationTriggers() []*AttributeValueObligationTriggerRequest { + if x != nil { + return x.ObligationTriggers + } + return nil +} + func (x *CreateAttributeValueRequest) GetMetadata() *common.MetadataMutable { if x != nil { return x.Metadata @@ -1204,7 +1423,7 @@ type CreateAttributeValueResponse struct { func (x *CreateAttributeValueResponse) Reset() { *x = CreateAttributeValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[19] + mi := &file_policy_attributes_attributes_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1217,7 +1436,7 @@ func (x *CreateAttributeValueResponse) String() string { func (*CreateAttributeValueResponse) ProtoMessage() {} func (x *CreateAttributeValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[19] + mi := &file_policy_attributes_attributes_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1230,7 +1449,7 @@ func (x *CreateAttributeValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAttributeValueResponse.ProtoReflect.Descriptor instead. func (*CreateAttributeValueResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{19} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{21} } func (x *CreateAttributeValueResponse) GetValue() *policy.Value { @@ -1256,7 +1475,7 @@ type UpdateAttributeValueRequest struct { func (x *UpdateAttributeValueRequest) Reset() { *x = UpdateAttributeValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[20] + mi := &file_policy_attributes_attributes_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1269,7 +1488,7 @@ func (x *UpdateAttributeValueRequest) String() string { func (*UpdateAttributeValueRequest) ProtoMessage() {} func (x *UpdateAttributeValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[20] + mi := &file_policy_attributes_attributes_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1282,7 +1501,7 @@ func (x *UpdateAttributeValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateAttributeValueRequest.ProtoReflect.Descriptor instead. func (*UpdateAttributeValueRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{20} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{22} } func (x *UpdateAttributeValueRequest) GetId() string { @@ -1317,7 +1536,7 @@ type UpdateAttributeValueResponse struct { func (x *UpdateAttributeValueResponse) Reset() { *x = UpdateAttributeValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[21] + mi := &file_policy_attributes_attributes_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1330,7 +1549,7 @@ func (x *UpdateAttributeValueResponse) String() string { func (*UpdateAttributeValueResponse) ProtoMessage() {} func (x *UpdateAttributeValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[21] + mi := &file_policy_attributes_attributes_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1343,7 +1562,7 @@ func (x *UpdateAttributeValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateAttributeValueResponse.ProtoReflect.Descriptor instead. func (*UpdateAttributeValueResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{21} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{23} } func (x *UpdateAttributeValueResponse) GetValue() *policy.Value { @@ -1365,7 +1584,7 @@ type DeactivateAttributeValueRequest struct { func (x *DeactivateAttributeValueRequest) Reset() { *x = DeactivateAttributeValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[22] + mi := &file_policy_attributes_attributes_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1378,7 +1597,7 @@ func (x *DeactivateAttributeValueRequest) String() string { func (*DeactivateAttributeValueRequest) ProtoMessage() {} func (x *DeactivateAttributeValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[22] + mi := &file_policy_attributes_attributes_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1391,7 +1610,7 @@ func (x *DeactivateAttributeValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeactivateAttributeValueRequest.ProtoReflect.Descriptor instead. func (*DeactivateAttributeValueRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{22} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{24} } func (x *DeactivateAttributeValueRequest) GetId() string { @@ -1412,7 +1631,7 @@ type DeactivateAttributeValueResponse struct { func (x *DeactivateAttributeValueResponse) Reset() { *x = DeactivateAttributeValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[23] + mi := &file_policy_attributes_attributes_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1425,7 +1644,7 @@ func (x *DeactivateAttributeValueResponse) String() string { func (*DeactivateAttributeValueResponse) ProtoMessage() {} func (x *DeactivateAttributeValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[23] + mi := &file_policy_attributes_attributes_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1438,7 +1657,7 @@ func (x *DeactivateAttributeValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeactivateAttributeValueResponse.ProtoReflect.Descriptor instead. func (*DeactivateAttributeValueResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{23} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{25} } func (x *DeactivateAttributeValueResponse) GetValue() *policy.Value { @@ -1461,7 +1680,7 @@ type GetAttributeValuesByFqnsRequest struct { func (x *GetAttributeValuesByFqnsRequest) Reset() { *x = GetAttributeValuesByFqnsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[24] + mi := &file_policy_attributes_attributes_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1474,7 +1693,7 @@ func (x *GetAttributeValuesByFqnsRequest) String() string { func (*GetAttributeValuesByFqnsRequest) ProtoMessage() {} func (x *GetAttributeValuesByFqnsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[24] + mi := &file_policy_attributes_attributes_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1487,7 +1706,7 @@ func (x *GetAttributeValuesByFqnsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAttributeValuesByFqnsRequest.ProtoReflect.Descriptor instead. func (*GetAttributeValuesByFqnsRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{24} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{26} } func (x *GetAttributeValuesByFqnsRequest) GetFqns() []string { @@ -1509,7 +1728,7 @@ type GetAttributeValuesByFqnsResponse struct { func (x *GetAttributeValuesByFqnsResponse) Reset() { *x = GetAttributeValuesByFqnsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[25] + mi := &file_policy_attributes_attributes_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1522,7 +1741,7 @@ func (x *GetAttributeValuesByFqnsResponse) String() string { func (*GetAttributeValuesByFqnsResponse) ProtoMessage() {} func (x *GetAttributeValuesByFqnsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[25] + mi := &file_policy_attributes_attributes_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1535,7 +1754,7 @@ func (x *GetAttributeValuesByFqnsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAttributeValuesByFqnsResponse.ProtoReflect.Descriptor instead. func (*GetAttributeValuesByFqnsResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{25} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{27} } func (x *GetAttributeValuesByFqnsResponse) GetFqnAttributeValues() map[string]*GetAttributeValuesByFqnsResponse_AttributeAndValue { @@ -1560,7 +1779,7 @@ type AssignKeyAccessServerToAttributeRequest struct { func (x *AssignKeyAccessServerToAttributeRequest) Reset() { *x = AssignKeyAccessServerToAttributeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[26] + mi := &file_policy_attributes_attributes_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1573,7 +1792,7 @@ func (x *AssignKeyAccessServerToAttributeRequest) String() string { func (*AssignKeyAccessServerToAttributeRequest) ProtoMessage() {} func (x *AssignKeyAccessServerToAttributeRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[26] + mi := &file_policy_attributes_attributes_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1586,7 +1805,7 @@ func (x *AssignKeyAccessServerToAttributeRequest) ProtoReflect() protoreflect.Me // Deprecated: Use AssignKeyAccessServerToAttributeRequest.ProtoReflect.Descriptor instead. func (*AssignKeyAccessServerToAttributeRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{26} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{28} } func (x *AssignKeyAccessServerToAttributeRequest) GetAttributeKeyAccessServer() *AttributeKeyAccessServer { @@ -1608,7 +1827,7 @@ type AssignKeyAccessServerToAttributeResponse struct { func (x *AssignKeyAccessServerToAttributeResponse) Reset() { *x = AssignKeyAccessServerToAttributeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[27] + mi := &file_policy_attributes_attributes_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1621,7 +1840,7 @@ func (x *AssignKeyAccessServerToAttributeResponse) String() string { func (*AssignKeyAccessServerToAttributeResponse) ProtoMessage() {} func (x *AssignKeyAccessServerToAttributeResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[27] + mi := &file_policy_attributes_attributes_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1634,7 +1853,7 @@ func (x *AssignKeyAccessServerToAttributeResponse) ProtoReflect() protoreflect.M // Deprecated: Use AssignKeyAccessServerToAttributeResponse.ProtoReflect.Descriptor instead. func (*AssignKeyAccessServerToAttributeResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{27} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{29} } func (x *AssignKeyAccessServerToAttributeResponse) GetAttributeKeyAccessServer() *AttributeKeyAccessServer { @@ -1659,7 +1878,7 @@ type RemoveKeyAccessServerFromAttributeRequest struct { func (x *RemoveKeyAccessServerFromAttributeRequest) Reset() { *x = RemoveKeyAccessServerFromAttributeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[28] + mi := &file_policy_attributes_attributes_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1672,7 +1891,7 @@ func (x *RemoveKeyAccessServerFromAttributeRequest) String() string { func (*RemoveKeyAccessServerFromAttributeRequest) ProtoMessage() {} func (x *RemoveKeyAccessServerFromAttributeRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[28] + mi := &file_policy_attributes_attributes_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1685,7 +1904,7 @@ func (x *RemoveKeyAccessServerFromAttributeRequest) ProtoReflect() protoreflect. // Deprecated: Use RemoveKeyAccessServerFromAttributeRequest.ProtoReflect.Descriptor instead. func (*RemoveKeyAccessServerFromAttributeRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{28} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{30} } func (x *RemoveKeyAccessServerFromAttributeRequest) GetAttributeKeyAccessServer() *AttributeKeyAccessServer { @@ -1707,7 +1926,7 @@ type RemoveKeyAccessServerFromAttributeResponse struct { func (x *RemoveKeyAccessServerFromAttributeResponse) Reset() { *x = RemoveKeyAccessServerFromAttributeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[29] + mi := &file_policy_attributes_attributes_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1720,7 +1939,7 @@ func (x *RemoveKeyAccessServerFromAttributeResponse) String() string { func (*RemoveKeyAccessServerFromAttributeResponse) ProtoMessage() {} func (x *RemoveKeyAccessServerFromAttributeResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[29] + mi := &file_policy_attributes_attributes_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1733,7 +1952,7 @@ func (x *RemoveKeyAccessServerFromAttributeResponse) ProtoReflect() protoreflect // Deprecated: Use RemoveKeyAccessServerFromAttributeResponse.ProtoReflect.Descriptor instead. func (*RemoveKeyAccessServerFromAttributeResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{29} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{31} } func (x *RemoveKeyAccessServerFromAttributeResponse) GetAttributeKeyAccessServer() *AttributeKeyAccessServer { @@ -1758,7 +1977,7 @@ type AssignKeyAccessServerToValueRequest struct { func (x *AssignKeyAccessServerToValueRequest) Reset() { *x = AssignKeyAccessServerToValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[30] + mi := &file_policy_attributes_attributes_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1771,7 +1990,7 @@ func (x *AssignKeyAccessServerToValueRequest) String() string { func (*AssignKeyAccessServerToValueRequest) ProtoMessage() {} func (x *AssignKeyAccessServerToValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[30] + mi := &file_policy_attributes_attributes_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1784,7 +2003,7 @@ func (x *AssignKeyAccessServerToValueRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use AssignKeyAccessServerToValueRequest.ProtoReflect.Descriptor instead. func (*AssignKeyAccessServerToValueRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{30} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{32} } func (x *AssignKeyAccessServerToValueRequest) GetValueKeyAccessServer() *ValueKeyAccessServer { @@ -1806,7 +2025,7 @@ type AssignKeyAccessServerToValueResponse struct { func (x *AssignKeyAccessServerToValueResponse) Reset() { *x = AssignKeyAccessServerToValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[31] + mi := &file_policy_attributes_attributes_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1819,7 +2038,7 @@ func (x *AssignKeyAccessServerToValueResponse) String() string { func (*AssignKeyAccessServerToValueResponse) ProtoMessage() {} func (x *AssignKeyAccessServerToValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[31] + mi := &file_policy_attributes_attributes_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1832,7 +2051,7 @@ func (x *AssignKeyAccessServerToValueResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use AssignKeyAccessServerToValueResponse.ProtoReflect.Descriptor instead. func (*AssignKeyAccessServerToValueResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{31} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{33} } func (x *AssignKeyAccessServerToValueResponse) GetValueKeyAccessServer() *ValueKeyAccessServer { @@ -1857,7 +2076,7 @@ type RemoveKeyAccessServerFromValueRequest struct { func (x *RemoveKeyAccessServerFromValueRequest) Reset() { *x = RemoveKeyAccessServerFromValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[32] + mi := &file_policy_attributes_attributes_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1870,7 +2089,7 @@ func (x *RemoveKeyAccessServerFromValueRequest) String() string { func (*RemoveKeyAccessServerFromValueRequest) ProtoMessage() {} func (x *RemoveKeyAccessServerFromValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[32] + mi := &file_policy_attributes_attributes_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1883,7 +2102,7 @@ func (x *RemoveKeyAccessServerFromValueRequest) ProtoReflect() protoreflect.Mess // Deprecated: Use RemoveKeyAccessServerFromValueRequest.ProtoReflect.Descriptor instead. func (*RemoveKeyAccessServerFromValueRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{32} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{34} } func (x *RemoveKeyAccessServerFromValueRequest) GetValueKeyAccessServer() *ValueKeyAccessServer { @@ -1905,7 +2124,7 @@ type RemoveKeyAccessServerFromValueResponse struct { func (x *RemoveKeyAccessServerFromValueResponse) Reset() { *x = RemoveKeyAccessServerFromValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[33] + mi := &file_policy_attributes_attributes_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1918,7 +2137,7 @@ func (x *RemoveKeyAccessServerFromValueResponse) String() string { func (*RemoveKeyAccessServerFromValueResponse) ProtoMessage() {} func (x *RemoveKeyAccessServerFromValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[33] + mi := &file_policy_attributes_attributes_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1931,7 +2150,7 @@ func (x *RemoveKeyAccessServerFromValueResponse) ProtoReflect() protoreflect.Mes // Deprecated: Use RemoveKeyAccessServerFromValueResponse.ProtoReflect.Descriptor instead. func (*RemoveKeyAccessServerFromValueResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{33} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{35} } func (x *RemoveKeyAccessServerFromValueResponse) GetValueKeyAccessServer() *ValueKeyAccessServer { @@ -1953,7 +2172,7 @@ type AssignPublicKeyToAttributeRequest struct { func (x *AssignPublicKeyToAttributeRequest) Reset() { *x = AssignPublicKeyToAttributeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[34] + mi := &file_policy_attributes_attributes_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1966,7 +2185,7 @@ func (x *AssignPublicKeyToAttributeRequest) String() string { func (*AssignPublicKeyToAttributeRequest) ProtoMessage() {} func (x *AssignPublicKeyToAttributeRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[34] + mi := &file_policy_attributes_attributes_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1979,7 +2198,7 @@ func (x *AssignPublicKeyToAttributeRequest) ProtoReflect() protoreflect.Message // Deprecated: Use AssignPublicKeyToAttributeRequest.ProtoReflect.Descriptor instead. func (*AssignPublicKeyToAttributeRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{34} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{36} } func (x *AssignPublicKeyToAttributeRequest) GetAttributeKey() *AttributeKey { @@ -2001,7 +2220,7 @@ type AssignPublicKeyToAttributeResponse struct { func (x *AssignPublicKeyToAttributeResponse) Reset() { *x = AssignPublicKeyToAttributeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[35] + mi := &file_policy_attributes_attributes_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2014,7 +2233,7 @@ func (x *AssignPublicKeyToAttributeResponse) String() string { func (*AssignPublicKeyToAttributeResponse) ProtoMessage() {} func (x *AssignPublicKeyToAttributeResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[35] + mi := &file_policy_attributes_attributes_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2027,7 +2246,7 @@ func (x *AssignPublicKeyToAttributeResponse) ProtoReflect() protoreflect.Message // Deprecated: Use AssignPublicKeyToAttributeResponse.ProtoReflect.Descriptor instead. func (*AssignPublicKeyToAttributeResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{35} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{37} } func (x *AssignPublicKeyToAttributeResponse) GetAttributeKey() *AttributeKey { @@ -2049,7 +2268,7 @@ type RemovePublicKeyFromAttributeRequest struct { func (x *RemovePublicKeyFromAttributeRequest) Reset() { *x = RemovePublicKeyFromAttributeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[36] + mi := &file_policy_attributes_attributes_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2062,7 +2281,7 @@ func (x *RemovePublicKeyFromAttributeRequest) String() string { func (*RemovePublicKeyFromAttributeRequest) ProtoMessage() {} func (x *RemovePublicKeyFromAttributeRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[36] + mi := &file_policy_attributes_attributes_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2075,7 +2294,7 @@ func (x *RemovePublicKeyFromAttributeRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use RemovePublicKeyFromAttributeRequest.ProtoReflect.Descriptor instead. func (*RemovePublicKeyFromAttributeRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{36} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{38} } func (x *RemovePublicKeyFromAttributeRequest) GetAttributeKey() *AttributeKey { @@ -2097,7 +2316,7 @@ type RemovePublicKeyFromAttributeResponse struct { func (x *RemovePublicKeyFromAttributeResponse) Reset() { *x = RemovePublicKeyFromAttributeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[37] + mi := &file_policy_attributes_attributes_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2110,7 +2329,7 @@ func (x *RemovePublicKeyFromAttributeResponse) String() string { func (*RemovePublicKeyFromAttributeResponse) ProtoMessage() {} func (x *RemovePublicKeyFromAttributeResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[37] + mi := &file_policy_attributes_attributes_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2123,7 +2342,7 @@ func (x *RemovePublicKeyFromAttributeResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use RemovePublicKeyFromAttributeResponse.ProtoReflect.Descriptor instead. func (*RemovePublicKeyFromAttributeResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{37} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{39} } func (x *RemovePublicKeyFromAttributeResponse) GetAttributeKey() *AttributeKey { @@ -2145,7 +2364,7 @@ type AssignPublicKeyToValueRequest struct { func (x *AssignPublicKeyToValueRequest) Reset() { *x = AssignPublicKeyToValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[38] + mi := &file_policy_attributes_attributes_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2158,7 +2377,7 @@ func (x *AssignPublicKeyToValueRequest) String() string { func (*AssignPublicKeyToValueRequest) ProtoMessage() {} func (x *AssignPublicKeyToValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[38] + mi := &file_policy_attributes_attributes_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2171,7 +2390,7 @@ func (x *AssignPublicKeyToValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AssignPublicKeyToValueRequest.ProtoReflect.Descriptor instead. func (*AssignPublicKeyToValueRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{38} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{40} } func (x *AssignPublicKeyToValueRequest) GetValueKey() *ValueKey { @@ -2193,7 +2412,7 @@ type AssignPublicKeyToValueResponse struct { func (x *AssignPublicKeyToValueResponse) Reset() { *x = AssignPublicKeyToValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[39] + mi := &file_policy_attributes_attributes_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2206,7 +2425,7 @@ func (x *AssignPublicKeyToValueResponse) String() string { func (*AssignPublicKeyToValueResponse) ProtoMessage() {} func (x *AssignPublicKeyToValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[39] + mi := &file_policy_attributes_attributes_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2219,7 +2438,7 @@ func (x *AssignPublicKeyToValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AssignPublicKeyToValueResponse.ProtoReflect.Descriptor instead. func (*AssignPublicKeyToValueResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{39} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{41} } func (x *AssignPublicKeyToValueResponse) GetValueKey() *ValueKey { @@ -2241,7 +2460,7 @@ type RemovePublicKeyFromValueRequest struct { func (x *RemovePublicKeyFromValueRequest) Reset() { *x = RemovePublicKeyFromValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[40] + mi := &file_policy_attributes_attributes_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2254,7 +2473,7 @@ func (x *RemovePublicKeyFromValueRequest) String() string { func (*RemovePublicKeyFromValueRequest) ProtoMessage() {} func (x *RemovePublicKeyFromValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[40] + mi := &file_policy_attributes_attributes_proto_msgTypes[42] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2267,7 +2486,7 @@ func (x *RemovePublicKeyFromValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemovePublicKeyFromValueRequest.ProtoReflect.Descriptor instead. func (*RemovePublicKeyFromValueRequest) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{40} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{42} } func (x *RemovePublicKeyFromValueRequest) GetValueKey() *ValueKey { @@ -2289,7 +2508,7 @@ type RemovePublicKeyFromValueResponse struct { func (x *RemovePublicKeyFromValueResponse) Reset() { *x = RemovePublicKeyFromValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[41] + mi := &file_policy_attributes_attributes_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2302,7 +2521,7 @@ func (x *RemovePublicKeyFromValueResponse) String() string { func (*RemovePublicKeyFromValueResponse) ProtoMessage() {} func (x *RemovePublicKeyFromValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[41] + mi := &file_policy_attributes_attributes_proto_msgTypes[43] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2315,7 +2534,7 @@ func (x *RemovePublicKeyFromValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemovePublicKeyFromValueResponse.ProtoReflect.Descriptor instead. func (*RemovePublicKeyFromValueResponse) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{41} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{43} } func (x *RemovePublicKeyFromValueResponse) GetValueKey() *ValueKey { @@ -2337,7 +2556,7 @@ type GetAttributeValuesByFqnsResponse_AttributeAndValue struct { func (x *GetAttributeValuesByFqnsResponse_AttributeAndValue) Reset() { *x = GetAttributeValuesByFqnsResponse_AttributeAndValue{} if protoimpl.UnsafeEnabled { - mi := &file_policy_attributes_attributes_proto_msgTypes[42] + mi := &file_policy_attributes_attributes_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2350,7 +2569,7 @@ func (x *GetAttributeValuesByFqnsResponse_AttributeAndValue) String() string { func (*GetAttributeValuesByFqnsResponse_AttributeAndValue) ProtoMessage() {} func (x *GetAttributeValuesByFqnsResponse_AttributeAndValue) ProtoReflect() protoreflect.Message { - mi := &file_policy_attributes_attributes_proto_msgTypes[42] + mi := &file_policy_attributes_attributes_proto_msgTypes[44] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2363,7 +2582,7 @@ func (x *GetAttributeValuesByFqnsResponse_AttributeAndValue) ProtoReflect() prot // Deprecated: Use GetAttributeValuesByFqnsResponse_AttributeAndValue.ProtoReflect.Descriptor instead. func (*GetAttributeValuesByFqnsResponse_AttributeAndValue) Descriptor() ([]byte, []int) { - return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{25, 0} + return file_policy_attributes_attributes_proto_rawDescGZIP(), []int{27, 0} } func (x *GetAttributeValuesByFqnsResponse_AttributeAndValue) GetAttribute() *policy.Attribute { @@ -2389,298 +2608,360 @@ var file_policy_attributes_attributes_proto_rawDesc = []byte{ 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, - 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x16, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x86, 0x01, 0x0a, 0x18, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, - 0x01, 0x01, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, - 0x39, 0x0a, 0x14, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, - 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x7a, - 0x0a, 0x14, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, - 0x01, 0x01, 0x52, 0x07, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x14, 0x6b, - 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, - 0xb0, 0x01, 0x01, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x62, 0x0a, 0x0c, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x0c, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x06, 0x6b, 0x65, - 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, - 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x22, 0x56, - 0x0a, 0x08, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x08, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, - 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x07, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x49, 0x64, 0x12, 0x22, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, + 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x16, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x86, 0x01, 0x0a, 0x18, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, + 0x64, 0x12, 0x39, 0x0a, 0x14, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x3a, 0x02, 0x18, 0x01, + 0x22, 0x7a, 0x0a, 0x14, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x07, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, 0x12, 0x39, 0x0a, + 0x14, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, + 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x62, 0x0a, 0x0c, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x0c, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, - 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x22, 0x99, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x33, 0x0a, - 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x81, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, - 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xbe, 0x03, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d, 0xba, 0x48, 0x08, 0xd8, - 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x18, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2d, 0x0a, - 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, - 0x0b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x03, - 0x66, 0x71, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, - 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0xaa, 0x02, 0xba, - 0x48, 0xa6, 0x02, 0x1a, 0xa2, 0x01, 0x0a, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, - 0x65, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x50, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, - 0x20, 0x75, 0x73, 0x65, 0x20, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, - 0x27, 0x69, 0x64, 0x27, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, - 0x65, 0x20, 0x6f, 0x66, 0x20, 0x27, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, - 0x69, 0x64, 0x27, 0x20, 0x6f, 0x72, 0x20, 0x27, 0x66, 0x71, 0x6e, 0x27, 0x2c, 0x20, 0x62, 0x75, - 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x1a, 0x3c, 0x21, 0x28, 0x68, 0x61, - 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, 0x20, 0x26, 0x26, 0x20, 0x28, 0x68, - 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x66, 0x71, 0x6e, 0x29, 0x29, 0x29, 0x1a, 0x7f, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, - 0x69, 0x72, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x33, 0x45, 0x69, 0x74, - 0x68, 0x65, 0x72, 0x20, 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, - 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x20, 0x6f, 0x72, - 0x20, 0x66, 0x71, 0x6e, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, - 0x1a, 0x37, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, 0x20, 0x7c, - 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, - 0x74, 0x68, 0x69, 0x73, 0x2e, 0x66, 0x71, 0x6e, 0x29, 0x42, 0x0c, 0x0a, 0x0a, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x47, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x41, 0x74, + 0x0b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x06, + 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, + 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, + 0x22, 0x56, 0x0a, 0x08, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x08, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, + 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x07, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, + 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x22, 0x96, 0x01, 0x0a, 0x0e, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x45, 0x0a, 0x05, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x53, + 0x6f, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x54, 0x79, 0x70, + 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x05, 0x66, 0x69, 0x65, + 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, + 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, + 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0xda, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, + 0x6e, 0x75, 0x6d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3f, 0x0a, + 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0x81, + 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x0a, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0a, + 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0xbe, 0x03, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d, 0xba, 0x48, 0x08, 0xd8, 0x01, 0x01, 0x72, 0x03, + 0xb0, 0x01, 0x01, 0x18, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2d, 0x0a, 0x0c, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, + 0x01, 0x48, 0x00, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0xaa, 0x02, 0xba, 0x48, 0xa6, 0x02, 0x1a, + 0xa2, 0x01, 0x0a, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x73, 0x12, 0x50, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x75, 0x73, 0x65, + 0x20, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x27, 0x69, 0x64, 0x27, + 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, + 0x20, 0x27, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x27, 0x20, + 0x6f, 0x72, 0x20, 0x27, 0x66, 0x71, 0x6e, 0x27, 0x2c, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, + 0x74, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x1a, 0x3c, 0x21, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, 0x20, 0x26, 0x26, 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, + 0x68, 0x69, 0x73, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, + 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x66, 0x71, + 0x6e, 0x29, 0x29, 0x29, 0x1a, 0x7f, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, + 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x33, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, + 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x66, 0x71, 0x6e, + 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x1a, 0x37, 0x68, 0x61, + 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, + 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x66, 0x71, 0x6e, 0x29, 0x42, 0x0c, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x22, 0x47, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0x89, 0x05, 0x0a, + 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x49, 0x64, 0x12, 0xaf, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x9a, 0x02, 0xba, 0x48, 0x96, 0x02, 0xba, 0x01, 0x8a, 0x02, 0x0a, 0x15, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb3, 0x01, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, + 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, + 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, + 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, + 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, + 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, + 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, + 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, + 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, + 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, + 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, + 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, + 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x45, + 0x6e, 0x75, 0x6d, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, 0x01, + 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x56, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x42, 0x3e, 0xba, 0x48, 0x3b, 0x92, 0x01, 0x38, 0x08, 0x00, + 0x18, 0x01, 0x22, 0x32, 0x72, 0x30, 0x18, 0xfd, 0x01, 0x32, 0x2b, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, + 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, + 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, + 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, + 0x0a, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x74, 0x72, 0x61, 0x76, 0x65, 0x72, 0x73, 0x61, + 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x54, 0x72, 0x61, 0x76, 0x65, 0x72, + 0x73, 0x61, 0x6c, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x4a, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x22, 0xbd, 0x01, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, + 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, + 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, + 0x76, 0x69, 0x6f, 0x72, 0x22, 0x4a, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x22, 0xc4, 0x04, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0xaf, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x9a, 0x02, 0xba, 0x48, 0x96, 0x02, 0xba, 0x01, - 0x8a, 0x02, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb3, 0x01, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, - 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, - 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, - 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, - 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, - 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, - 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, - 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, - 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, - 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, - 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, - 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x04, 0x72, 0x75, - 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, - 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x56, 0x0a, 0x06, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x42, 0x3e, 0xba, 0x48, 0x3b, 0x92, - 0x01, 0x38, 0x08, 0x00, 0x18, 0x01, 0x22, 0x32, 0x72, 0x30, 0x18, 0xfd, 0x01, 0x32, 0x2b, 0x5e, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x4a, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x22, 0xbd, 0x01, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, + 0x22, 0x36, 0x0a, 0x1a, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, - 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, - 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, - 0x69, 0x6f, 0x72, 0x22, 0x4a, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, - 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, - 0x36, 0x0a, 0x1a, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, - 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4e, 0x0a, 0x1b, 0x44, 0x65, 0x61, 0x63, 0x74, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xab, 0x03, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x0d, 0xba, 0x48, 0x08, 0xd8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x18, 0x01, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x25, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, - 0x00, 0x52, 0x07, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x03, 0x66, 0x71, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, - 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0x9a, 0x02, 0xba, 0x48, 0x96, - 0x02, 0x1a, 0x9a, 0x01, 0x0a, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x5f, - 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x4c, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x75, - 0x73, 0x65, 0x20, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x27, 0x69, - 0x64, 0x27, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, 0x20, - 0x6f, 0x66, 0x20, 0x27, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x27, 0x20, 0x6f, 0x72, - 0x20, 0x27, 0x66, 0x71, 0x6e, 0x27, 0x2c, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, - 0x62, 0x6f, 0x74, 0x68, 0x1a, 0x38, 0x21, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, - 0x2e, 0x69, 0x64, 0x29, 0x20, 0x26, 0x26, 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, - 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x66, 0x71, 0x6e, 0x29, 0x29, 0x29, 0x1a, 0x77, - 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, - 0x73, 0x12, 0x2f, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, - 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x20, - 0x6f, 0x72, 0x20, 0x66, 0x71, 0x6e, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, - 0x65, 0x74, 0x1a, 0x33, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, - 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x66, 0x71, 0x6e, 0x29, 0x42, 0x0c, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x40, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xad, 0x01, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4e, 0x0a, 0x1b, 0x44, 0x65, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xab, 0x03, 0x0a, 0x18, 0x47, 0x65, 0x74, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x0d, 0xba, 0x48, 0x08, 0xd8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x18, 0x01, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x25, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, + 0x48, 0x00, 0x52, 0x07, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x03, 0x66, + 0x71, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, + 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0x9a, 0x02, 0xba, 0x48, + 0x96, 0x02, 0x1a, 0x9a, 0x01, 0x0a, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, + 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x4c, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, + 0x75, 0x73, 0x65, 0x20, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x27, + 0x69, 0x64, 0x27, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, + 0x20, 0x6f, 0x66, 0x20, 0x27, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x27, 0x20, 0x6f, + 0x72, 0x20, 0x27, 0x66, 0x71, 0x6e, 0x27, 0x2c, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, + 0x20, 0x62, 0x6f, 0x74, 0x68, 0x1a, 0x38, 0x21, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, + 0x73, 0x2e, 0x69, 0x64, 0x29, 0x20, 0x26, 0x26, 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, + 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x66, 0x71, 0x6e, 0x29, 0x29, 0x29, 0x1a, + 0x77, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x73, 0x12, 0x2f, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x69, 0x64, 0x20, 0x6f, 0x72, + 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, + 0x20, 0x6f, 0x72, 0x20, 0x66, 0x71, 0x6e, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, + 0x73, 0x65, 0x74, 0x1a, 0x33, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, + 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, + 0x68, 0x69, 0x73, 0x2e, 0x66, 0x71, 0x6e, 0x29, 0x42, 0x0c, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x40, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xad, 0x01, 0x0a, 0x1a, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x41, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, + 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7a, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, - 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, - 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7a, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x34, 0x0a, - 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0xc5, 0x03, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, - 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, - 0x12, 0xb4, 0x02, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x9d, 0x02, 0xba, 0x48, 0x99, 0x02, 0xba, 0x01, 0x8d, 0x02, 0x0a, 0x16, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x12, 0xb5, 0x01, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x20, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, - 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, - 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, - 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, - 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, - 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, - 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, - 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, - 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, - 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x34, + 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x02, 0x0a, 0x26, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x4a, 0x0a, 0x10, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x49, 0x64, 0x46, 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0f, 0x6f, 0x62, 0x6c, 0x69, + 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x03, - 0x10, 0x04, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x22, 0x43, 0x0a, 0x1c, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x22, 0xd1, 0x01, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, - 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, - 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, - 0x61, 0x76, 0x69, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x52, 0x07, 0x6d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x22, 0x43, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, + 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb1, 0x04, 0x0a, + 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0xb4, 0x02, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x9d, 0x02, 0xba, 0x48, 0x99, 0x02, + 0xba, 0x01, 0x8d, 0x02, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb5, 0x01, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, + 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, + 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, + 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, + 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, + 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x64, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, + 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, + 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, + 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, + 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, + 0x29, 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x6a, 0x0a, 0x13, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, + 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x39, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x12, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x12, 0x33, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x22, 0x43, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, + 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, + 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, + 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x22, 0x43, 0x0a, 0x1c, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3b, + 0x0a, 0x1f, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x47, 0x0a, 0x20, 0x44, + 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x22, 0x54, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0x92, 0x01, 0x05, 0x08, 0x01, 0x10, + 0xfa, 0x01, 0x52, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x0a, + 0x77, 0x69, 0x74, 0x68, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x9b, 0x03, 0x0a, 0x20, 0x47, + 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x42, 0x79, 0x46, 0x71, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x7d, 0x0a, 0x14, 0x66, 0x71, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x46, 0x71, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x66, 0x71, 0x6e, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x69, + 0x0a, 0x11, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x41, 0x6e, 0x64, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3b, 0x0a, 0x1f, 0x44, 0x65, 0x61, - 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, - 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x47, 0x0a, 0x20, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, - 0x54, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, - 0x42, 0x0b, 0xba, 0x48, 0x08, 0x92, 0x01, 0x05, 0x08, 0x01, 0x10, 0xfa, 0x01, 0x52, 0x04, 0x66, - 0x71, 0x6e, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x9b, 0x03, 0x0a, 0x20, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, - 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7d, 0x0a, 0x14, 0x66, 0x71, - 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, - 0x79, 0x46, 0x71, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x71, + 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x8c, 0x01, 0x0a, 0x17, 0x46, 0x71, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x66, 0x71, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x69, 0x0a, 0x11, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x41, 0x6e, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2f, - 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, - 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x8c, 0x01, 0x0a, 0x17, 0x46, 0x71, 0x6e, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x5b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x45, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, 0x6e, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x41, 0x6e, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x99, 0x01, 0x0a, 0x27, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x41, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x5b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, + 0x71, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x41, 0x6e, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x99, 0x01, 0x0a, 0x27, 0x41, 0x73, 0x73, + 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x6a, 0x0a, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x3a, 0x02, 0x18, 0x01, 0x22, 0x9a, 0x01, 0x0a, 0x28, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x6a, 0x0a, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x52, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, + 0x01, 0x22, 0x9b, 0x01, 0x0a, 0x29, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x6a, 0x0a, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, @@ -2689,296 +2970,285 @@ var file_policy_attributes_attributes_proto_rawDesc = []byte{ 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, - 0x9a, 0x01, 0x0a, 0x28, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x1b, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x9b, 0x01, 0x0a, - 0x29, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x6a, 0x0a, 0x1b, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x9c, 0x01, 0x0a, 0x2a, 0x52, - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x1b, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x89, 0x01, 0x0a, 0x23, 0x41, 0x73, - 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x9c, 0x01, 0x0a, 0x2a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, + 0x0a, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, + 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x89, + 0x01, 0x0a, 0x23, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5e, 0x0a, 0x17, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x14, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x8a, 0x01, 0x0a, 0x24, 0x41, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x17, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x14, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x8b, 0x01, 0x0a, 0x25, 0x52, 0x65, 0x6d, 0x6f, + 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5e, 0x0a, 0x17, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x14, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x8a, 0x01, 0x0a, 0x24, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, - 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, - 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, - 0x0a, 0x17, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x14, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, - 0x18, 0x01, 0x22, 0x8b, 0x01, 0x0a, 0x25, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5e, 0x0a, 0x17, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x14, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, - 0x22, 0x8c, 0x01, 0x0a, 0x26, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x17, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x14, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, - 0x71, 0x0a, 0x21, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x4c, 0x0a, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, - 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x22, 0x6a, 0x0a, 0x22, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x52, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x73, - 0x0a, 0x23, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4c, 0x0a, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, - 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x4b, 0x65, 0x79, 0x22, 0x6c, 0x0a, 0x24, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, + 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x8c, 0x01, 0x0a, 0x26, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, + 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x5e, 0x0a, 0x17, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x14, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x3a, 0x02, 0x18, 0x01, 0x22, 0x71, 0x0a, 0x21, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4c, 0x0a, 0x0d, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, + 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x6a, 0x0a, 0x22, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, + 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x4b, 0x65, 0x79, 0x22, 0x73, 0x0a, 0x23, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, - 0x79, 0x22, 0x61, 0x0a, 0x1d, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x40, 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, - 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x4b, 0x65, 0x79, 0x22, 0x5a, 0x0a, 0x1e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, - 0x22, 0x63, 0x0a, 0x1f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x40, 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x5c, 0x0a, 0x20, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4c, 0x0a, 0x0d, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x6c, 0x0a, 0x24, 0x52, 0x65, 0x6d, 0x6f, + 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x44, 0x0a, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x61, 0x0a, 0x1d, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x40, 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, + 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x5a, 0x0a, 0x1e, 0x41, 0x73, 0x73, + 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x63, 0x0a, 0x1f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x4b, 0x65, 0x79, 0x32, 0xf2, 0x13, 0x0a, 0x11, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6a, 0x0a, 0x0e, 0x4c, 0x69, 0x73, - 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x70, 0x6f, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x40, 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x79, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2d, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, - 0x12, 0x64, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x12, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0xa1, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, - 0x71, 0x6e, 0x73, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, 0x6e, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, - 0x46, 0x71, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x2f, 0x2a, 0x2f, 0x66, 0x71, 0x6e, 0x90, 0x02, 0x01, 0x12, 0x6a, 0x0a, 0x0f, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x29, 0x2e, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, + 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x5c, 0x0a, 0x20, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, + 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, + 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x2a, 0xa3, 0x01, 0x0a, 0x12, 0x53, 0x6f, 0x72, + 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x24, 0x0a, 0x20, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, + 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x41, 0x54, + 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x41, + 0x4d, 0x45, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x41, 0x54, 0x54, + 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, + 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x02, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x4f, 0x52, + 0x54, 0x5f, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x32, 0xdc, + 0x13, 0x0a, 0x11, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x6a, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, + 0x12, 0x7c, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0x88, 0x02, 0x01, 0x90, 0x02, 0x01, 0x12, 0x64, + 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x26, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x03, 0x90, 0x02, 0x01, 0x12, 0x88, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, 0x6e, + 0x73, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, 0x6e, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x71, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, + 0x6a, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x12, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6a, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6a, 0x0a, 0x0f, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x29, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x76, 0x0a, 0x13, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x44, 0x65, - 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x61, - 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x73, 0x0a, 0x11, 0x47, 0x65, - 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x76, 0x0a, 0x13, 0x44, 0x65, 0x61, 0x63, 0x74, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2d, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x73, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x03, 0x90, 0x02, 0x01, 0x12, 0x79, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, - 0x79, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x79, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x79, 0x0a, 0x14, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x85, 0x01, 0x0a, 0x18, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa0, 0x01, - 0x0a, 0x20, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x12, 0x3a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x85, 0x01, 0x0a, 0x18, 0x44, + 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, + 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0xa0, 0x01, 0x0a, 0x20, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, - 0x12, 0xa6, 0x01, 0x0a, 0x22, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, - 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x3a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, + 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0xa6, 0x01, 0x0a, 0x22, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, - 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x94, 0x01, 0x0a, 0x1c, 0x41, 0x73, - 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x36, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, - 0x12, 0x9a, 0x01, 0x0a, 0x1e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x38, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, - 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x8b, 0x01, - 0x0a, 0x1a, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x34, 0x2e, 0x70, + 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, + 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x94, + 0x01, 0x0a, 0x1c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x36, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, + 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x9a, 0x01, 0x0a, 0x1e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, + 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x38, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, + 0x02, 0x01, 0x12, 0x8b, 0x01, 0x0a, 0x1a, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x91, 0x01, 0x0a, 0x1c, - 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, - 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x36, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x7f, 0x0a, 0x16, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, - 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, - 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, - 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x85, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x32, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x65, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, + 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x91, 0x01, 0x0a, 0x1c, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x12, 0x36, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xc8, 0x01, 0x0a, 0x15, 0x63, 0x6f, 0x6d, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, + 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x7f, 0x0a, 0x16, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x42, 0x0f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, - 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0xa2, 0x02, 0x03, 0x50, 0x41, 0x58, 0xaa, 0x02, 0x11, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0xca, 0x02, 0x11, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0xe2, 0x02, - 0x1d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, - 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x85, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xc8, 0x01, + 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, + 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0xa2, 0x02, 0x03, 0x50, 0x41, 0x58, 0xaa, 0x02, 0x11, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0xca, + 0x02, 0x11, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0xe2, 0x02, 0x1d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2993,148 +3263,166 @@ func file_policy_attributes_attributes_proto_rawDescGZIP() []byte { return file_policy_attributes_attributes_proto_rawDescData } -var file_policy_attributes_attributes_proto_msgTypes = make([]protoimpl.MessageInfo, 44) +var file_policy_attributes_attributes_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_policy_attributes_attributes_proto_msgTypes = make([]protoimpl.MessageInfo, 46) var file_policy_attributes_attributes_proto_goTypes = []interface{}{ - (*AttributeKeyAccessServer)(nil), // 0: policy.attributes.AttributeKeyAccessServer - (*ValueKeyAccessServer)(nil), // 1: policy.attributes.ValueKeyAccessServer - (*AttributeKey)(nil), // 2: policy.attributes.AttributeKey - (*ValueKey)(nil), // 3: policy.attributes.ValueKey - (*ListAttributesRequest)(nil), // 4: policy.attributes.ListAttributesRequest - (*ListAttributesResponse)(nil), // 5: policy.attributes.ListAttributesResponse - (*GetAttributeRequest)(nil), // 6: policy.attributes.GetAttributeRequest - (*GetAttributeResponse)(nil), // 7: policy.attributes.GetAttributeResponse - (*CreateAttributeRequest)(nil), // 8: policy.attributes.CreateAttributeRequest - (*CreateAttributeResponse)(nil), // 9: policy.attributes.CreateAttributeResponse - (*UpdateAttributeRequest)(nil), // 10: policy.attributes.UpdateAttributeRequest - (*UpdateAttributeResponse)(nil), // 11: policy.attributes.UpdateAttributeResponse - (*DeactivateAttributeRequest)(nil), // 12: policy.attributes.DeactivateAttributeRequest - (*DeactivateAttributeResponse)(nil), // 13: policy.attributes.DeactivateAttributeResponse - (*GetAttributeValueRequest)(nil), // 14: policy.attributes.GetAttributeValueRequest - (*GetAttributeValueResponse)(nil), // 15: policy.attributes.GetAttributeValueResponse - (*ListAttributeValuesRequest)(nil), // 16: policy.attributes.ListAttributeValuesRequest - (*ListAttributeValuesResponse)(nil), // 17: policy.attributes.ListAttributeValuesResponse - (*CreateAttributeValueRequest)(nil), // 18: policy.attributes.CreateAttributeValueRequest - (*CreateAttributeValueResponse)(nil), // 19: policy.attributes.CreateAttributeValueResponse - (*UpdateAttributeValueRequest)(nil), // 20: policy.attributes.UpdateAttributeValueRequest - (*UpdateAttributeValueResponse)(nil), // 21: policy.attributes.UpdateAttributeValueResponse - (*DeactivateAttributeValueRequest)(nil), // 22: policy.attributes.DeactivateAttributeValueRequest - (*DeactivateAttributeValueResponse)(nil), // 23: policy.attributes.DeactivateAttributeValueResponse - (*GetAttributeValuesByFqnsRequest)(nil), // 24: policy.attributes.GetAttributeValuesByFqnsRequest - (*GetAttributeValuesByFqnsResponse)(nil), // 25: policy.attributes.GetAttributeValuesByFqnsResponse - (*AssignKeyAccessServerToAttributeRequest)(nil), // 26: policy.attributes.AssignKeyAccessServerToAttributeRequest - (*AssignKeyAccessServerToAttributeResponse)(nil), // 27: policy.attributes.AssignKeyAccessServerToAttributeResponse - (*RemoveKeyAccessServerFromAttributeRequest)(nil), // 28: policy.attributes.RemoveKeyAccessServerFromAttributeRequest - (*RemoveKeyAccessServerFromAttributeResponse)(nil), // 29: policy.attributes.RemoveKeyAccessServerFromAttributeResponse - (*AssignKeyAccessServerToValueRequest)(nil), // 30: policy.attributes.AssignKeyAccessServerToValueRequest - (*AssignKeyAccessServerToValueResponse)(nil), // 31: policy.attributes.AssignKeyAccessServerToValueResponse - (*RemoveKeyAccessServerFromValueRequest)(nil), // 32: policy.attributes.RemoveKeyAccessServerFromValueRequest - (*RemoveKeyAccessServerFromValueResponse)(nil), // 33: policy.attributes.RemoveKeyAccessServerFromValueResponse - (*AssignPublicKeyToAttributeRequest)(nil), // 34: policy.attributes.AssignPublicKeyToAttributeRequest - (*AssignPublicKeyToAttributeResponse)(nil), // 35: policy.attributes.AssignPublicKeyToAttributeResponse - (*RemovePublicKeyFromAttributeRequest)(nil), // 36: policy.attributes.RemovePublicKeyFromAttributeRequest - (*RemovePublicKeyFromAttributeResponse)(nil), // 37: policy.attributes.RemovePublicKeyFromAttributeResponse - (*AssignPublicKeyToValueRequest)(nil), // 38: policy.attributes.AssignPublicKeyToValueRequest - (*AssignPublicKeyToValueResponse)(nil), // 39: policy.attributes.AssignPublicKeyToValueResponse - (*RemovePublicKeyFromValueRequest)(nil), // 40: policy.attributes.RemovePublicKeyFromValueRequest - (*RemovePublicKeyFromValueResponse)(nil), // 41: policy.attributes.RemovePublicKeyFromValueResponse - (*GetAttributeValuesByFqnsResponse_AttributeAndValue)(nil), // 42: policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue - nil, // 43: policy.attributes.GetAttributeValuesByFqnsResponse.FqnAttributeValuesEntry - (common.ActiveStateEnum)(0), // 44: common.ActiveStateEnum - (*policy.PageRequest)(nil), // 45: policy.PageRequest - (*policy.Attribute)(nil), // 46: policy.Attribute - (*policy.PageResponse)(nil), // 47: policy.PageResponse - (policy.AttributeRuleTypeEnum)(0), // 48: policy.AttributeRuleTypeEnum - (*common.MetadataMutable)(nil), // 49: common.MetadataMutable - (common.MetadataUpdateEnum)(0), // 50: common.MetadataUpdateEnum - (*policy.Value)(nil), // 51: policy.Value + (SortAttributesType)(0), // 0: policy.attributes.SortAttributesType + (*AttributeKeyAccessServer)(nil), // 1: policy.attributes.AttributeKeyAccessServer + (*ValueKeyAccessServer)(nil), // 2: policy.attributes.ValueKeyAccessServer + (*AttributeKey)(nil), // 3: policy.attributes.AttributeKey + (*ValueKey)(nil), // 4: policy.attributes.ValueKey + (*AttributesSort)(nil), // 5: policy.attributes.AttributesSort + (*ListAttributesRequest)(nil), // 6: policy.attributes.ListAttributesRequest + (*ListAttributesResponse)(nil), // 7: policy.attributes.ListAttributesResponse + (*GetAttributeRequest)(nil), // 8: policy.attributes.GetAttributeRequest + (*GetAttributeResponse)(nil), // 9: policy.attributes.GetAttributeResponse + (*CreateAttributeRequest)(nil), // 10: policy.attributes.CreateAttributeRequest + (*CreateAttributeResponse)(nil), // 11: policy.attributes.CreateAttributeResponse + (*UpdateAttributeRequest)(nil), // 12: policy.attributes.UpdateAttributeRequest + (*UpdateAttributeResponse)(nil), // 13: policy.attributes.UpdateAttributeResponse + (*DeactivateAttributeRequest)(nil), // 14: policy.attributes.DeactivateAttributeRequest + (*DeactivateAttributeResponse)(nil), // 15: policy.attributes.DeactivateAttributeResponse + (*GetAttributeValueRequest)(nil), // 16: policy.attributes.GetAttributeValueRequest + (*GetAttributeValueResponse)(nil), // 17: policy.attributes.GetAttributeValueResponse + (*ListAttributeValuesRequest)(nil), // 18: policy.attributes.ListAttributeValuesRequest + (*ListAttributeValuesResponse)(nil), // 19: policy.attributes.ListAttributeValuesResponse + (*AttributeValueObligationTriggerRequest)(nil), // 20: policy.attributes.AttributeValueObligationTriggerRequest + (*CreateAttributeValueRequest)(nil), // 21: policy.attributes.CreateAttributeValueRequest + (*CreateAttributeValueResponse)(nil), // 22: policy.attributes.CreateAttributeValueResponse + (*UpdateAttributeValueRequest)(nil), // 23: policy.attributes.UpdateAttributeValueRequest + (*UpdateAttributeValueResponse)(nil), // 24: policy.attributes.UpdateAttributeValueResponse + (*DeactivateAttributeValueRequest)(nil), // 25: policy.attributes.DeactivateAttributeValueRequest + (*DeactivateAttributeValueResponse)(nil), // 26: policy.attributes.DeactivateAttributeValueResponse + (*GetAttributeValuesByFqnsRequest)(nil), // 27: policy.attributes.GetAttributeValuesByFqnsRequest + (*GetAttributeValuesByFqnsResponse)(nil), // 28: policy.attributes.GetAttributeValuesByFqnsResponse + (*AssignKeyAccessServerToAttributeRequest)(nil), // 29: policy.attributes.AssignKeyAccessServerToAttributeRequest + (*AssignKeyAccessServerToAttributeResponse)(nil), // 30: policy.attributes.AssignKeyAccessServerToAttributeResponse + (*RemoveKeyAccessServerFromAttributeRequest)(nil), // 31: policy.attributes.RemoveKeyAccessServerFromAttributeRequest + (*RemoveKeyAccessServerFromAttributeResponse)(nil), // 32: policy.attributes.RemoveKeyAccessServerFromAttributeResponse + (*AssignKeyAccessServerToValueRequest)(nil), // 33: policy.attributes.AssignKeyAccessServerToValueRequest + (*AssignKeyAccessServerToValueResponse)(nil), // 34: policy.attributes.AssignKeyAccessServerToValueResponse + (*RemoveKeyAccessServerFromValueRequest)(nil), // 35: policy.attributes.RemoveKeyAccessServerFromValueRequest + (*RemoveKeyAccessServerFromValueResponse)(nil), // 36: policy.attributes.RemoveKeyAccessServerFromValueResponse + (*AssignPublicKeyToAttributeRequest)(nil), // 37: policy.attributes.AssignPublicKeyToAttributeRequest + (*AssignPublicKeyToAttributeResponse)(nil), // 38: policy.attributes.AssignPublicKeyToAttributeResponse + (*RemovePublicKeyFromAttributeRequest)(nil), // 39: policy.attributes.RemovePublicKeyFromAttributeRequest + (*RemovePublicKeyFromAttributeResponse)(nil), // 40: policy.attributes.RemovePublicKeyFromAttributeResponse + (*AssignPublicKeyToValueRequest)(nil), // 41: policy.attributes.AssignPublicKeyToValueRequest + (*AssignPublicKeyToValueResponse)(nil), // 42: policy.attributes.AssignPublicKeyToValueResponse + (*RemovePublicKeyFromValueRequest)(nil), // 43: policy.attributes.RemovePublicKeyFromValueRequest + (*RemovePublicKeyFromValueResponse)(nil), // 44: policy.attributes.RemovePublicKeyFromValueResponse + (*GetAttributeValuesByFqnsResponse_AttributeAndValue)(nil), // 45: policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue + nil, // 46: policy.attributes.GetAttributeValuesByFqnsResponse.FqnAttributeValuesEntry + (policy.SortDirection)(0), // 47: policy.SortDirection + (common.ActiveStateEnum)(0), // 48: common.ActiveStateEnum + (*policy.PageRequest)(nil), // 49: policy.PageRequest + (*policy.Attribute)(nil), // 50: policy.Attribute + (*policy.PageResponse)(nil), // 51: policy.PageResponse + (policy.AttributeRuleTypeEnum)(0), // 52: policy.AttributeRuleTypeEnum + (*wrapperspb.BoolValue)(nil), // 53: google.protobuf.BoolValue + (*common.MetadataMutable)(nil), // 54: common.MetadataMutable + (common.MetadataUpdateEnum)(0), // 55: common.MetadataUpdateEnum + (*policy.Value)(nil), // 56: policy.Value + (*common.IdFqnIdentifier)(nil), // 57: common.IdFqnIdentifier + (*common.IdNameIdentifier)(nil), // 58: common.IdNameIdentifier + (*policy.RequestContext)(nil), // 59: policy.RequestContext } var file_policy_attributes_attributes_proto_depIdxs = []int32{ - 44, // 0: policy.attributes.ListAttributesRequest.state:type_name -> common.ActiveStateEnum - 45, // 1: policy.attributes.ListAttributesRequest.pagination:type_name -> policy.PageRequest - 46, // 2: policy.attributes.ListAttributesResponse.attributes:type_name -> policy.Attribute - 47, // 3: policy.attributes.ListAttributesResponse.pagination:type_name -> policy.PageResponse - 46, // 4: policy.attributes.GetAttributeResponse.attribute:type_name -> policy.Attribute - 48, // 5: policy.attributes.CreateAttributeRequest.rule:type_name -> policy.AttributeRuleTypeEnum - 49, // 6: policy.attributes.CreateAttributeRequest.metadata:type_name -> common.MetadataMutable - 46, // 7: policy.attributes.CreateAttributeResponse.attribute:type_name -> policy.Attribute - 49, // 8: policy.attributes.UpdateAttributeRequest.metadata:type_name -> common.MetadataMutable - 50, // 9: policy.attributes.UpdateAttributeRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 46, // 10: policy.attributes.UpdateAttributeResponse.attribute:type_name -> policy.Attribute - 46, // 11: policy.attributes.DeactivateAttributeResponse.attribute:type_name -> policy.Attribute - 51, // 12: policy.attributes.GetAttributeValueResponse.value:type_name -> policy.Value - 44, // 13: policy.attributes.ListAttributeValuesRequest.state:type_name -> common.ActiveStateEnum - 45, // 14: policy.attributes.ListAttributeValuesRequest.pagination:type_name -> policy.PageRequest - 51, // 15: policy.attributes.ListAttributeValuesResponse.values:type_name -> policy.Value - 47, // 16: policy.attributes.ListAttributeValuesResponse.pagination:type_name -> policy.PageResponse - 49, // 17: policy.attributes.CreateAttributeValueRequest.metadata:type_name -> common.MetadataMutable - 51, // 18: policy.attributes.CreateAttributeValueResponse.value:type_name -> policy.Value - 49, // 19: policy.attributes.UpdateAttributeValueRequest.metadata:type_name -> common.MetadataMutable - 50, // 20: policy.attributes.UpdateAttributeValueRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 51, // 21: policy.attributes.UpdateAttributeValueResponse.value:type_name -> policy.Value - 51, // 22: policy.attributes.DeactivateAttributeValueResponse.value:type_name -> policy.Value - 43, // 23: policy.attributes.GetAttributeValuesByFqnsResponse.fqn_attribute_values:type_name -> policy.attributes.GetAttributeValuesByFqnsResponse.FqnAttributeValuesEntry - 0, // 24: policy.attributes.AssignKeyAccessServerToAttributeRequest.attribute_key_access_server:type_name -> policy.attributes.AttributeKeyAccessServer - 0, // 25: policy.attributes.AssignKeyAccessServerToAttributeResponse.attribute_key_access_server:type_name -> policy.attributes.AttributeKeyAccessServer - 0, // 26: policy.attributes.RemoveKeyAccessServerFromAttributeRequest.attribute_key_access_server:type_name -> policy.attributes.AttributeKeyAccessServer - 0, // 27: policy.attributes.RemoveKeyAccessServerFromAttributeResponse.attribute_key_access_server:type_name -> policy.attributes.AttributeKeyAccessServer - 1, // 28: policy.attributes.AssignKeyAccessServerToValueRequest.value_key_access_server:type_name -> policy.attributes.ValueKeyAccessServer - 1, // 29: policy.attributes.AssignKeyAccessServerToValueResponse.value_key_access_server:type_name -> policy.attributes.ValueKeyAccessServer - 1, // 30: policy.attributes.RemoveKeyAccessServerFromValueRequest.value_key_access_server:type_name -> policy.attributes.ValueKeyAccessServer - 1, // 31: policy.attributes.RemoveKeyAccessServerFromValueResponse.value_key_access_server:type_name -> policy.attributes.ValueKeyAccessServer - 2, // 32: policy.attributes.AssignPublicKeyToAttributeRequest.attribute_key:type_name -> policy.attributes.AttributeKey - 2, // 33: policy.attributes.AssignPublicKeyToAttributeResponse.attribute_key:type_name -> policy.attributes.AttributeKey - 2, // 34: policy.attributes.RemovePublicKeyFromAttributeRequest.attribute_key:type_name -> policy.attributes.AttributeKey - 2, // 35: policy.attributes.RemovePublicKeyFromAttributeResponse.attribute_key:type_name -> policy.attributes.AttributeKey - 3, // 36: policy.attributes.AssignPublicKeyToValueRequest.value_key:type_name -> policy.attributes.ValueKey - 3, // 37: policy.attributes.AssignPublicKeyToValueResponse.value_key:type_name -> policy.attributes.ValueKey - 3, // 38: policy.attributes.RemovePublicKeyFromValueRequest.value_key:type_name -> policy.attributes.ValueKey - 3, // 39: policy.attributes.RemovePublicKeyFromValueResponse.value_key:type_name -> policy.attributes.ValueKey - 46, // 40: policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue.attribute:type_name -> policy.Attribute - 51, // 41: policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue.value:type_name -> policy.Value - 42, // 42: policy.attributes.GetAttributeValuesByFqnsResponse.FqnAttributeValuesEntry.value:type_name -> policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue - 4, // 43: policy.attributes.AttributesService.ListAttributes:input_type -> policy.attributes.ListAttributesRequest - 16, // 44: policy.attributes.AttributesService.ListAttributeValues:input_type -> policy.attributes.ListAttributeValuesRequest - 6, // 45: policy.attributes.AttributesService.GetAttribute:input_type -> policy.attributes.GetAttributeRequest - 24, // 46: policy.attributes.AttributesService.GetAttributeValuesByFqns:input_type -> policy.attributes.GetAttributeValuesByFqnsRequest - 8, // 47: policy.attributes.AttributesService.CreateAttribute:input_type -> policy.attributes.CreateAttributeRequest - 10, // 48: policy.attributes.AttributesService.UpdateAttribute:input_type -> policy.attributes.UpdateAttributeRequest - 12, // 49: policy.attributes.AttributesService.DeactivateAttribute:input_type -> policy.attributes.DeactivateAttributeRequest - 14, // 50: policy.attributes.AttributesService.GetAttributeValue:input_type -> policy.attributes.GetAttributeValueRequest - 18, // 51: policy.attributes.AttributesService.CreateAttributeValue:input_type -> policy.attributes.CreateAttributeValueRequest - 20, // 52: policy.attributes.AttributesService.UpdateAttributeValue:input_type -> policy.attributes.UpdateAttributeValueRequest - 22, // 53: policy.attributes.AttributesService.DeactivateAttributeValue:input_type -> policy.attributes.DeactivateAttributeValueRequest - 26, // 54: policy.attributes.AttributesService.AssignKeyAccessServerToAttribute:input_type -> policy.attributes.AssignKeyAccessServerToAttributeRequest - 28, // 55: policy.attributes.AttributesService.RemoveKeyAccessServerFromAttribute:input_type -> policy.attributes.RemoveKeyAccessServerFromAttributeRequest - 30, // 56: policy.attributes.AttributesService.AssignKeyAccessServerToValue:input_type -> policy.attributes.AssignKeyAccessServerToValueRequest - 32, // 57: policy.attributes.AttributesService.RemoveKeyAccessServerFromValue:input_type -> policy.attributes.RemoveKeyAccessServerFromValueRequest - 34, // 58: policy.attributes.AttributesService.AssignPublicKeyToAttribute:input_type -> policy.attributes.AssignPublicKeyToAttributeRequest - 36, // 59: policy.attributes.AttributesService.RemovePublicKeyFromAttribute:input_type -> policy.attributes.RemovePublicKeyFromAttributeRequest - 38, // 60: policy.attributes.AttributesService.AssignPublicKeyToValue:input_type -> policy.attributes.AssignPublicKeyToValueRequest - 40, // 61: policy.attributes.AttributesService.RemovePublicKeyFromValue:input_type -> policy.attributes.RemovePublicKeyFromValueRequest - 5, // 62: policy.attributes.AttributesService.ListAttributes:output_type -> policy.attributes.ListAttributesResponse - 17, // 63: policy.attributes.AttributesService.ListAttributeValues:output_type -> policy.attributes.ListAttributeValuesResponse - 7, // 64: policy.attributes.AttributesService.GetAttribute:output_type -> policy.attributes.GetAttributeResponse - 25, // 65: policy.attributes.AttributesService.GetAttributeValuesByFqns:output_type -> policy.attributes.GetAttributeValuesByFqnsResponse - 9, // 66: policy.attributes.AttributesService.CreateAttribute:output_type -> policy.attributes.CreateAttributeResponse - 11, // 67: policy.attributes.AttributesService.UpdateAttribute:output_type -> policy.attributes.UpdateAttributeResponse - 13, // 68: policy.attributes.AttributesService.DeactivateAttribute:output_type -> policy.attributes.DeactivateAttributeResponse - 15, // 69: policy.attributes.AttributesService.GetAttributeValue:output_type -> policy.attributes.GetAttributeValueResponse - 19, // 70: policy.attributes.AttributesService.CreateAttributeValue:output_type -> policy.attributes.CreateAttributeValueResponse - 21, // 71: policy.attributes.AttributesService.UpdateAttributeValue:output_type -> policy.attributes.UpdateAttributeValueResponse - 23, // 72: policy.attributes.AttributesService.DeactivateAttributeValue:output_type -> policy.attributes.DeactivateAttributeValueResponse - 27, // 73: policy.attributes.AttributesService.AssignKeyAccessServerToAttribute:output_type -> policy.attributes.AssignKeyAccessServerToAttributeResponse - 29, // 74: policy.attributes.AttributesService.RemoveKeyAccessServerFromAttribute:output_type -> policy.attributes.RemoveKeyAccessServerFromAttributeResponse - 31, // 75: policy.attributes.AttributesService.AssignKeyAccessServerToValue:output_type -> policy.attributes.AssignKeyAccessServerToValueResponse - 33, // 76: policy.attributes.AttributesService.RemoveKeyAccessServerFromValue:output_type -> policy.attributes.RemoveKeyAccessServerFromValueResponse - 35, // 77: policy.attributes.AttributesService.AssignPublicKeyToAttribute:output_type -> policy.attributes.AssignPublicKeyToAttributeResponse - 37, // 78: policy.attributes.AttributesService.RemovePublicKeyFromAttribute:output_type -> policy.attributes.RemovePublicKeyFromAttributeResponse - 39, // 79: policy.attributes.AttributesService.AssignPublicKeyToValue:output_type -> policy.attributes.AssignPublicKeyToValueResponse - 41, // 80: policy.attributes.AttributesService.RemovePublicKeyFromValue:output_type -> policy.attributes.RemovePublicKeyFromValueResponse - 62, // [62:81] is the sub-list for method output_type - 43, // [43:62] is the sub-list for method input_type - 43, // [43:43] is the sub-list for extension type_name - 43, // [43:43] is the sub-list for extension extendee - 0, // [0:43] is the sub-list for field type_name + 0, // 0: policy.attributes.AttributesSort.field:type_name -> policy.attributes.SortAttributesType + 47, // 1: policy.attributes.AttributesSort.direction:type_name -> policy.SortDirection + 48, // 2: policy.attributes.ListAttributesRequest.state:type_name -> common.ActiveStateEnum + 49, // 3: policy.attributes.ListAttributesRequest.pagination:type_name -> policy.PageRequest + 5, // 4: policy.attributes.ListAttributesRequest.sort:type_name -> policy.attributes.AttributesSort + 50, // 5: policy.attributes.ListAttributesResponse.attributes:type_name -> policy.Attribute + 51, // 6: policy.attributes.ListAttributesResponse.pagination:type_name -> policy.PageResponse + 50, // 7: policy.attributes.GetAttributeResponse.attribute:type_name -> policy.Attribute + 52, // 8: policy.attributes.CreateAttributeRequest.rule:type_name -> policy.AttributeRuleTypeEnum + 53, // 9: policy.attributes.CreateAttributeRequest.allow_traversal:type_name -> google.protobuf.BoolValue + 54, // 10: policy.attributes.CreateAttributeRequest.metadata:type_name -> common.MetadataMutable + 50, // 11: policy.attributes.CreateAttributeResponse.attribute:type_name -> policy.Attribute + 54, // 12: policy.attributes.UpdateAttributeRequest.metadata:type_name -> common.MetadataMutable + 55, // 13: policy.attributes.UpdateAttributeRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 50, // 14: policy.attributes.UpdateAttributeResponse.attribute:type_name -> policy.Attribute + 50, // 15: policy.attributes.DeactivateAttributeResponse.attribute:type_name -> policy.Attribute + 56, // 16: policy.attributes.GetAttributeValueResponse.value:type_name -> policy.Value + 48, // 17: policy.attributes.ListAttributeValuesRequest.state:type_name -> common.ActiveStateEnum + 49, // 18: policy.attributes.ListAttributeValuesRequest.pagination:type_name -> policy.PageRequest + 56, // 19: policy.attributes.ListAttributeValuesResponse.values:type_name -> policy.Value + 51, // 20: policy.attributes.ListAttributeValuesResponse.pagination:type_name -> policy.PageResponse + 57, // 21: policy.attributes.AttributeValueObligationTriggerRequest.obligation_value:type_name -> common.IdFqnIdentifier + 58, // 22: policy.attributes.AttributeValueObligationTriggerRequest.action:type_name -> common.IdNameIdentifier + 59, // 23: policy.attributes.AttributeValueObligationTriggerRequest.context:type_name -> policy.RequestContext + 54, // 24: policy.attributes.AttributeValueObligationTriggerRequest.metadata:type_name -> common.MetadataMutable + 20, // 25: policy.attributes.CreateAttributeValueRequest.obligation_triggers:type_name -> policy.attributes.AttributeValueObligationTriggerRequest + 54, // 26: policy.attributes.CreateAttributeValueRequest.metadata:type_name -> common.MetadataMutable + 56, // 27: policy.attributes.CreateAttributeValueResponse.value:type_name -> policy.Value + 54, // 28: policy.attributes.UpdateAttributeValueRequest.metadata:type_name -> common.MetadataMutable + 55, // 29: policy.attributes.UpdateAttributeValueRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 56, // 30: policy.attributes.UpdateAttributeValueResponse.value:type_name -> policy.Value + 56, // 31: policy.attributes.DeactivateAttributeValueResponse.value:type_name -> policy.Value + 46, // 32: policy.attributes.GetAttributeValuesByFqnsResponse.fqn_attribute_values:type_name -> policy.attributes.GetAttributeValuesByFqnsResponse.FqnAttributeValuesEntry + 1, // 33: policy.attributes.AssignKeyAccessServerToAttributeRequest.attribute_key_access_server:type_name -> policy.attributes.AttributeKeyAccessServer + 1, // 34: policy.attributes.AssignKeyAccessServerToAttributeResponse.attribute_key_access_server:type_name -> policy.attributes.AttributeKeyAccessServer + 1, // 35: policy.attributes.RemoveKeyAccessServerFromAttributeRequest.attribute_key_access_server:type_name -> policy.attributes.AttributeKeyAccessServer + 1, // 36: policy.attributes.RemoveKeyAccessServerFromAttributeResponse.attribute_key_access_server:type_name -> policy.attributes.AttributeKeyAccessServer + 2, // 37: policy.attributes.AssignKeyAccessServerToValueRequest.value_key_access_server:type_name -> policy.attributes.ValueKeyAccessServer + 2, // 38: policy.attributes.AssignKeyAccessServerToValueResponse.value_key_access_server:type_name -> policy.attributes.ValueKeyAccessServer + 2, // 39: policy.attributes.RemoveKeyAccessServerFromValueRequest.value_key_access_server:type_name -> policy.attributes.ValueKeyAccessServer + 2, // 40: policy.attributes.RemoveKeyAccessServerFromValueResponse.value_key_access_server:type_name -> policy.attributes.ValueKeyAccessServer + 3, // 41: policy.attributes.AssignPublicKeyToAttributeRequest.attribute_key:type_name -> policy.attributes.AttributeKey + 3, // 42: policy.attributes.AssignPublicKeyToAttributeResponse.attribute_key:type_name -> policy.attributes.AttributeKey + 3, // 43: policy.attributes.RemovePublicKeyFromAttributeRequest.attribute_key:type_name -> policy.attributes.AttributeKey + 3, // 44: policy.attributes.RemovePublicKeyFromAttributeResponse.attribute_key:type_name -> policy.attributes.AttributeKey + 4, // 45: policy.attributes.AssignPublicKeyToValueRequest.value_key:type_name -> policy.attributes.ValueKey + 4, // 46: policy.attributes.AssignPublicKeyToValueResponse.value_key:type_name -> policy.attributes.ValueKey + 4, // 47: policy.attributes.RemovePublicKeyFromValueRequest.value_key:type_name -> policy.attributes.ValueKey + 4, // 48: policy.attributes.RemovePublicKeyFromValueResponse.value_key:type_name -> policy.attributes.ValueKey + 50, // 49: policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue.attribute:type_name -> policy.Attribute + 56, // 50: policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue.value:type_name -> policy.Value + 45, // 51: policy.attributes.GetAttributeValuesByFqnsResponse.FqnAttributeValuesEntry.value:type_name -> policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue + 6, // 52: policy.attributes.AttributesService.ListAttributes:input_type -> policy.attributes.ListAttributesRequest + 18, // 53: policy.attributes.AttributesService.ListAttributeValues:input_type -> policy.attributes.ListAttributeValuesRequest + 8, // 54: policy.attributes.AttributesService.GetAttribute:input_type -> policy.attributes.GetAttributeRequest + 27, // 55: policy.attributes.AttributesService.GetAttributeValuesByFqns:input_type -> policy.attributes.GetAttributeValuesByFqnsRequest + 10, // 56: policy.attributes.AttributesService.CreateAttribute:input_type -> policy.attributes.CreateAttributeRequest + 12, // 57: policy.attributes.AttributesService.UpdateAttribute:input_type -> policy.attributes.UpdateAttributeRequest + 14, // 58: policy.attributes.AttributesService.DeactivateAttribute:input_type -> policy.attributes.DeactivateAttributeRequest + 16, // 59: policy.attributes.AttributesService.GetAttributeValue:input_type -> policy.attributes.GetAttributeValueRequest + 21, // 60: policy.attributes.AttributesService.CreateAttributeValue:input_type -> policy.attributes.CreateAttributeValueRequest + 23, // 61: policy.attributes.AttributesService.UpdateAttributeValue:input_type -> policy.attributes.UpdateAttributeValueRequest + 25, // 62: policy.attributes.AttributesService.DeactivateAttributeValue:input_type -> policy.attributes.DeactivateAttributeValueRequest + 29, // 63: policy.attributes.AttributesService.AssignKeyAccessServerToAttribute:input_type -> policy.attributes.AssignKeyAccessServerToAttributeRequest + 31, // 64: policy.attributes.AttributesService.RemoveKeyAccessServerFromAttribute:input_type -> policy.attributes.RemoveKeyAccessServerFromAttributeRequest + 33, // 65: policy.attributes.AttributesService.AssignKeyAccessServerToValue:input_type -> policy.attributes.AssignKeyAccessServerToValueRequest + 35, // 66: policy.attributes.AttributesService.RemoveKeyAccessServerFromValue:input_type -> policy.attributes.RemoveKeyAccessServerFromValueRequest + 37, // 67: policy.attributes.AttributesService.AssignPublicKeyToAttribute:input_type -> policy.attributes.AssignPublicKeyToAttributeRequest + 39, // 68: policy.attributes.AttributesService.RemovePublicKeyFromAttribute:input_type -> policy.attributes.RemovePublicKeyFromAttributeRequest + 41, // 69: policy.attributes.AttributesService.AssignPublicKeyToValue:input_type -> policy.attributes.AssignPublicKeyToValueRequest + 43, // 70: policy.attributes.AttributesService.RemovePublicKeyFromValue:input_type -> policy.attributes.RemovePublicKeyFromValueRequest + 7, // 71: policy.attributes.AttributesService.ListAttributes:output_type -> policy.attributes.ListAttributesResponse + 19, // 72: policy.attributes.AttributesService.ListAttributeValues:output_type -> policy.attributes.ListAttributeValuesResponse + 9, // 73: policy.attributes.AttributesService.GetAttribute:output_type -> policy.attributes.GetAttributeResponse + 28, // 74: policy.attributes.AttributesService.GetAttributeValuesByFqns:output_type -> policy.attributes.GetAttributeValuesByFqnsResponse + 11, // 75: policy.attributes.AttributesService.CreateAttribute:output_type -> policy.attributes.CreateAttributeResponse + 13, // 76: policy.attributes.AttributesService.UpdateAttribute:output_type -> policy.attributes.UpdateAttributeResponse + 15, // 77: policy.attributes.AttributesService.DeactivateAttribute:output_type -> policy.attributes.DeactivateAttributeResponse + 17, // 78: policy.attributes.AttributesService.GetAttributeValue:output_type -> policy.attributes.GetAttributeValueResponse + 22, // 79: policy.attributes.AttributesService.CreateAttributeValue:output_type -> policy.attributes.CreateAttributeValueResponse + 24, // 80: policy.attributes.AttributesService.UpdateAttributeValue:output_type -> policy.attributes.UpdateAttributeValueResponse + 26, // 81: policy.attributes.AttributesService.DeactivateAttributeValue:output_type -> policy.attributes.DeactivateAttributeValueResponse + 30, // 82: policy.attributes.AttributesService.AssignKeyAccessServerToAttribute:output_type -> policy.attributes.AssignKeyAccessServerToAttributeResponse + 32, // 83: policy.attributes.AttributesService.RemoveKeyAccessServerFromAttribute:output_type -> policy.attributes.RemoveKeyAccessServerFromAttributeResponse + 34, // 84: policy.attributes.AttributesService.AssignKeyAccessServerToValue:output_type -> policy.attributes.AssignKeyAccessServerToValueResponse + 36, // 85: policy.attributes.AttributesService.RemoveKeyAccessServerFromValue:output_type -> policy.attributes.RemoveKeyAccessServerFromValueResponse + 38, // 86: policy.attributes.AttributesService.AssignPublicKeyToAttribute:output_type -> policy.attributes.AssignPublicKeyToAttributeResponse + 40, // 87: policy.attributes.AttributesService.RemovePublicKeyFromAttribute:output_type -> policy.attributes.RemovePublicKeyFromAttributeResponse + 42, // 88: policy.attributes.AttributesService.AssignPublicKeyToValue:output_type -> policy.attributes.AssignPublicKeyToValueResponse + 44, // 89: policy.attributes.AttributesService.RemovePublicKeyFromValue:output_type -> policy.attributes.RemovePublicKeyFromValueResponse + 71, // [71:90] is the sub-list for method output_type + 52, // [52:71] is the sub-list for method input_type + 52, // [52:52] is the sub-list for extension type_name + 52, // [52:52] is the sub-list for extension extendee + 0, // [0:52] is the sub-list for field type_name } func init() { file_policy_attributes_attributes_proto_init() } @@ -3192,7 +3480,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListAttributesRequest); i { + switch v := v.(*AttributesSort); i { case 0: return &v.state case 1: @@ -3204,7 +3492,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListAttributesResponse); i { + switch v := v.(*ListAttributesRequest); i { case 0: return &v.state case 1: @@ -3216,7 +3504,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetAttributeRequest); i { + switch v := v.(*ListAttributesResponse); i { case 0: return &v.state case 1: @@ -3228,7 +3516,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetAttributeResponse); i { + switch v := v.(*GetAttributeRequest); i { case 0: return &v.state case 1: @@ -3240,7 +3528,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateAttributeRequest); i { + switch v := v.(*GetAttributeResponse); i { case 0: return &v.state case 1: @@ -3252,7 +3540,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateAttributeResponse); i { + switch v := v.(*CreateAttributeRequest); i { case 0: return &v.state case 1: @@ -3264,7 +3552,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateAttributeRequest); i { + switch v := v.(*CreateAttributeResponse); i { case 0: return &v.state case 1: @@ -3276,7 +3564,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateAttributeResponse); i { + switch v := v.(*UpdateAttributeRequest); i { case 0: return &v.state case 1: @@ -3288,7 +3576,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeactivateAttributeRequest); i { + switch v := v.(*UpdateAttributeResponse); i { case 0: return &v.state case 1: @@ -3300,7 +3588,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeactivateAttributeResponse); i { + switch v := v.(*DeactivateAttributeRequest); i { case 0: return &v.state case 1: @@ -3312,7 +3600,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetAttributeValueRequest); i { + switch v := v.(*DeactivateAttributeResponse); i { case 0: return &v.state case 1: @@ -3324,7 +3612,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetAttributeValueResponse); i { + switch v := v.(*GetAttributeValueRequest); i { case 0: return &v.state case 1: @@ -3336,7 +3624,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListAttributeValuesRequest); i { + switch v := v.(*GetAttributeValueResponse); i { case 0: return &v.state case 1: @@ -3348,7 +3636,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListAttributeValuesResponse); i { + switch v := v.(*ListAttributeValuesRequest); i { case 0: return &v.state case 1: @@ -3360,7 +3648,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateAttributeValueRequest); i { + switch v := v.(*ListAttributeValuesResponse); i { case 0: return &v.state case 1: @@ -3372,7 +3660,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateAttributeValueResponse); i { + switch v := v.(*AttributeValueObligationTriggerRequest); i { case 0: return &v.state case 1: @@ -3384,7 +3672,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateAttributeValueRequest); i { + switch v := v.(*CreateAttributeValueRequest); i { case 0: return &v.state case 1: @@ -3396,7 +3684,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateAttributeValueResponse); i { + switch v := v.(*CreateAttributeValueResponse); i { case 0: return &v.state case 1: @@ -3408,7 +3696,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeactivateAttributeValueRequest); i { + switch v := v.(*UpdateAttributeValueRequest); i { case 0: return &v.state case 1: @@ -3420,7 +3708,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeactivateAttributeValueResponse); i { + switch v := v.(*UpdateAttributeValueResponse); i { case 0: return &v.state case 1: @@ -3432,7 +3720,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetAttributeValuesByFqnsRequest); i { + switch v := v.(*DeactivateAttributeValueRequest); i { case 0: return &v.state case 1: @@ -3444,7 +3732,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetAttributeValuesByFqnsResponse); i { + switch v := v.(*DeactivateAttributeValueResponse); i { case 0: return &v.state case 1: @@ -3456,7 +3744,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignKeyAccessServerToAttributeRequest); i { + switch v := v.(*GetAttributeValuesByFqnsRequest); i { case 0: return &v.state case 1: @@ -3468,7 +3756,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignKeyAccessServerToAttributeResponse); i { + switch v := v.(*GetAttributeValuesByFqnsResponse); i { case 0: return &v.state case 1: @@ -3480,7 +3768,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveKeyAccessServerFromAttributeRequest); i { + switch v := v.(*AssignKeyAccessServerToAttributeRequest); i { case 0: return &v.state case 1: @@ -3492,7 +3780,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveKeyAccessServerFromAttributeResponse); i { + switch v := v.(*AssignKeyAccessServerToAttributeResponse); i { case 0: return &v.state case 1: @@ -3504,7 +3792,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignKeyAccessServerToValueRequest); i { + switch v := v.(*RemoveKeyAccessServerFromAttributeRequest); i { case 0: return &v.state case 1: @@ -3516,7 +3804,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignKeyAccessServerToValueResponse); i { + switch v := v.(*RemoveKeyAccessServerFromAttributeResponse); i { case 0: return &v.state case 1: @@ -3528,7 +3816,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveKeyAccessServerFromValueRequest); i { + switch v := v.(*AssignKeyAccessServerToValueRequest); i { case 0: return &v.state case 1: @@ -3540,7 +3828,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveKeyAccessServerFromValueResponse); i { + switch v := v.(*AssignKeyAccessServerToValueResponse); i { case 0: return &v.state case 1: @@ -3552,7 +3840,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignPublicKeyToAttributeRequest); i { + switch v := v.(*RemoveKeyAccessServerFromValueRequest); i { case 0: return &v.state case 1: @@ -3564,7 +3852,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignPublicKeyToAttributeResponse); i { + switch v := v.(*RemoveKeyAccessServerFromValueResponse); i { case 0: return &v.state case 1: @@ -3576,7 +3864,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemovePublicKeyFromAttributeRequest); i { + switch v := v.(*AssignPublicKeyToAttributeRequest); i { case 0: return &v.state case 1: @@ -3588,7 +3876,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemovePublicKeyFromAttributeResponse); i { + switch v := v.(*AssignPublicKeyToAttributeResponse); i { case 0: return &v.state case 1: @@ -3600,7 +3888,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignPublicKeyToValueRequest); i { + switch v := v.(*RemovePublicKeyFromAttributeRequest); i { case 0: return &v.state case 1: @@ -3612,7 +3900,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignPublicKeyToValueResponse); i { + switch v := v.(*RemovePublicKeyFromAttributeResponse); i { case 0: return &v.state case 1: @@ -3624,7 +3912,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemovePublicKeyFromValueRequest); i { + switch v := v.(*AssignPublicKeyToValueRequest); i { case 0: return &v.state case 1: @@ -3636,7 +3924,7 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemovePublicKeyFromValueResponse); i { + switch v := v.(*AssignPublicKeyToValueResponse); i { case 0: return &v.state case 1: @@ -3648,6 +3936,30 @@ func file_policy_attributes_attributes_proto_init() { } } file_policy_attributes_attributes_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemovePublicKeyFromValueRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_attributes_attributes_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemovePublicKeyFromValueResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_attributes_attributes_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetAttributeValuesByFqnsResponse_AttributeAndValue); i { case 0: return &v.state @@ -3660,11 +3972,11 @@ func file_policy_attributes_attributes_proto_init() { } } } - file_policy_attributes_attributes_proto_msgTypes[6].OneofWrappers = []interface{}{ + file_policy_attributes_attributes_proto_msgTypes[7].OneofWrappers = []interface{}{ (*GetAttributeRequest_AttributeId)(nil), (*GetAttributeRequest_Fqn)(nil), } - file_policy_attributes_attributes_proto_msgTypes[14].OneofWrappers = []interface{}{ + file_policy_attributes_attributes_proto_msgTypes[15].OneofWrappers = []interface{}{ (*GetAttributeValueRequest_ValueId)(nil), (*GetAttributeValueRequest_Fqn)(nil), } @@ -3673,13 +3985,14 @@ func file_policy_attributes_attributes_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_policy_attributes_attributes_proto_rawDesc, - NumEnums: 0, - NumMessages: 44, + NumEnums: 1, + NumMessages: 46, NumExtensions: 0, NumServices: 1, }, GoTypes: file_policy_attributes_attributes_proto_goTypes, DependencyIndexes: file_policy_attributes_attributes_proto_depIdxs, + EnumInfos: file_policy_attributes_attributes_proto_enumTypes, MessageInfos: file_policy_attributes_attributes_proto_msgTypes, }.Build() File_policy_attributes_attributes_proto = out.File diff --git a/protocol/go/policy/attributes/attributes.pb.gw.go b/protocol/go/policy/attributes/attributes.pb.gw.go deleted file mode 100644 index 184d48ad2d..0000000000 --- a/protocol/go/policy/attributes/attributes.pb.gw.go +++ /dev/null @@ -1,173 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: policy/attributes/attributes.proto - -/* -Package attributes is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package attributes - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -var ( - filter_AttributesService_GetAttributeValuesByFqns_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} -) - -func request_AttributesService_GetAttributeValuesByFqns_0(ctx context.Context, marshaler runtime.Marshaler, client AttributesServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetAttributeValuesByFqnsRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttributesService_GetAttributeValuesByFqns_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.GetAttributeValuesByFqns(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_AttributesService_GetAttributeValuesByFqns_0(ctx context.Context, marshaler runtime.Marshaler, server AttributesServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetAttributeValuesByFqnsRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttributesService_GetAttributeValuesByFqns_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.GetAttributeValuesByFqns(ctx, &protoReq) - return msg, metadata, err - -} - -// RegisterAttributesServiceHandlerServer registers the http handlers for service AttributesService to "mux". -// UnaryRPC :call AttributesServiceServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAttributesServiceHandlerFromEndpoint instead. -func RegisterAttributesServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AttributesServiceServer) error { - - mux.Handle("GET", pattern_AttributesService_GetAttributeValuesByFqns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/policy.attributes.AttributesService/GetAttributeValuesByFqns", runtime.WithHTTPPathPattern("/attributes/*/fqn")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_AttributesService_GetAttributeValuesByFqns_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AttributesService_GetAttributeValuesByFqns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterAttributesServiceHandlerFromEndpoint is same as RegisterAttributesServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterAttributesServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterAttributesServiceHandler(ctx, mux, conn) -} - -// RegisterAttributesServiceHandler registers the http handlers for service AttributesService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterAttributesServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterAttributesServiceHandlerClient(ctx, mux, NewAttributesServiceClient(conn)) -} - -// RegisterAttributesServiceHandlerClient registers the http handlers for service AttributesService -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AttributesServiceClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AttributesServiceClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "AttributesServiceClient" to call the correct interceptors. -func RegisterAttributesServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AttributesServiceClient) error { - - mux.Handle("GET", pattern_AttributesService_GetAttributeValuesByFqns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/policy.attributes.AttributesService/GetAttributeValuesByFqns", runtime.WithHTTPPathPattern("/attributes/*/fqn")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_AttributesService_GetAttributeValuesByFqns_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_AttributesService_GetAttributeValuesByFqns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_AttributesService_GetAttributeValuesByFqns_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 2, 1}, []string{"attributes", "fqn"}, "")) -) - -var ( - forward_AttributesService_GetAttributeValuesByFqns_0 = runtime.ForwardResponseMessage -) diff --git a/protocol/go/policy/attributes/attributes_grpc.pb.go b/protocol/go/policy/attributes/attributes_grpc.pb.go index 016b317787..62bf01e409 100644 --- a/protocol/go/policy/attributes/attributes_grpc.pb.go +++ b/protocol/go/policy/attributes/attributes_grpc.pb.go @@ -48,6 +48,9 @@ type AttributesServiceClient interface { // Attribute RPCs // --------------------------------------- ListAttributes(ctx context.Context, in *ListAttributesRequest, opts ...grpc.CallOption) (*ListAttributesResponse, error) + // Deprecated: Do not use. + // Deprecated + // Use GetAttribute ListAttributeValues(ctx context.Context, in *ListAttributeValuesRequest, opts ...grpc.CallOption) (*ListAttributeValuesResponse, error) GetAttribute(ctx context.Context, in *GetAttributeRequest, opts ...grpc.CallOption) (*GetAttributeResponse, error) GetAttributeValuesByFqns(ctx context.Context, in *GetAttributeValuesByFqnsRequest, opts ...grpc.CallOption) (*GetAttributeValuesByFqnsResponse, error) @@ -96,6 +99,7 @@ func (c *attributesServiceClient) ListAttributes(ctx context.Context, in *ListAt return out, nil } +// Deprecated: Do not use. func (c *attributesServiceClient) ListAttributeValues(ctx context.Context, in *ListAttributeValuesRequest, opts ...grpc.CallOption) (*ListAttributeValuesResponse, error) { out := new(ListAttributeValuesResponse) err := c.cc.Invoke(ctx, AttributesService_ListAttributeValues_FullMethodName, in, out, opts...) @@ -270,6 +274,9 @@ type AttributesServiceServer interface { // Attribute RPCs // --------------------------------------- ListAttributes(context.Context, *ListAttributesRequest) (*ListAttributesResponse, error) + // Deprecated: Do not use. + // Deprecated + // Use GetAttribute ListAttributeValues(context.Context, *ListAttributeValuesRequest) (*ListAttributeValuesResponse, error) GetAttribute(context.Context, *GetAttributeRequest) (*GetAttributeResponse, error) GetAttributeValuesByFqns(context.Context, *GetAttributeValuesByFqnsRequest) (*GetAttributeValuesByFqnsResponse, error) diff --git a/protocol/go/policy/attributes/attributesconnect/attributes.connect.go b/protocol/go/policy/attributes/attributesconnect/attributes.connect.go index 340730d987..ac88811568 100644 --- a/protocol/go/policy/attributes/attributesconnect/attributes.connect.go +++ b/protocol/go/policy/attributes/attributesconnect/attributes.connect.go @@ -92,36 +92,16 @@ const ( AttributesServiceRemovePublicKeyFromValueProcedure = "/policy.attributes.AttributesService/RemovePublicKeyFromValue" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - attributesServiceServiceDescriptor = attributes.File_policy_attributes_attributes_proto.Services().ByName("AttributesService") - attributesServiceListAttributesMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("ListAttributes") - attributesServiceListAttributeValuesMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("ListAttributeValues") - attributesServiceGetAttributeMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("GetAttribute") - attributesServiceGetAttributeValuesByFqnsMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("GetAttributeValuesByFqns") - attributesServiceCreateAttributeMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("CreateAttribute") - attributesServiceUpdateAttributeMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("UpdateAttribute") - attributesServiceDeactivateAttributeMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("DeactivateAttribute") - attributesServiceGetAttributeValueMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("GetAttributeValue") - attributesServiceCreateAttributeValueMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("CreateAttributeValue") - attributesServiceUpdateAttributeValueMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("UpdateAttributeValue") - attributesServiceDeactivateAttributeValueMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("DeactivateAttributeValue") - attributesServiceAssignKeyAccessServerToAttributeMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("AssignKeyAccessServerToAttribute") - attributesServiceRemoveKeyAccessServerFromAttributeMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("RemoveKeyAccessServerFromAttribute") - attributesServiceAssignKeyAccessServerToValueMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("AssignKeyAccessServerToValue") - attributesServiceRemoveKeyAccessServerFromValueMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("RemoveKeyAccessServerFromValue") - attributesServiceAssignPublicKeyToAttributeMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("AssignPublicKeyToAttribute") - attributesServiceRemovePublicKeyFromAttributeMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("RemovePublicKeyFromAttribute") - attributesServiceAssignPublicKeyToValueMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("AssignPublicKeyToValue") - attributesServiceRemovePublicKeyFromValueMethodDescriptor = attributesServiceServiceDescriptor.Methods().ByName("RemovePublicKeyFromValue") -) - // AttributesServiceClient is a client for the policy.attributes.AttributesService service. type AttributesServiceClient interface { // --------------------------------------* // Attribute RPCs // --------------------------------------- ListAttributes(context.Context, *connect.Request[attributes.ListAttributesRequest]) (*connect.Response[attributes.ListAttributesResponse], error) + // Deprecated + // Use GetAttribute + // + // Deprecated: do not use. ListAttributeValues(context.Context, *connect.Request[attributes.ListAttributeValuesRequest]) (*connect.Response[attributes.ListAttributeValuesResponse], error) GetAttribute(context.Context, *connect.Request[attributes.GetAttributeRequest]) (*connect.Response[attributes.GetAttributeResponse], error) GetAttributeValuesByFqns(context.Context, *connect.Request[attributes.GetAttributeValuesByFqnsRequest]) (*connect.Response[attributes.GetAttributeValuesByFqnsResponse], error) @@ -166,124 +146,125 @@ type AttributesServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewAttributesServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AttributesServiceClient { baseURL = strings.TrimRight(baseURL, "/") + attributesServiceMethods := attributes.File_policy_attributes_attributes_proto.Services().ByName("AttributesService").Methods() return &attributesServiceClient{ listAttributes: connect.NewClient[attributes.ListAttributesRequest, attributes.ListAttributesResponse]( httpClient, baseURL+AttributesServiceListAttributesProcedure, - connect.WithSchema(attributesServiceListAttributesMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("ListAttributes")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), listAttributeValues: connect.NewClient[attributes.ListAttributeValuesRequest, attributes.ListAttributeValuesResponse]( httpClient, baseURL+AttributesServiceListAttributeValuesProcedure, - connect.WithSchema(attributesServiceListAttributeValuesMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("ListAttributeValues")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getAttribute: connect.NewClient[attributes.GetAttributeRequest, attributes.GetAttributeResponse]( httpClient, baseURL+AttributesServiceGetAttributeProcedure, - connect.WithSchema(attributesServiceGetAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("GetAttribute")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getAttributeValuesByFqns: connect.NewClient[attributes.GetAttributeValuesByFqnsRequest, attributes.GetAttributeValuesByFqnsResponse]( httpClient, baseURL+AttributesServiceGetAttributeValuesByFqnsProcedure, - connect.WithSchema(attributesServiceGetAttributeValuesByFqnsMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("GetAttributeValuesByFqns")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createAttribute: connect.NewClient[attributes.CreateAttributeRequest, attributes.CreateAttributeResponse]( httpClient, baseURL+AttributesServiceCreateAttributeProcedure, - connect.WithSchema(attributesServiceCreateAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("CreateAttribute")), connect.WithClientOptions(opts...), ), updateAttribute: connect.NewClient[attributes.UpdateAttributeRequest, attributes.UpdateAttributeResponse]( httpClient, baseURL+AttributesServiceUpdateAttributeProcedure, - connect.WithSchema(attributesServiceUpdateAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("UpdateAttribute")), connect.WithClientOptions(opts...), ), deactivateAttribute: connect.NewClient[attributes.DeactivateAttributeRequest, attributes.DeactivateAttributeResponse]( httpClient, baseURL+AttributesServiceDeactivateAttributeProcedure, - connect.WithSchema(attributesServiceDeactivateAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("DeactivateAttribute")), connect.WithClientOptions(opts...), ), getAttributeValue: connect.NewClient[attributes.GetAttributeValueRequest, attributes.GetAttributeValueResponse]( httpClient, baseURL+AttributesServiceGetAttributeValueProcedure, - connect.WithSchema(attributesServiceGetAttributeValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("GetAttributeValue")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createAttributeValue: connect.NewClient[attributes.CreateAttributeValueRequest, attributes.CreateAttributeValueResponse]( httpClient, baseURL+AttributesServiceCreateAttributeValueProcedure, - connect.WithSchema(attributesServiceCreateAttributeValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("CreateAttributeValue")), connect.WithClientOptions(opts...), ), updateAttributeValue: connect.NewClient[attributes.UpdateAttributeValueRequest, attributes.UpdateAttributeValueResponse]( httpClient, baseURL+AttributesServiceUpdateAttributeValueProcedure, - connect.WithSchema(attributesServiceUpdateAttributeValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("UpdateAttributeValue")), connect.WithClientOptions(opts...), ), deactivateAttributeValue: connect.NewClient[attributes.DeactivateAttributeValueRequest, attributes.DeactivateAttributeValueResponse]( httpClient, baseURL+AttributesServiceDeactivateAttributeValueProcedure, - connect.WithSchema(attributesServiceDeactivateAttributeValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("DeactivateAttributeValue")), connect.WithClientOptions(opts...), ), assignKeyAccessServerToAttribute: connect.NewClient[attributes.AssignKeyAccessServerToAttributeRequest, attributes.AssignKeyAccessServerToAttributeResponse]( httpClient, baseURL+AttributesServiceAssignKeyAccessServerToAttributeProcedure, - connect.WithSchema(attributesServiceAssignKeyAccessServerToAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("AssignKeyAccessServerToAttribute")), connect.WithClientOptions(opts...), ), removeKeyAccessServerFromAttribute: connect.NewClient[attributes.RemoveKeyAccessServerFromAttributeRequest, attributes.RemoveKeyAccessServerFromAttributeResponse]( httpClient, baseURL+AttributesServiceRemoveKeyAccessServerFromAttributeProcedure, - connect.WithSchema(attributesServiceRemoveKeyAccessServerFromAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("RemoveKeyAccessServerFromAttribute")), connect.WithClientOptions(opts...), ), assignKeyAccessServerToValue: connect.NewClient[attributes.AssignKeyAccessServerToValueRequest, attributes.AssignKeyAccessServerToValueResponse]( httpClient, baseURL+AttributesServiceAssignKeyAccessServerToValueProcedure, - connect.WithSchema(attributesServiceAssignKeyAccessServerToValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("AssignKeyAccessServerToValue")), connect.WithClientOptions(opts...), ), removeKeyAccessServerFromValue: connect.NewClient[attributes.RemoveKeyAccessServerFromValueRequest, attributes.RemoveKeyAccessServerFromValueResponse]( httpClient, baseURL+AttributesServiceRemoveKeyAccessServerFromValueProcedure, - connect.WithSchema(attributesServiceRemoveKeyAccessServerFromValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("RemoveKeyAccessServerFromValue")), connect.WithClientOptions(opts...), ), assignPublicKeyToAttribute: connect.NewClient[attributes.AssignPublicKeyToAttributeRequest, attributes.AssignPublicKeyToAttributeResponse]( httpClient, baseURL+AttributesServiceAssignPublicKeyToAttributeProcedure, - connect.WithSchema(attributesServiceAssignPublicKeyToAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("AssignPublicKeyToAttribute")), connect.WithClientOptions(opts...), ), removePublicKeyFromAttribute: connect.NewClient[attributes.RemovePublicKeyFromAttributeRequest, attributes.RemovePublicKeyFromAttributeResponse]( httpClient, baseURL+AttributesServiceRemovePublicKeyFromAttributeProcedure, - connect.WithSchema(attributesServiceRemovePublicKeyFromAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("RemovePublicKeyFromAttribute")), connect.WithClientOptions(opts...), ), assignPublicKeyToValue: connect.NewClient[attributes.AssignPublicKeyToValueRequest, attributes.AssignPublicKeyToValueResponse]( httpClient, baseURL+AttributesServiceAssignPublicKeyToValueProcedure, - connect.WithSchema(attributesServiceAssignPublicKeyToValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("AssignPublicKeyToValue")), connect.WithClientOptions(opts...), ), removePublicKeyFromValue: connect.NewClient[attributes.RemovePublicKeyFromValueRequest, attributes.RemovePublicKeyFromValueResponse]( httpClient, baseURL+AttributesServiceRemovePublicKeyFromValueProcedure, - connect.WithSchema(attributesServiceRemovePublicKeyFromValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("RemovePublicKeyFromValue")), connect.WithClientOptions(opts...), ), } @@ -318,6 +299,8 @@ func (c *attributesServiceClient) ListAttributes(ctx context.Context, req *conne } // ListAttributeValues calls policy.attributes.AttributesService.ListAttributeValues. +// +// Deprecated: do not use. func (c *attributesServiceClient) ListAttributeValues(ctx context.Context, req *connect.Request[attributes.ListAttributeValuesRequest]) (*connect.Response[attributes.ListAttributeValuesResponse], error) { return c.listAttributeValues.CallUnary(ctx, req) } @@ -426,6 +409,10 @@ type AttributesServiceHandler interface { // Attribute RPCs // --------------------------------------- ListAttributes(context.Context, *connect.Request[attributes.ListAttributesRequest]) (*connect.Response[attributes.ListAttributesResponse], error) + // Deprecated + // Use GetAttribute + // + // Deprecated: do not use. ListAttributeValues(context.Context, *connect.Request[attributes.ListAttributeValuesRequest]) (*connect.Response[attributes.ListAttributeValuesResponse], error) GetAttribute(context.Context, *connect.Request[attributes.GetAttributeRequest]) (*connect.Response[attributes.GetAttributeResponse], error) GetAttributeValuesByFqns(context.Context, *connect.Request[attributes.GetAttributeValuesByFqnsRequest]) (*connect.Response[attributes.GetAttributeValuesByFqnsResponse], error) @@ -467,123 +454,124 @@ type AttributesServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewAttributesServiceHandler(svc AttributesServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + attributesServiceMethods := attributes.File_policy_attributes_attributes_proto.Services().ByName("AttributesService").Methods() attributesServiceListAttributesHandler := connect.NewUnaryHandler( AttributesServiceListAttributesProcedure, svc.ListAttributes, - connect.WithSchema(attributesServiceListAttributesMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("ListAttributes")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) attributesServiceListAttributeValuesHandler := connect.NewUnaryHandler( AttributesServiceListAttributeValuesProcedure, svc.ListAttributeValues, - connect.WithSchema(attributesServiceListAttributeValuesMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("ListAttributeValues")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) attributesServiceGetAttributeHandler := connect.NewUnaryHandler( AttributesServiceGetAttributeProcedure, svc.GetAttribute, - connect.WithSchema(attributesServiceGetAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("GetAttribute")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) attributesServiceGetAttributeValuesByFqnsHandler := connect.NewUnaryHandler( AttributesServiceGetAttributeValuesByFqnsProcedure, svc.GetAttributeValuesByFqns, - connect.WithSchema(attributesServiceGetAttributeValuesByFqnsMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("GetAttributeValuesByFqns")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) attributesServiceCreateAttributeHandler := connect.NewUnaryHandler( AttributesServiceCreateAttributeProcedure, svc.CreateAttribute, - connect.WithSchema(attributesServiceCreateAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("CreateAttribute")), connect.WithHandlerOptions(opts...), ) attributesServiceUpdateAttributeHandler := connect.NewUnaryHandler( AttributesServiceUpdateAttributeProcedure, svc.UpdateAttribute, - connect.WithSchema(attributesServiceUpdateAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("UpdateAttribute")), connect.WithHandlerOptions(opts...), ) attributesServiceDeactivateAttributeHandler := connect.NewUnaryHandler( AttributesServiceDeactivateAttributeProcedure, svc.DeactivateAttribute, - connect.WithSchema(attributesServiceDeactivateAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("DeactivateAttribute")), connect.WithHandlerOptions(opts...), ) attributesServiceGetAttributeValueHandler := connect.NewUnaryHandler( AttributesServiceGetAttributeValueProcedure, svc.GetAttributeValue, - connect.WithSchema(attributesServiceGetAttributeValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("GetAttributeValue")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) attributesServiceCreateAttributeValueHandler := connect.NewUnaryHandler( AttributesServiceCreateAttributeValueProcedure, svc.CreateAttributeValue, - connect.WithSchema(attributesServiceCreateAttributeValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("CreateAttributeValue")), connect.WithHandlerOptions(opts...), ) attributesServiceUpdateAttributeValueHandler := connect.NewUnaryHandler( AttributesServiceUpdateAttributeValueProcedure, svc.UpdateAttributeValue, - connect.WithSchema(attributesServiceUpdateAttributeValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("UpdateAttributeValue")), connect.WithHandlerOptions(opts...), ) attributesServiceDeactivateAttributeValueHandler := connect.NewUnaryHandler( AttributesServiceDeactivateAttributeValueProcedure, svc.DeactivateAttributeValue, - connect.WithSchema(attributesServiceDeactivateAttributeValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("DeactivateAttributeValue")), connect.WithHandlerOptions(opts...), ) attributesServiceAssignKeyAccessServerToAttributeHandler := connect.NewUnaryHandler( AttributesServiceAssignKeyAccessServerToAttributeProcedure, svc.AssignKeyAccessServerToAttribute, - connect.WithSchema(attributesServiceAssignKeyAccessServerToAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("AssignKeyAccessServerToAttribute")), connect.WithHandlerOptions(opts...), ) attributesServiceRemoveKeyAccessServerFromAttributeHandler := connect.NewUnaryHandler( AttributesServiceRemoveKeyAccessServerFromAttributeProcedure, svc.RemoveKeyAccessServerFromAttribute, - connect.WithSchema(attributesServiceRemoveKeyAccessServerFromAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("RemoveKeyAccessServerFromAttribute")), connect.WithHandlerOptions(opts...), ) attributesServiceAssignKeyAccessServerToValueHandler := connect.NewUnaryHandler( AttributesServiceAssignKeyAccessServerToValueProcedure, svc.AssignKeyAccessServerToValue, - connect.WithSchema(attributesServiceAssignKeyAccessServerToValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("AssignKeyAccessServerToValue")), connect.WithHandlerOptions(opts...), ) attributesServiceRemoveKeyAccessServerFromValueHandler := connect.NewUnaryHandler( AttributesServiceRemoveKeyAccessServerFromValueProcedure, svc.RemoveKeyAccessServerFromValue, - connect.WithSchema(attributesServiceRemoveKeyAccessServerFromValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("RemoveKeyAccessServerFromValue")), connect.WithHandlerOptions(opts...), ) attributesServiceAssignPublicKeyToAttributeHandler := connect.NewUnaryHandler( AttributesServiceAssignPublicKeyToAttributeProcedure, svc.AssignPublicKeyToAttribute, - connect.WithSchema(attributesServiceAssignPublicKeyToAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("AssignPublicKeyToAttribute")), connect.WithHandlerOptions(opts...), ) attributesServiceRemovePublicKeyFromAttributeHandler := connect.NewUnaryHandler( AttributesServiceRemovePublicKeyFromAttributeProcedure, svc.RemovePublicKeyFromAttribute, - connect.WithSchema(attributesServiceRemovePublicKeyFromAttributeMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("RemovePublicKeyFromAttribute")), connect.WithHandlerOptions(opts...), ) attributesServiceAssignPublicKeyToValueHandler := connect.NewUnaryHandler( AttributesServiceAssignPublicKeyToValueProcedure, svc.AssignPublicKeyToValue, - connect.WithSchema(attributesServiceAssignPublicKeyToValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("AssignPublicKeyToValue")), connect.WithHandlerOptions(opts...), ) attributesServiceRemovePublicKeyFromValueHandler := connect.NewUnaryHandler( AttributesServiceRemovePublicKeyFromValueProcedure, svc.RemovePublicKeyFromValue, - connect.WithSchema(attributesServiceRemovePublicKeyFromValueMethodDescriptor), + connect.WithSchema(attributesServiceMethods.ByName("RemovePublicKeyFromValue")), connect.WithHandlerOptions(opts...), ) return "/policy.attributes.AttributesService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/policy/enums.gen.go b/protocol/go/policy/enums.gen.go new file mode 100644 index 0000000000..87bc32e7c8 --- /dev/null +++ b/protocol/go/policy/enums.gen.go @@ -0,0 +1,62 @@ +// Code generated by protocol/codegen. DO NOT EDIT. + +package policy + +import ( + "github.com/opentdf/platform/protocol/go/common" +) + +// Shorthand constants for SubjectMappingOperatorEnum. +// +// Example: +// +// condition := &Condition{ +// SubjectExternalSelectorValue: ".email", +// Operator: OperatorInContains, +// SubjectExternalValues: []string{"@example.com"}, +// } +const ( + OperatorIn = SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN + OperatorNotIn = SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN + OperatorInContains = SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN_CONTAINS +) + +// Shorthand constants for ConditionBooleanTypeEnum. +// +// Example: +// +// group := &ConditionGroup{ +// BooleanOperator: BooleanAnd, +// Conditions: conditions, +// } +const ( + BooleanAnd = ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND + BooleanOr = ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR +) + +// Shorthand constants for AttributeRuleTypeEnum. +// +// Example: +// +// req := &attributes.CreateAttributeRequest{ +// Name: "clearance", +// Rule: RuleHierarchy, +// } +const ( + RuleAllOf = AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF + RuleAnyOf = AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF + RuleHierarchy = AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY +) + +// Shorthand constants for ActiveStateEnum (from the common package). +// +// Example: +// +// req := &attributes.ListAttributesRequest{ +// State: StateActive, +// } +const ( + StateActive = common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE + StateInactive = common.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE + StateAny = common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY +) diff --git a/protocol/go/policy/kasregistry/kasregistryconnect/key_access_server_registry.connect.go b/protocol/go/policy/kasregistry/kasregistryconnect/key_access_server_registry.connect.go index 562bc022cf..24bfde47b3 100644 --- a/protocol/go/policy/kasregistry/kasregistryconnect/key_access_server_registry.connect.go +++ b/protocol/go/policy/kasregistry/kasregistryconnect/key_access_server_registry.connect.go @@ -78,25 +78,6 @@ const ( KeyAccessServerRegistryServiceListKeyMappingsProcedure = "/policy.kasregistry.KeyAccessServerRegistryService/ListKeyMappings" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - keyAccessServerRegistryServiceServiceDescriptor = kasregistry.File_policy_kasregistry_key_access_server_registry_proto.Services().ByName("KeyAccessServerRegistryService") - keyAccessServerRegistryServiceListKeyAccessServersMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("ListKeyAccessServers") - keyAccessServerRegistryServiceGetKeyAccessServerMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("GetKeyAccessServer") - keyAccessServerRegistryServiceCreateKeyAccessServerMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("CreateKeyAccessServer") - keyAccessServerRegistryServiceUpdateKeyAccessServerMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("UpdateKeyAccessServer") - keyAccessServerRegistryServiceDeleteKeyAccessServerMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("DeleteKeyAccessServer") - keyAccessServerRegistryServiceListKeyAccessServerGrantsMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("ListKeyAccessServerGrants") - keyAccessServerRegistryServiceCreateKeyMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("CreateKey") - keyAccessServerRegistryServiceGetKeyMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("GetKey") - keyAccessServerRegistryServiceListKeysMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("ListKeys") - keyAccessServerRegistryServiceUpdateKeyMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("UpdateKey") - keyAccessServerRegistryServiceRotateKeyMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("RotateKey") - keyAccessServerRegistryServiceSetBaseKeyMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("SetBaseKey") - keyAccessServerRegistryServiceGetBaseKeyMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("GetBaseKey") - keyAccessServerRegistryServiceListKeyMappingsMethodDescriptor = keyAccessServerRegistryServiceServiceDescriptor.Methods().ByName("ListKeyMappings") -) - // KeyAccessServerRegistryServiceClient is a client for the // policy.kasregistry.KeyAccessServerRegistryService service. type KeyAccessServerRegistryServiceClient interface { @@ -138,92 +119,93 @@ type KeyAccessServerRegistryServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewKeyAccessServerRegistryServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) KeyAccessServerRegistryServiceClient { baseURL = strings.TrimRight(baseURL, "/") + keyAccessServerRegistryServiceMethods := kasregistry.File_policy_kasregistry_key_access_server_registry_proto.Services().ByName("KeyAccessServerRegistryService").Methods() return &keyAccessServerRegistryServiceClient{ listKeyAccessServers: connect.NewClient[kasregistry.ListKeyAccessServersRequest, kasregistry.ListKeyAccessServersResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceListKeyAccessServersProcedure, - connect.WithSchema(keyAccessServerRegistryServiceListKeyAccessServersMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("ListKeyAccessServers")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getKeyAccessServer: connect.NewClient[kasregistry.GetKeyAccessServerRequest, kasregistry.GetKeyAccessServerResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceGetKeyAccessServerProcedure, - connect.WithSchema(keyAccessServerRegistryServiceGetKeyAccessServerMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("GetKeyAccessServer")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createKeyAccessServer: connect.NewClient[kasregistry.CreateKeyAccessServerRequest, kasregistry.CreateKeyAccessServerResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceCreateKeyAccessServerProcedure, - connect.WithSchema(keyAccessServerRegistryServiceCreateKeyAccessServerMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("CreateKeyAccessServer")), connect.WithClientOptions(opts...), ), updateKeyAccessServer: connect.NewClient[kasregistry.UpdateKeyAccessServerRequest, kasregistry.UpdateKeyAccessServerResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceUpdateKeyAccessServerProcedure, - connect.WithSchema(keyAccessServerRegistryServiceUpdateKeyAccessServerMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("UpdateKeyAccessServer")), connect.WithClientOptions(opts...), ), deleteKeyAccessServer: connect.NewClient[kasregistry.DeleteKeyAccessServerRequest, kasregistry.DeleteKeyAccessServerResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceDeleteKeyAccessServerProcedure, - connect.WithSchema(keyAccessServerRegistryServiceDeleteKeyAccessServerMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("DeleteKeyAccessServer")), connect.WithClientOptions(opts...), ), listKeyAccessServerGrants: connect.NewClient[kasregistry.ListKeyAccessServerGrantsRequest, kasregistry.ListKeyAccessServerGrantsResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceListKeyAccessServerGrantsProcedure, - connect.WithSchema(keyAccessServerRegistryServiceListKeyAccessServerGrantsMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("ListKeyAccessServerGrants")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createKey: connect.NewClient[kasregistry.CreateKeyRequest, kasregistry.CreateKeyResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceCreateKeyProcedure, - connect.WithSchema(keyAccessServerRegistryServiceCreateKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("CreateKey")), connect.WithClientOptions(opts...), ), getKey: connect.NewClient[kasregistry.GetKeyRequest, kasregistry.GetKeyResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceGetKeyProcedure, - connect.WithSchema(keyAccessServerRegistryServiceGetKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("GetKey")), connect.WithClientOptions(opts...), ), listKeys: connect.NewClient[kasregistry.ListKeysRequest, kasregistry.ListKeysResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceListKeysProcedure, - connect.WithSchema(keyAccessServerRegistryServiceListKeysMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("ListKeys")), connect.WithClientOptions(opts...), ), updateKey: connect.NewClient[kasregistry.UpdateKeyRequest, kasregistry.UpdateKeyResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceUpdateKeyProcedure, - connect.WithSchema(keyAccessServerRegistryServiceUpdateKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("UpdateKey")), connect.WithClientOptions(opts...), ), rotateKey: connect.NewClient[kasregistry.RotateKeyRequest, kasregistry.RotateKeyResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceRotateKeyProcedure, - connect.WithSchema(keyAccessServerRegistryServiceRotateKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("RotateKey")), connect.WithClientOptions(opts...), ), setBaseKey: connect.NewClient[kasregistry.SetBaseKeyRequest, kasregistry.SetBaseKeyResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceSetBaseKeyProcedure, - connect.WithSchema(keyAccessServerRegistryServiceSetBaseKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("SetBaseKey")), connect.WithClientOptions(opts...), ), getBaseKey: connect.NewClient[kasregistry.GetBaseKeyRequest, kasregistry.GetBaseKeyResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceGetBaseKeyProcedure, - connect.WithSchema(keyAccessServerRegistryServiceGetBaseKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("GetBaseKey")), connect.WithClientOptions(opts...), ), listKeyMappings: connect.NewClient[kasregistry.ListKeyMappingsRequest, kasregistry.ListKeyMappingsResponse]( httpClient, baseURL+KeyAccessServerRegistryServiceListKeyMappingsProcedure, - connect.WithSchema(keyAccessServerRegistryServiceListKeyMappingsMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("ListKeyMappings")), connect.WithClientOptions(opts...), ), } @@ -361,91 +343,92 @@ type KeyAccessServerRegistryServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewKeyAccessServerRegistryServiceHandler(svc KeyAccessServerRegistryServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + keyAccessServerRegistryServiceMethods := kasregistry.File_policy_kasregistry_key_access_server_registry_proto.Services().ByName("KeyAccessServerRegistryService").Methods() keyAccessServerRegistryServiceListKeyAccessServersHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceListKeyAccessServersProcedure, svc.ListKeyAccessServers, - connect.WithSchema(keyAccessServerRegistryServiceListKeyAccessServersMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("ListKeyAccessServers")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceGetKeyAccessServerHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceGetKeyAccessServerProcedure, svc.GetKeyAccessServer, - connect.WithSchema(keyAccessServerRegistryServiceGetKeyAccessServerMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("GetKeyAccessServer")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceCreateKeyAccessServerHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceCreateKeyAccessServerProcedure, svc.CreateKeyAccessServer, - connect.WithSchema(keyAccessServerRegistryServiceCreateKeyAccessServerMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("CreateKeyAccessServer")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceUpdateKeyAccessServerHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceUpdateKeyAccessServerProcedure, svc.UpdateKeyAccessServer, - connect.WithSchema(keyAccessServerRegistryServiceUpdateKeyAccessServerMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("UpdateKeyAccessServer")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceDeleteKeyAccessServerHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceDeleteKeyAccessServerProcedure, svc.DeleteKeyAccessServer, - connect.WithSchema(keyAccessServerRegistryServiceDeleteKeyAccessServerMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("DeleteKeyAccessServer")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceListKeyAccessServerGrantsHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceListKeyAccessServerGrantsProcedure, svc.ListKeyAccessServerGrants, - connect.WithSchema(keyAccessServerRegistryServiceListKeyAccessServerGrantsMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("ListKeyAccessServerGrants")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceCreateKeyHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceCreateKeyProcedure, svc.CreateKey, - connect.WithSchema(keyAccessServerRegistryServiceCreateKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("CreateKey")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceGetKeyHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceGetKeyProcedure, svc.GetKey, - connect.WithSchema(keyAccessServerRegistryServiceGetKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("GetKey")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceListKeysHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceListKeysProcedure, svc.ListKeys, - connect.WithSchema(keyAccessServerRegistryServiceListKeysMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("ListKeys")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceUpdateKeyHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceUpdateKeyProcedure, svc.UpdateKey, - connect.WithSchema(keyAccessServerRegistryServiceUpdateKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("UpdateKey")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceRotateKeyHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceRotateKeyProcedure, svc.RotateKey, - connect.WithSchema(keyAccessServerRegistryServiceRotateKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("RotateKey")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceSetBaseKeyHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceSetBaseKeyProcedure, svc.SetBaseKey, - connect.WithSchema(keyAccessServerRegistryServiceSetBaseKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("SetBaseKey")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceGetBaseKeyHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceGetBaseKeyProcedure, svc.GetBaseKey, - connect.WithSchema(keyAccessServerRegistryServiceGetBaseKeyMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("GetBaseKey")), connect.WithHandlerOptions(opts...), ) keyAccessServerRegistryServiceListKeyMappingsHandler := connect.NewUnaryHandler( KeyAccessServerRegistryServiceListKeyMappingsProcedure, svc.ListKeyMappings, - connect.WithSchema(keyAccessServerRegistryServiceListKeyMappingsMethodDescriptor), + connect.WithSchema(keyAccessServerRegistryServiceMethods.ByName("ListKeyMappings")), connect.WithHandlerOptions(opts...), ) return "/policy.kasregistry.KeyAccessServerRegistryService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/policy/kasregistry/key_access_server_registry.pb.go b/protocol/go/policy/kasregistry/key_access_server_registry.pb.go index 8e5c7b58b5..5bb3c2a08b 100644 --- a/protocol/go/policy/kasregistry/key_access_server_registry.pb.go +++ b/protocol/go/policy/kasregistry/key_access_server_registry.pb.go @@ -10,7 +10,6 @@ import ( _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" common "github.com/opentdf/platform/protocol/go/common" policy "github.com/opentdf/platform/protocol/go/policy" - _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -24,6 +23,113 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type SortKeyAccessServersType int32 + +const ( + SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED SortKeyAccessServersType = 0 + SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME SortKeyAccessServersType = 1 + SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_URI SortKeyAccessServersType = 2 + SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT SortKeyAccessServersType = 3 + SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT SortKeyAccessServersType = 4 +) + +// Enum value maps for SortKeyAccessServersType. +var ( + SortKeyAccessServersType_name = map[int32]string{ + 0: "SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED", + 1: "SORT_KEY_ACCESS_SERVERS_TYPE_NAME", + 2: "SORT_KEY_ACCESS_SERVERS_TYPE_URI", + 3: "SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT", + 4: "SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT", + } + SortKeyAccessServersType_value = map[string]int32{ + "SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED": 0, + "SORT_KEY_ACCESS_SERVERS_TYPE_NAME": 1, + "SORT_KEY_ACCESS_SERVERS_TYPE_URI": 2, + "SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT": 3, + "SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT": 4, + } +) + +func (x SortKeyAccessServersType) Enum() *SortKeyAccessServersType { + p := new(SortKeyAccessServersType) + *p = x + return p +} + +func (x SortKeyAccessServersType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortKeyAccessServersType) Descriptor() protoreflect.EnumDescriptor { + return file_policy_kasregistry_key_access_server_registry_proto_enumTypes[0].Descriptor() +} + +func (SortKeyAccessServersType) Type() protoreflect.EnumType { + return &file_policy_kasregistry_key_access_server_registry_proto_enumTypes[0] +} + +func (x SortKeyAccessServersType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortKeyAccessServersType.Descriptor instead. +func (SortKeyAccessServersType) EnumDescriptor() ([]byte, []int) { + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{0} +} + +type SortKasKeysType int32 + +const ( + SortKasKeysType_SORT_KAS_KEYS_TYPE_UNSPECIFIED SortKasKeysType = 0 + SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID SortKasKeysType = 1 + SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT SortKasKeysType = 2 + SortKasKeysType_SORT_KAS_KEYS_TYPE_UPDATED_AT SortKasKeysType = 3 +) + +// Enum value maps for SortKasKeysType. +var ( + SortKasKeysType_name = map[int32]string{ + 0: "SORT_KAS_KEYS_TYPE_UNSPECIFIED", + 1: "SORT_KAS_KEYS_TYPE_KEY_ID", + 2: "SORT_KAS_KEYS_TYPE_CREATED_AT", + 3: "SORT_KAS_KEYS_TYPE_UPDATED_AT", + } + SortKasKeysType_value = map[string]int32{ + "SORT_KAS_KEYS_TYPE_UNSPECIFIED": 0, + "SORT_KAS_KEYS_TYPE_KEY_ID": 1, + "SORT_KAS_KEYS_TYPE_CREATED_AT": 2, + "SORT_KAS_KEYS_TYPE_UPDATED_AT": 3, + } +) + +func (x SortKasKeysType) Enum() *SortKasKeysType { + p := new(SortKasKeysType) + *p = x + return p +} + +func (x SortKasKeysType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortKasKeysType) Descriptor() protoreflect.EnumDescriptor { + return file_policy_kasregistry_key_access_server_registry_proto_enumTypes[1].Descriptor() +} + +func (SortKasKeysType) Type() protoreflect.EnumType { + return &file_policy_kasregistry_key_access_server_registry_proto_enumTypes[1] +} + +func (x SortKasKeysType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortKasKeysType.Descriptor instead. +func (SortKasKeysType) EnumDescriptor() ([]byte, []int) { + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{1} +} + type GetKeyAccessServerRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -179,6 +285,61 @@ func (x *GetKeyAccessServerResponse) GetKeyAccessServer() *policy.KeyAccessServe return nil } +type KeyAccessServersSort struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field SortKeyAccessServersType `protobuf:"varint,1,opt,name=field,proto3,enum=policy.kasregistry.SortKeyAccessServersType" json:"field,omitempty"` + Direction policy.SortDirection `protobuf:"varint,2,opt,name=direction,proto3,enum=policy.SortDirection" json:"direction,omitempty"` +} + +func (x *KeyAccessServersSort) Reset() { + *x = KeyAccessServersSort{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyAccessServersSort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyAccessServersSort) ProtoMessage() {} + +func (x *KeyAccessServersSort) ProtoReflect() protoreflect.Message { + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyAccessServersSort.ProtoReflect.Descriptor instead. +func (*KeyAccessServersSort) Descriptor() ([]byte, []int) { + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{2} +} + +func (x *KeyAccessServersSort) GetField() SortKeyAccessServersType { + if x != nil { + return x.Field + } + return SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED +} + +func (x *KeyAccessServersSort) GetDirection() policy.SortDirection { + if x != nil { + return x.Direction + } + return policy.SortDirection(0) +} + type ListKeyAccessServersRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -186,12 +347,18 @@ type ListKeyAccessServersRequest struct { // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + Sort []*KeyAccessServersSort `protobuf:"bytes,11,rep,name=sort,proto3" json:"sort,omitempty"` } func (x *ListKeyAccessServersRequest) Reset() { *x = ListKeyAccessServersRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[2] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -204,7 +371,7 @@ func (x *ListKeyAccessServersRequest) String() string { func (*ListKeyAccessServersRequest) ProtoMessage() {} func (x *ListKeyAccessServersRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[2] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -217,7 +384,7 @@ func (x *ListKeyAccessServersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListKeyAccessServersRequest.ProtoReflect.Descriptor instead. func (*ListKeyAccessServersRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{2} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{3} } func (x *ListKeyAccessServersRequest) GetPagination() *policy.PageRequest { @@ -227,6 +394,13 @@ func (x *ListKeyAccessServersRequest) GetPagination() *policy.PageRequest { return nil } +func (x *ListKeyAccessServersRequest) GetSort() []*KeyAccessServersSort { + if x != nil { + return x.Sort + } + return nil +} + type ListKeyAccessServersResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -239,7 +413,7 @@ type ListKeyAccessServersResponse struct { func (x *ListKeyAccessServersResponse) Reset() { *x = ListKeyAccessServersResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[3] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -252,7 +426,7 @@ func (x *ListKeyAccessServersResponse) String() string { func (*ListKeyAccessServersResponse) ProtoMessage() {} func (x *ListKeyAccessServersResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[3] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -265,7 +439,7 @@ func (x *ListKeyAccessServersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListKeyAccessServersResponse.ProtoReflect.Descriptor instead. func (*ListKeyAccessServersResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{3} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{4} } func (x *ListKeyAccessServersResponse) GetKeyAccessServers() []*policy.KeyAccessServer { @@ -282,6 +456,61 @@ func (x *ListKeyAccessServersResponse) GetPagination() *policy.PageResponse { return nil } +type KasKeysSort struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field SortKasKeysType `protobuf:"varint,1,opt,name=field,proto3,enum=policy.kasregistry.SortKasKeysType" json:"field,omitempty"` + Direction policy.SortDirection `protobuf:"varint,2,opt,name=direction,proto3,enum=policy.SortDirection" json:"direction,omitempty"` +} + +func (x *KasKeysSort) Reset() { + *x = KasKeysSort{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KasKeysSort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KasKeysSort) ProtoMessage() {} + +func (x *KasKeysSort) ProtoReflect() protoreflect.Message { + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KasKeysSort.ProtoReflect.Descriptor instead. +func (*KasKeysSort) Descriptor() ([]byte, []int) { + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{5} +} + +func (x *KasKeysSort) GetField() SortKasKeysType { + if x != nil { + return x.Field + } + return SortKasKeysType_SORT_KAS_KEYS_TYPE_UNSPECIFIED +} + +func (x *KasKeysSort) GetDirection() policy.SortDirection { + if x != nil { + return x.Direction + } + return policy.SortDirection(0) +} + type CreateKeyAccessServerRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -302,7 +531,7 @@ type CreateKeyAccessServerRequest struct { func (x *CreateKeyAccessServerRequest) Reset() { *x = CreateKeyAccessServerRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[4] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -315,7 +544,7 @@ func (x *CreateKeyAccessServerRequest) String() string { func (*CreateKeyAccessServerRequest) ProtoMessage() {} func (x *CreateKeyAccessServerRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[4] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -328,7 +557,7 @@ func (x *CreateKeyAccessServerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateKeyAccessServerRequest.ProtoReflect.Descriptor instead. func (*CreateKeyAccessServerRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{4} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{6} } func (x *CreateKeyAccessServerRequest) GetUri() string { @@ -377,7 +606,7 @@ type CreateKeyAccessServerResponse struct { func (x *CreateKeyAccessServerResponse) Reset() { *x = CreateKeyAccessServerResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[5] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -390,7 +619,7 @@ func (x *CreateKeyAccessServerResponse) String() string { func (*CreateKeyAccessServerResponse) ProtoMessage() {} func (x *CreateKeyAccessServerResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[5] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -403,7 +632,7 @@ func (x *CreateKeyAccessServerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateKeyAccessServerResponse.ProtoReflect.Descriptor instead. func (*CreateKeyAccessServerResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{5} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{7} } func (x *CreateKeyAccessServerResponse) GetKeyAccessServer() *policy.KeyAccessServer { @@ -442,7 +671,7 @@ type UpdateKeyAccessServerRequest struct { func (x *UpdateKeyAccessServerRequest) Reset() { *x = UpdateKeyAccessServerRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[6] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -455,7 +684,7 @@ func (x *UpdateKeyAccessServerRequest) String() string { func (*UpdateKeyAccessServerRequest) ProtoMessage() {} func (x *UpdateKeyAccessServerRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[6] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -468,7 +697,7 @@ func (x *UpdateKeyAccessServerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateKeyAccessServerRequest.ProtoReflect.Descriptor instead. func (*UpdateKeyAccessServerRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{6} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{8} } func (x *UpdateKeyAccessServerRequest) GetId() string { @@ -531,7 +760,7 @@ type UpdateKeyAccessServerResponse struct { func (x *UpdateKeyAccessServerResponse) Reset() { *x = UpdateKeyAccessServerResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[7] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -544,7 +773,7 @@ func (x *UpdateKeyAccessServerResponse) String() string { func (*UpdateKeyAccessServerResponse) ProtoMessage() {} func (x *UpdateKeyAccessServerResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[7] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -557,7 +786,7 @@ func (x *UpdateKeyAccessServerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateKeyAccessServerResponse.ProtoReflect.Descriptor instead. func (*UpdateKeyAccessServerResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{7} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{9} } func (x *UpdateKeyAccessServerResponse) GetKeyAccessServer() *policy.KeyAccessServer { @@ -579,7 +808,7 @@ type DeleteKeyAccessServerRequest struct { func (x *DeleteKeyAccessServerRequest) Reset() { *x = DeleteKeyAccessServerRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[8] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -592,7 +821,7 @@ func (x *DeleteKeyAccessServerRequest) String() string { func (*DeleteKeyAccessServerRequest) ProtoMessage() {} func (x *DeleteKeyAccessServerRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[8] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -605,7 +834,7 @@ func (x *DeleteKeyAccessServerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteKeyAccessServerRequest.ProtoReflect.Descriptor instead. func (*DeleteKeyAccessServerRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{8} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{10} } func (x *DeleteKeyAccessServerRequest) GetId() string { @@ -626,7 +855,7 @@ type DeleteKeyAccessServerResponse struct { func (x *DeleteKeyAccessServerResponse) Reset() { *x = DeleteKeyAccessServerResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[9] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -639,7 +868,7 @@ func (x *DeleteKeyAccessServerResponse) String() string { func (*DeleteKeyAccessServerResponse) ProtoMessage() {} func (x *DeleteKeyAccessServerResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[9] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -652,7 +881,7 @@ func (x *DeleteKeyAccessServerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteKeyAccessServerResponse.ProtoReflect.Descriptor instead. func (*DeleteKeyAccessServerResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{9} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{11} } func (x *DeleteKeyAccessServerResponse) GetKeyAccessServer() *policy.KeyAccessServer { @@ -675,7 +904,7 @@ type GrantedPolicyObject struct { func (x *GrantedPolicyObject) Reset() { *x = GrantedPolicyObject{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[10] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -688,7 +917,7 @@ func (x *GrantedPolicyObject) String() string { func (*GrantedPolicyObject) ProtoMessage() {} func (x *GrantedPolicyObject) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[10] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -701,7 +930,7 @@ func (x *GrantedPolicyObject) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantedPolicyObject.ProtoReflect.Descriptor instead. func (*GrantedPolicyObject) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{10} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{12} } func (x *GrantedPolicyObject) GetId() string { @@ -733,7 +962,7 @@ type KeyAccessServerGrants struct { func (x *KeyAccessServerGrants) Reset() { *x = KeyAccessServerGrants{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[11] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -746,7 +975,7 @@ func (x *KeyAccessServerGrants) String() string { func (*KeyAccessServerGrants) ProtoMessage() {} func (x *KeyAccessServerGrants) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[11] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -759,7 +988,7 @@ func (x *KeyAccessServerGrants) ProtoReflect() protoreflect.Message { // Deprecated: Use KeyAccessServerGrants.ProtoReflect.Descriptor instead. func (*KeyAccessServerGrants) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{11} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{13} } func (x *KeyAccessServerGrants) GetKeyAccessServer() *policy.KeyAccessServer { @@ -806,7 +1035,7 @@ type CreatePublicKeyRequest struct { func (x *CreatePublicKeyRequest) Reset() { *x = CreatePublicKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[12] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -819,7 +1048,7 @@ func (x *CreatePublicKeyRequest) String() string { func (*CreatePublicKeyRequest) ProtoMessage() {} func (x *CreatePublicKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[12] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -832,7 +1061,7 @@ func (x *CreatePublicKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatePublicKeyRequest.ProtoReflect.Descriptor instead. func (*CreatePublicKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{12} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{14} } func (x *CreatePublicKeyRequest) GetKasId() string { @@ -867,7 +1096,7 @@ type CreatePublicKeyResponse struct { func (x *CreatePublicKeyResponse) Reset() { *x = CreatePublicKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[13] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -880,7 +1109,7 @@ func (x *CreatePublicKeyResponse) String() string { func (*CreatePublicKeyResponse) ProtoMessage() {} func (x *CreatePublicKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[13] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -893,7 +1122,7 @@ func (x *CreatePublicKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatePublicKeyResponse.ProtoReflect.Descriptor instead. func (*CreatePublicKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{13} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{15} } func (x *CreatePublicKeyResponse) GetKey() *policy.Key { @@ -917,7 +1146,7 @@ type GetPublicKeyRequest struct { func (x *GetPublicKeyRequest) Reset() { *x = GetPublicKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[14] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -930,7 +1159,7 @@ func (x *GetPublicKeyRequest) String() string { func (*GetPublicKeyRequest) ProtoMessage() {} func (x *GetPublicKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[14] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -943,7 +1172,7 @@ func (x *GetPublicKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPublicKeyRequest.ProtoReflect.Descriptor instead. func (*GetPublicKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{14} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{16} } func (m *GetPublicKeyRequest) GetIdentifier() isGetPublicKeyRequest_Identifier { @@ -981,7 +1210,7 @@ type GetPublicKeyResponse struct { func (x *GetPublicKeyResponse) Reset() { *x = GetPublicKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[15] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -994,7 +1223,7 @@ func (x *GetPublicKeyResponse) String() string { func (*GetPublicKeyResponse) ProtoMessage() {} func (x *GetPublicKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[15] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1007,7 +1236,7 @@ func (x *GetPublicKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPublicKeyResponse.ProtoReflect.Descriptor instead. func (*GetPublicKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{15} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{17} } func (x *GetPublicKeyResponse) GetKey() *policy.Key { @@ -1035,7 +1264,7 @@ type ListPublicKeysRequest struct { func (x *ListPublicKeysRequest) Reset() { *x = ListPublicKeysRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[16] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1048,7 +1277,7 @@ func (x *ListPublicKeysRequest) String() string { func (*ListPublicKeysRequest) ProtoMessage() {} func (x *ListPublicKeysRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[16] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1061,7 +1290,7 @@ func (x *ListPublicKeysRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPublicKeysRequest.ProtoReflect.Descriptor instead. func (*ListPublicKeysRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{16} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{18} } func (m *ListPublicKeysRequest) GetKasFilter() isListPublicKeysRequest_KasFilter { @@ -1136,7 +1365,7 @@ type ListPublicKeysResponse struct { func (x *ListPublicKeysResponse) Reset() { *x = ListPublicKeysResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[17] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1149,7 +1378,7 @@ func (x *ListPublicKeysResponse) String() string { func (*ListPublicKeysResponse) ProtoMessage() {} func (x *ListPublicKeysResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[17] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1162,7 +1391,7 @@ func (x *ListPublicKeysResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPublicKeysResponse.ProtoReflect.Descriptor instead. func (*ListPublicKeysResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{17} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{19} } func (x *ListPublicKeysResponse) GetKeys() []*policy.Key { @@ -1199,7 +1428,7 @@ type ListPublicKeyMappingRequest struct { func (x *ListPublicKeyMappingRequest) Reset() { *x = ListPublicKeyMappingRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[18] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1212,7 +1441,7 @@ func (x *ListPublicKeyMappingRequest) String() string { func (*ListPublicKeyMappingRequest) ProtoMessage() {} func (x *ListPublicKeyMappingRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[18] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1225,7 +1454,7 @@ func (x *ListPublicKeyMappingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPublicKeyMappingRequest.ProtoReflect.Descriptor instead. func (*ListPublicKeyMappingRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{18} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{20} } func (m *ListPublicKeyMappingRequest) GetKasFilter() isListPublicKeyMappingRequest_KasFilter { @@ -1307,7 +1536,7 @@ type ListPublicKeyMappingResponse struct { func (x *ListPublicKeyMappingResponse) Reset() { *x = ListPublicKeyMappingResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[19] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1320,7 +1549,7 @@ func (x *ListPublicKeyMappingResponse) String() string { func (*ListPublicKeyMappingResponse) ProtoMessage() {} func (x *ListPublicKeyMappingResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[19] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1333,7 +1562,7 @@ func (x *ListPublicKeyMappingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPublicKeyMappingResponse.ProtoReflect.Descriptor instead. func (*ListPublicKeyMappingResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{19} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{21} } func (x *ListPublicKeyMappingResponse) GetPublicKeyMappings() []*ListPublicKeyMappingResponse_PublicKeyMapping { @@ -1366,7 +1595,7 @@ type UpdatePublicKeyRequest struct { func (x *UpdatePublicKeyRequest) Reset() { *x = UpdatePublicKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[20] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1379,7 +1608,7 @@ func (x *UpdatePublicKeyRequest) String() string { func (*UpdatePublicKeyRequest) ProtoMessage() {} func (x *UpdatePublicKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[20] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1392,7 +1621,7 @@ func (x *UpdatePublicKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatePublicKeyRequest.ProtoReflect.Descriptor instead. func (*UpdatePublicKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{20} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{22} } func (x *UpdatePublicKeyRequest) GetId() string { @@ -1427,7 +1656,7 @@ type UpdatePublicKeyResponse struct { func (x *UpdatePublicKeyResponse) Reset() { *x = UpdatePublicKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[21] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1440,7 +1669,7 @@ func (x *UpdatePublicKeyResponse) String() string { func (*UpdatePublicKeyResponse) ProtoMessage() {} func (x *UpdatePublicKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[21] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1453,7 +1682,7 @@ func (x *UpdatePublicKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatePublicKeyResponse.ProtoReflect.Descriptor instead. func (*UpdatePublicKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{21} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{23} } func (x *UpdatePublicKeyResponse) GetKey() *policy.Key { @@ -1474,7 +1703,7 @@ type DeactivatePublicKeyRequest struct { func (x *DeactivatePublicKeyRequest) Reset() { *x = DeactivatePublicKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[22] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1487,7 +1716,7 @@ func (x *DeactivatePublicKeyRequest) String() string { func (*DeactivatePublicKeyRequest) ProtoMessage() {} func (x *DeactivatePublicKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[22] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1500,7 +1729,7 @@ func (x *DeactivatePublicKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeactivatePublicKeyRequest.ProtoReflect.Descriptor instead. func (*DeactivatePublicKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{22} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{24} } func (x *DeactivatePublicKeyRequest) GetId() string { @@ -1521,7 +1750,7 @@ type DeactivatePublicKeyResponse struct { func (x *DeactivatePublicKeyResponse) Reset() { *x = DeactivatePublicKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[23] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1534,7 +1763,7 @@ func (x *DeactivatePublicKeyResponse) String() string { func (*DeactivatePublicKeyResponse) ProtoMessage() {} func (x *DeactivatePublicKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[23] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1547,7 +1776,7 @@ func (x *DeactivatePublicKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeactivatePublicKeyResponse.ProtoReflect.Descriptor instead. func (*DeactivatePublicKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{23} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{25} } func (x *DeactivatePublicKeyResponse) GetKey() *policy.Key { @@ -1568,7 +1797,7 @@ type ActivatePublicKeyRequest struct { func (x *ActivatePublicKeyRequest) Reset() { *x = ActivatePublicKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[24] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1581,7 +1810,7 @@ func (x *ActivatePublicKeyRequest) String() string { func (*ActivatePublicKeyRequest) ProtoMessage() {} func (x *ActivatePublicKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[24] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1594,7 +1823,7 @@ func (x *ActivatePublicKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ActivatePublicKeyRequest.ProtoReflect.Descriptor instead. func (*ActivatePublicKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{24} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{26} } func (x *ActivatePublicKeyRequest) GetId() string { @@ -1615,7 +1844,7 @@ type ActivatePublicKeyResponse struct { func (x *ActivatePublicKeyResponse) Reset() { *x = ActivatePublicKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[25] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1628,7 +1857,7 @@ func (x *ActivatePublicKeyResponse) String() string { func (*ActivatePublicKeyResponse) ProtoMessage() {} func (x *ActivatePublicKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[25] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1641,7 +1870,7 @@ func (x *ActivatePublicKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ActivatePublicKeyResponse.ProtoReflect.Descriptor instead. func (*ActivatePublicKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{25} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{27} } func (x *ActivatePublicKeyResponse) GetKey() *policy.Key { @@ -1684,7 +1913,7 @@ type ListKeyAccessServerGrantsRequest struct { func (x *ListKeyAccessServerGrantsRequest) Reset() { *x = ListKeyAccessServerGrantsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[26] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1697,7 +1926,7 @@ func (x *ListKeyAccessServerGrantsRequest) String() string { func (*ListKeyAccessServerGrantsRequest) ProtoMessage() {} func (x *ListKeyAccessServerGrantsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[26] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1710,7 +1939,7 @@ func (x *ListKeyAccessServerGrantsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListKeyAccessServerGrantsRequest.ProtoReflect.Descriptor instead. func (*ListKeyAccessServerGrantsRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{26} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{28} } func (x *ListKeyAccessServerGrantsRequest) GetKasId() string { @@ -1757,7 +1986,7 @@ type ListKeyAccessServerGrantsResponse struct { func (x *ListKeyAccessServerGrantsResponse) Reset() { *x = ListKeyAccessServerGrantsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[27] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1770,7 +1999,7 @@ func (x *ListKeyAccessServerGrantsResponse) String() string { func (*ListKeyAccessServerGrantsResponse) ProtoMessage() {} func (x *ListKeyAccessServerGrantsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[27] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1783,7 +2012,7 @@ func (x *ListKeyAccessServerGrantsResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListKeyAccessServerGrantsResponse.ProtoReflect.Descriptor instead. func (*ListKeyAccessServerGrantsResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{27} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{29} } // Deprecated: Marked as deprecated in policy/kasregistry/key_access_server_registry.proto. @@ -1830,7 +2059,7 @@ type CreateKeyRequest struct { func (x *CreateKeyRequest) Reset() { *x = CreateKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[28] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1843,7 +2072,7 @@ func (x *CreateKeyRequest) String() string { func (*CreateKeyRequest) ProtoMessage() {} func (x *CreateKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[28] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1856,7 +2085,7 @@ func (x *CreateKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateKeyRequest.ProtoReflect.Descriptor instead. func (*CreateKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{28} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{30} } func (x *CreateKeyRequest) GetKasId() string { @@ -1934,7 +2163,7 @@ type CreateKeyResponse struct { func (x *CreateKeyResponse) Reset() { *x = CreateKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[29] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1947,7 +2176,7 @@ func (x *CreateKeyResponse) String() string { func (*CreateKeyResponse) ProtoMessage() {} func (x *CreateKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[29] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1960,7 +2189,7 @@ func (x *CreateKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateKeyResponse.ProtoReflect.Descriptor instead. func (*CreateKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{29} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{31} } func (x *CreateKeyResponse) GetKasKey() *policy.KasKey { @@ -1986,7 +2215,7 @@ type GetKeyRequest struct { func (x *GetKeyRequest) Reset() { *x = GetKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[30] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1999,7 +2228,7 @@ func (x *GetKeyRequest) String() string { func (*GetKeyRequest) ProtoMessage() {} func (x *GetKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[30] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2012,7 +2241,7 @@ func (x *GetKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetKeyRequest.ProtoReflect.Descriptor instead. func (*GetKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{30} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{32} } func (m *GetKeyRequest) GetIdentifier() isGetKeyRequest_Identifier { @@ -2064,7 +2293,7 @@ type GetKeyResponse struct { func (x *GetKeyResponse) Reset() { *x = GetKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[31] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2077,7 +2306,7 @@ func (x *GetKeyResponse) String() string { func (*GetKeyResponse) ProtoMessage() {} func (x *GetKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[31] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2090,7 +2319,7 @@ func (x *GetKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetKeyResponse.ProtoReflect.Descriptor instead. func (*GetKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{31} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{33} } func (x *GetKeyResponse) GetKasKey() *policy.KasKey { @@ -2117,12 +2346,18 @@ type ListKeysRequest struct { Legacy *bool `protobuf:"varint,8,opt,name=legacy,proto3,oneof" json:"legacy,omitempty"` // Filter for legacy keys // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` // Pagination request for the list of keys + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + Sort []*KasKeysSort `protobuf:"bytes,11,rep,name=sort,proto3" json:"sort,omitempty"` } func (x *ListKeysRequest) Reset() { *x = ListKeysRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[32] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2135,7 +2370,7 @@ func (x *ListKeysRequest) String() string { func (*ListKeysRequest) ProtoMessage() {} func (x *ListKeysRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[32] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2148,7 +2383,7 @@ func (x *ListKeysRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListKeysRequest.ProtoReflect.Descriptor instead. func (*ListKeysRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{32} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{34} } func (x *ListKeysRequest) GetKeyAlgorithm() policy.Algorithm { @@ -2200,6 +2435,13 @@ func (x *ListKeysRequest) GetPagination() *policy.PageRequest { return nil } +func (x *ListKeysRequest) GetSort() []*KasKeysSort { + if x != nil { + return x.Sort + } + return nil +} + type isListKeysRequest_KasFilter interface { isListKeysRequest_KasFilter() } @@ -2235,7 +2477,7 @@ type ListKeysResponse struct { func (x *ListKeysResponse) Reset() { *x = ListKeysResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[33] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2248,7 +2490,7 @@ func (x *ListKeysResponse) String() string { func (*ListKeysResponse) ProtoMessage() {} func (x *ListKeysResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[33] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2261,7 +2503,7 @@ func (x *ListKeysResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListKeysResponse.ProtoReflect.Descriptor instead. func (*ListKeysResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{33} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{35} } func (x *ListKeysResponse) GetKasKeys() []*policy.KasKey { @@ -2295,7 +2537,7 @@ type UpdateKeyRequest struct { func (x *UpdateKeyRequest) Reset() { *x = UpdateKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[34] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2308,7 +2550,7 @@ func (x *UpdateKeyRequest) String() string { func (*UpdateKeyRequest) ProtoMessage() {} func (x *UpdateKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[34] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2321,7 +2563,7 @@ func (x *UpdateKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateKeyRequest.ProtoReflect.Descriptor instead. func (*UpdateKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{34} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{36} } func (x *UpdateKeyRequest) GetId() string { @@ -2357,7 +2599,7 @@ type UpdateKeyResponse struct { func (x *UpdateKeyResponse) Reset() { *x = UpdateKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[35] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2370,7 +2612,7 @@ func (x *UpdateKeyResponse) String() string { func (*UpdateKeyResponse) ProtoMessage() {} func (x *UpdateKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[35] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2383,7 +2625,7 @@ func (x *UpdateKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateKeyResponse.ProtoReflect.Descriptor instead. func (*UpdateKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{35} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{37} } func (x *UpdateKeyResponse) GetKasKey() *policy.KasKey { @@ -2414,7 +2656,7 @@ type KasKeyIdentifier struct { func (x *KasKeyIdentifier) Reset() { *x = KasKeyIdentifier{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[36] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2427,7 +2669,7 @@ func (x *KasKeyIdentifier) String() string { func (*KasKeyIdentifier) ProtoMessage() {} func (x *KasKeyIdentifier) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[36] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2440,7 +2682,7 @@ func (x *KasKeyIdentifier) ProtoReflect() protoreflect.Message { // Deprecated: Use KasKeyIdentifier.ProtoReflect.Descriptor instead. func (*KasKeyIdentifier) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{36} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{38} } func (m *KasKeyIdentifier) GetIdentifier() isKasKeyIdentifier_Identifier { @@ -2519,7 +2761,7 @@ type RotateKeyRequest struct { func (x *RotateKeyRequest) Reset() { *x = RotateKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[37] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2532,7 +2774,7 @@ func (x *RotateKeyRequest) String() string { func (*RotateKeyRequest) ProtoMessage() {} func (x *RotateKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[37] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2545,7 +2787,7 @@ func (x *RotateKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateKeyRequest.ProtoReflect.Descriptor instead. func (*RotateKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{37} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{39} } func (m *RotateKeyRequest) GetActiveKey() isRotateKeyRequest_ActiveKey { @@ -2608,7 +2850,7 @@ type ChangeMappings struct { func (x *ChangeMappings) Reset() { *x = ChangeMappings{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[38] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2621,7 +2863,7 @@ func (x *ChangeMappings) String() string { func (*ChangeMappings) ProtoMessage() {} func (x *ChangeMappings) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[38] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2634,7 +2876,7 @@ func (x *ChangeMappings) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeMappings.ProtoReflect.Descriptor instead. func (*ChangeMappings) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{38} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{40} } func (x *ChangeMappings) GetId() string { @@ -2666,7 +2908,7 @@ type RotatedResources struct { func (x *RotatedResources) Reset() { *x = RotatedResources{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[39] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2679,7 +2921,7 @@ func (x *RotatedResources) String() string { func (*RotatedResources) ProtoMessage() {} func (x *RotatedResources) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[39] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2692,7 +2934,7 @@ func (x *RotatedResources) ProtoReflect() protoreflect.Message { // Deprecated: Use RotatedResources.ProtoReflect.Descriptor instead. func (*RotatedResources) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{39} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{41} } func (x *RotatedResources) GetRotatedOutKey() *policy.KasKey { @@ -2738,7 +2980,7 @@ type RotateKeyResponse struct { func (x *RotateKeyResponse) Reset() { *x = RotateKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[40] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2751,7 +2993,7 @@ func (x *RotateKeyResponse) String() string { func (*RotateKeyResponse) ProtoMessage() {} func (x *RotateKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[40] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[42] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2764,7 +3006,7 @@ func (x *RotateKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateKeyResponse.ProtoReflect.Descriptor instead. func (*RotateKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{40} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{42} } func (x *RotateKeyResponse) GetKasKey() *policy.KasKey { @@ -2800,7 +3042,7 @@ type SetBaseKeyRequest struct { func (x *SetBaseKeyRequest) Reset() { *x = SetBaseKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[41] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2813,7 +3055,7 @@ func (x *SetBaseKeyRequest) String() string { func (*SetBaseKeyRequest) ProtoMessage() {} func (x *SetBaseKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[41] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[43] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2826,7 +3068,7 @@ func (x *SetBaseKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetBaseKeyRequest.ProtoReflect.Descriptor instead. func (*SetBaseKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{41} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{43} } func (m *SetBaseKeyRequest) GetActiveKey() isSetBaseKeyRequest_ActiveKey { @@ -2877,7 +3119,7 @@ type GetBaseKeyRequest struct { func (x *GetBaseKeyRequest) Reset() { *x = GetBaseKeyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[42] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2890,7 +3132,7 @@ func (x *GetBaseKeyRequest) String() string { func (*GetBaseKeyRequest) ProtoMessage() {} func (x *GetBaseKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[42] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[44] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2903,7 +3145,7 @@ func (x *GetBaseKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBaseKeyRequest.ProtoReflect.Descriptor instead. func (*GetBaseKeyRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{42} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{44} } type GetBaseKeyResponse struct { @@ -2917,7 +3159,7 @@ type GetBaseKeyResponse struct { func (x *GetBaseKeyResponse) Reset() { *x = GetBaseKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[43] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2930,7 +3172,7 @@ func (x *GetBaseKeyResponse) String() string { func (*GetBaseKeyResponse) ProtoMessage() {} func (x *GetBaseKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[43] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[45] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2943,7 +3185,7 @@ func (x *GetBaseKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBaseKeyResponse.ProtoReflect.Descriptor instead. func (*GetBaseKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{43} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{45} } func (x *GetBaseKeyResponse) GetBaseKey() *policy.SimpleKasKey { @@ -2965,7 +3207,7 @@ type SetBaseKeyResponse struct { func (x *SetBaseKeyResponse) Reset() { *x = SetBaseKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[44] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2978,7 +3220,7 @@ func (x *SetBaseKeyResponse) String() string { func (*SetBaseKeyResponse) ProtoMessage() {} func (x *SetBaseKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[44] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[46] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2991,7 +3233,7 @@ func (x *SetBaseKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetBaseKeyResponse.ProtoReflect.Descriptor instead. func (*SetBaseKeyResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{44} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{46} } func (x *SetBaseKeyResponse) GetNewBaseKey() *policy.SimpleKasKey { @@ -3020,7 +3262,7 @@ type MappedPolicyObject struct { func (x *MappedPolicyObject) Reset() { *x = MappedPolicyObject{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[45] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3033,7 +3275,7 @@ func (x *MappedPolicyObject) String() string { func (*MappedPolicyObject) ProtoMessage() {} func (x *MappedPolicyObject) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[45] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[47] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3046,7 +3288,7 @@ func (x *MappedPolicyObject) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedPolicyObject.ProtoReflect.Descriptor instead. func (*MappedPolicyObject) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{45} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{47} } func (x *MappedPolicyObject) GetId() string { @@ -3078,7 +3320,7 @@ type KeyMapping struct { func (x *KeyMapping) Reset() { *x = KeyMapping{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[46] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3091,7 +3333,7 @@ func (x *KeyMapping) String() string { func (*KeyMapping) ProtoMessage() {} func (x *KeyMapping) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[46] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[48] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3104,7 +3346,7 @@ func (x *KeyMapping) ProtoReflect() protoreflect.Message { // Deprecated: Use KeyMapping.ProtoReflect.Descriptor instead. func (*KeyMapping) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{46} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{48} } func (x *KeyMapping) GetKid() string { @@ -3158,7 +3400,7 @@ type ListKeyMappingsRequest struct { func (x *ListKeyMappingsRequest) Reset() { *x = ListKeyMappingsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[47] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3171,7 +3413,7 @@ func (x *ListKeyMappingsRequest) String() string { func (*ListKeyMappingsRequest) ProtoMessage() {} func (x *ListKeyMappingsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[47] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[49] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3184,7 +3426,7 @@ func (x *ListKeyMappingsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListKeyMappingsRequest.ProtoReflect.Descriptor instead. func (*ListKeyMappingsRequest) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{47} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{49} } func (m *ListKeyMappingsRequest) GetIdentifier() isListKeyMappingsRequest_Identifier { @@ -3243,7 +3485,7 @@ type ListKeyMappingsResponse struct { func (x *ListKeyMappingsResponse) Reset() { *x = ListKeyMappingsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[48] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3256,7 +3498,7 @@ func (x *ListKeyMappingsResponse) String() string { func (*ListKeyMappingsResponse) ProtoMessage() {} func (x *ListKeyMappingsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[48] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[50] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3269,7 +3511,7 @@ func (x *ListKeyMappingsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListKeyMappingsResponse.ProtoReflect.Descriptor instead. func (*ListKeyMappingsResponse) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{48} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{50} } func (x *ListKeyMappingsResponse) GetKeyMappings() []*KeyMapping { @@ -3300,7 +3542,7 @@ type ListPublicKeyMappingResponse_PublicKeyMapping struct { func (x *ListPublicKeyMappingResponse_PublicKeyMapping) Reset() { *x = ListPublicKeyMappingResponse_PublicKeyMapping{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[49] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3313,7 +3555,7 @@ func (x *ListPublicKeyMappingResponse_PublicKeyMapping) String() string { func (*ListPublicKeyMappingResponse_PublicKeyMapping) ProtoMessage() {} func (x *ListPublicKeyMappingResponse_PublicKeyMapping) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[49] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[51] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3326,7 +3568,7 @@ func (x *ListPublicKeyMappingResponse_PublicKeyMapping) ProtoReflect() protorefl // Deprecated: Use ListPublicKeyMappingResponse_PublicKeyMapping.ProtoReflect.Descriptor instead. func (*ListPublicKeyMappingResponse_PublicKeyMapping) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{19, 0} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{21, 0} } func (x *ListPublicKeyMappingResponse_PublicKeyMapping) GetKasId() string { @@ -3371,7 +3613,7 @@ type ListPublicKeyMappingResponse_PublicKey struct { func (x *ListPublicKeyMappingResponse_PublicKey) Reset() { *x = ListPublicKeyMappingResponse_PublicKey{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[50] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3384,7 +3626,7 @@ func (x *ListPublicKeyMappingResponse_PublicKey) String() string { func (*ListPublicKeyMappingResponse_PublicKey) ProtoMessage() {} func (x *ListPublicKeyMappingResponse_PublicKey) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[50] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[52] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3397,7 +3639,7 @@ func (x *ListPublicKeyMappingResponse_PublicKey) ProtoReflect() protoreflect.Mes // Deprecated: Use ListPublicKeyMappingResponse_PublicKey.ProtoReflect.Descriptor instead. func (*ListPublicKeyMappingResponse_PublicKey) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{19, 1} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{21, 1} } func (x *ListPublicKeyMappingResponse_PublicKey) GetKey() *policy.Key { @@ -3440,7 +3682,7 @@ type ListPublicKeyMappingResponse_Association struct { func (x *ListPublicKeyMappingResponse_Association) Reset() { *x = ListPublicKeyMappingResponse_Association{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[51] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3453,7 +3695,7 @@ func (x *ListPublicKeyMappingResponse_Association) String() string { func (*ListPublicKeyMappingResponse_Association) ProtoMessage() {} func (x *ListPublicKeyMappingResponse_Association) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[51] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[53] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3466,7 +3708,7 @@ func (x *ListPublicKeyMappingResponse_Association) ProtoReflect() protoreflect.M // Deprecated: Use ListPublicKeyMappingResponse_Association.ProtoReflect.Descriptor instead. func (*ListPublicKeyMappingResponse_Association) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{19, 2} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{21, 2} } func (x *ListPublicKeyMappingResponse_Association) GetId() string { @@ -3508,7 +3750,7 @@ type RotateKeyRequest_NewKey struct { func (x *RotateKeyRequest_NewKey) Reset() { *x = RotateKeyRequest_NewKey{} if protoimpl.UnsafeEnabled { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[52] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3521,7 +3763,7 @@ func (x *RotateKeyRequest_NewKey) String() string { func (*RotateKeyRequest_NewKey) ProtoMessage() {} func (x *RotateKeyRequest_NewKey) ProtoReflect() protoreflect.Message { - mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[52] + mi := &file_policy_kasregistry_key_access_server_registry_proto_msgTypes[54] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3534,7 +3776,7 @@ func (x *RotateKeyRequest_NewKey) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateKeyRequest_NewKey.ProtoReflect.Descriptor instead. func (*RotateKeyRequest_NewKey) Descriptor() ([]byte, []int) { - return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{37, 0} + return file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP(), []int{39, 0} } func (x *RotateKeyRequest_NewKey) GetKeyId() string { @@ -3596,945 +3838,996 @@ var file_policy_kasregistry_key_access_server_registry_proto_rawDesc = []byte{ 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, - 0x16, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe4, 0x03, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x0d, 0xba, 0x48, 0x08, 0xd8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x18, 0x01, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, - 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, - 0x00, 0x52, 0x03, 0x75, 0x72, 0x69, 0x3a, 0xb7, 0x02, 0xba, 0x48, 0xb3, 0x02, 0x1a, 0xa8, 0x01, - 0x0a, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x69, 0x65, 0x6c, - 0x64, 0x73, 0x12, 0x4a, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x75, 0x73, 0x65, 0x20, 0x64, - 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x27, 0x69, 0x64, 0x27, 0x20, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x27, - 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x27, 0x20, 0x6f, 0x72, 0x20, 0x27, 0x75, 0x72, 0x69, 0x27, - 0x2c, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x1a, 0x48, - 0x21, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, 0x20, 0x26, - 0x26, 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x61, 0x73, 0x5f, - 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, - 0x75, 0x72, 0x69, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, - 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x29, 0x29, 0x29, 0x1a, 0x85, 0x01, 0x0a, 0x0f, 0x72, 0x65, 0x71, - 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, 0x45, 0x69, - 0x74, 0x68, 0x65, 0x72, 0x20, 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, - 0x66, 0x20, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x75, 0x72, 0x69, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x1a, 0x43, 0x68, 0x61, 0x73, - 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, - 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, - 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x75, 0x72, 0x69, 0x29, 0x20, 0x7c, - 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x29, - 0x42, 0x0c, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x61, - 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x11, - 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x22, 0x52, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x9b, 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x12, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x10, 0x6b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x34, 0x0a, - 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x06, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x87, 0x02, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0xf4, 0x01, 0xba, 0x48, 0xf0, 0x01, 0xba, 0x01, 0xec, 0x01, 0x0a, 0x0a, 0x75, - 0x72, 0x69, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xcf, 0x01, 0x55, 0x52, 0x49, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, - 0x55, 0x52, 0x4c, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x68, 0x74, 0x74, 0x70, - 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x27, 0x29, 0x20, - 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, - 0x20, 0x45, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6d, 0x75, - 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, - 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, - 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2c, - 0x20, 0x63, 0x61, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x68, 0x79, 0x70, - 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, - 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x61, - 0x6e, 0x64, 0x20, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x73, 0x2e, 0x1a, 0x0c, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x69, 0x73, 0x55, 0x72, 0x69, 0x28, 0x29, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x30, - 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x12, 0x40, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, - 0x00, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x12, 0xc1, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, - 0x09, 0x42, 0xac, 0x02, 0xba, 0x48, 0xa8, 0x02, 0xba, 0x01, 0x9c, 0x02, 0x0a, 0x0f, 0x6b, 0x61, - 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb3, 0x01, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, 0x53, 0x20, 0x6e, - 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, - 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, - 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, - 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, - 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, - 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, 0x53, 0x20, 0x6e, 0x61, 0x6d, - 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, - 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, - 0x73, 0x65, 0x2e, 0x1a, 0x53, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, - 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, - 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, - 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, - 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, - 0x29, 0x20, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, - 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x64, 0x0a, 0x1d, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x11, - 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x22, 0xa5, 0x07, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xac, 0x02, 0x0a, - 0x03, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x99, 0x02, 0xba, 0x48, 0x95, - 0x02, 0xba, 0x01, 0x91, 0x02, 0x0a, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, - 0x75, 0x72, 0x69, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xd8, 0x01, 0x4f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x55, 0x52, 0x49, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, - 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x52, 0x4c, 0x20, 0x28, 0x65, - 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, - 0x6d, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x27, 0x29, 0x20, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, - 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x20, 0x45, 0x61, 0x63, 0x68, 0x20, - 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, - 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, - 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2c, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, - 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, - 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x73, 0x6c, 0x61, - 0x73, 0x68, 0x65, 0x73, 0x2e, 0x1a, 0x1f, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, - 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, - 0x73, 0x55, 0x72, 0x69, 0x28, 0x29, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x30, 0x0a, 0x0a, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x40, 0x0a, - 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x00, 0x82, 0x01, - 0x02, 0x10, 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, - 0xbc, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x42, 0xa7, - 0x02, 0xba, 0x48, 0xa3, 0x02, 0xba, 0x01, 0x97, 0x02, 0x0a, 0x0f, 0x6b, 0x61, 0x73, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb3, 0x01, 0x52, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, 0x53, 0x20, 0x6e, 0x61, 0x6d, 0x65, - 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, - 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, - 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, - 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, - 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, - 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, 0x53, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, - 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, - 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, - 0x1a, 0x4e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, - 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, - 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, - 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, - 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, - 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, - 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, - 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x64, 0x0a, 0x1d, 0x55, 0x70, 0x64, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x16, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe4, 0x03, 0x0a, 0x19, 0x47, 0x65, + 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0d, 0xba, 0x48, 0x08, 0xd8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, + 0x18, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, + 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, + 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, + 0x01, 0x48, 0x00, 0x52, 0x03, 0x75, 0x72, 0x69, 0x3a, 0xb7, 0x02, 0xba, 0x48, 0xb3, 0x02, 0x1a, + 0xa8, 0x01, 0x0a, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x73, 0x12, 0x4a, 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x75, 0x73, 0x65, + 0x20, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x27, 0x69, 0x64, 0x27, + 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, + 0x20, 0x27, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x27, 0x20, 0x6f, 0x72, 0x20, 0x27, 0x75, 0x72, + 0x69, 0x27, 0x2c, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x6f, 0x74, 0x68, + 0x1a, 0x48, 0x21, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, + 0x20, 0x26, 0x26, 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x61, + 0x73, 0x5f, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, + 0x73, 0x2e, 0x75, 0x72, 0x69, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x29, 0x29, 0x29, 0x1a, 0x85, 0x01, 0x0a, 0x0f, 0x72, + 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, + 0x45, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6f, 0x6e, 0x65, + 0x20, 0x6f, 0x66, 0x20, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x75, 0x72, + 0x69, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x1a, 0x43, 0x68, + 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, + 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x29, 0x20, + 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x75, 0x72, 0x69, 0x29, + 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x61, 0x6d, + 0x65, 0x29, 0x42, 0x0c, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x22, 0x61, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, + 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x22, 0xa3, 0x01, 0x0a, 0x14, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x4c, 0x0a, 0x05, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, + 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, + 0x02, 0x10, 0x01, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x9a, 0x01, 0x0a, 0x1b, 0x4c, 0x69, + 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x46, + 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, + 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0x9b, 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x4b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x12, 0x6b, 0x65, 0x79, 0x5f, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x10, 0x6b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x34, + 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x91, 0x01, 0x0a, 0x0b, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, + 0x53, 0x6f, 0x72, 0x74, 0x12, 0x43, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x4b, 0x61, 0x73, + 0x4b, 0x65, 0x79, 0x73, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, + 0x10, 0x01, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x64, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x06, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x6b, 0x65, - 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0f, - 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, - 0x38, 0x0a, 0x1c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x64, 0x0a, 0x1d, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x6b, 0x65, - 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0f, - 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, - 0x37, 0x0a, 0x13, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xd0, 0x02, 0x0a, 0x15, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, - 0x74, 0x73, 0x12, 0x43, 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x52, 0x0a, 0x10, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0f, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x52, 0x0a, 0x10, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, - 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x74, - 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0f, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, - 0x4a, 0x0a, 0x0c, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, - 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x74, - 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0b, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x22, 0x9e, 0x01, 0x0a, 0x16, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, - 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, - 0x01, 0x01, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x38, 0x0a, 0x17, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, - 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, - 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x35, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x1d, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xca, - 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, - 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x08, 0x6b, - 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, - 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x25, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, - 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0c, 0x0a, - 0x0a, 0x6b, 0x61, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x6f, 0x0a, 0x16, 0x4c, - 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, - 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x81, 0x02, 0x0a, - 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x06, - 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, - 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, - 0x24, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x61, - 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, - 0x01, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x2f, 0x0a, 0x0d, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xd8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x52, 0x0b, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x33, 0x0a, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x87, 0x02, 0x0a, 0x03, 0x75, 0x72, + 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0xf4, 0x01, 0xba, 0x48, 0xf0, 0x01, 0xba, 0x01, + 0xec, 0x01, 0x0a, 0x0a, 0x75, 0x72, 0x69, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xcf, + 0x01, 0x55, 0x52, 0x49, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x52, 0x4c, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, + 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x27, 0x29, 0x20, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x62, 0x79, + 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x73, 0x65, 0x67, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x20, 0x45, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, + 0x64, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, + 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, + 0x63, 0x74, 0x65, 0x72, 0x2c, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, + 0x72, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x73, 0x2e, + 0x1a, 0x0c, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x73, 0x55, 0x72, 0x69, 0x28, 0x29, 0x52, 0x03, + 0x75, 0x72, 0x69, 0x12, 0x30, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x40, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x42, 0x0b, + 0xba, 0x48, 0x08, 0xc8, 0x01, 0x00, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0xc1, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x42, 0xac, 0x02, 0xba, 0x48, 0xa8, 0x02, 0xba, 0x01, 0x9c, + 0x02, 0x0a, 0x0f, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x12, 0xb3, 0x01, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, + 0x4b, 0x41, 0x53, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, + 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, + 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, + 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, + 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, + 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, + 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, + 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, + 0x53, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, + 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, + 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x53, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, + 0x68, 0x69, 0x73, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, + 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, + 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, + 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0xc8, 0x01, 0x00, + 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, + 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x22, 0x64, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x43, 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0xa5, 0x07, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, + 0x64, 0x12, 0xac, 0x02, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x99, 0x02, 0xba, 0x48, 0x95, 0x02, 0xba, 0x01, 0x91, 0x02, 0x0a, 0x13, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x72, 0x69, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, + 0xd8, 0x01, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x55, 0x52, 0x49, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, + 0x52, 0x4c, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, + 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x27, 0x29, 0x20, 0x66, + 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x20, + 0x45, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6d, 0x75, 0x73, + 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, 0x20, + 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, + 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2c, 0x20, + 0x63, 0x61, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x68, 0x79, 0x70, 0x68, + 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, + 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x61, 0x6e, + 0x64, 0x20, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x73, 0x2e, 0x1a, 0x1f, 0x73, 0x69, 0x7a, 0x65, + 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, + 0x68, 0x69, 0x73, 0x2e, 0x69, 0x73, 0x55, 0x72, 0x69, 0x28, 0x29, 0x52, 0x03, 0x75, 0x72, 0x69, + 0x12, 0x30, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x12, 0x40, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x42, 0x0b, 0xba, 0x48, 0x08, + 0xc8, 0x01, 0x00, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0xbc, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, + 0x01, 0x28, 0x09, 0x42, 0xa7, 0x02, 0xba, 0x48, 0xa3, 0x02, 0xba, 0x01, 0x97, 0x02, 0x0a, 0x0f, + 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, + 0xb3, 0x01, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, 0x53, + 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, + 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, + 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, + 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, + 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, + 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, + 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, 0x53, 0x20, 0x6e, + 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, + 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, + 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x4e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, + 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, + 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, + 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, + 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, + 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, + 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x64, + 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x22, 0x38, 0x0a, 0x1c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x64, + 0x0a, 0x1d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x22, 0x37, 0x0a, 0x13, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x66, + 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xd0, 0x02, + 0x0a, 0x15, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x43, 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0f, 0x6b, 0x65, 0x79, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x52, 0x0a, 0x10, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x72, 0x61, 0x6e, + 0x74, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x0f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, + 0x12, 0x52, 0x0a, 0x10, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, + 0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x52, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x47, 0x72, + 0x61, 0x6e, 0x74, 0x73, 0x12, 0x4a, 0x0a, 0x0c, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, + 0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x52, 0x0b, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, + 0x22, 0x9e, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x06, 0x6b, + 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, + 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x22, 0x38, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x13, 0x47, + 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x42, 0x0c, + 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x35, 0x0a, 0x14, + 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x22, 0xca, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, + 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, + 0x12, 0x24, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, + 0x61, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, + 0x69, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, + 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0c, 0x0a, 0x0a, 0x6b, 0x61, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x22, 0xf6, 0x05, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x71, 0x0a, 0x13, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, - 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x41, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x52, 0x11, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, - 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0xba, 0x01, 0x0a, 0x10, 0x50, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, - 0x15, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x5b, 0x0a, 0x0b, 0x70, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x3a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x70, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x73, 0x1a, 0xbe, 0x02, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x54, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x06, + 0x22, 0x6f, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x6b, 0x65, + 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, + 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0x81, 0x02, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, + 0x61, 0x73, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, + 0x00, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, 0x6b, 0x61, + 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, + 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, + 0x69, 0x12, 0x2f, 0x0a, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, + 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xd8, 0x01, 0x01, + 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x49, 0x64, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0c, 0x0a, 0x0a, 0x6b, 0x61, 0x73, 0x5f, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xf6, 0x05, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x13, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x11, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, + 0xba, 0x01, 0x0a, 0x10, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, + 0x70, 0x69, 0x6e, 0x67, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6b, + 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, + 0x61, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, + 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, + 0x5b, 0x0a, 0x0b, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, + 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x52, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x73, 0x1a, 0xbe, 0x02, 0x0a, + 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x54, 0x0a, 0x06, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x73, 0x73, 0x6f, + 0x63, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, + 0x5e, 0x0a, 0x0b, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x5e, 0x0a, 0x0b, 0x64, 0x65, - 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x41, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, - 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5c, 0x0a, 0x0a, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, + 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x5c, 0x0a, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x1a, 0x2f, 0x0a, + 0x0b, 0x41, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, + 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xbd, + 0x01, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, + 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x38, + 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x36, 0x0a, 0x1a, 0x44, 0x65, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, + 0x22, 0x3c, 0x0a, 0x1b, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1d, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x34, + 0x0a, 0x18, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, + 0x52, 0x02, 0x69, 0x64, 0x22, 0x3a, 0x0a, 0x19, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x22, 0xa5, 0x07, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xcb, 0x01, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0xb3, 0x01, 0xba, 0x48, 0xaf, 0x01, 0xba, 0x01, 0xab, + 0x01, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x75, 0x69, 0x64, + 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x23, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, + 0x6c, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, + 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x55, 0x49, 0x44, 0x1a, 0x6e, 0x73, 0x69, + 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, + 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5b, + 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x38, 0x7d, 0x2d, 0x5b, 0x30, + 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, + 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, + 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, + 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x31, 0x32, 0x7d, 0x27, 0x29, 0x52, 0x05, 0x6b, 0x61, + 0x73, 0x49, 0x64, 0x12, 0xb3, 0x02, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x99, 0x02, 0xba, 0x48, 0x95, 0x02, 0xba, 0x01, 0x91, 0x02, + 0x0a, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x72, 0x69, 0x5f, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xd8, 0x01, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, + 0x20, 0x55, 0x52, 0x49, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x52, 0x4c, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, + 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x27, 0x29, 0x20, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x62, 0x79, + 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x73, 0x65, 0x67, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x20, 0x45, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, + 0x64, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, + 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, + 0x63, 0x74, 0x65, 0x72, 0x2c, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, + 0x72, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x73, 0x2e, + 0x1a, 0x1f, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, + 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x73, 0x55, 0x72, 0x69, 0x28, + 0x29, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0xc3, 0x02, 0x0a, 0x08, 0x6b, 0x61, + 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0xa7, 0x02, 0xba, + 0x48, 0xa3, 0x02, 0xba, 0x01, 0x97, 0x02, 0x0a, 0x0f, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb3, 0x01, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, 0x53, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, + 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, + 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, + 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, + 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x64, 0x20, 0x4b, 0x41, 0x53, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, + 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, + 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x4e, + 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, + 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, + 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, + 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, + 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, + 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x02, 0x18, 0x01, 0x22, 0xa4, 0x01, 0x0a, 0x21, 0x4c, 0x69, 0x73, + 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, + 0x0a, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x2e, 0x41, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x1a, 0x2f, 0x0a, 0x0b, 0x41, 0x73, 0x73, 0x6f, - 0x63, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xbd, 0x01, 0x0a, 0x16, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, - 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, - 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, - 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x38, 0x0a, 0x17, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x22, 0x36, 0x0a, 0x1a, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, - 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x3c, 0x0a, 0x1b, 0x44, - 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x34, 0x0a, 0x18, 0x41, 0x63, 0x74, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, - 0x3a, 0x0a, 0x19, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xa5, 0x07, 0x0a, 0x20, - 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0xcb, 0x01, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0xb3, 0x01, 0xba, 0x48, 0xaf, 0x01, 0xba, 0x01, 0xab, 0x01, 0x0a, 0x14, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x12, 0x23, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x66, 0x69, 0x65, - 0x6c, 0x64, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x20, 0x55, 0x55, 0x49, 0x44, 0x1a, 0x6e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, - 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, - 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, - 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x38, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, - 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, - 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, - 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, - 0x5d, 0x7b, 0x31, 0x32, 0x7d, 0x27, 0x29, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0xb3, - 0x02, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x99, 0x02, 0xba, 0x48, 0x95, 0x02, 0xba, 0x01, 0x91, 0x02, 0x0a, 0x13, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x72, 0x69, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x12, 0xd8, 0x01, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x55, 0x52, 0x49, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, - 0x55, 0x52, 0x4c, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x68, 0x74, 0x74, 0x70, - 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x27, 0x29, 0x20, - 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, - 0x20, 0x45, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6d, 0x75, - 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, - 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, - 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2c, - 0x20, 0x63, 0x61, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x68, 0x79, 0x70, - 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, - 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x61, - 0x6e, 0x64, 0x20, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x73, 0x2e, 0x1a, 0x1f, 0x73, 0x69, 0x7a, - 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x73, 0x55, 0x72, 0x69, 0x28, 0x29, 0x52, 0x06, 0x6b, 0x61, - 0x73, 0x55, 0x72, 0x69, 0x12, 0xc3, 0x02, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0xa7, 0x02, 0xba, 0x48, 0xa3, 0x02, 0xba, 0x01, - 0x97, 0x02, 0x0a, 0x0f, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x12, 0xb3, 0x01, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x20, 0x4b, 0x41, 0x53, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, - 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, - 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, - 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, - 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, - 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, - 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, - 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x4b, - 0x41, 0x53, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, - 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, - 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x4e, 0x73, 0x69, 0x7a, 0x65, 0x28, - 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, - 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, - 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, - 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, - 0x01, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, - 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3a, - 0x02, 0x18, 0x01, 0x22, 0xa4, 0x01, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x67, 0x72, 0x61, - 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, - 0x61, 0x6e, 0x74, 0x73, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, - 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x02, 0x18, 0x01, 0x22, 0xcc, 0x0c, 0x0a, 0x10, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1f, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, - 0x12, 0x1e, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, - 0x12, 0xa4, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, - 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x6c, 0xba, 0x48, 0x69, - 0xba, 0x01, 0x66, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, + 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x02, 0x18, 0x01, 0x22, + 0xd5, 0x0c, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x05, + 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, + 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0xad, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, + 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, + 0x42, 0x75, 0xba, 0x48, 0x72, 0xba, 0x01, 0x6f, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, + 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, + 0x34, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, + 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x1a, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, + 0x31, 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, + 0x2c, 0x20, 0x37, 0x2c, 0x20, 0x38, 0x5d, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, + 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x93, 0x01, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x67, 0xba, 0x48, 0x64, 0xba, 0x01, + 0x61, 0x0a, 0x10, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x64, 0x65, 0x66, 0x69, + 0x6e, 0x65, 0x64, 0x12, 0x35, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, + 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x20, 0x28, 0x31, 0x2d, 0x34, 0x29, 0x2e, 0x1a, 0x16, 0x74, 0x68, 0x69, 0x73, + 0x20, 0x3e, 0x3d, 0x20, 0x31, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3c, 0x3d, + 0x20, 0x34, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, + 0x01, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, + 0x3d, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, + 0x74, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, + 0x0d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x2c, + 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x65, + 0x67, 0x61, 0x63, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0xbb, 0x07, 0xba, 0x48, 0xb7, 0x07, + 0x1a, 0x97, 0x03, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x5f, + 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xbc, 0x01, 0x54, 0x68, 0x65, 0x20, 0x77, + 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, + 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, + 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, + 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x2e, 0x20, 0x54, 0x68, + 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x6d, 0x75, + 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x6b, + 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, + 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, + 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, + 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xb0, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, + 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, + 0x3d, 0x3d, 0x20, 0x32, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, + 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, + 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, + 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, + 0x6b, 0x65, 0x79, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xf4, 0x02, 0x0a, 0x26, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, + 0x64, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, + 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xa8, 0x01, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x20, 0x69, 0x64, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, + 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, + 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, + 0x4f, 0x54, 0x45, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, + 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, + 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, + 0x45, 0x59, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, + 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, + 0x1a, 0x9e, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, + 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, + 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, + 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x29, 0x20, + 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, + 0x29, 0x1a, 0xa3, 0x01, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x48, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, + 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, + 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, + 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, + 0x4c, 0x59, 0x2e, 0x1a, 0x32, 0x21, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, + 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x20, 0x26, 0x26, 0x20, 0x68, 0x61, 0x73, + 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x5f, 0x63, 0x74, 0x78, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, + 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, + 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0x7a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x42, 0x13, 0x0a, 0x0a, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, + 0x01, 0x22, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, + 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0xde, 0x03, 0x0a, + 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0xb0, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x78, 0xba, 0x48, 0x75, + 0xba, 0x01, 0x72, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, - 0x1a, 0x17, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, 0x32, 0x2c, - 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x5d, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x41, 0x6c, - 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x93, 0x01, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, - 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x67, 0xba, 0x48, 0x64, - 0xba, 0x01, 0x61, 0x0a, 0x10, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x64, 0x65, - 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x35, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, - 0x6f, 0x64, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, - 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x20, 0x28, 0x31, 0x2d, 0x34, 0x29, 0x2e, 0x1a, 0x16, 0x74, 0x68, - 0x69, 0x73, 0x20, 0x3e, 0x3d, 0x20, 0x31, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, - 0x3c, 0x3d, 0x20, 0x34, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x42, 0x0a, - 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x42, 0x06, 0xba, 0x48, 0x03, - 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, - 0x78, 0x12, 0x3d, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, - 0x5f, 0x63, 0x74, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, - 0x78, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, - 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x12, 0x16, - 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, - 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, - 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0xbb, 0x07, 0xba, 0x48, - 0xb7, 0x07, 0x1a, 0x97, 0x03, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, - 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, - 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xbc, 0x01, 0x54, 0x68, 0x65, - 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x69, 0x73, 0x20, - 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, - 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, - 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, - 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, - 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x2e, 0x20, - 0x54, 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, - 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, - 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x20, 0x6f, 0x72, 0x20, - 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, - 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xb0, 0x01, 0x28, 0x28, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, - 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, - 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, - 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x21, 0x3d, 0x20, 0x27, - 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, - 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, - 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x1a, 0x23, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x30, 0x2c, 0x20, 0x31, 0x2c, + 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, + 0x37, 0x2c, 0x20, 0x38, 0x5d, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, + 0x74, 0x68, 0x6d, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, + 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, + 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, + 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, + 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, + 0x55, 0x72, 0x69, 0x12, 0x1b, 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x88, 0x01, 0x01, + 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, + 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, + 0x73, 0x6f, 0x72, 0x74, 0x42, 0x0c, 0x0a, 0x0a, 0x6b, 0x61, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x22, 0x73, 0x0a, + 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x29, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, + 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x34, 0x0a, 0x0a, + 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0x86, 0x03, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, + 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x3a, 0xcc, 0x01, 0xba, + 0x48, 0xc8, 0x01, 0x1a, 0xc5, 0x01, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, + 0x12, 0x52, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x20, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, + 0x62, 0x65, 0x20, 0x65, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x41, 0x50, 0x50, 0x45, 0x4e, 0x44, + 0x20, 0x6f, 0x72, 0x20, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x2c, 0x20, 0x77, 0x68, 0x65, + 0x6e, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x2e, 0x1a, 0x55, 0x28, 0x28, 0x21, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, + 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x29, 0x29, 0x20, 0x7c, 0x7c, 0x20, + 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, + 0x76, 0x69, 0x6f, 0x72, 0x20, 0x21, 0x3d, 0x20, 0x30, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, + 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0xa4, 0x01, 0x0a, 0x10, 0x4b, 0x61, + 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x21, + 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, + 0x64, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x1e, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, + 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x75, 0x72, 0x69, + 0x12, 0x19, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x42, 0x13, 0x0a, 0x0a, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, + 0x22, 0xee, 0x0e, 0x0a, 0x10, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x07, 0x6e, + 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x2e, 0x4e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6e, 0x65, 0x77, 0x4b, 0x65, + 0x79, 0x1a, 0xd8, 0x04, 0x0a, 0x06, 0x4e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x06, + 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, + 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0xa6, 0x01, 0x0a, + 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, + 0x74, 0x68, 0x6d, 0x42, 0x75, 0xba, 0x48, 0x72, 0xba, 0x01, 0x6f, 0x0a, 0x15, 0x6b, 0x65, 0x79, + 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, + 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, + 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, + 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, + 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x1a, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, + 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, + 0x2c, 0x20, 0x36, 0x2c, 0x20, 0x37, 0x2c, 0x20, 0x38, 0x5d, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, + 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x9e, 0x01, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x72, 0xba, 0x48, 0x6f, 0xba, 0x01, + 0x67, 0x0a, 0x14, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x5f, + 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x39, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, + 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, + 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x20, 0x28, 0x31, 0x2d, 0x34, + 0x29, 0x2e, 0x1a, 0x14, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, + 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x5d, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x07, 0x6b, + 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x43, 0x74, 0x78, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x3d, 0x0a, 0x0f, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, + 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0xcd, 0x08, 0xba, + 0x48, 0xc9, 0x08, 0x1a, 0xd8, 0x03, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, + 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xcd, 0x01, 0x46, 0x6f, + 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x79, 0x2c, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x69, + 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, + 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, + 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, + 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, + 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, + 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, + 0x79, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, + 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, + 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x20, 0x6f, + 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, + 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xe0, 0x01, 0x28, 0x28, + 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, + 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, + 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, - 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xf4, 0x02, 0x0a, - 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x72, - 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xa8, 0x01, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x20, 0x69, 0x64, 0x20, 0x69, 0x73, 0x20, - 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, - 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, - 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, - 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, - 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, - 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, - 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, - 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, - 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, - 0x59, 0x2e, 0x1a, 0x9e, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, - 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, - 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x3d, 0x3d, 0x20, 0x27, - 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, - 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, - 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, - 0x27, 0x27, 0x29, 0x1a, 0xa3, 0x01, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, - 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x48, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x20, 0x6d, 0x75, 0x73, - 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x20, 0x69, 0x66, 0x20, - 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, - 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, - 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0x32, 0x21, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, - 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x20, 0x26, 0x26, 0x20, 0x68, - 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, - 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, - 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, - 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0x7a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x42, 0x13, - 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, - 0x02, 0x08, 0x01, 0x22, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0x96, - 0x03, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0xa7, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, - 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x6f, 0xba, - 0x48, 0x6c, 0xba, 0x01, 0x69, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, - 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, - 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x2e, 0x1a, 0x1a, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x30, 0x2c, 0x20, - 0x31, 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x5d, 0x52, 0x0c, - 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x21, 0x0a, 0x06, - 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, - 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, - 0x24, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x61, - 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, - 0x01, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x1b, 0x0a, 0x06, - 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x06, - 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0c, - 0x0a, 0x0a, 0x6b, 0x61, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x09, 0x0a, 0x07, - 0x5f, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x22, 0x73, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, - 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x08, 0x6b, - 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, - 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x03, 0x0a, - 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, - 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, - 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, - 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x3a, 0xcc, 0x01, 0xba, 0x48, 0xc8, 0x01, 0x1a, 0xc5, 0x01, - 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x12, 0x52, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x20, 0x62, 0x65, 0x68, 0x61, - 0x76, 0x69, 0x6f, 0x72, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x69, 0x74, - 0x68, 0x65, 0x72, 0x20, 0x41, 0x50, 0x50, 0x45, 0x4e, 0x44, 0x20, 0x6f, 0x72, 0x20, 0x52, 0x45, - 0x50, 0x4c, 0x41, 0x43, 0x45, 0x2c, 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x1a, 0x55, - 0x28, 0x28, 0x21, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x29, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, - 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x29, 0x20, 0x26, 0x26, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x20, 0x21, - 0x3d, 0x20, 0x30, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, - 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, - 0x4b, 0x65, 0x79, 0x22, 0xa4, 0x01, 0x0a, 0x10, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, - 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, - 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, - 0x10, 0x01, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x03, 0x75, 0x72, - 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, - 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x19, 0x0a, 0x03, 0x6b, 0x69, - 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, - 0x52, 0x03, 0x6b, 0x69, 0x64, 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0xe5, 0x0e, 0x0a, 0x10, 0x52, - 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, - 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x07, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, - 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4e, 0x65, 0x77, - 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x1a, 0xcf, 0x04, 0x0a, 0x06, - 0x4e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, - 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x9d, 0x01, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, - 0x69, 0x74, 0x68, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x6c, 0xba, - 0x48, 0x69, 0xba, 0x01, 0x66, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, - 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, - 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x2e, 0x1a, 0x17, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, - 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x5d, 0x52, 0x09, 0x61, 0x6c, 0x67, - 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x9e, 0x01, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, - 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x72, 0xba, 0x48, 0x6f, 0xba, - 0x01, 0x67, 0x0a, 0x14, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, - 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x39, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x65, - 0x77, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, - 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, - 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x20, 0x28, 0x31, 0x2d, - 0x34, 0x29, 0x2e, 0x1a, 0x14, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, - 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x5d, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x07, - 0x6b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x43, 0x74, 0x78, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x3d, 0x0a, 0x0f, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, 0x0d, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0xcd, 0x08, - 0xba, 0x48, 0xc9, 0x08, 0x1a, 0xd8, 0x03, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xcd, 0x01, 0x46, - 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x79, 0x2c, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, + 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, + 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, + 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x20, 0x7c, 0x7c, 0x20, + 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, + 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, + 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, + 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xb5, + 0x03, 0x0a, 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, + 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xb9, 0x01, 0x46, 0x6f, 0x72, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x79, 0x2c, 0x20, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x20, 0x69, 0x64, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, - 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, - 0x4b, 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, - 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, - 0x59, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, - 0x65, 0x79, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, - 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, - 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x20, - 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, - 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xe0, 0x01, 0x28, - 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, - 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, - 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, - 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, 0x70, - 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, + 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, + 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, + 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x6d, 0x75, 0x73, + 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4b, + 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, + 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x4b, 0x45, 0x59, 0x5f, + 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, + 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xce, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, + 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, + 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, + 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, + 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, + 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, - 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x20, 0x7c, 0x7c, + 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, - 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, - 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, - 0xb5, 0x03, 0x0a, 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, - 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xb9, 0x01, 0x46, 0x6f, 0x72, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x79, 0x2c, 0x20, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x20, 0x69, 0x64, - 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, - 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, - 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, - 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, - 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x6d, 0x75, - 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x66, 0x6f, 0x72, 0x20, - 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, - 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x4b, 0x45, 0x59, - 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, - 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xce, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, - 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, - 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, - 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, - 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, - 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, - 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, - 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x20, 0x7c, - 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, - 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x29, 0x20, 0x26, 0x26, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, - 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xb3, 0x01, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x5f, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x12, - 0x48, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, - 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, - 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, - 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, - 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0x42, 0x21, 0x28, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, - 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x20, 0x26, 0x26, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, - 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x29, 0x29, 0x42, 0x13, 0x0a, - 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x05, 0xba, 0x48, 0x02, - 0x08, 0x01, 0x22, 0x32, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xe3, 0x02, 0x0a, 0x10, 0x52, 0x6f, 0x74, 0x61, 0x74, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x0f, 0x72, - 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x75, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, - 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x4f, 0x75, 0x74, - 0x4b, 0x65, 0x79, 0x12, 0x66, 0x0a, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x1b, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, 0x18, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0x73, 0x52, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x51, 0x0a, 0x12, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, - 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x11, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x8f, 0x01, 0x0a, - 0x11, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x29, 0x20, 0x26, 0x26, 0x20, + 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, + 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xb3, 0x01, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x48, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x20, + 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, + 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, + 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0x42, 0x21, 0x28, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, + 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x20, 0x26, 0x26, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x29, 0x29, 0x42, 0x13, 0x0a, 0x0a, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, + 0x01, 0x22, 0x32, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xe3, 0x02, 0x0a, 0x10, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x0f, 0x72, 0x6f, + 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x75, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, - 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x51, 0x0a, 0x11, 0x72, - 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, - 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x10, 0x72, 0x6f, - 0x74, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x7e, - 0x0a, 0x11, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x42, 0x13, 0x0a, 0x0a, 0x61, 0x63, 0x74, - 0x69, 0x76, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x13, - 0x0a, 0x11, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x45, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x62, 0x61, 0x73, - 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, - 0x79, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x8e, 0x01, 0x0a, 0x12, 0x53, - 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x36, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x6e, - 0x65, 0x77, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x40, 0x0a, 0x11, 0x70, 0x72, 0x65, - 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, - 0x69, 0x6f, 0x75, 0x73, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x36, 0x0a, 0x12, 0x4d, - 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x66, 0x71, 0x6e, 0x22, 0xb4, 0x02, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x55, 0x0a, - 0x12, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, - 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x52, 0x11, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x73, 0x12, 0x55, 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4d, 0x0a, 0x0e, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x05, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0d, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x16, 0x4c, - 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x0a, 0x70, - 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, - 0xba, 0x48, 0x02, 0x08, 0x00, 0x22, 0x92, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, - 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x65, 0x79, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x6b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, - 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xb5, 0x0c, 0x0a, 0x1e, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x99, 0x01, - 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x15, 0x12, 0x13, 0x2f, 0x6b, 0x65, 0x79, 0x2d, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x2d, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x90, 0x02, 0x01, 0x12, 0x78, 0x0a, 0x12, 0x47, 0x65, 0x74, - 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, - 0x2d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, - 0x90, 0x02, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, + 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x4f, 0x75, 0x74, 0x4b, + 0x65, 0x79, 0x12, 0x66, 0x0a, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, + 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x1b, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, 0x18, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, + 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, + 0x52, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x51, 0x0a, 0x12, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, + 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x11, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x8f, 0x01, 0x0a, 0x11, + 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, + 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x51, 0x0a, 0x11, 0x72, 0x6f, + 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, + 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x10, 0x72, 0x6f, 0x74, + 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x7e, 0x0a, + 0x11, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, + 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x42, 0x13, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x13, 0x0a, + 0x11, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x45, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, + 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x8e, 0x01, 0x0a, 0x12, 0x53, 0x65, + 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x36, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x6e, 0x65, + 0x77, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x40, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, + 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, 0x69, + 0x6f, 0x75, 0x73, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x36, 0x0a, 0x12, 0x4d, 0x61, + 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, + 0x71, 0x6e, 0x22, 0xb4, 0x02, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x55, 0x0a, 0x12, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, + 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x52, 0x11, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x55, 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4d, 0x0a, 0x0e, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0d, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x16, 0x4c, 0x69, + 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, + 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, + 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, + 0x48, 0x02, 0x08, 0x00, 0x22, 0x92, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x41, 0x0a, 0x0c, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x6b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, + 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0xef, 0x01, 0x0a, 0x18, 0x53, 0x6f, + 0x72, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x28, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, + 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, + 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, + 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x01, 0x12, 0x24, 0x0a, 0x20, 0x53, + 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, + 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x52, 0x49, 0x10, + 0x02, 0x12, 0x2b, 0x0a, 0x27, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, + 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x12, 0x2b, + 0x0a, 0x27, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, + 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x04, 0x2a, 0x9a, 0x01, 0x0a, 0x0f, + 0x53, 0x6f, 0x72, 0x74, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x22, 0x0a, 0x1e, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x53, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, + 0x4b, 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x49, 0x44, + 0x10, 0x01, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, 0x4b, + 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, + 0x5f, 0x41, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, + 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, + 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x32, 0x99, 0x0c, 0x0a, 0x1e, 0x4b, 0x65, 0x79, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, 0x0a, 0x14, 0x4c, + 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, + 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x78, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x12, 0x2d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, - 0x73, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, - 0x88, 0x02, 0x01, 0x90, 0x02, 0x01, 0x12, 0x5a, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x51, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x08, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, - 0x73, 0x12, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, - 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x09, 0x52, 0x6f, - 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, - 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5d, 0x0a, 0x0a, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, - 0x65, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, - 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, - 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x6f, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, + 0x6e, 0x74, 0x73, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x06, 0x88, 0x02, 0x01, 0x90, 0x02, 0x01, 0x12, 0x5a, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, + 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, - 0x2e, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5d, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, - 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, - 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x6c, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x08, 0x4c, 0x69, 0x73, 0x74, 0x4b, + 0x65, 0x79, 0x73, 0x12, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x42, 0xdb, 0x01, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x42, 0x1c, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, - 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6b, 0x61, - 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x4b, 0x58, 0xaa, - 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0xca, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4b, 0x61, - 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xe2, 0x02, 0x1e, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x5c, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x5c, 0x47, - 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x5a, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x09, + 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, + 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5d, 0x0a, 0x0a, 0x53, 0x65, 0x74, 0x42, + 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x74, 0x42, + 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x2e, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5d, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, + 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, + 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, + 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6c, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, + 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, + 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, + 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x42, 0xdb, 0x01, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x42, + 0x1c, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, + 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, + 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x4b, + 0x58, 0xaa, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xca, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, + 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xe2, 0x02, 0x1e, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, + 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -4549,191 +4842,203 @@ func file_policy_kasregistry_key_access_server_registry_proto_rawDescGZIP() []by return file_policy_kasregistry_key_access_server_registry_proto_rawDescData } -var file_policy_kasregistry_key_access_server_registry_proto_msgTypes = make([]protoimpl.MessageInfo, 53) +var file_policy_kasregistry_key_access_server_registry_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_policy_kasregistry_key_access_server_registry_proto_msgTypes = make([]protoimpl.MessageInfo, 55) var file_policy_kasregistry_key_access_server_registry_proto_goTypes = []interface{}{ - (*GetKeyAccessServerRequest)(nil), // 0: policy.kasregistry.GetKeyAccessServerRequest - (*GetKeyAccessServerResponse)(nil), // 1: policy.kasregistry.GetKeyAccessServerResponse - (*ListKeyAccessServersRequest)(nil), // 2: policy.kasregistry.ListKeyAccessServersRequest - (*ListKeyAccessServersResponse)(nil), // 3: policy.kasregistry.ListKeyAccessServersResponse - (*CreateKeyAccessServerRequest)(nil), // 4: policy.kasregistry.CreateKeyAccessServerRequest - (*CreateKeyAccessServerResponse)(nil), // 5: policy.kasregistry.CreateKeyAccessServerResponse - (*UpdateKeyAccessServerRequest)(nil), // 6: policy.kasregistry.UpdateKeyAccessServerRequest - (*UpdateKeyAccessServerResponse)(nil), // 7: policy.kasregistry.UpdateKeyAccessServerResponse - (*DeleteKeyAccessServerRequest)(nil), // 8: policy.kasregistry.DeleteKeyAccessServerRequest - (*DeleteKeyAccessServerResponse)(nil), // 9: policy.kasregistry.DeleteKeyAccessServerResponse - (*GrantedPolicyObject)(nil), // 10: policy.kasregistry.GrantedPolicyObject - (*KeyAccessServerGrants)(nil), // 11: policy.kasregistry.KeyAccessServerGrants - (*CreatePublicKeyRequest)(nil), // 12: policy.kasregistry.CreatePublicKeyRequest - (*CreatePublicKeyResponse)(nil), // 13: policy.kasregistry.CreatePublicKeyResponse - (*GetPublicKeyRequest)(nil), // 14: policy.kasregistry.GetPublicKeyRequest - (*GetPublicKeyResponse)(nil), // 15: policy.kasregistry.GetPublicKeyResponse - (*ListPublicKeysRequest)(nil), // 16: policy.kasregistry.ListPublicKeysRequest - (*ListPublicKeysResponse)(nil), // 17: policy.kasregistry.ListPublicKeysResponse - (*ListPublicKeyMappingRequest)(nil), // 18: policy.kasregistry.ListPublicKeyMappingRequest - (*ListPublicKeyMappingResponse)(nil), // 19: policy.kasregistry.ListPublicKeyMappingResponse - (*UpdatePublicKeyRequest)(nil), // 20: policy.kasregistry.UpdatePublicKeyRequest - (*UpdatePublicKeyResponse)(nil), // 21: policy.kasregistry.UpdatePublicKeyResponse - (*DeactivatePublicKeyRequest)(nil), // 22: policy.kasregistry.DeactivatePublicKeyRequest - (*DeactivatePublicKeyResponse)(nil), // 23: policy.kasregistry.DeactivatePublicKeyResponse - (*ActivatePublicKeyRequest)(nil), // 24: policy.kasregistry.ActivatePublicKeyRequest - (*ActivatePublicKeyResponse)(nil), // 25: policy.kasregistry.ActivatePublicKeyResponse - (*ListKeyAccessServerGrantsRequest)(nil), // 26: policy.kasregistry.ListKeyAccessServerGrantsRequest - (*ListKeyAccessServerGrantsResponse)(nil), // 27: policy.kasregistry.ListKeyAccessServerGrantsResponse - (*CreateKeyRequest)(nil), // 28: policy.kasregistry.CreateKeyRequest - (*CreateKeyResponse)(nil), // 29: policy.kasregistry.CreateKeyResponse - (*GetKeyRequest)(nil), // 30: policy.kasregistry.GetKeyRequest - (*GetKeyResponse)(nil), // 31: policy.kasregistry.GetKeyResponse - (*ListKeysRequest)(nil), // 32: policy.kasregistry.ListKeysRequest - (*ListKeysResponse)(nil), // 33: policy.kasregistry.ListKeysResponse - (*UpdateKeyRequest)(nil), // 34: policy.kasregistry.UpdateKeyRequest - (*UpdateKeyResponse)(nil), // 35: policy.kasregistry.UpdateKeyResponse - (*KasKeyIdentifier)(nil), // 36: policy.kasregistry.KasKeyIdentifier - (*RotateKeyRequest)(nil), // 37: policy.kasregistry.RotateKeyRequest - (*ChangeMappings)(nil), // 38: policy.kasregistry.ChangeMappings - (*RotatedResources)(nil), // 39: policy.kasregistry.RotatedResources - (*RotateKeyResponse)(nil), // 40: policy.kasregistry.RotateKeyResponse - (*SetBaseKeyRequest)(nil), // 41: policy.kasregistry.SetBaseKeyRequest - (*GetBaseKeyRequest)(nil), // 42: policy.kasregistry.GetBaseKeyRequest - (*GetBaseKeyResponse)(nil), // 43: policy.kasregistry.GetBaseKeyResponse - (*SetBaseKeyResponse)(nil), // 44: policy.kasregistry.SetBaseKeyResponse - (*MappedPolicyObject)(nil), // 45: policy.kasregistry.MappedPolicyObject - (*KeyMapping)(nil), // 46: policy.kasregistry.KeyMapping - (*ListKeyMappingsRequest)(nil), // 47: policy.kasregistry.ListKeyMappingsRequest - (*ListKeyMappingsResponse)(nil), // 48: policy.kasregistry.ListKeyMappingsResponse - (*ListPublicKeyMappingResponse_PublicKeyMapping)(nil), // 49: policy.kasregistry.ListPublicKeyMappingResponse.PublicKeyMapping - (*ListPublicKeyMappingResponse_PublicKey)(nil), // 50: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey - (*ListPublicKeyMappingResponse_Association)(nil), // 51: policy.kasregistry.ListPublicKeyMappingResponse.Association - (*RotateKeyRequest_NewKey)(nil), // 52: policy.kasregistry.RotateKeyRequest.NewKey - (*policy.KeyAccessServer)(nil), // 53: policy.KeyAccessServer - (*policy.PageRequest)(nil), // 54: policy.PageRequest - (*policy.PageResponse)(nil), // 55: policy.PageResponse - (*policy.PublicKey)(nil), // 56: policy.PublicKey - (policy.SourceType)(0), // 57: policy.SourceType - (*common.MetadataMutable)(nil), // 58: common.MetadataMutable - (common.MetadataUpdateEnum)(0), // 59: common.MetadataUpdateEnum - (*policy.KasPublicKey)(nil), // 60: policy.KasPublicKey - (*policy.Key)(nil), // 61: policy.Key - (policy.Algorithm)(0), // 62: policy.Algorithm - (policy.KeyMode)(0), // 63: policy.KeyMode - (*policy.PublicKeyCtx)(nil), // 64: policy.PublicKeyCtx - (*policy.PrivateKeyCtx)(nil), // 65: policy.PrivateKeyCtx - (*policy.KasKey)(nil), // 66: policy.KasKey - (*policy.SimpleKasKey)(nil), // 67: policy.SimpleKasKey + (SortKeyAccessServersType)(0), // 0: policy.kasregistry.SortKeyAccessServersType + (SortKasKeysType)(0), // 1: policy.kasregistry.SortKasKeysType + (*GetKeyAccessServerRequest)(nil), // 2: policy.kasregistry.GetKeyAccessServerRequest + (*GetKeyAccessServerResponse)(nil), // 3: policy.kasregistry.GetKeyAccessServerResponse + (*KeyAccessServersSort)(nil), // 4: policy.kasregistry.KeyAccessServersSort + (*ListKeyAccessServersRequest)(nil), // 5: policy.kasregistry.ListKeyAccessServersRequest + (*ListKeyAccessServersResponse)(nil), // 6: policy.kasregistry.ListKeyAccessServersResponse + (*KasKeysSort)(nil), // 7: policy.kasregistry.KasKeysSort + (*CreateKeyAccessServerRequest)(nil), // 8: policy.kasregistry.CreateKeyAccessServerRequest + (*CreateKeyAccessServerResponse)(nil), // 9: policy.kasregistry.CreateKeyAccessServerResponse + (*UpdateKeyAccessServerRequest)(nil), // 10: policy.kasregistry.UpdateKeyAccessServerRequest + (*UpdateKeyAccessServerResponse)(nil), // 11: policy.kasregistry.UpdateKeyAccessServerResponse + (*DeleteKeyAccessServerRequest)(nil), // 12: policy.kasregistry.DeleteKeyAccessServerRequest + (*DeleteKeyAccessServerResponse)(nil), // 13: policy.kasregistry.DeleteKeyAccessServerResponse + (*GrantedPolicyObject)(nil), // 14: policy.kasregistry.GrantedPolicyObject + (*KeyAccessServerGrants)(nil), // 15: policy.kasregistry.KeyAccessServerGrants + (*CreatePublicKeyRequest)(nil), // 16: policy.kasregistry.CreatePublicKeyRequest + (*CreatePublicKeyResponse)(nil), // 17: policy.kasregistry.CreatePublicKeyResponse + (*GetPublicKeyRequest)(nil), // 18: policy.kasregistry.GetPublicKeyRequest + (*GetPublicKeyResponse)(nil), // 19: policy.kasregistry.GetPublicKeyResponse + (*ListPublicKeysRequest)(nil), // 20: policy.kasregistry.ListPublicKeysRequest + (*ListPublicKeysResponse)(nil), // 21: policy.kasregistry.ListPublicKeysResponse + (*ListPublicKeyMappingRequest)(nil), // 22: policy.kasregistry.ListPublicKeyMappingRequest + (*ListPublicKeyMappingResponse)(nil), // 23: policy.kasregistry.ListPublicKeyMappingResponse + (*UpdatePublicKeyRequest)(nil), // 24: policy.kasregistry.UpdatePublicKeyRequest + (*UpdatePublicKeyResponse)(nil), // 25: policy.kasregistry.UpdatePublicKeyResponse + (*DeactivatePublicKeyRequest)(nil), // 26: policy.kasregistry.DeactivatePublicKeyRequest + (*DeactivatePublicKeyResponse)(nil), // 27: policy.kasregistry.DeactivatePublicKeyResponse + (*ActivatePublicKeyRequest)(nil), // 28: policy.kasregistry.ActivatePublicKeyRequest + (*ActivatePublicKeyResponse)(nil), // 29: policy.kasregistry.ActivatePublicKeyResponse + (*ListKeyAccessServerGrantsRequest)(nil), // 30: policy.kasregistry.ListKeyAccessServerGrantsRequest + (*ListKeyAccessServerGrantsResponse)(nil), // 31: policy.kasregistry.ListKeyAccessServerGrantsResponse + (*CreateKeyRequest)(nil), // 32: policy.kasregistry.CreateKeyRequest + (*CreateKeyResponse)(nil), // 33: policy.kasregistry.CreateKeyResponse + (*GetKeyRequest)(nil), // 34: policy.kasregistry.GetKeyRequest + (*GetKeyResponse)(nil), // 35: policy.kasregistry.GetKeyResponse + (*ListKeysRequest)(nil), // 36: policy.kasregistry.ListKeysRequest + (*ListKeysResponse)(nil), // 37: policy.kasregistry.ListKeysResponse + (*UpdateKeyRequest)(nil), // 38: policy.kasregistry.UpdateKeyRequest + (*UpdateKeyResponse)(nil), // 39: policy.kasregistry.UpdateKeyResponse + (*KasKeyIdentifier)(nil), // 40: policy.kasregistry.KasKeyIdentifier + (*RotateKeyRequest)(nil), // 41: policy.kasregistry.RotateKeyRequest + (*ChangeMappings)(nil), // 42: policy.kasregistry.ChangeMappings + (*RotatedResources)(nil), // 43: policy.kasregistry.RotatedResources + (*RotateKeyResponse)(nil), // 44: policy.kasregistry.RotateKeyResponse + (*SetBaseKeyRequest)(nil), // 45: policy.kasregistry.SetBaseKeyRequest + (*GetBaseKeyRequest)(nil), // 46: policy.kasregistry.GetBaseKeyRequest + (*GetBaseKeyResponse)(nil), // 47: policy.kasregistry.GetBaseKeyResponse + (*SetBaseKeyResponse)(nil), // 48: policy.kasregistry.SetBaseKeyResponse + (*MappedPolicyObject)(nil), // 49: policy.kasregistry.MappedPolicyObject + (*KeyMapping)(nil), // 50: policy.kasregistry.KeyMapping + (*ListKeyMappingsRequest)(nil), // 51: policy.kasregistry.ListKeyMappingsRequest + (*ListKeyMappingsResponse)(nil), // 52: policy.kasregistry.ListKeyMappingsResponse + (*ListPublicKeyMappingResponse_PublicKeyMapping)(nil), // 53: policy.kasregistry.ListPublicKeyMappingResponse.PublicKeyMapping + (*ListPublicKeyMappingResponse_PublicKey)(nil), // 54: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey + (*ListPublicKeyMappingResponse_Association)(nil), // 55: policy.kasregistry.ListPublicKeyMappingResponse.Association + (*RotateKeyRequest_NewKey)(nil), // 56: policy.kasregistry.RotateKeyRequest.NewKey + (*policy.KeyAccessServer)(nil), // 57: policy.KeyAccessServer + (policy.SortDirection)(0), // 58: policy.SortDirection + (*policy.PageRequest)(nil), // 59: policy.PageRequest + (*policy.PageResponse)(nil), // 60: policy.PageResponse + (*policy.PublicKey)(nil), // 61: policy.PublicKey + (policy.SourceType)(0), // 62: policy.SourceType + (*common.MetadataMutable)(nil), // 63: common.MetadataMutable + (common.MetadataUpdateEnum)(0), // 64: common.MetadataUpdateEnum + (*policy.KasPublicKey)(nil), // 65: policy.KasPublicKey + (*policy.Key)(nil), // 66: policy.Key + (policy.Algorithm)(0), // 67: policy.Algorithm + (policy.KeyMode)(0), // 68: policy.KeyMode + (*policy.PublicKeyCtx)(nil), // 69: policy.PublicKeyCtx + (*policy.PrivateKeyCtx)(nil), // 70: policy.PrivateKeyCtx + (*policy.KasKey)(nil), // 71: policy.KasKey + (*policy.SimpleKasKey)(nil), // 72: policy.SimpleKasKey } var file_policy_kasregistry_key_access_server_registry_proto_depIdxs = []int32{ - 53, // 0: policy.kasregistry.GetKeyAccessServerResponse.key_access_server:type_name -> policy.KeyAccessServer - 54, // 1: policy.kasregistry.ListKeyAccessServersRequest.pagination:type_name -> policy.PageRequest - 53, // 2: policy.kasregistry.ListKeyAccessServersResponse.key_access_servers:type_name -> policy.KeyAccessServer - 55, // 3: policy.kasregistry.ListKeyAccessServersResponse.pagination:type_name -> policy.PageResponse - 56, // 4: policy.kasregistry.CreateKeyAccessServerRequest.public_key:type_name -> policy.PublicKey - 57, // 5: policy.kasregistry.CreateKeyAccessServerRequest.source_type:type_name -> policy.SourceType - 58, // 6: policy.kasregistry.CreateKeyAccessServerRequest.metadata:type_name -> common.MetadataMutable - 53, // 7: policy.kasregistry.CreateKeyAccessServerResponse.key_access_server:type_name -> policy.KeyAccessServer - 56, // 8: policy.kasregistry.UpdateKeyAccessServerRequest.public_key:type_name -> policy.PublicKey - 57, // 9: policy.kasregistry.UpdateKeyAccessServerRequest.source_type:type_name -> policy.SourceType - 58, // 10: policy.kasregistry.UpdateKeyAccessServerRequest.metadata:type_name -> common.MetadataMutable - 59, // 11: policy.kasregistry.UpdateKeyAccessServerRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 53, // 12: policy.kasregistry.UpdateKeyAccessServerResponse.key_access_server:type_name -> policy.KeyAccessServer - 53, // 13: policy.kasregistry.DeleteKeyAccessServerResponse.key_access_server:type_name -> policy.KeyAccessServer - 53, // 14: policy.kasregistry.KeyAccessServerGrants.key_access_server:type_name -> policy.KeyAccessServer - 10, // 15: policy.kasregistry.KeyAccessServerGrants.namespace_grants:type_name -> policy.kasregistry.GrantedPolicyObject - 10, // 16: policy.kasregistry.KeyAccessServerGrants.attribute_grants:type_name -> policy.kasregistry.GrantedPolicyObject - 10, // 17: policy.kasregistry.KeyAccessServerGrants.value_grants:type_name -> policy.kasregistry.GrantedPolicyObject - 60, // 18: policy.kasregistry.CreatePublicKeyRequest.key:type_name -> policy.KasPublicKey - 58, // 19: policy.kasregistry.CreatePublicKeyRequest.metadata:type_name -> common.MetadataMutable - 61, // 20: policy.kasregistry.CreatePublicKeyResponse.key:type_name -> policy.Key - 61, // 21: policy.kasregistry.GetPublicKeyResponse.key:type_name -> policy.Key - 54, // 22: policy.kasregistry.ListPublicKeysRequest.pagination:type_name -> policy.PageRequest - 61, // 23: policy.kasregistry.ListPublicKeysResponse.keys:type_name -> policy.Key - 55, // 24: policy.kasregistry.ListPublicKeysResponse.pagination:type_name -> policy.PageResponse - 54, // 25: policy.kasregistry.ListPublicKeyMappingRequest.pagination:type_name -> policy.PageRequest - 49, // 26: policy.kasregistry.ListPublicKeyMappingResponse.public_key_mappings:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.PublicKeyMapping - 55, // 27: policy.kasregistry.ListPublicKeyMappingResponse.pagination:type_name -> policy.PageResponse - 58, // 28: policy.kasregistry.UpdatePublicKeyRequest.metadata:type_name -> common.MetadataMutable - 59, // 29: policy.kasregistry.UpdatePublicKeyRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 61, // 30: policy.kasregistry.UpdatePublicKeyResponse.key:type_name -> policy.Key - 61, // 31: policy.kasregistry.DeactivatePublicKeyResponse.key:type_name -> policy.Key - 61, // 32: policy.kasregistry.ActivatePublicKeyResponse.key:type_name -> policy.Key - 54, // 33: policy.kasregistry.ListKeyAccessServerGrantsRequest.pagination:type_name -> policy.PageRequest - 11, // 34: policy.kasregistry.ListKeyAccessServerGrantsResponse.grants:type_name -> policy.kasregistry.KeyAccessServerGrants - 55, // 35: policy.kasregistry.ListKeyAccessServerGrantsResponse.pagination:type_name -> policy.PageResponse - 62, // 36: policy.kasregistry.CreateKeyRequest.key_algorithm:type_name -> policy.Algorithm - 63, // 37: policy.kasregistry.CreateKeyRequest.key_mode:type_name -> policy.KeyMode - 64, // 38: policy.kasregistry.CreateKeyRequest.public_key_ctx:type_name -> policy.PublicKeyCtx - 65, // 39: policy.kasregistry.CreateKeyRequest.private_key_ctx:type_name -> policy.PrivateKeyCtx - 58, // 40: policy.kasregistry.CreateKeyRequest.metadata:type_name -> common.MetadataMutable - 66, // 41: policy.kasregistry.CreateKeyResponse.kas_key:type_name -> policy.KasKey - 36, // 42: policy.kasregistry.GetKeyRequest.key:type_name -> policy.kasregistry.KasKeyIdentifier - 66, // 43: policy.kasregistry.GetKeyResponse.kas_key:type_name -> policy.KasKey - 62, // 44: policy.kasregistry.ListKeysRequest.key_algorithm:type_name -> policy.Algorithm - 54, // 45: policy.kasregistry.ListKeysRequest.pagination:type_name -> policy.PageRequest - 66, // 46: policy.kasregistry.ListKeysResponse.kas_keys:type_name -> policy.KasKey - 55, // 47: policy.kasregistry.ListKeysResponse.pagination:type_name -> policy.PageResponse - 58, // 48: policy.kasregistry.UpdateKeyRequest.metadata:type_name -> common.MetadataMutable - 59, // 49: policy.kasregistry.UpdateKeyRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 66, // 50: policy.kasregistry.UpdateKeyResponse.kas_key:type_name -> policy.KasKey - 36, // 51: policy.kasregistry.RotateKeyRequest.key:type_name -> policy.kasregistry.KasKeyIdentifier - 52, // 52: policy.kasregistry.RotateKeyRequest.new_key:type_name -> policy.kasregistry.RotateKeyRequest.NewKey - 66, // 53: policy.kasregistry.RotatedResources.rotated_out_key:type_name -> policy.KasKey - 38, // 54: policy.kasregistry.RotatedResources.attribute_definition_mappings:type_name -> policy.kasregistry.ChangeMappings - 38, // 55: policy.kasregistry.RotatedResources.attribute_value_mappings:type_name -> policy.kasregistry.ChangeMappings - 38, // 56: policy.kasregistry.RotatedResources.namespace_mappings:type_name -> policy.kasregistry.ChangeMappings - 66, // 57: policy.kasregistry.RotateKeyResponse.kas_key:type_name -> policy.KasKey - 39, // 58: policy.kasregistry.RotateKeyResponse.rotated_resources:type_name -> policy.kasregistry.RotatedResources - 36, // 59: policy.kasregistry.SetBaseKeyRequest.key:type_name -> policy.kasregistry.KasKeyIdentifier - 67, // 60: policy.kasregistry.GetBaseKeyResponse.base_key:type_name -> policy.SimpleKasKey - 67, // 61: policy.kasregistry.SetBaseKeyResponse.new_base_key:type_name -> policy.SimpleKasKey - 67, // 62: policy.kasregistry.SetBaseKeyResponse.previous_base_key:type_name -> policy.SimpleKasKey - 45, // 63: policy.kasregistry.KeyMapping.namespace_mappings:type_name -> policy.kasregistry.MappedPolicyObject - 45, // 64: policy.kasregistry.KeyMapping.attribute_mappings:type_name -> policy.kasregistry.MappedPolicyObject - 45, // 65: policy.kasregistry.KeyMapping.value_mappings:type_name -> policy.kasregistry.MappedPolicyObject - 36, // 66: policy.kasregistry.ListKeyMappingsRequest.key:type_name -> policy.kasregistry.KasKeyIdentifier - 54, // 67: policy.kasregistry.ListKeyMappingsRequest.pagination:type_name -> policy.PageRequest - 46, // 68: policy.kasregistry.ListKeyMappingsResponse.key_mappings:type_name -> policy.kasregistry.KeyMapping - 55, // 69: policy.kasregistry.ListKeyMappingsResponse.pagination:type_name -> policy.PageResponse - 50, // 70: policy.kasregistry.ListPublicKeyMappingResponse.PublicKeyMapping.public_keys:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.PublicKey - 61, // 71: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey.key:type_name -> policy.Key - 51, // 72: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey.values:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.Association - 51, // 73: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey.definitions:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.Association - 51, // 74: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey.namespaces:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.Association - 62, // 75: policy.kasregistry.RotateKeyRequest.NewKey.algorithm:type_name -> policy.Algorithm - 63, // 76: policy.kasregistry.RotateKeyRequest.NewKey.key_mode:type_name -> policy.KeyMode - 64, // 77: policy.kasregistry.RotateKeyRequest.NewKey.public_key_ctx:type_name -> policy.PublicKeyCtx - 65, // 78: policy.kasregistry.RotateKeyRequest.NewKey.private_key_ctx:type_name -> policy.PrivateKeyCtx - 58, // 79: policy.kasregistry.RotateKeyRequest.NewKey.metadata:type_name -> common.MetadataMutable - 2, // 80: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServers:input_type -> policy.kasregistry.ListKeyAccessServersRequest - 0, // 81: policy.kasregistry.KeyAccessServerRegistryService.GetKeyAccessServer:input_type -> policy.kasregistry.GetKeyAccessServerRequest - 4, // 82: policy.kasregistry.KeyAccessServerRegistryService.CreateKeyAccessServer:input_type -> policy.kasregistry.CreateKeyAccessServerRequest - 6, // 83: policy.kasregistry.KeyAccessServerRegistryService.UpdateKeyAccessServer:input_type -> policy.kasregistry.UpdateKeyAccessServerRequest - 8, // 84: policy.kasregistry.KeyAccessServerRegistryService.DeleteKeyAccessServer:input_type -> policy.kasregistry.DeleteKeyAccessServerRequest - 26, // 85: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServerGrants:input_type -> policy.kasregistry.ListKeyAccessServerGrantsRequest - 28, // 86: policy.kasregistry.KeyAccessServerRegistryService.CreateKey:input_type -> policy.kasregistry.CreateKeyRequest - 30, // 87: policy.kasregistry.KeyAccessServerRegistryService.GetKey:input_type -> policy.kasregistry.GetKeyRequest - 32, // 88: policy.kasregistry.KeyAccessServerRegistryService.ListKeys:input_type -> policy.kasregistry.ListKeysRequest - 34, // 89: policy.kasregistry.KeyAccessServerRegistryService.UpdateKey:input_type -> policy.kasregistry.UpdateKeyRequest - 37, // 90: policy.kasregistry.KeyAccessServerRegistryService.RotateKey:input_type -> policy.kasregistry.RotateKeyRequest - 41, // 91: policy.kasregistry.KeyAccessServerRegistryService.SetBaseKey:input_type -> policy.kasregistry.SetBaseKeyRequest - 42, // 92: policy.kasregistry.KeyAccessServerRegistryService.GetBaseKey:input_type -> policy.kasregistry.GetBaseKeyRequest - 47, // 93: policy.kasregistry.KeyAccessServerRegistryService.ListKeyMappings:input_type -> policy.kasregistry.ListKeyMappingsRequest - 3, // 94: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServers:output_type -> policy.kasregistry.ListKeyAccessServersResponse - 1, // 95: policy.kasregistry.KeyAccessServerRegistryService.GetKeyAccessServer:output_type -> policy.kasregistry.GetKeyAccessServerResponse - 5, // 96: policy.kasregistry.KeyAccessServerRegistryService.CreateKeyAccessServer:output_type -> policy.kasregistry.CreateKeyAccessServerResponse - 7, // 97: policy.kasregistry.KeyAccessServerRegistryService.UpdateKeyAccessServer:output_type -> policy.kasregistry.UpdateKeyAccessServerResponse - 9, // 98: policy.kasregistry.KeyAccessServerRegistryService.DeleteKeyAccessServer:output_type -> policy.kasregistry.DeleteKeyAccessServerResponse - 27, // 99: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServerGrants:output_type -> policy.kasregistry.ListKeyAccessServerGrantsResponse - 29, // 100: policy.kasregistry.KeyAccessServerRegistryService.CreateKey:output_type -> policy.kasregistry.CreateKeyResponse - 31, // 101: policy.kasregistry.KeyAccessServerRegistryService.GetKey:output_type -> policy.kasregistry.GetKeyResponse - 33, // 102: policy.kasregistry.KeyAccessServerRegistryService.ListKeys:output_type -> policy.kasregistry.ListKeysResponse - 35, // 103: policy.kasregistry.KeyAccessServerRegistryService.UpdateKey:output_type -> policy.kasregistry.UpdateKeyResponse - 40, // 104: policy.kasregistry.KeyAccessServerRegistryService.RotateKey:output_type -> policy.kasregistry.RotateKeyResponse - 44, // 105: policy.kasregistry.KeyAccessServerRegistryService.SetBaseKey:output_type -> policy.kasregistry.SetBaseKeyResponse - 43, // 106: policy.kasregistry.KeyAccessServerRegistryService.GetBaseKey:output_type -> policy.kasregistry.GetBaseKeyResponse - 48, // 107: policy.kasregistry.KeyAccessServerRegistryService.ListKeyMappings:output_type -> policy.kasregistry.ListKeyMappingsResponse - 94, // [94:108] is the sub-list for method output_type - 80, // [80:94] is the sub-list for method input_type - 80, // [80:80] is the sub-list for extension type_name - 80, // [80:80] is the sub-list for extension extendee - 0, // [0:80] is the sub-list for field type_name + 57, // 0: policy.kasregistry.GetKeyAccessServerResponse.key_access_server:type_name -> policy.KeyAccessServer + 0, // 1: policy.kasregistry.KeyAccessServersSort.field:type_name -> policy.kasregistry.SortKeyAccessServersType + 58, // 2: policy.kasregistry.KeyAccessServersSort.direction:type_name -> policy.SortDirection + 59, // 3: policy.kasregistry.ListKeyAccessServersRequest.pagination:type_name -> policy.PageRequest + 4, // 4: policy.kasregistry.ListKeyAccessServersRequest.sort:type_name -> policy.kasregistry.KeyAccessServersSort + 57, // 5: policy.kasregistry.ListKeyAccessServersResponse.key_access_servers:type_name -> policy.KeyAccessServer + 60, // 6: policy.kasregistry.ListKeyAccessServersResponse.pagination:type_name -> policy.PageResponse + 1, // 7: policy.kasregistry.KasKeysSort.field:type_name -> policy.kasregistry.SortKasKeysType + 58, // 8: policy.kasregistry.KasKeysSort.direction:type_name -> policy.SortDirection + 61, // 9: policy.kasregistry.CreateKeyAccessServerRequest.public_key:type_name -> policy.PublicKey + 62, // 10: policy.kasregistry.CreateKeyAccessServerRequest.source_type:type_name -> policy.SourceType + 63, // 11: policy.kasregistry.CreateKeyAccessServerRequest.metadata:type_name -> common.MetadataMutable + 57, // 12: policy.kasregistry.CreateKeyAccessServerResponse.key_access_server:type_name -> policy.KeyAccessServer + 61, // 13: policy.kasregistry.UpdateKeyAccessServerRequest.public_key:type_name -> policy.PublicKey + 62, // 14: policy.kasregistry.UpdateKeyAccessServerRequest.source_type:type_name -> policy.SourceType + 63, // 15: policy.kasregistry.UpdateKeyAccessServerRequest.metadata:type_name -> common.MetadataMutable + 64, // 16: policy.kasregistry.UpdateKeyAccessServerRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 57, // 17: policy.kasregistry.UpdateKeyAccessServerResponse.key_access_server:type_name -> policy.KeyAccessServer + 57, // 18: policy.kasregistry.DeleteKeyAccessServerResponse.key_access_server:type_name -> policy.KeyAccessServer + 57, // 19: policy.kasregistry.KeyAccessServerGrants.key_access_server:type_name -> policy.KeyAccessServer + 14, // 20: policy.kasregistry.KeyAccessServerGrants.namespace_grants:type_name -> policy.kasregistry.GrantedPolicyObject + 14, // 21: policy.kasregistry.KeyAccessServerGrants.attribute_grants:type_name -> policy.kasregistry.GrantedPolicyObject + 14, // 22: policy.kasregistry.KeyAccessServerGrants.value_grants:type_name -> policy.kasregistry.GrantedPolicyObject + 65, // 23: policy.kasregistry.CreatePublicKeyRequest.key:type_name -> policy.KasPublicKey + 63, // 24: policy.kasregistry.CreatePublicKeyRequest.metadata:type_name -> common.MetadataMutable + 66, // 25: policy.kasregistry.CreatePublicKeyResponse.key:type_name -> policy.Key + 66, // 26: policy.kasregistry.GetPublicKeyResponse.key:type_name -> policy.Key + 59, // 27: policy.kasregistry.ListPublicKeysRequest.pagination:type_name -> policy.PageRequest + 66, // 28: policy.kasregistry.ListPublicKeysResponse.keys:type_name -> policy.Key + 60, // 29: policy.kasregistry.ListPublicKeysResponse.pagination:type_name -> policy.PageResponse + 59, // 30: policy.kasregistry.ListPublicKeyMappingRequest.pagination:type_name -> policy.PageRequest + 53, // 31: policy.kasregistry.ListPublicKeyMappingResponse.public_key_mappings:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.PublicKeyMapping + 60, // 32: policy.kasregistry.ListPublicKeyMappingResponse.pagination:type_name -> policy.PageResponse + 63, // 33: policy.kasregistry.UpdatePublicKeyRequest.metadata:type_name -> common.MetadataMutable + 64, // 34: policy.kasregistry.UpdatePublicKeyRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 66, // 35: policy.kasregistry.UpdatePublicKeyResponse.key:type_name -> policy.Key + 66, // 36: policy.kasregistry.DeactivatePublicKeyResponse.key:type_name -> policy.Key + 66, // 37: policy.kasregistry.ActivatePublicKeyResponse.key:type_name -> policy.Key + 59, // 38: policy.kasregistry.ListKeyAccessServerGrantsRequest.pagination:type_name -> policy.PageRequest + 15, // 39: policy.kasregistry.ListKeyAccessServerGrantsResponse.grants:type_name -> policy.kasregistry.KeyAccessServerGrants + 60, // 40: policy.kasregistry.ListKeyAccessServerGrantsResponse.pagination:type_name -> policy.PageResponse + 67, // 41: policy.kasregistry.CreateKeyRequest.key_algorithm:type_name -> policy.Algorithm + 68, // 42: policy.kasregistry.CreateKeyRequest.key_mode:type_name -> policy.KeyMode + 69, // 43: policy.kasregistry.CreateKeyRequest.public_key_ctx:type_name -> policy.PublicKeyCtx + 70, // 44: policy.kasregistry.CreateKeyRequest.private_key_ctx:type_name -> policy.PrivateKeyCtx + 63, // 45: policy.kasregistry.CreateKeyRequest.metadata:type_name -> common.MetadataMutable + 71, // 46: policy.kasregistry.CreateKeyResponse.kas_key:type_name -> policy.KasKey + 40, // 47: policy.kasregistry.GetKeyRequest.key:type_name -> policy.kasregistry.KasKeyIdentifier + 71, // 48: policy.kasregistry.GetKeyResponse.kas_key:type_name -> policy.KasKey + 67, // 49: policy.kasregistry.ListKeysRequest.key_algorithm:type_name -> policy.Algorithm + 59, // 50: policy.kasregistry.ListKeysRequest.pagination:type_name -> policy.PageRequest + 7, // 51: policy.kasregistry.ListKeysRequest.sort:type_name -> policy.kasregistry.KasKeysSort + 71, // 52: policy.kasregistry.ListKeysResponse.kas_keys:type_name -> policy.KasKey + 60, // 53: policy.kasregistry.ListKeysResponse.pagination:type_name -> policy.PageResponse + 63, // 54: policy.kasregistry.UpdateKeyRequest.metadata:type_name -> common.MetadataMutable + 64, // 55: policy.kasregistry.UpdateKeyRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 71, // 56: policy.kasregistry.UpdateKeyResponse.kas_key:type_name -> policy.KasKey + 40, // 57: policy.kasregistry.RotateKeyRequest.key:type_name -> policy.kasregistry.KasKeyIdentifier + 56, // 58: policy.kasregistry.RotateKeyRequest.new_key:type_name -> policy.kasregistry.RotateKeyRequest.NewKey + 71, // 59: policy.kasregistry.RotatedResources.rotated_out_key:type_name -> policy.KasKey + 42, // 60: policy.kasregistry.RotatedResources.attribute_definition_mappings:type_name -> policy.kasregistry.ChangeMappings + 42, // 61: policy.kasregistry.RotatedResources.attribute_value_mappings:type_name -> policy.kasregistry.ChangeMappings + 42, // 62: policy.kasregistry.RotatedResources.namespace_mappings:type_name -> policy.kasregistry.ChangeMappings + 71, // 63: policy.kasregistry.RotateKeyResponse.kas_key:type_name -> policy.KasKey + 43, // 64: policy.kasregistry.RotateKeyResponse.rotated_resources:type_name -> policy.kasregistry.RotatedResources + 40, // 65: policy.kasregistry.SetBaseKeyRequest.key:type_name -> policy.kasregistry.KasKeyIdentifier + 72, // 66: policy.kasregistry.GetBaseKeyResponse.base_key:type_name -> policy.SimpleKasKey + 72, // 67: policy.kasregistry.SetBaseKeyResponse.new_base_key:type_name -> policy.SimpleKasKey + 72, // 68: policy.kasregistry.SetBaseKeyResponse.previous_base_key:type_name -> policy.SimpleKasKey + 49, // 69: policy.kasregistry.KeyMapping.namespace_mappings:type_name -> policy.kasregistry.MappedPolicyObject + 49, // 70: policy.kasregistry.KeyMapping.attribute_mappings:type_name -> policy.kasregistry.MappedPolicyObject + 49, // 71: policy.kasregistry.KeyMapping.value_mappings:type_name -> policy.kasregistry.MappedPolicyObject + 40, // 72: policy.kasregistry.ListKeyMappingsRequest.key:type_name -> policy.kasregistry.KasKeyIdentifier + 59, // 73: policy.kasregistry.ListKeyMappingsRequest.pagination:type_name -> policy.PageRequest + 50, // 74: policy.kasregistry.ListKeyMappingsResponse.key_mappings:type_name -> policy.kasregistry.KeyMapping + 60, // 75: policy.kasregistry.ListKeyMappingsResponse.pagination:type_name -> policy.PageResponse + 54, // 76: policy.kasregistry.ListPublicKeyMappingResponse.PublicKeyMapping.public_keys:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.PublicKey + 66, // 77: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey.key:type_name -> policy.Key + 55, // 78: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey.values:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.Association + 55, // 79: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey.definitions:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.Association + 55, // 80: policy.kasregistry.ListPublicKeyMappingResponse.PublicKey.namespaces:type_name -> policy.kasregistry.ListPublicKeyMappingResponse.Association + 67, // 81: policy.kasregistry.RotateKeyRequest.NewKey.algorithm:type_name -> policy.Algorithm + 68, // 82: policy.kasregistry.RotateKeyRequest.NewKey.key_mode:type_name -> policy.KeyMode + 69, // 83: policy.kasregistry.RotateKeyRequest.NewKey.public_key_ctx:type_name -> policy.PublicKeyCtx + 70, // 84: policy.kasregistry.RotateKeyRequest.NewKey.private_key_ctx:type_name -> policy.PrivateKeyCtx + 63, // 85: policy.kasregistry.RotateKeyRequest.NewKey.metadata:type_name -> common.MetadataMutable + 5, // 86: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServers:input_type -> policy.kasregistry.ListKeyAccessServersRequest + 2, // 87: policy.kasregistry.KeyAccessServerRegistryService.GetKeyAccessServer:input_type -> policy.kasregistry.GetKeyAccessServerRequest + 8, // 88: policy.kasregistry.KeyAccessServerRegistryService.CreateKeyAccessServer:input_type -> policy.kasregistry.CreateKeyAccessServerRequest + 10, // 89: policy.kasregistry.KeyAccessServerRegistryService.UpdateKeyAccessServer:input_type -> policy.kasregistry.UpdateKeyAccessServerRequest + 12, // 90: policy.kasregistry.KeyAccessServerRegistryService.DeleteKeyAccessServer:input_type -> policy.kasregistry.DeleteKeyAccessServerRequest + 30, // 91: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServerGrants:input_type -> policy.kasregistry.ListKeyAccessServerGrantsRequest + 32, // 92: policy.kasregistry.KeyAccessServerRegistryService.CreateKey:input_type -> policy.kasregistry.CreateKeyRequest + 34, // 93: policy.kasregistry.KeyAccessServerRegistryService.GetKey:input_type -> policy.kasregistry.GetKeyRequest + 36, // 94: policy.kasregistry.KeyAccessServerRegistryService.ListKeys:input_type -> policy.kasregistry.ListKeysRequest + 38, // 95: policy.kasregistry.KeyAccessServerRegistryService.UpdateKey:input_type -> policy.kasregistry.UpdateKeyRequest + 41, // 96: policy.kasregistry.KeyAccessServerRegistryService.RotateKey:input_type -> policy.kasregistry.RotateKeyRequest + 45, // 97: policy.kasregistry.KeyAccessServerRegistryService.SetBaseKey:input_type -> policy.kasregistry.SetBaseKeyRequest + 46, // 98: policy.kasregistry.KeyAccessServerRegistryService.GetBaseKey:input_type -> policy.kasregistry.GetBaseKeyRequest + 51, // 99: policy.kasregistry.KeyAccessServerRegistryService.ListKeyMappings:input_type -> policy.kasregistry.ListKeyMappingsRequest + 6, // 100: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServers:output_type -> policy.kasregistry.ListKeyAccessServersResponse + 3, // 101: policy.kasregistry.KeyAccessServerRegistryService.GetKeyAccessServer:output_type -> policy.kasregistry.GetKeyAccessServerResponse + 9, // 102: policy.kasregistry.KeyAccessServerRegistryService.CreateKeyAccessServer:output_type -> policy.kasregistry.CreateKeyAccessServerResponse + 11, // 103: policy.kasregistry.KeyAccessServerRegistryService.UpdateKeyAccessServer:output_type -> policy.kasregistry.UpdateKeyAccessServerResponse + 13, // 104: policy.kasregistry.KeyAccessServerRegistryService.DeleteKeyAccessServer:output_type -> policy.kasregistry.DeleteKeyAccessServerResponse + 31, // 105: policy.kasregistry.KeyAccessServerRegistryService.ListKeyAccessServerGrants:output_type -> policy.kasregistry.ListKeyAccessServerGrantsResponse + 33, // 106: policy.kasregistry.KeyAccessServerRegistryService.CreateKey:output_type -> policy.kasregistry.CreateKeyResponse + 35, // 107: policy.kasregistry.KeyAccessServerRegistryService.GetKey:output_type -> policy.kasregistry.GetKeyResponse + 37, // 108: policy.kasregistry.KeyAccessServerRegistryService.ListKeys:output_type -> policy.kasregistry.ListKeysResponse + 39, // 109: policy.kasregistry.KeyAccessServerRegistryService.UpdateKey:output_type -> policy.kasregistry.UpdateKeyResponse + 44, // 110: policy.kasregistry.KeyAccessServerRegistryService.RotateKey:output_type -> policy.kasregistry.RotateKeyResponse + 48, // 111: policy.kasregistry.KeyAccessServerRegistryService.SetBaseKey:output_type -> policy.kasregistry.SetBaseKeyResponse + 47, // 112: policy.kasregistry.KeyAccessServerRegistryService.GetBaseKey:output_type -> policy.kasregistry.GetBaseKeyResponse + 52, // 113: policy.kasregistry.KeyAccessServerRegistryService.ListKeyMappings:output_type -> policy.kasregistry.ListKeyMappingsResponse + 100, // [100:114] is the sub-list for method output_type + 86, // [86:100] is the sub-list for method input_type + 86, // [86:86] is the sub-list for extension type_name + 86, // [86:86] is the sub-list for extension extendee + 0, // [0:86] is the sub-list for field type_name } func init() { file_policy_kasregistry_key_access_server_registry_proto_init() } @@ -4767,7 +5072,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListKeyAccessServersRequest); i { + switch v := v.(*KeyAccessServersSort); i { case 0: return &v.state case 1: @@ -4779,7 +5084,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListKeyAccessServersResponse); i { + switch v := v.(*ListKeyAccessServersRequest); i { case 0: return &v.state case 1: @@ -4791,7 +5096,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateKeyAccessServerRequest); i { + switch v := v.(*ListKeyAccessServersResponse); i { case 0: return &v.state case 1: @@ -4803,7 +5108,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateKeyAccessServerResponse); i { + switch v := v.(*KasKeysSort); i { case 0: return &v.state case 1: @@ -4815,7 +5120,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateKeyAccessServerRequest); i { + switch v := v.(*CreateKeyAccessServerRequest); i { case 0: return &v.state case 1: @@ -4827,7 +5132,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateKeyAccessServerResponse); i { + switch v := v.(*CreateKeyAccessServerResponse); i { case 0: return &v.state case 1: @@ -4839,7 +5144,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteKeyAccessServerRequest); i { + switch v := v.(*UpdateKeyAccessServerRequest); i { case 0: return &v.state case 1: @@ -4851,7 +5156,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteKeyAccessServerResponse); i { + switch v := v.(*UpdateKeyAccessServerResponse); i { case 0: return &v.state case 1: @@ -4863,7 +5168,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GrantedPolicyObject); i { + switch v := v.(*DeleteKeyAccessServerRequest); i { case 0: return &v.state case 1: @@ -4875,7 +5180,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*KeyAccessServerGrants); i { + switch v := v.(*DeleteKeyAccessServerResponse); i { case 0: return &v.state case 1: @@ -4887,7 +5192,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreatePublicKeyRequest); i { + switch v := v.(*GrantedPolicyObject); i { case 0: return &v.state case 1: @@ -4899,7 +5204,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreatePublicKeyResponse); i { + switch v := v.(*KeyAccessServerGrants); i { case 0: return &v.state case 1: @@ -4911,7 +5216,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetPublicKeyRequest); i { + switch v := v.(*CreatePublicKeyRequest); i { case 0: return &v.state case 1: @@ -4923,7 +5228,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetPublicKeyResponse); i { + switch v := v.(*CreatePublicKeyResponse); i { case 0: return &v.state case 1: @@ -4935,7 +5240,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPublicKeysRequest); i { + switch v := v.(*GetPublicKeyRequest); i { case 0: return &v.state case 1: @@ -4947,7 +5252,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPublicKeysResponse); i { + switch v := v.(*GetPublicKeyResponse); i { case 0: return &v.state case 1: @@ -4959,7 +5264,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPublicKeyMappingRequest); i { + switch v := v.(*ListPublicKeysRequest); i { case 0: return &v.state case 1: @@ -4971,7 +5276,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPublicKeyMappingResponse); i { + switch v := v.(*ListPublicKeysResponse); i { case 0: return &v.state case 1: @@ -4983,7 +5288,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdatePublicKeyRequest); i { + switch v := v.(*ListPublicKeyMappingRequest); i { case 0: return &v.state case 1: @@ -4995,7 +5300,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdatePublicKeyResponse); i { + switch v := v.(*ListPublicKeyMappingResponse); i { case 0: return &v.state case 1: @@ -5007,7 +5312,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeactivatePublicKeyRequest); i { + switch v := v.(*UpdatePublicKeyRequest); i { case 0: return &v.state case 1: @@ -5019,7 +5324,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeactivatePublicKeyResponse); i { + switch v := v.(*UpdatePublicKeyResponse); i { case 0: return &v.state case 1: @@ -5031,7 +5336,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ActivatePublicKeyRequest); i { + switch v := v.(*DeactivatePublicKeyRequest); i { case 0: return &v.state case 1: @@ -5043,7 +5348,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ActivatePublicKeyResponse); i { + switch v := v.(*DeactivatePublicKeyResponse); i { case 0: return &v.state case 1: @@ -5055,7 +5360,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListKeyAccessServerGrantsRequest); i { + switch v := v.(*ActivatePublicKeyRequest); i { case 0: return &v.state case 1: @@ -5067,7 +5372,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListKeyAccessServerGrantsResponse); i { + switch v := v.(*ActivatePublicKeyResponse); i { case 0: return &v.state case 1: @@ -5079,7 +5384,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateKeyRequest); i { + switch v := v.(*ListKeyAccessServerGrantsRequest); i { case 0: return &v.state case 1: @@ -5091,7 +5396,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateKeyResponse); i { + switch v := v.(*ListKeyAccessServerGrantsResponse); i { case 0: return &v.state case 1: @@ -5103,7 +5408,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetKeyRequest); i { + switch v := v.(*CreateKeyRequest); i { case 0: return &v.state case 1: @@ -5115,7 +5420,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetKeyResponse); i { + switch v := v.(*CreateKeyResponse); i { case 0: return &v.state case 1: @@ -5127,7 +5432,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListKeysRequest); i { + switch v := v.(*GetKeyRequest); i { case 0: return &v.state case 1: @@ -5139,7 +5444,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListKeysResponse); i { + switch v := v.(*GetKeyResponse); i { case 0: return &v.state case 1: @@ -5151,7 +5456,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateKeyRequest); i { + switch v := v.(*ListKeysRequest); i { case 0: return &v.state case 1: @@ -5163,7 +5468,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateKeyResponse); i { + switch v := v.(*ListKeysResponse); i { case 0: return &v.state case 1: @@ -5175,7 +5480,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*KasKeyIdentifier); i { + switch v := v.(*UpdateKeyRequest); i { case 0: return &v.state case 1: @@ -5187,7 +5492,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RotateKeyRequest); i { + switch v := v.(*UpdateKeyResponse); i { case 0: return &v.state case 1: @@ -5199,7 +5504,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChangeMappings); i { + switch v := v.(*KasKeyIdentifier); i { case 0: return &v.state case 1: @@ -5211,7 +5516,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RotatedResources); i { + switch v := v.(*RotateKeyRequest); i { case 0: return &v.state case 1: @@ -5223,7 +5528,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RotateKeyResponse); i { + switch v := v.(*ChangeMappings); i { case 0: return &v.state case 1: @@ -5235,7 +5540,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetBaseKeyRequest); i { + switch v := v.(*RotatedResources); i { case 0: return &v.state case 1: @@ -5247,7 +5552,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetBaseKeyRequest); i { + switch v := v.(*RotateKeyResponse); i { case 0: return &v.state case 1: @@ -5259,7 +5564,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetBaseKeyResponse); i { + switch v := v.(*SetBaseKeyRequest); i { case 0: return &v.state case 1: @@ -5271,7 +5576,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetBaseKeyResponse); i { + switch v := v.(*GetBaseKeyRequest); i { case 0: return &v.state case 1: @@ -5283,7 +5588,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MappedPolicyObject); i { + switch v := v.(*GetBaseKeyResponse); i { case 0: return &v.state case 1: @@ -5295,7 +5600,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*KeyMapping); i { + switch v := v.(*SetBaseKeyResponse); i { case 0: return &v.state case 1: @@ -5307,7 +5612,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListKeyMappingsRequest); i { + switch v := v.(*MappedPolicyObject); i { case 0: return &v.state case 1: @@ -5319,7 +5624,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListKeyMappingsResponse); i { + switch v := v.(*KeyMapping); i { case 0: return &v.state case 1: @@ -5331,7 +5636,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPublicKeyMappingResponse_PublicKeyMapping); i { + switch v := v.(*ListKeyMappingsRequest); i { case 0: return &v.state case 1: @@ -5343,7 +5648,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[50].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPublicKeyMappingResponse_PublicKey); i { + switch v := v.(*ListKeyMappingsResponse); i { case 0: return &v.state case 1: @@ -5355,7 +5660,7 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[51].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPublicKeyMappingResponse_Association); i { + switch v := v.(*ListPublicKeyMappingResponse_PublicKeyMapping); i { case 0: return &v.state case 1: @@ -5367,6 +5672,30 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { } } file_policy_kasregistry_key_access_server_registry_proto_msgTypes[52].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListPublicKeyMappingResponse_PublicKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[53].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListPublicKeyMappingResponse_Association); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[54].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RotateKeyRequest_NewKey); i { case 0: return &v.state @@ -5384,42 +5713,42 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { (*GetKeyAccessServerRequest_Name)(nil), (*GetKeyAccessServerRequest_Uri)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[14].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[16].OneofWrappers = []interface{}{ (*GetPublicKeyRequest_Id)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[16].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[18].OneofWrappers = []interface{}{ (*ListPublicKeysRequest_KasId)(nil), (*ListPublicKeysRequest_KasName)(nil), (*ListPublicKeysRequest_KasUri)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[18].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[20].OneofWrappers = []interface{}{ (*ListPublicKeyMappingRequest_KasId)(nil), (*ListPublicKeyMappingRequest_KasName)(nil), (*ListPublicKeyMappingRequest_KasUri)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[30].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[32].OneofWrappers = []interface{}{ (*GetKeyRequest_Id)(nil), (*GetKeyRequest_Key)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[32].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[34].OneofWrappers = []interface{}{ (*ListKeysRequest_KasId)(nil), (*ListKeysRequest_KasName)(nil), (*ListKeysRequest_KasUri)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[36].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[38].OneofWrappers = []interface{}{ (*KasKeyIdentifier_KasId)(nil), (*KasKeyIdentifier_Name)(nil), (*KasKeyIdentifier_Uri)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[37].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[39].OneofWrappers = []interface{}{ (*RotateKeyRequest_Id)(nil), (*RotateKeyRequest_Key)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[41].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[43].OneofWrappers = []interface{}{ (*SetBaseKeyRequest_Id)(nil), (*SetBaseKeyRequest_Key)(nil), } - file_policy_kasregistry_key_access_server_registry_proto_msgTypes[47].OneofWrappers = []interface{}{ + file_policy_kasregistry_key_access_server_registry_proto_msgTypes[49].OneofWrappers = []interface{}{ (*ListKeyMappingsRequest_Id)(nil), (*ListKeyMappingsRequest_Key)(nil), } @@ -5428,13 +5757,14 @@ func file_policy_kasregistry_key_access_server_registry_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_policy_kasregistry_key_access_server_registry_proto_rawDesc, - NumEnums: 0, - NumMessages: 53, + NumEnums: 2, + NumMessages: 55, NumExtensions: 0, NumServices: 1, }, GoTypes: file_policy_kasregistry_key_access_server_registry_proto_goTypes, DependencyIndexes: file_policy_kasregistry_key_access_server_registry_proto_depIdxs, + EnumInfos: file_policy_kasregistry_key_access_server_registry_proto_enumTypes, MessageInfos: file_policy_kasregistry_key_access_server_registry_proto_msgTypes, }.Build() File_policy_kasregistry_key_access_server_registry_proto = out.File diff --git a/protocol/go/policy/kasregistry/key_access_server_registry.pb.gw.go b/protocol/go/policy/kasregistry/key_access_server_registry.pb.gw.go deleted file mode 100644 index 7f015f6e17..0000000000 --- a/protocol/go/policy/kasregistry/key_access_server_registry.pb.gw.go +++ /dev/null @@ -1,173 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: policy/kasregistry/key_access_server_registry.proto - -/* -Package kasregistry is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package kasregistry - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -var ( - filter_KeyAccessServerRegistryService_ListKeyAccessServers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} -) - -func request_KeyAccessServerRegistryService_ListKeyAccessServers_0(ctx context.Context, marshaler runtime.Marshaler, client KeyAccessServerRegistryServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ListKeyAccessServersRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_KeyAccessServerRegistryService_ListKeyAccessServers_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.ListKeyAccessServers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_KeyAccessServerRegistryService_ListKeyAccessServers_0(ctx context.Context, marshaler runtime.Marshaler, server KeyAccessServerRegistryServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ListKeyAccessServersRequest - var metadata runtime.ServerMetadata - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_KeyAccessServerRegistryService_ListKeyAccessServers_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.ListKeyAccessServers(ctx, &protoReq) - return msg, metadata, err - -} - -// RegisterKeyAccessServerRegistryServiceHandlerServer registers the http handlers for service KeyAccessServerRegistryService to "mux". -// UnaryRPC :call KeyAccessServerRegistryServiceServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterKeyAccessServerRegistryServiceHandlerFromEndpoint instead. -func RegisterKeyAccessServerRegistryServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server KeyAccessServerRegistryServiceServer) error { - - mux.Handle("GET", pattern_KeyAccessServerRegistryService_ListKeyAccessServers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/policy.kasregistry.KeyAccessServerRegistryService/ListKeyAccessServers", runtime.WithHTTPPathPattern("/key-access-servers")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_KeyAccessServerRegistryService_ListKeyAccessServers_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_KeyAccessServerRegistryService_ListKeyAccessServers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterKeyAccessServerRegistryServiceHandlerFromEndpoint is same as RegisterKeyAccessServerRegistryServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterKeyAccessServerRegistryServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterKeyAccessServerRegistryServiceHandler(ctx, mux, conn) -} - -// RegisterKeyAccessServerRegistryServiceHandler registers the http handlers for service KeyAccessServerRegistryService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterKeyAccessServerRegistryServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterKeyAccessServerRegistryServiceHandlerClient(ctx, mux, NewKeyAccessServerRegistryServiceClient(conn)) -} - -// RegisterKeyAccessServerRegistryServiceHandlerClient registers the http handlers for service KeyAccessServerRegistryService -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "KeyAccessServerRegistryServiceClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "KeyAccessServerRegistryServiceClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "KeyAccessServerRegistryServiceClient" to call the correct interceptors. -func RegisterKeyAccessServerRegistryServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client KeyAccessServerRegistryServiceClient) error { - - mux.Handle("GET", pattern_KeyAccessServerRegistryService_ListKeyAccessServers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/policy.kasregistry.KeyAccessServerRegistryService/ListKeyAccessServers", runtime.WithHTTPPathPattern("/key-access-servers")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_KeyAccessServerRegistryService_ListKeyAccessServers_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_KeyAccessServerRegistryService_ListKeyAccessServers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_KeyAccessServerRegistryService_ListKeyAccessServers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"key-access-servers"}, "")) -) - -var ( - forward_KeyAccessServerRegistryService_ListKeyAccessServers_0 = runtime.ForwardResponseMessage -) diff --git a/protocol/go/policy/keymanagement/keymanagementconnect/key_management.connect.go b/protocol/go/policy/keymanagement/keymanagementconnect/key_management.connect.go index 5fa9587c84..5564be49c9 100644 --- a/protocol/go/policy/keymanagement/keymanagementconnect/key_management.connect.go +++ b/protocol/go/policy/keymanagement/keymanagementconnect/key_management.connect.go @@ -50,16 +50,6 @@ const ( KeyManagementServiceDeleteProviderConfigProcedure = "/policy.keymanagement.KeyManagementService/DeleteProviderConfig" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - keyManagementServiceServiceDescriptor = keymanagement.File_policy_keymanagement_key_management_proto.Services().ByName("KeyManagementService") - keyManagementServiceCreateProviderConfigMethodDescriptor = keyManagementServiceServiceDescriptor.Methods().ByName("CreateProviderConfig") - keyManagementServiceGetProviderConfigMethodDescriptor = keyManagementServiceServiceDescriptor.Methods().ByName("GetProviderConfig") - keyManagementServiceListProviderConfigsMethodDescriptor = keyManagementServiceServiceDescriptor.Methods().ByName("ListProviderConfigs") - keyManagementServiceUpdateProviderConfigMethodDescriptor = keyManagementServiceServiceDescriptor.Methods().ByName("UpdateProviderConfig") - keyManagementServiceDeleteProviderConfigMethodDescriptor = keyManagementServiceServiceDescriptor.Methods().ByName("DeleteProviderConfig") -) - // KeyManagementServiceClient is a client for the policy.keymanagement.KeyManagementService service. type KeyManagementServiceClient interface { // Key Management @@ -80,35 +70,36 @@ type KeyManagementServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewKeyManagementServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) KeyManagementServiceClient { baseURL = strings.TrimRight(baseURL, "/") + keyManagementServiceMethods := keymanagement.File_policy_keymanagement_key_management_proto.Services().ByName("KeyManagementService").Methods() return &keyManagementServiceClient{ createProviderConfig: connect.NewClient[keymanagement.CreateProviderConfigRequest, keymanagement.CreateProviderConfigResponse]( httpClient, baseURL+KeyManagementServiceCreateProviderConfigProcedure, - connect.WithSchema(keyManagementServiceCreateProviderConfigMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("CreateProviderConfig")), connect.WithClientOptions(opts...), ), getProviderConfig: connect.NewClient[keymanagement.GetProviderConfigRequest, keymanagement.GetProviderConfigResponse]( httpClient, baseURL+KeyManagementServiceGetProviderConfigProcedure, - connect.WithSchema(keyManagementServiceGetProviderConfigMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("GetProviderConfig")), connect.WithClientOptions(opts...), ), listProviderConfigs: connect.NewClient[keymanagement.ListProviderConfigsRequest, keymanagement.ListProviderConfigsResponse]( httpClient, baseURL+KeyManagementServiceListProviderConfigsProcedure, - connect.WithSchema(keyManagementServiceListProviderConfigsMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("ListProviderConfigs")), connect.WithClientOptions(opts...), ), updateProviderConfig: connect.NewClient[keymanagement.UpdateProviderConfigRequest, keymanagement.UpdateProviderConfigResponse]( httpClient, baseURL+KeyManagementServiceUpdateProviderConfigProcedure, - connect.WithSchema(keyManagementServiceUpdateProviderConfigMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("UpdateProviderConfig")), connect.WithClientOptions(opts...), ), deleteProviderConfig: connect.NewClient[keymanagement.DeleteProviderConfigRequest, keymanagement.DeleteProviderConfigResponse]( httpClient, baseURL+KeyManagementServiceDeleteProviderConfigProcedure, - connect.WithSchema(keyManagementServiceDeleteProviderConfigMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("DeleteProviderConfig")), connect.WithClientOptions(opts...), ), } @@ -166,34 +157,35 @@ type KeyManagementServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewKeyManagementServiceHandler(svc KeyManagementServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + keyManagementServiceMethods := keymanagement.File_policy_keymanagement_key_management_proto.Services().ByName("KeyManagementService").Methods() keyManagementServiceCreateProviderConfigHandler := connect.NewUnaryHandler( KeyManagementServiceCreateProviderConfigProcedure, svc.CreateProviderConfig, - connect.WithSchema(keyManagementServiceCreateProviderConfigMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("CreateProviderConfig")), connect.WithHandlerOptions(opts...), ) keyManagementServiceGetProviderConfigHandler := connect.NewUnaryHandler( KeyManagementServiceGetProviderConfigProcedure, svc.GetProviderConfig, - connect.WithSchema(keyManagementServiceGetProviderConfigMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("GetProviderConfig")), connect.WithHandlerOptions(opts...), ) keyManagementServiceListProviderConfigsHandler := connect.NewUnaryHandler( KeyManagementServiceListProviderConfigsProcedure, svc.ListProviderConfigs, - connect.WithSchema(keyManagementServiceListProviderConfigsMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("ListProviderConfigs")), connect.WithHandlerOptions(opts...), ) keyManagementServiceUpdateProviderConfigHandler := connect.NewUnaryHandler( KeyManagementServiceUpdateProviderConfigProcedure, svc.UpdateProviderConfig, - connect.WithSchema(keyManagementServiceUpdateProviderConfigMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("UpdateProviderConfig")), connect.WithHandlerOptions(opts...), ) keyManagementServiceDeleteProviderConfigHandler := connect.NewUnaryHandler( KeyManagementServiceDeleteProviderConfigProcedure, svc.DeleteProviderConfig, - connect.WithSchema(keyManagementServiceDeleteProviderConfigMethodDescriptor), + connect.WithSchema(keyManagementServiceMethods.ByName("DeleteProviderConfig")), connect.WithHandlerOptions(opts...), ) return "/policy.keymanagement.KeyManagementService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/policy/namespaces/namespaces.pb.go b/protocol/go/policy/namespaces/namespaces.pb.go index 42b0e13c11..b1a2e24a8f 100644 --- a/protocol/go/policy/namespaces/namespaces.pb.go +++ b/protocol/go/policy/namespaces/namespaces.pb.go @@ -23,6 +23,61 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type SortNamespacesType int32 + +const ( + SortNamespacesType_SORT_NAMESPACES_TYPE_UNSPECIFIED SortNamespacesType = 0 + SortNamespacesType_SORT_NAMESPACES_TYPE_NAME SortNamespacesType = 1 + SortNamespacesType_SORT_NAMESPACES_TYPE_FQN SortNamespacesType = 2 + SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT SortNamespacesType = 3 + SortNamespacesType_SORT_NAMESPACES_TYPE_UPDATED_AT SortNamespacesType = 4 +) + +// Enum value maps for SortNamespacesType. +var ( + SortNamespacesType_name = map[int32]string{ + 0: "SORT_NAMESPACES_TYPE_UNSPECIFIED", + 1: "SORT_NAMESPACES_TYPE_NAME", + 2: "SORT_NAMESPACES_TYPE_FQN", + 3: "SORT_NAMESPACES_TYPE_CREATED_AT", + 4: "SORT_NAMESPACES_TYPE_UPDATED_AT", + } + SortNamespacesType_value = map[string]int32{ + "SORT_NAMESPACES_TYPE_UNSPECIFIED": 0, + "SORT_NAMESPACES_TYPE_NAME": 1, + "SORT_NAMESPACES_TYPE_FQN": 2, + "SORT_NAMESPACES_TYPE_CREATED_AT": 3, + "SORT_NAMESPACES_TYPE_UPDATED_AT": 4, + } +) + +func (x SortNamespacesType) Enum() *SortNamespacesType { + p := new(SortNamespacesType) + *p = x + return p +} + +func (x SortNamespacesType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortNamespacesType) Descriptor() protoreflect.EnumDescriptor { + return file_policy_namespaces_namespaces_proto_enumTypes[0].Descriptor() +} + +func (SortNamespacesType) Type() protoreflect.EnumType { + return &file_policy_namespaces_namespaces_proto_enumTypes[0] +} + +func (x SortNamespacesType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortNamespacesType.Descriptor instead. +func (SortNamespacesType) EnumDescriptor() ([]byte, []int) { + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{0} +} + // Deprecated // // Deprecated: Marked as deprecated in policy/namespaces/namespaces.proto. @@ -281,6 +336,61 @@ func (x *GetNamespaceResponse) GetNamespace() *policy.Namespace { return nil } +type NamespacesSort struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field SortNamespacesType `protobuf:"varint,1,opt,name=field,proto3,enum=policy.namespaces.SortNamespacesType" json:"field,omitempty"` + Direction policy.SortDirection `protobuf:"varint,2,opt,name=direction,proto3,enum=policy.SortDirection" json:"direction,omitempty"` +} + +func (x *NamespacesSort) Reset() { + *x = NamespacesSort{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_namespaces_namespaces_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NamespacesSort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NamespacesSort) ProtoMessage() {} + +func (x *NamespacesSort) ProtoReflect() protoreflect.Message { + mi := &file_policy_namespaces_namespaces_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NamespacesSort.ProtoReflect.Descriptor instead. +func (*NamespacesSort) Descriptor() ([]byte, []int) { + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{4} +} + +func (x *NamespacesSort) GetField() SortNamespacesType { + if x != nil { + return x.Field + } + return SortNamespacesType_SORT_NAMESPACES_TYPE_UNSPECIFIED +} + +func (x *NamespacesSort) GetDirection() policy.SortDirection { + if x != nil { + return x.Direction + } + return policy.SortDirection(0) +} + type ListNamespacesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -291,12 +401,18 @@ type ListNamespacesRequest struct { State common.ActiveStateEnum `protobuf:"varint,1,opt,name=state,proto3,enum=common.ActiveStateEnum" json:"state,omitempty"` // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + Sort []*NamespacesSort `protobuf:"bytes,11,rep,name=sort,proto3" json:"sort,omitempty"` } func (x *ListNamespacesRequest) Reset() { *x = ListNamespacesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[4] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -309,7 +425,7 @@ func (x *ListNamespacesRequest) String() string { func (*ListNamespacesRequest) ProtoMessage() {} func (x *ListNamespacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[4] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -322,7 +438,7 @@ func (x *ListNamespacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNamespacesRequest.ProtoReflect.Descriptor instead. func (*ListNamespacesRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{4} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{5} } func (x *ListNamespacesRequest) GetState() common.ActiveStateEnum { @@ -339,6 +455,13 @@ func (x *ListNamespacesRequest) GetPagination() *policy.PageRequest { return nil } +func (x *ListNamespacesRequest) GetSort() []*NamespacesSort { + if x != nil { + return x.Sort + } + return nil +} + type ListNamespacesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -351,7 +474,7 @@ type ListNamespacesResponse struct { func (x *ListNamespacesResponse) Reset() { *x = ListNamespacesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[5] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -364,7 +487,7 @@ func (x *ListNamespacesResponse) String() string { func (*ListNamespacesResponse) ProtoMessage() {} func (x *ListNamespacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[5] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -377,7 +500,7 @@ func (x *ListNamespacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNamespacesResponse.ProtoReflect.Descriptor instead. func (*ListNamespacesResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{5} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{6} } func (x *ListNamespacesResponse) GetNamespaces() []*policy.Namespace { @@ -408,7 +531,7 @@ type CreateNamespaceRequest struct { func (x *CreateNamespaceRequest) Reset() { *x = CreateNamespaceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[6] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -421,7 +544,7 @@ func (x *CreateNamespaceRequest) String() string { func (*CreateNamespaceRequest) ProtoMessage() {} func (x *CreateNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[6] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -434,7 +557,7 @@ func (x *CreateNamespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateNamespaceRequest.ProtoReflect.Descriptor instead. func (*CreateNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{6} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{7} } func (x *CreateNamespaceRequest) GetName() string { @@ -462,7 +585,7 @@ type CreateNamespaceResponse struct { func (x *CreateNamespaceResponse) Reset() { *x = CreateNamespaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[7] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -475,7 +598,7 @@ func (x *CreateNamespaceResponse) String() string { func (*CreateNamespaceResponse) ProtoMessage() {} func (x *CreateNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[7] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -488,7 +611,7 @@ func (x *CreateNamespaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateNamespaceResponse.ProtoReflect.Descriptor instead. func (*CreateNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{7} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{8} } func (x *CreateNamespaceResponse) GetNamespace() *policy.Namespace { @@ -513,7 +636,7 @@ type UpdateNamespaceRequest struct { func (x *UpdateNamespaceRequest) Reset() { *x = UpdateNamespaceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[8] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -526,7 +649,7 @@ func (x *UpdateNamespaceRequest) String() string { func (*UpdateNamespaceRequest) ProtoMessage() {} func (x *UpdateNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[8] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -539,7 +662,7 @@ func (x *UpdateNamespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateNamespaceRequest.ProtoReflect.Descriptor instead. func (*UpdateNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{8} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{9} } func (x *UpdateNamespaceRequest) GetId() string { @@ -574,7 +697,7 @@ type UpdateNamespaceResponse struct { func (x *UpdateNamespaceResponse) Reset() { *x = UpdateNamespaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[9] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -587,7 +710,7 @@ func (x *UpdateNamespaceResponse) String() string { func (*UpdateNamespaceResponse) ProtoMessage() {} func (x *UpdateNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[9] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -600,7 +723,7 @@ func (x *UpdateNamespaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateNamespaceResponse.ProtoReflect.Descriptor instead. func (*UpdateNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{9} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{10} } func (x *UpdateNamespaceResponse) GetNamespace() *policy.Namespace { @@ -622,7 +745,7 @@ type DeactivateNamespaceRequest struct { func (x *DeactivateNamespaceRequest) Reset() { *x = DeactivateNamespaceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[10] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -635,7 +758,7 @@ func (x *DeactivateNamespaceRequest) String() string { func (*DeactivateNamespaceRequest) ProtoMessage() {} func (x *DeactivateNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[10] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -648,7 +771,7 @@ func (x *DeactivateNamespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeactivateNamespaceRequest.ProtoReflect.Descriptor instead. func (*DeactivateNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{10} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{11} } func (x *DeactivateNamespaceRequest) GetId() string { @@ -667,7 +790,7 @@ type DeactivateNamespaceResponse struct { func (x *DeactivateNamespaceResponse) Reset() { *x = DeactivateNamespaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[11] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -680,7 +803,7 @@ func (x *DeactivateNamespaceResponse) String() string { func (*DeactivateNamespaceResponse) ProtoMessage() {} func (x *DeactivateNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[11] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -693,7 +816,7 @@ func (x *DeactivateNamespaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeactivateNamespaceResponse.ProtoReflect.Descriptor instead. func (*DeactivateNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{11} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{12} } // Deprecated: utilize AssignPublicKeyToNamespaceRequest @@ -710,7 +833,7 @@ type AssignKeyAccessServerToNamespaceRequest struct { func (x *AssignKeyAccessServerToNamespaceRequest) Reset() { *x = AssignKeyAccessServerToNamespaceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[12] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -723,7 +846,7 @@ func (x *AssignKeyAccessServerToNamespaceRequest) String() string { func (*AssignKeyAccessServerToNamespaceRequest) ProtoMessage() {} func (x *AssignKeyAccessServerToNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[12] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -736,7 +859,7 @@ func (x *AssignKeyAccessServerToNamespaceRequest) ProtoReflect() protoreflect.Me // Deprecated: Use AssignKeyAccessServerToNamespaceRequest.ProtoReflect.Descriptor instead. func (*AssignKeyAccessServerToNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{12} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{13} } func (x *AssignKeyAccessServerToNamespaceRequest) GetNamespaceKeyAccessServer() *NamespaceKeyAccessServer { @@ -757,7 +880,7 @@ type AssignKeyAccessServerToNamespaceResponse struct { func (x *AssignKeyAccessServerToNamespaceResponse) Reset() { *x = AssignKeyAccessServerToNamespaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[13] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -770,7 +893,7 @@ func (x *AssignKeyAccessServerToNamespaceResponse) String() string { func (*AssignKeyAccessServerToNamespaceResponse) ProtoMessage() {} func (x *AssignKeyAccessServerToNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[13] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -783,7 +906,7 @@ func (x *AssignKeyAccessServerToNamespaceResponse) ProtoReflect() protoreflect.M // Deprecated: Use AssignKeyAccessServerToNamespaceResponse.ProtoReflect.Descriptor instead. func (*AssignKeyAccessServerToNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{13} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{14} } func (x *AssignKeyAccessServerToNamespaceResponse) GetNamespaceKeyAccessServer() *NamespaceKeyAccessServer { @@ -807,7 +930,7 @@ type RemoveKeyAccessServerFromNamespaceRequest struct { func (x *RemoveKeyAccessServerFromNamespaceRequest) Reset() { *x = RemoveKeyAccessServerFromNamespaceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[14] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -820,7 +943,7 @@ func (x *RemoveKeyAccessServerFromNamespaceRequest) String() string { func (*RemoveKeyAccessServerFromNamespaceRequest) ProtoMessage() {} func (x *RemoveKeyAccessServerFromNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[14] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -833,7 +956,7 @@ func (x *RemoveKeyAccessServerFromNamespaceRequest) ProtoReflect() protoreflect. // Deprecated: Use RemoveKeyAccessServerFromNamespaceRequest.ProtoReflect.Descriptor instead. func (*RemoveKeyAccessServerFromNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{14} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{15} } func (x *RemoveKeyAccessServerFromNamespaceRequest) GetNamespaceKeyAccessServer() *NamespaceKeyAccessServer { @@ -854,7 +977,7 @@ type RemoveKeyAccessServerFromNamespaceResponse struct { func (x *RemoveKeyAccessServerFromNamespaceResponse) Reset() { *x = RemoveKeyAccessServerFromNamespaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[15] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -867,7 +990,7 @@ func (x *RemoveKeyAccessServerFromNamespaceResponse) String() string { func (*RemoveKeyAccessServerFromNamespaceResponse) ProtoMessage() {} func (x *RemoveKeyAccessServerFromNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[15] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -880,7 +1003,7 @@ func (x *RemoveKeyAccessServerFromNamespaceResponse) ProtoReflect() protoreflect // Deprecated: Use RemoveKeyAccessServerFromNamespaceResponse.ProtoReflect.Descriptor instead. func (*RemoveKeyAccessServerFromNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{15} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{16} } func (x *RemoveKeyAccessServerFromNamespaceResponse) GetNamespaceKeyAccessServer() *NamespaceKeyAccessServer { @@ -902,7 +1025,7 @@ type AssignPublicKeyToNamespaceRequest struct { func (x *AssignPublicKeyToNamespaceRequest) Reset() { *x = AssignPublicKeyToNamespaceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[16] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -915,7 +1038,7 @@ func (x *AssignPublicKeyToNamespaceRequest) String() string { func (*AssignPublicKeyToNamespaceRequest) ProtoMessage() {} func (x *AssignPublicKeyToNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[16] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -928,7 +1051,7 @@ func (x *AssignPublicKeyToNamespaceRequest) ProtoReflect() protoreflect.Message // Deprecated: Use AssignPublicKeyToNamespaceRequest.ProtoReflect.Descriptor instead. func (*AssignPublicKeyToNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{16} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{17} } func (x *AssignPublicKeyToNamespaceRequest) GetNamespaceKey() *NamespaceKey { @@ -949,7 +1072,7 @@ type AssignPublicKeyToNamespaceResponse struct { func (x *AssignPublicKeyToNamespaceResponse) Reset() { *x = AssignPublicKeyToNamespaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[17] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -962,7 +1085,7 @@ func (x *AssignPublicKeyToNamespaceResponse) String() string { func (*AssignPublicKeyToNamespaceResponse) ProtoMessage() {} func (x *AssignPublicKeyToNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[17] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -975,7 +1098,7 @@ func (x *AssignPublicKeyToNamespaceResponse) ProtoReflect() protoreflect.Message // Deprecated: Use AssignPublicKeyToNamespaceResponse.ProtoReflect.Descriptor instead. func (*AssignPublicKeyToNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{17} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{18} } func (x *AssignPublicKeyToNamespaceResponse) GetNamespaceKey() *NamespaceKey { @@ -996,7 +1119,7 @@ type RemovePublicKeyFromNamespaceRequest struct { func (x *RemovePublicKeyFromNamespaceRequest) Reset() { *x = RemovePublicKeyFromNamespaceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[18] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1009,7 +1132,7 @@ func (x *RemovePublicKeyFromNamespaceRequest) String() string { func (*RemovePublicKeyFromNamespaceRequest) ProtoMessage() {} func (x *RemovePublicKeyFromNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[18] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1022,7 +1145,7 @@ func (x *RemovePublicKeyFromNamespaceRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use RemovePublicKeyFromNamespaceRequest.ProtoReflect.Descriptor instead. func (*RemovePublicKeyFromNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{18} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{19} } func (x *RemovePublicKeyFromNamespaceRequest) GetNamespaceKey() *NamespaceKey { @@ -1043,7 +1166,7 @@ type RemovePublicKeyFromNamespaceResponse struct { func (x *RemovePublicKeyFromNamespaceResponse) Reset() { *x = RemovePublicKeyFromNamespaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[19] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1056,7 +1179,7 @@ func (x *RemovePublicKeyFromNamespaceResponse) String() string { func (*RemovePublicKeyFromNamespaceResponse) ProtoMessage() {} func (x *RemovePublicKeyFromNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[19] + mi := &file_policy_namespaces_namespaces_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1069,7 +1192,7 @@ func (x *RemovePublicKeyFromNamespaceResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use RemovePublicKeyFromNamespaceResponse.ProtoReflect.Descriptor instead. func (*RemovePublicKeyFromNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{19} + return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{20} } func (x *RemovePublicKeyFromNamespaceResponse) GetNamespaceKey() *NamespaceKey { @@ -1079,282 +1202,6 @@ func (x *RemovePublicKeyFromNamespaceResponse) GetNamespaceKey() *NamespaceKey { return nil } -// Maps a namespace to a certificate (similar to NamespaceKey pattern) -type NamespaceCertificate struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Required - namespace identifier (id or fqn) - Namespace *common.IdFqnIdentifier `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - // Required (The id from the Certificate object) - CertificateId string `protobuf:"bytes,2,opt,name=certificate_id,json=certificateId,proto3" json:"certificate_id,omitempty"` -} - -func (x *NamespaceCertificate) Reset() { - *x = NamespaceCertificate{} - if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *NamespaceCertificate) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*NamespaceCertificate) ProtoMessage() {} - -func (x *NamespaceCertificate) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use NamespaceCertificate.ProtoReflect.Descriptor instead. -func (*NamespaceCertificate) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{20} -} - -func (x *NamespaceCertificate) GetNamespace() *common.IdFqnIdentifier { - if x != nil { - return x.Namespace - } - return nil -} - -func (x *NamespaceCertificate) GetCertificateId() string { - if x != nil { - return x.CertificateId - } - return "" -} - -type AssignCertificateToNamespaceRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Required - namespace identifier (id or fqn) - Namespace *common.IdFqnIdentifier `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - // Required - PEM format certificate - Pem string `protobuf:"bytes,2,opt,name=pem,proto3" json:"pem,omitempty"` - // Optional - Metadata *common.MetadataMutable `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` -} - -func (x *AssignCertificateToNamespaceRequest) Reset() { - *x = AssignCertificateToNamespaceRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AssignCertificateToNamespaceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AssignCertificateToNamespaceRequest) ProtoMessage() {} - -func (x *AssignCertificateToNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AssignCertificateToNamespaceRequest.ProtoReflect.Descriptor instead. -func (*AssignCertificateToNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{21} -} - -func (x *AssignCertificateToNamespaceRequest) GetNamespace() *common.IdFqnIdentifier { - if x != nil { - return x.Namespace - } - return nil -} - -func (x *AssignCertificateToNamespaceRequest) GetPem() string { - if x != nil { - return x.Pem - } - return "" -} - -func (x *AssignCertificateToNamespaceRequest) GetMetadata() *common.MetadataMutable { - if x != nil { - return x.Metadata - } - return nil -} - -type AssignCertificateToNamespaceResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The mapping of the namespace to the certificate. - NamespaceCertificate *NamespaceCertificate `protobuf:"bytes,1,opt,name=namespace_certificate,json=namespaceCertificate,proto3" json:"namespace_certificate,omitempty"` - Certificate *policy.Certificate `protobuf:"bytes,2,opt,name=certificate,proto3" json:"certificate,omitempty"` // Return the full certificate object for convenience -} - -func (x *AssignCertificateToNamespaceResponse) Reset() { - *x = AssignCertificateToNamespaceResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AssignCertificateToNamespaceResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AssignCertificateToNamespaceResponse) ProtoMessage() {} - -func (x *AssignCertificateToNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AssignCertificateToNamespaceResponse.ProtoReflect.Descriptor instead. -func (*AssignCertificateToNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{22} -} - -func (x *AssignCertificateToNamespaceResponse) GetNamespaceCertificate() *NamespaceCertificate { - if x != nil { - return x.NamespaceCertificate - } - return nil -} - -func (x *AssignCertificateToNamespaceResponse) GetCertificate() *policy.Certificate { - if x != nil { - return x.Certificate - } - return nil -} - -type RemoveCertificateFromNamespaceRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The namespace and certificate to unassign. - NamespaceCertificate *NamespaceCertificate `protobuf:"bytes,1,opt,name=namespace_certificate,json=namespaceCertificate,proto3" json:"namespace_certificate,omitempty"` -} - -func (x *RemoveCertificateFromNamespaceRequest) Reset() { - *x = RemoveCertificateFromNamespaceRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *RemoveCertificateFromNamespaceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RemoveCertificateFromNamespaceRequest) ProtoMessage() {} - -func (x *RemoveCertificateFromNamespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[23] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RemoveCertificateFromNamespaceRequest.ProtoReflect.Descriptor instead. -func (*RemoveCertificateFromNamespaceRequest) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{23} -} - -func (x *RemoveCertificateFromNamespaceRequest) GetNamespaceCertificate() *NamespaceCertificate { - if x != nil { - return x.NamespaceCertificate - } - return nil -} - -type RemoveCertificateFromNamespaceResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The unassigned namespace and certificate. - NamespaceCertificate *NamespaceCertificate `protobuf:"bytes,1,opt,name=namespace_certificate,json=namespaceCertificate,proto3" json:"namespace_certificate,omitempty"` -} - -func (x *RemoveCertificateFromNamespaceResponse) Reset() { - *x = RemoveCertificateFromNamespaceResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *RemoveCertificateFromNamespaceResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RemoveCertificateFromNamespaceResponse) ProtoMessage() {} - -func (x *RemoveCertificateFromNamespaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_namespaces_namespaces_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RemoveCertificateFromNamespaceResponse.ProtoReflect.Descriptor instead. -func (*RemoveCertificateFromNamespaceResponse) Descriptor() ([]byte, []int) { - return file_policy_namespaces_namespaces_proto_rawDescGZIP(), []int{24} -} - -func (x *RemoveCertificateFromNamespaceResponse) GetNamespaceCertificate() *NamespaceCertificate { - if x != nil { - return x.NamespaceCertificate - } - return nil -} - var File_policy_namespaces_namespaces_proto protoreflect.FileDescriptor var file_policy_namespaces_namespaces_proto_rawDesc = []byte{ @@ -1414,313 +1261,271 @@ var file_policy_namespaces_namespaces_proto_rawDesc = []byte{ 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x7b, - 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, - 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x81, 0x01, 0x0a, 0x16, - 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x0a, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0xfe, 0x04, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xae, 0x04, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x99, 0x04, 0xba, 0x48, 0x95, 0x04, - 0xba, 0x01, 0x89, 0x04, 0x0a, 0x10, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xa1, 0x03, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x20, 0x49, 0x74, 0x20, - 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x20, 0x61, - 0x74, 0x20, 0x6c, 0x65, 0x61, 0x73, 0x74, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x64, 0x6f, 0x74, 0x2c, - 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x65, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, - 0x6e, 0x74, 0x20, 0x28, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x29, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x69, 0x6e, 0x67, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x77, - 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, - 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x45, - 0x61, 0x63, 0x68, 0x20, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, - 0x65, 0x20, 0x31, 0x20, 0x74, 0x6f, 0x20, 0x36, 0x33, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, - 0x74, 0x65, 0x72, 0x73, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, - 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, - 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, - 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, - 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x74, 0x6f, 0x70, 0x2d, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x20, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x20, 0x28, 0x74, 0x68, 0x65, 0x20, 0x6c, 0x61, - 0x73, 0x74, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x20, 0x64, 0x6f, 0x74, 0x29, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, - 0x61, 0x74, 0x20, 0x6c, 0x65, 0x61, 0x73, 0x74, 0x20, 0x74, 0x77, 0x6f, 0x20, 0x61, 0x6c, 0x70, - 0x68, 0x61, 0x62, 0x65, 0x74, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, - 0x72, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, - 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, - 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x51, 0x74, 0x68, 0x69, 0x73, - 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x28, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, - 0x2d, 0x39, 0x5c, 0x5c, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x5c, 0x5c, 0x2e, 0x29, 0x2b, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x5d, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, - 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, - 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x4a, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xbd, 0x01, 0x0a, - 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, - 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x4a, 0x0a, 0x17, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x36, 0x0a, 0x1a, 0x44, 0x65, 0x61, 0x63, - 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, - 0x22, 0x1d, 0x0a, 0x1b, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x99, 0x01, 0x0a, 0x27, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x6a, 0x0a, 0x1b, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x96, 0x01, 0x0a, 0x28, - 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x1b, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x22, 0x9b, 0x01, 0x0a, 0x29, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, - 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x6a, 0x0a, 0x1b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, - 0x18, 0x01, 0x22, 0x98, 0x01, 0x0a, 0x2a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x6a, 0x0a, 0x1b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, - 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x52, 0x18, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x71, 0x0a, - 0x21, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x4c, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, - 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, - 0x22, 0x6a, 0x0a, 0x22, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x0c, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x73, 0x0a, 0x23, - 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, - 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x4c, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, - 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, - 0x79, 0x22, 0x6c, 0x0a, 0x24, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, - 0x79, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x22, - 0x89, 0x01, 0x0a, 0x14, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x3d, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x64, 0x46, 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x32, 0x0a, 0x0e, 0x63, 0x65, 0x72, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0d, 0x63, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x49, 0x64, 0x22, 0xb3, 0x01, 0x0a, 0x23, - 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x3d, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x49, 0x64, 0x46, 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, - 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x12, 0x18, 0x0a, 0x03, 0x70, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x03, 0x70, 0x65, 0x6d, 0x12, 0x33, 0x0a, 0x08, + 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x96, + 0x01, 0x0a, 0x0e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x53, 0x6f, 0x72, + 0x74, 0x12, 0x45, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x73, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x73, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, + 0x01, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xbc, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3f, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, + 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0x81, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x31, 0x0a, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, + 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xfe, 0x04, 0x0a, 0x16, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xae, 0x04, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x99, 0x04, 0xba, 0x48, 0x95, 0x04, 0xba, 0x01, 0x89, 0x04, 0x0a, + 0x10, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x12, 0xa1, 0x03, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x20, 0x6d, 0x75, + 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x73, 0x68, 0x6f, 0x75, 0x6c, + 0x64, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x20, 0x61, 0x74, 0x20, 0x6c, 0x65, 0x61, + 0x73, 0x74, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x64, 0x6f, 0x74, 0x2c, 0x20, 0x77, 0x69, 0x74, 0x68, + 0x20, 0x65, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x28, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x29, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x61, + 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, + 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, + 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x45, 0x61, 0x63, 0x68, 0x20, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x31, 0x20, 0x74, + 0x6f, 0x20, 0x36, 0x33, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x20, + 0x6c, 0x6f, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, + 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, + 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, + 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, + 0x68, 0x65, 0x20, 0x74, 0x6f, 0x70, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x20, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x20, 0x28, 0x74, 0x68, 0x65, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x73, 0x65, + 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x20, 0x64, 0x6f, 0x74, 0x29, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, + 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x74, 0x20, 0x6c, 0x65, + 0x61, 0x73, 0x74, 0x20, 0x74, 0x77, 0x6f, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x62, 0x65, 0x74, + 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2e, 0x20, 0x54, + 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, + 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, + 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x51, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, + 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, 0x2d, + 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, + 0x39, 0x5d, 0x29, 0x3f, 0x5c, 0x5c, 0x2e, 0x29, 0x2b, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, + 0x5d, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x4a, 0x0a, 0x17, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xbd, 0x01, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x22, 0xbb, 0x01, 0x0a, 0x24, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x43, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x15, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x52, 0x14, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, - 0x8d, 0x01, 0x0a, 0x25, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x64, 0x0a, 0x15, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, + 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, + 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x4a, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x22, 0x36, 0x0a, 0x1a, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1d, 0x0a, 0x1b, 0x44, + 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x99, 0x01, 0x0a, 0x27, 0x41, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x6a, 0x0a, 0x1b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x96, 0x01, 0x0a, 0x28, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x1b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x14, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, - 0x86, 0x01, 0x0a, 0x26, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x15, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x52, 0x14, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x32, 0xd0, 0x0b, 0x0a, 0x10, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x64, 0x0a, - 0x0c, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x26, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, - 0x90, 0x02, 0x01, 0x12, 0x6a, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, + 0x9b, 0x01, 0x0a, 0x29, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x6a, 0x0a, + 0x1b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, + 0x18, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0x98, 0x01, + 0x0a, 0x2a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x1b, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x18, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x71, 0x0a, 0x21, 0x41, 0x73, 0x73, 0x69, + 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x4e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4c, 0x0a, + 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x6a, 0x0a, 0x22, 0x41, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x73, 0x0a, 0x23, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4c, + 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x6c, 0x0a, 0x24, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, + 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x2a, 0xc1, 0x01, 0x0a, 0x12, 0x53, + 0x6f, 0x72, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x53, 0x50, + 0x41, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f, 0x52, 0x54, 0x5f, + 0x4e, 0x41, 0x4d, 0x45, 0x53, 0x50, 0x41, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4e, + 0x41, 0x4d, 0x45, 0x53, 0x50, 0x41, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, + 0x51, 0x4e, 0x10, 0x02, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4e, 0x41, 0x4d, + 0x45, 0x53, 0x50, 0x41, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, + 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x4f, 0x52, + 0x54, 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x53, 0x50, 0x41, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x04, 0x32, 0xa2, + 0x09, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x64, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x12, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, + 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x6a, 0x0a, 0x0e, 0x4c, 0x69, 0x73, + 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, - 0x6a, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x12, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6a, 0x0a, 0x0f, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x76, 0x0a, 0x13, 0x44, 0x65, 0x61, 0x63, 0x74, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2d, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0xa0, 0x01, 0x0a, 0x20, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x12, 0x3a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, - 0x02, 0x01, 0x12, 0xa6, 0x01, 0x0a, 0x22, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x65, - 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, - 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x8b, 0x01, 0x0a, 0x1a, - 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, - 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x6a, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x6a, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x76, 0x0a, + 0x13, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x2d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa0, 0x01, 0x0a, 0x20, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, + 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, + 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x3a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x41, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0xa6, 0x01, 0x0a, 0x22, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, + 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, + 0x01, 0x12, 0x8b, 0x01, 0x0a, 0x1a, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x91, 0x01, 0x0a, 0x1c, 0x52, 0x65, - 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, - 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x36, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x52, - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, - 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x91, 0x01, - 0x0a, 0x1c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x36, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, - 0x6e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x97, 0x01, 0x0a, 0x1e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x12, 0x38, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xc8, 0x01, 0x0a, 0x15, - 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x73, 0x42, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, - 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, - 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x73, 0xa2, 0x02, 0x03, 0x50, 0x4e, 0x58, 0xaa, 0x02, 0x11, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0xca, 0x02, 0x11, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0xe2, 0x02, 0x1d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0xea, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x4e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x54, 0x6f, 0x4e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x91, 0x01, 0x0a, 0x1c, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x12, 0x36, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x46, 0x72, 0x6f, 0x6d, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x42, 0xc8, 0x01, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x42, 0x0f, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, + 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0xa2, 0x02, 0x03, 0x50, 0x4e, + 0x58, 0xaa, 0x02, 0x11, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x73, 0xca, 0x02, 0x11, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0xe2, 0x02, 0x1d, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x5c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x5c, 0x47, 0x50, + 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x3a, 0x3a, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1735,95 +1540,84 @@ func file_policy_namespaces_namespaces_proto_rawDescGZIP() []byte { return file_policy_namespaces_namespaces_proto_rawDescData } -var file_policy_namespaces_namespaces_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_policy_namespaces_namespaces_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_policy_namespaces_namespaces_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_policy_namespaces_namespaces_proto_goTypes = []interface{}{ - (*NamespaceKeyAccessServer)(nil), // 0: policy.namespaces.NamespaceKeyAccessServer - (*NamespaceKey)(nil), // 1: policy.namespaces.NamespaceKey - (*GetNamespaceRequest)(nil), // 2: policy.namespaces.GetNamespaceRequest - (*GetNamespaceResponse)(nil), // 3: policy.namespaces.GetNamespaceResponse - (*ListNamespacesRequest)(nil), // 4: policy.namespaces.ListNamespacesRequest - (*ListNamespacesResponse)(nil), // 5: policy.namespaces.ListNamespacesResponse - (*CreateNamespaceRequest)(nil), // 6: policy.namespaces.CreateNamespaceRequest - (*CreateNamespaceResponse)(nil), // 7: policy.namespaces.CreateNamespaceResponse - (*UpdateNamespaceRequest)(nil), // 8: policy.namespaces.UpdateNamespaceRequest - (*UpdateNamespaceResponse)(nil), // 9: policy.namespaces.UpdateNamespaceResponse - (*DeactivateNamespaceRequest)(nil), // 10: policy.namespaces.DeactivateNamespaceRequest - (*DeactivateNamespaceResponse)(nil), // 11: policy.namespaces.DeactivateNamespaceResponse - (*AssignKeyAccessServerToNamespaceRequest)(nil), // 12: policy.namespaces.AssignKeyAccessServerToNamespaceRequest - (*AssignKeyAccessServerToNamespaceResponse)(nil), // 13: policy.namespaces.AssignKeyAccessServerToNamespaceResponse - (*RemoveKeyAccessServerFromNamespaceRequest)(nil), // 14: policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest - (*RemoveKeyAccessServerFromNamespaceResponse)(nil), // 15: policy.namespaces.RemoveKeyAccessServerFromNamespaceResponse - (*AssignPublicKeyToNamespaceRequest)(nil), // 16: policy.namespaces.AssignPublicKeyToNamespaceRequest - (*AssignPublicKeyToNamespaceResponse)(nil), // 17: policy.namespaces.AssignPublicKeyToNamespaceResponse - (*RemovePublicKeyFromNamespaceRequest)(nil), // 18: policy.namespaces.RemovePublicKeyFromNamespaceRequest - (*RemovePublicKeyFromNamespaceResponse)(nil), // 19: policy.namespaces.RemovePublicKeyFromNamespaceResponse - (*NamespaceCertificate)(nil), // 20: policy.namespaces.NamespaceCertificate - (*AssignCertificateToNamespaceRequest)(nil), // 21: policy.namespaces.AssignCertificateToNamespaceRequest - (*AssignCertificateToNamespaceResponse)(nil), // 22: policy.namespaces.AssignCertificateToNamespaceResponse - (*RemoveCertificateFromNamespaceRequest)(nil), // 23: policy.namespaces.RemoveCertificateFromNamespaceRequest - (*RemoveCertificateFromNamespaceResponse)(nil), // 24: policy.namespaces.RemoveCertificateFromNamespaceResponse - (*policy.Namespace)(nil), // 25: policy.Namespace - (common.ActiveStateEnum)(0), // 26: common.ActiveStateEnum - (*policy.PageRequest)(nil), // 27: policy.PageRequest - (*policy.PageResponse)(nil), // 28: policy.PageResponse - (*common.MetadataMutable)(nil), // 29: common.MetadataMutable - (common.MetadataUpdateEnum)(0), // 30: common.MetadataUpdateEnum - (*common.IdFqnIdentifier)(nil), // 31: common.IdFqnIdentifier - (*policy.Certificate)(nil), // 32: policy.Certificate + (SortNamespacesType)(0), // 0: policy.namespaces.SortNamespacesType + (*NamespaceKeyAccessServer)(nil), // 1: policy.namespaces.NamespaceKeyAccessServer + (*NamespaceKey)(nil), // 2: policy.namespaces.NamespaceKey + (*GetNamespaceRequest)(nil), // 3: policy.namespaces.GetNamespaceRequest + (*GetNamespaceResponse)(nil), // 4: policy.namespaces.GetNamespaceResponse + (*NamespacesSort)(nil), // 5: policy.namespaces.NamespacesSort + (*ListNamespacesRequest)(nil), // 6: policy.namespaces.ListNamespacesRequest + (*ListNamespacesResponse)(nil), // 7: policy.namespaces.ListNamespacesResponse + (*CreateNamespaceRequest)(nil), // 8: policy.namespaces.CreateNamespaceRequest + (*CreateNamespaceResponse)(nil), // 9: policy.namespaces.CreateNamespaceResponse + (*UpdateNamespaceRequest)(nil), // 10: policy.namespaces.UpdateNamespaceRequest + (*UpdateNamespaceResponse)(nil), // 11: policy.namespaces.UpdateNamespaceResponse + (*DeactivateNamespaceRequest)(nil), // 12: policy.namespaces.DeactivateNamespaceRequest + (*DeactivateNamespaceResponse)(nil), // 13: policy.namespaces.DeactivateNamespaceResponse + (*AssignKeyAccessServerToNamespaceRequest)(nil), // 14: policy.namespaces.AssignKeyAccessServerToNamespaceRequest + (*AssignKeyAccessServerToNamespaceResponse)(nil), // 15: policy.namespaces.AssignKeyAccessServerToNamespaceResponse + (*RemoveKeyAccessServerFromNamespaceRequest)(nil), // 16: policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest + (*RemoveKeyAccessServerFromNamespaceResponse)(nil), // 17: policy.namespaces.RemoveKeyAccessServerFromNamespaceResponse + (*AssignPublicKeyToNamespaceRequest)(nil), // 18: policy.namespaces.AssignPublicKeyToNamespaceRequest + (*AssignPublicKeyToNamespaceResponse)(nil), // 19: policy.namespaces.AssignPublicKeyToNamespaceResponse + (*RemovePublicKeyFromNamespaceRequest)(nil), // 20: policy.namespaces.RemovePublicKeyFromNamespaceRequest + (*RemovePublicKeyFromNamespaceResponse)(nil), // 21: policy.namespaces.RemovePublicKeyFromNamespaceResponse + (*policy.Namespace)(nil), // 22: policy.Namespace + (policy.SortDirection)(0), // 23: policy.SortDirection + (common.ActiveStateEnum)(0), // 24: common.ActiveStateEnum + (*policy.PageRequest)(nil), // 25: policy.PageRequest + (*policy.PageResponse)(nil), // 26: policy.PageResponse + (*common.MetadataMutable)(nil), // 27: common.MetadataMutable + (common.MetadataUpdateEnum)(0), // 28: common.MetadataUpdateEnum } var file_policy_namespaces_namespaces_proto_depIdxs = []int32{ - 25, // 0: policy.namespaces.GetNamespaceResponse.namespace:type_name -> policy.Namespace - 26, // 1: policy.namespaces.ListNamespacesRequest.state:type_name -> common.ActiveStateEnum - 27, // 2: policy.namespaces.ListNamespacesRequest.pagination:type_name -> policy.PageRequest - 25, // 3: policy.namespaces.ListNamespacesResponse.namespaces:type_name -> policy.Namespace - 28, // 4: policy.namespaces.ListNamespacesResponse.pagination:type_name -> policy.PageResponse - 29, // 5: policy.namespaces.CreateNamespaceRequest.metadata:type_name -> common.MetadataMutable - 25, // 6: policy.namespaces.CreateNamespaceResponse.namespace:type_name -> policy.Namespace - 29, // 7: policy.namespaces.UpdateNamespaceRequest.metadata:type_name -> common.MetadataMutable - 30, // 8: policy.namespaces.UpdateNamespaceRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 25, // 9: policy.namespaces.UpdateNamespaceResponse.namespace:type_name -> policy.Namespace - 0, // 10: policy.namespaces.AssignKeyAccessServerToNamespaceRequest.namespace_key_access_server:type_name -> policy.namespaces.NamespaceKeyAccessServer - 0, // 11: policy.namespaces.AssignKeyAccessServerToNamespaceResponse.namespace_key_access_server:type_name -> policy.namespaces.NamespaceKeyAccessServer - 0, // 12: policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest.namespace_key_access_server:type_name -> policy.namespaces.NamespaceKeyAccessServer - 0, // 13: policy.namespaces.RemoveKeyAccessServerFromNamespaceResponse.namespace_key_access_server:type_name -> policy.namespaces.NamespaceKeyAccessServer - 1, // 14: policy.namespaces.AssignPublicKeyToNamespaceRequest.namespace_key:type_name -> policy.namespaces.NamespaceKey - 1, // 15: policy.namespaces.AssignPublicKeyToNamespaceResponse.namespace_key:type_name -> policy.namespaces.NamespaceKey - 1, // 16: policy.namespaces.RemovePublicKeyFromNamespaceRequest.namespace_key:type_name -> policy.namespaces.NamespaceKey - 1, // 17: policy.namespaces.RemovePublicKeyFromNamespaceResponse.namespace_key:type_name -> policy.namespaces.NamespaceKey - 31, // 18: policy.namespaces.NamespaceCertificate.namespace:type_name -> common.IdFqnIdentifier - 31, // 19: policy.namespaces.AssignCertificateToNamespaceRequest.namespace:type_name -> common.IdFqnIdentifier - 29, // 20: policy.namespaces.AssignCertificateToNamespaceRequest.metadata:type_name -> common.MetadataMutable - 20, // 21: policy.namespaces.AssignCertificateToNamespaceResponse.namespace_certificate:type_name -> policy.namespaces.NamespaceCertificate - 32, // 22: policy.namespaces.AssignCertificateToNamespaceResponse.certificate:type_name -> policy.Certificate - 20, // 23: policy.namespaces.RemoveCertificateFromNamespaceRequest.namespace_certificate:type_name -> policy.namespaces.NamespaceCertificate - 20, // 24: policy.namespaces.RemoveCertificateFromNamespaceResponse.namespace_certificate:type_name -> policy.namespaces.NamespaceCertificate - 2, // 25: policy.namespaces.NamespaceService.GetNamespace:input_type -> policy.namespaces.GetNamespaceRequest - 4, // 26: policy.namespaces.NamespaceService.ListNamespaces:input_type -> policy.namespaces.ListNamespacesRequest - 6, // 27: policy.namespaces.NamespaceService.CreateNamespace:input_type -> policy.namespaces.CreateNamespaceRequest - 8, // 28: policy.namespaces.NamespaceService.UpdateNamespace:input_type -> policy.namespaces.UpdateNamespaceRequest - 10, // 29: policy.namespaces.NamespaceService.DeactivateNamespace:input_type -> policy.namespaces.DeactivateNamespaceRequest - 12, // 30: policy.namespaces.NamespaceService.AssignKeyAccessServerToNamespace:input_type -> policy.namespaces.AssignKeyAccessServerToNamespaceRequest - 14, // 31: policy.namespaces.NamespaceService.RemoveKeyAccessServerFromNamespace:input_type -> policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest - 16, // 32: policy.namespaces.NamespaceService.AssignPublicKeyToNamespace:input_type -> policy.namespaces.AssignPublicKeyToNamespaceRequest - 18, // 33: policy.namespaces.NamespaceService.RemovePublicKeyFromNamespace:input_type -> policy.namespaces.RemovePublicKeyFromNamespaceRequest - 21, // 34: policy.namespaces.NamespaceService.AssignCertificateToNamespace:input_type -> policy.namespaces.AssignCertificateToNamespaceRequest - 23, // 35: policy.namespaces.NamespaceService.RemoveCertificateFromNamespace:input_type -> policy.namespaces.RemoveCertificateFromNamespaceRequest - 3, // 36: policy.namespaces.NamespaceService.GetNamespace:output_type -> policy.namespaces.GetNamespaceResponse - 5, // 37: policy.namespaces.NamespaceService.ListNamespaces:output_type -> policy.namespaces.ListNamespacesResponse - 7, // 38: policy.namespaces.NamespaceService.CreateNamespace:output_type -> policy.namespaces.CreateNamespaceResponse - 9, // 39: policy.namespaces.NamespaceService.UpdateNamespace:output_type -> policy.namespaces.UpdateNamespaceResponse - 11, // 40: policy.namespaces.NamespaceService.DeactivateNamespace:output_type -> policy.namespaces.DeactivateNamespaceResponse - 13, // 41: policy.namespaces.NamespaceService.AssignKeyAccessServerToNamespace:output_type -> policy.namespaces.AssignKeyAccessServerToNamespaceResponse - 15, // 42: policy.namespaces.NamespaceService.RemoveKeyAccessServerFromNamespace:output_type -> policy.namespaces.RemoveKeyAccessServerFromNamespaceResponse - 17, // 43: policy.namespaces.NamespaceService.AssignPublicKeyToNamespace:output_type -> policy.namespaces.AssignPublicKeyToNamespaceResponse - 19, // 44: policy.namespaces.NamespaceService.RemovePublicKeyFromNamespace:output_type -> policy.namespaces.RemovePublicKeyFromNamespaceResponse - 22, // 45: policy.namespaces.NamespaceService.AssignCertificateToNamespace:output_type -> policy.namespaces.AssignCertificateToNamespaceResponse - 24, // 46: policy.namespaces.NamespaceService.RemoveCertificateFromNamespace:output_type -> policy.namespaces.RemoveCertificateFromNamespaceResponse - 36, // [36:47] is the sub-list for method output_type - 25, // [25:36] is the sub-list for method input_type - 25, // [25:25] is the sub-list for extension type_name - 25, // [25:25] is the sub-list for extension extendee - 0, // [0:25] is the sub-list for field type_name + 22, // 0: policy.namespaces.GetNamespaceResponse.namespace:type_name -> policy.Namespace + 0, // 1: policy.namespaces.NamespacesSort.field:type_name -> policy.namespaces.SortNamespacesType + 23, // 2: policy.namespaces.NamespacesSort.direction:type_name -> policy.SortDirection + 24, // 3: policy.namespaces.ListNamespacesRequest.state:type_name -> common.ActiveStateEnum + 25, // 4: policy.namespaces.ListNamespacesRequest.pagination:type_name -> policy.PageRequest + 5, // 5: policy.namespaces.ListNamespacesRequest.sort:type_name -> policy.namespaces.NamespacesSort + 22, // 6: policy.namespaces.ListNamespacesResponse.namespaces:type_name -> policy.Namespace + 26, // 7: policy.namespaces.ListNamespacesResponse.pagination:type_name -> policy.PageResponse + 27, // 8: policy.namespaces.CreateNamespaceRequest.metadata:type_name -> common.MetadataMutable + 22, // 9: policy.namespaces.CreateNamespaceResponse.namespace:type_name -> policy.Namespace + 27, // 10: policy.namespaces.UpdateNamespaceRequest.metadata:type_name -> common.MetadataMutable + 28, // 11: policy.namespaces.UpdateNamespaceRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 22, // 12: policy.namespaces.UpdateNamespaceResponse.namespace:type_name -> policy.Namespace + 1, // 13: policy.namespaces.AssignKeyAccessServerToNamespaceRequest.namespace_key_access_server:type_name -> policy.namespaces.NamespaceKeyAccessServer + 1, // 14: policy.namespaces.AssignKeyAccessServerToNamespaceResponse.namespace_key_access_server:type_name -> policy.namespaces.NamespaceKeyAccessServer + 1, // 15: policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest.namespace_key_access_server:type_name -> policy.namespaces.NamespaceKeyAccessServer + 1, // 16: policy.namespaces.RemoveKeyAccessServerFromNamespaceResponse.namespace_key_access_server:type_name -> policy.namespaces.NamespaceKeyAccessServer + 2, // 17: policy.namespaces.AssignPublicKeyToNamespaceRequest.namespace_key:type_name -> policy.namespaces.NamespaceKey + 2, // 18: policy.namespaces.AssignPublicKeyToNamespaceResponse.namespace_key:type_name -> policy.namespaces.NamespaceKey + 2, // 19: policy.namespaces.RemovePublicKeyFromNamespaceRequest.namespace_key:type_name -> policy.namespaces.NamespaceKey + 2, // 20: policy.namespaces.RemovePublicKeyFromNamespaceResponse.namespace_key:type_name -> policy.namespaces.NamespaceKey + 3, // 21: policy.namespaces.NamespaceService.GetNamespace:input_type -> policy.namespaces.GetNamespaceRequest + 6, // 22: policy.namespaces.NamespaceService.ListNamespaces:input_type -> policy.namespaces.ListNamespacesRequest + 8, // 23: policy.namespaces.NamespaceService.CreateNamespace:input_type -> policy.namespaces.CreateNamespaceRequest + 10, // 24: policy.namespaces.NamespaceService.UpdateNamespace:input_type -> policy.namespaces.UpdateNamespaceRequest + 12, // 25: policy.namespaces.NamespaceService.DeactivateNamespace:input_type -> policy.namespaces.DeactivateNamespaceRequest + 14, // 26: policy.namespaces.NamespaceService.AssignKeyAccessServerToNamespace:input_type -> policy.namespaces.AssignKeyAccessServerToNamespaceRequest + 16, // 27: policy.namespaces.NamespaceService.RemoveKeyAccessServerFromNamespace:input_type -> policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest + 18, // 28: policy.namespaces.NamespaceService.AssignPublicKeyToNamespace:input_type -> policy.namespaces.AssignPublicKeyToNamespaceRequest + 20, // 29: policy.namespaces.NamespaceService.RemovePublicKeyFromNamespace:input_type -> policy.namespaces.RemovePublicKeyFromNamespaceRequest + 4, // 30: policy.namespaces.NamespaceService.GetNamespace:output_type -> policy.namespaces.GetNamespaceResponse + 7, // 31: policy.namespaces.NamespaceService.ListNamespaces:output_type -> policy.namespaces.ListNamespacesResponse + 9, // 32: policy.namespaces.NamespaceService.CreateNamespace:output_type -> policy.namespaces.CreateNamespaceResponse + 11, // 33: policy.namespaces.NamespaceService.UpdateNamespace:output_type -> policy.namespaces.UpdateNamespaceResponse + 13, // 34: policy.namespaces.NamespaceService.DeactivateNamespace:output_type -> policy.namespaces.DeactivateNamespaceResponse + 15, // 35: policy.namespaces.NamespaceService.AssignKeyAccessServerToNamespace:output_type -> policy.namespaces.AssignKeyAccessServerToNamespaceResponse + 17, // 36: policy.namespaces.NamespaceService.RemoveKeyAccessServerFromNamespace:output_type -> policy.namespaces.RemoveKeyAccessServerFromNamespaceResponse + 19, // 37: policy.namespaces.NamespaceService.AssignPublicKeyToNamespace:output_type -> policy.namespaces.AssignPublicKeyToNamespaceResponse + 21, // 38: policy.namespaces.NamespaceService.RemovePublicKeyFromNamespace:output_type -> policy.namespaces.RemovePublicKeyFromNamespaceResponse + 30, // [30:39] is the sub-list for method output_type + 21, // [21:30] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_policy_namespaces_namespaces_proto_init() } @@ -1881,7 +1675,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListNamespacesRequest); i { + switch v := v.(*NamespacesSort); i { case 0: return &v.state case 1: @@ -1893,7 +1687,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListNamespacesResponse); i { + switch v := v.(*ListNamespacesRequest); i { case 0: return &v.state case 1: @@ -1905,7 +1699,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateNamespaceRequest); i { + switch v := v.(*ListNamespacesResponse); i { case 0: return &v.state case 1: @@ -1917,7 +1711,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateNamespaceResponse); i { + switch v := v.(*CreateNamespaceRequest); i { case 0: return &v.state case 1: @@ -1929,7 +1723,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateNamespaceRequest); i { + switch v := v.(*CreateNamespaceResponse); i { case 0: return &v.state case 1: @@ -1941,7 +1735,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateNamespaceResponse); i { + switch v := v.(*UpdateNamespaceRequest); i { case 0: return &v.state case 1: @@ -1953,7 +1747,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeactivateNamespaceRequest); i { + switch v := v.(*UpdateNamespaceResponse); i { case 0: return &v.state case 1: @@ -1965,7 +1759,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeactivateNamespaceResponse); i { + switch v := v.(*DeactivateNamespaceRequest); i { case 0: return &v.state case 1: @@ -1977,7 +1771,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignKeyAccessServerToNamespaceRequest); i { + switch v := v.(*DeactivateNamespaceResponse); i { case 0: return &v.state case 1: @@ -1989,7 +1783,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignKeyAccessServerToNamespaceResponse); i { + switch v := v.(*AssignKeyAccessServerToNamespaceRequest); i { case 0: return &v.state case 1: @@ -2001,7 +1795,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveKeyAccessServerFromNamespaceRequest); i { + switch v := v.(*AssignKeyAccessServerToNamespaceResponse); i { case 0: return &v.state case 1: @@ -2013,7 +1807,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveKeyAccessServerFromNamespaceResponse); i { + switch v := v.(*RemoveKeyAccessServerFromNamespaceRequest); i { case 0: return &v.state case 1: @@ -2025,7 +1819,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignPublicKeyToNamespaceRequest); i { + switch v := v.(*RemoveKeyAccessServerFromNamespaceResponse); i { case 0: return &v.state case 1: @@ -2037,7 +1831,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignPublicKeyToNamespaceResponse); i { + switch v := v.(*AssignPublicKeyToNamespaceRequest); i { case 0: return &v.state case 1: @@ -2049,7 +1843,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemovePublicKeyFromNamespaceRequest); i { + switch v := v.(*AssignPublicKeyToNamespaceResponse); i { case 0: return &v.state case 1: @@ -2061,7 +1855,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemovePublicKeyFromNamespaceResponse); i { + switch v := v.(*RemovePublicKeyFromNamespaceRequest); i { case 0: return &v.state case 1: @@ -2073,55 +1867,7 @@ func file_policy_namespaces_namespaces_proto_init() { } } file_policy_namespaces_namespaces_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NamespaceCertificate); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_policy_namespaces_namespaces_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignCertificateToNamespaceRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_policy_namespaces_namespaces_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AssignCertificateToNamespaceResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_policy_namespaces_namespaces_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveCertificateFromNamespaceRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_policy_namespaces_namespaces_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveCertificateFromNamespaceResponse); i { + switch v := v.(*RemovePublicKeyFromNamespaceResponse); i { case 0: return &v.state case 1: @@ -2142,13 +1888,14 @@ func file_policy_namespaces_namespaces_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_policy_namespaces_namespaces_proto_rawDesc, - NumEnums: 0, - NumMessages: 25, + NumEnums: 1, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, GoTypes: file_policy_namespaces_namespaces_proto_goTypes, DependencyIndexes: file_policy_namespaces_namespaces_proto_depIdxs, + EnumInfos: file_policy_namespaces_namespaces_proto_enumTypes, MessageInfos: file_policy_namespaces_namespaces_proto_msgTypes, }.Build() File_policy_namespaces_namespaces_proto = out.File diff --git a/protocol/go/policy/namespaces/namespaces_grpc.pb.go b/protocol/go/policy/namespaces/namespaces_grpc.pb.go index bc3dce0428..7188a57a96 100644 --- a/protocol/go/policy/namespaces/namespaces_grpc.pb.go +++ b/protocol/go/policy/namespaces/namespaces_grpc.pb.go @@ -28,8 +28,6 @@ const ( NamespaceService_RemoveKeyAccessServerFromNamespace_FullMethodName = "/policy.namespaces.NamespaceService/RemoveKeyAccessServerFromNamespace" NamespaceService_AssignPublicKeyToNamespace_FullMethodName = "/policy.namespaces.NamespaceService/AssignPublicKeyToNamespace" NamespaceService_RemovePublicKeyFromNamespace_FullMethodName = "/policy.namespaces.NamespaceService/RemovePublicKeyFromNamespace" - NamespaceService_AssignCertificateToNamespace_FullMethodName = "/policy.namespaces.NamespaceService/AssignCertificateToNamespace" - NamespaceService_RemoveCertificateFromNamespace_FullMethodName = "/policy.namespaces.NamespaceService/RemoveCertificateFromNamespace" ) // NamespaceServiceClient is the client API for NamespaceService service. @@ -52,9 +50,6 @@ type NamespaceServiceClient interface { // --------------------------------------- AssignPublicKeyToNamespace(ctx context.Context, in *AssignPublicKeyToNamespaceRequest, opts ...grpc.CallOption) (*AssignPublicKeyToNamespaceResponse, error) RemovePublicKeyFromNamespace(ctx context.Context, in *RemovePublicKeyFromNamespaceRequest, opts ...grpc.CallOption) (*RemovePublicKeyFromNamespaceResponse, error) - // Namespace <> Certificate RPCs - AssignCertificateToNamespace(ctx context.Context, in *AssignCertificateToNamespaceRequest, opts ...grpc.CallOption) (*AssignCertificateToNamespaceResponse, error) - RemoveCertificateFromNamespace(ctx context.Context, in *RemoveCertificateFromNamespaceRequest, opts ...grpc.CallOption) (*RemoveCertificateFromNamespaceResponse, error) } type namespaceServiceClient struct { @@ -148,24 +143,6 @@ func (c *namespaceServiceClient) RemovePublicKeyFromNamespace(ctx context.Contex return out, nil } -func (c *namespaceServiceClient) AssignCertificateToNamespace(ctx context.Context, in *AssignCertificateToNamespaceRequest, opts ...grpc.CallOption) (*AssignCertificateToNamespaceResponse, error) { - out := new(AssignCertificateToNamespaceResponse) - err := c.cc.Invoke(ctx, NamespaceService_AssignCertificateToNamespace_FullMethodName, in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *namespaceServiceClient) RemoveCertificateFromNamespace(ctx context.Context, in *RemoveCertificateFromNamespaceRequest, opts ...grpc.CallOption) (*RemoveCertificateFromNamespaceResponse, error) { - out := new(RemoveCertificateFromNamespaceResponse) - err := c.cc.Invoke(ctx, NamespaceService_RemoveCertificateFromNamespace_FullMethodName, in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - // NamespaceServiceServer is the server API for NamespaceService service. // All implementations must embed UnimplementedNamespaceServiceServer // for forward compatibility @@ -186,9 +163,6 @@ type NamespaceServiceServer interface { // --------------------------------------- AssignPublicKeyToNamespace(context.Context, *AssignPublicKeyToNamespaceRequest) (*AssignPublicKeyToNamespaceResponse, error) RemovePublicKeyFromNamespace(context.Context, *RemovePublicKeyFromNamespaceRequest) (*RemovePublicKeyFromNamespaceResponse, error) - // Namespace <> Certificate RPCs - AssignCertificateToNamespace(context.Context, *AssignCertificateToNamespaceRequest) (*AssignCertificateToNamespaceResponse, error) - RemoveCertificateFromNamespace(context.Context, *RemoveCertificateFromNamespaceRequest) (*RemoveCertificateFromNamespaceResponse, error) mustEmbedUnimplementedNamespaceServiceServer() } @@ -223,12 +197,6 @@ func (UnimplementedNamespaceServiceServer) AssignPublicKeyToNamespace(context.Co func (UnimplementedNamespaceServiceServer) RemovePublicKeyFromNamespace(context.Context, *RemovePublicKeyFromNamespaceRequest) (*RemovePublicKeyFromNamespaceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RemovePublicKeyFromNamespace not implemented") } -func (UnimplementedNamespaceServiceServer) AssignCertificateToNamespace(context.Context, *AssignCertificateToNamespaceRequest) (*AssignCertificateToNamespaceResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method AssignCertificateToNamespace not implemented") -} -func (UnimplementedNamespaceServiceServer) RemoveCertificateFromNamespace(context.Context, *RemoveCertificateFromNamespaceRequest) (*RemoveCertificateFromNamespaceResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RemoveCertificateFromNamespace not implemented") -} func (UnimplementedNamespaceServiceServer) mustEmbedUnimplementedNamespaceServiceServer() {} // UnsafeNamespaceServiceServer may be embedded to opt out of forward compatibility for this service. @@ -404,42 +372,6 @@ func _NamespaceService_RemovePublicKeyFromNamespace_Handler(srv interface{}, ctx return interceptor(ctx, in, info, handler) } -func _NamespaceService_AssignCertificateToNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(AssignCertificateToNamespaceRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(NamespaceServiceServer).AssignCertificateToNamespace(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: NamespaceService_AssignCertificateToNamespace_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(NamespaceServiceServer).AssignCertificateToNamespace(ctx, req.(*AssignCertificateToNamespaceRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _NamespaceService_RemoveCertificateFromNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RemoveCertificateFromNamespaceRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(NamespaceServiceServer).RemoveCertificateFromNamespace(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: NamespaceService_RemoveCertificateFromNamespace_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(NamespaceServiceServer).RemoveCertificateFromNamespace(ctx, req.(*RemoveCertificateFromNamespaceRequest)) - } - return interceptor(ctx, in, info, handler) -} - // NamespaceService_ServiceDesc is the grpc.ServiceDesc for NamespaceService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -483,14 +415,6 @@ var NamespaceService_ServiceDesc = grpc.ServiceDesc{ MethodName: "RemovePublicKeyFromNamespace", Handler: _NamespaceService_RemovePublicKeyFromNamespace_Handler, }, - { - MethodName: "AssignCertificateToNamespace", - Handler: _NamespaceService_AssignCertificateToNamespace_Handler, - }, - { - MethodName: "RemoveCertificateFromNamespace", - Handler: _NamespaceService_RemoveCertificateFromNamespace_Handler, - }, }, Streams: []grpc.StreamDesc{}, Metadata: "policy/namespaces/namespaces.proto", diff --git a/protocol/go/policy/namespaces/namespacesconnect/namespaces.connect.go b/protocol/go/policy/namespaces/namespacesconnect/namespaces.connect.go index 7a4782cad1..4cb7c34956 100644 --- a/protocol/go/policy/namespaces/namespacesconnect/namespaces.connect.go +++ b/protocol/go/policy/namespaces/namespacesconnect/namespaces.connect.go @@ -60,28 +60,6 @@ const ( // NamespaceServiceRemovePublicKeyFromNamespaceProcedure is the fully-qualified name of the // NamespaceService's RemovePublicKeyFromNamespace RPC. NamespaceServiceRemovePublicKeyFromNamespaceProcedure = "/policy.namespaces.NamespaceService/RemovePublicKeyFromNamespace" - // NamespaceServiceAssignCertificateToNamespaceProcedure is the fully-qualified name of the - // NamespaceService's AssignCertificateToNamespace RPC. - NamespaceServiceAssignCertificateToNamespaceProcedure = "/policy.namespaces.NamespaceService/AssignCertificateToNamespace" - // NamespaceServiceRemoveCertificateFromNamespaceProcedure is the fully-qualified name of the - // NamespaceService's RemoveCertificateFromNamespace RPC. - NamespaceServiceRemoveCertificateFromNamespaceProcedure = "/policy.namespaces.NamespaceService/RemoveCertificateFromNamespace" -) - -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - namespaceServiceServiceDescriptor = namespaces.File_policy_namespaces_namespaces_proto.Services().ByName("NamespaceService") - namespaceServiceGetNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("GetNamespace") - namespaceServiceListNamespacesMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("ListNamespaces") - namespaceServiceCreateNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("CreateNamespace") - namespaceServiceUpdateNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("UpdateNamespace") - namespaceServiceDeactivateNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("DeactivateNamespace") - namespaceServiceAssignKeyAccessServerToNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("AssignKeyAccessServerToNamespace") - namespaceServiceRemoveKeyAccessServerFromNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("RemoveKeyAccessServerFromNamespace") - namespaceServiceAssignPublicKeyToNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("AssignPublicKeyToNamespace") - namespaceServiceRemovePublicKeyFromNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("RemovePublicKeyFromNamespace") - namespaceServiceAssignCertificateToNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("AssignCertificateToNamespace") - namespaceServiceRemoveCertificateFromNamespaceMethodDescriptor = namespaceServiceServiceDescriptor.Methods().ByName("RemoveCertificateFromNamespace") ) // NamespaceServiceClient is a client for the policy.namespaces.NamespaceService service. @@ -104,9 +82,6 @@ type NamespaceServiceClient interface { // --------------------------------------- AssignPublicKeyToNamespace(context.Context, *connect.Request[namespaces.AssignPublicKeyToNamespaceRequest]) (*connect.Response[namespaces.AssignPublicKeyToNamespaceResponse], error) RemovePublicKeyFromNamespace(context.Context, *connect.Request[namespaces.RemovePublicKeyFromNamespaceRequest]) (*connect.Response[namespaces.RemovePublicKeyFromNamespaceResponse], error) - // Namespace <> Certificate RPCs - AssignCertificateToNamespace(context.Context, *connect.Request[namespaces.AssignCertificateToNamespaceRequest]) (*connect.Response[namespaces.AssignCertificateToNamespaceResponse], error) - RemoveCertificateFromNamespace(context.Context, *connect.Request[namespaces.RemoveCertificateFromNamespaceRequest]) (*connect.Response[namespaces.RemoveCertificateFromNamespaceResponse], error) } // NewNamespaceServiceClient constructs a client for the policy.namespaces.NamespaceService service. @@ -118,73 +93,62 @@ type NamespaceServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewNamespaceServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) NamespaceServiceClient { baseURL = strings.TrimRight(baseURL, "/") + namespaceServiceMethods := namespaces.File_policy_namespaces_namespaces_proto.Services().ByName("NamespaceService").Methods() return &namespaceServiceClient{ getNamespace: connect.NewClient[namespaces.GetNamespaceRequest, namespaces.GetNamespaceResponse]( httpClient, baseURL+NamespaceServiceGetNamespaceProcedure, - connect.WithSchema(namespaceServiceGetNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("GetNamespace")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), listNamespaces: connect.NewClient[namespaces.ListNamespacesRequest, namespaces.ListNamespacesResponse]( httpClient, baseURL+NamespaceServiceListNamespacesProcedure, - connect.WithSchema(namespaceServiceListNamespacesMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("ListNamespaces")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createNamespace: connect.NewClient[namespaces.CreateNamespaceRequest, namespaces.CreateNamespaceResponse]( httpClient, baseURL+NamespaceServiceCreateNamespaceProcedure, - connect.WithSchema(namespaceServiceCreateNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("CreateNamespace")), connect.WithClientOptions(opts...), ), updateNamespace: connect.NewClient[namespaces.UpdateNamespaceRequest, namespaces.UpdateNamespaceResponse]( httpClient, baseURL+NamespaceServiceUpdateNamespaceProcedure, - connect.WithSchema(namespaceServiceUpdateNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("UpdateNamespace")), connect.WithClientOptions(opts...), ), deactivateNamespace: connect.NewClient[namespaces.DeactivateNamespaceRequest, namespaces.DeactivateNamespaceResponse]( httpClient, baseURL+NamespaceServiceDeactivateNamespaceProcedure, - connect.WithSchema(namespaceServiceDeactivateNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("DeactivateNamespace")), connect.WithClientOptions(opts...), ), assignKeyAccessServerToNamespace: connect.NewClient[namespaces.AssignKeyAccessServerToNamespaceRequest, namespaces.AssignKeyAccessServerToNamespaceResponse]( httpClient, baseURL+NamespaceServiceAssignKeyAccessServerToNamespaceProcedure, - connect.WithSchema(namespaceServiceAssignKeyAccessServerToNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("AssignKeyAccessServerToNamespace")), connect.WithClientOptions(opts...), ), removeKeyAccessServerFromNamespace: connect.NewClient[namespaces.RemoveKeyAccessServerFromNamespaceRequest, namespaces.RemoveKeyAccessServerFromNamespaceResponse]( httpClient, baseURL+NamespaceServiceRemoveKeyAccessServerFromNamespaceProcedure, - connect.WithSchema(namespaceServiceRemoveKeyAccessServerFromNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("RemoveKeyAccessServerFromNamespace")), connect.WithClientOptions(opts...), ), assignPublicKeyToNamespace: connect.NewClient[namespaces.AssignPublicKeyToNamespaceRequest, namespaces.AssignPublicKeyToNamespaceResponse]( httpClient, baseURL+NamespaceServiceAssignPublicKeyToNamespaceProcedure, - connect.WithSchema(namespaceServiceAssignPublicKeyToNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("AssignPublicKeyToNamespace")), connect.WithClientOptions(opts...), ), removePublicKeyFromNamespace: connect.NewClient[namespaces.RemovePublicKeyFromNamespaceRequest, namespaces.RemovePublicKeyFromNamespaceResponse]( httpClient, baseURL+NamespaceServiceRemovePublicKeyFromNamespaceProcedure, - connect.WithSchema(namespaceServiceRemovePublicKeyFromNamespaceMethodDescriptor), - connect.WithClientOptions(opts...), - ), - assignCertificateToNamespace: connect.NewClient[namespaces.AssignCertificateToNamespaceRequest, namespaces.AssignCertificateToNamespaceResponse]( - httpClient, - baseURL+NamespaceServiceAssignCertificateToNamespaceProcedure, - connect.WithSchema(namespaceServiceAssignCertificateToNamespaceMethodDescriptor), - connect.WithClientOptions(opts...), - ), - removeCertificateFromNamespace: connect.NewClient[namespaces.RemoveCertificateFromNamespaceRequest, namespaces.RemoveCertificateFromNamespaceResponse]( - httpClient, - baseURL+NamespaceServiceRemoveCertificateFromNamespaceProcedure, - connect.WithSchema(namespaceServiceRemoveCertificateFromNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("RemovePublicKeyFromNamespace")), connect.WithClientOptions(opts...), ), } @@ -201,8 +165,6 @@ type namespaceServiceClient struct { removeKeyAccessServerFromNamespace *connect.Client[namespaces.RemoveKeyAccessServerFromNamespaceRequest, namespaces.RemoveKeyAccessServerFromNamespaceResponse] assignPublicKeyToNamespace *connect.Client[namespaces.AssignPublicKeyToNamespaceRequest, namespaces.AssignPublicKeyToNamespaceResponse] removePublicKeyFromNamespace *connect.Client[namespaces.RemovePublicKeyFromNamespaceRequest, namespaces.RemovePublicKeyFromNamespaceResponse] - assignCertificateToNamespace *connect.Client[namespaces.AssignCertificateToNamespaceRequest, namespaces.AssignCertificateToNamespaceResponse] - removeCertificateFromNamespace *connect.Client[namespaces.RemoveCertificateFromNamespaceRequest, namespaces.RemoveCertificateFromNamespaceResponse] } // GetNamespace calls policy.namespaces.NamespaceService.GetNamespace. @@ -257,18 +219,6 @@ func (c *namespaceServiceClient) RemovePublicKeyFromNamespace(ctx context.Contex return c.removePublicKeyFromNamespace.CallUnary(ctx, req) } -// AssignCertificateToNamespace calls -// policy.namespaces.NamespaceService.AssignCertificateToNamespace. -func (c *namespaceServiceClient) AssignCertificateToNamespace(ctx context.Context, req *connect.Request[namespaces.AssignCertificateToNamespaceRequest]) (*connect.Response[namespaces.AssignCertificateToNamespaceResponse], error) { - return c.assignCertificateToNamespace.CallUnary(ctx, req) -} - -// RemoveCertificateFromNamespace calls -// policy.namespaces.NamespaceService.RemoveCertificateFromNamespace. -func (c *namespaceServiceClient) RemoveCertificateFromNamespace(ctx context.Context, req *connect.Request[namespaces.RemoveCertificateFromNamespaceRequest]) (*connect.Response[namespaces.RemoveCertificateFromNamespaceResponse], error) { - return c.removeCertificateFromNamespace.CallUnary(ctx, req) -} - // NamespaceServiceHandler is an implementation of the policy.namespaces.NamespaceService service. type NamespaceServiceHandler interface { GetNamespace(context.Context, *connect.Request[namespaces.GetNamespaceRequest]) (*connect.Response[namespaces.GetNamespaceResponse], error) @@ -289,9 +239,6 @@ type NamespaceServiceHandler interface { // --------------------------------------- AssignPublicKeyToNamespace(context.Context, *connect.Request[namespaces.AssignPublicKeyToNamespaceRequest]) (*connect.Response[namespaces.AssignPublicKeyToNamespaceResponse], error) RemovePublicKeyFromNamespace(context.Context, *connect.Request[namespaces.RemovePublicKeyFromNamespaceRequest]) (*connect.Response[namespaces.RemovePublicKeyFromNamespaceResponse], error) - // Namespace <> Certificate RPCs - AssignCertificateToNamespace(context.Context, *connect.Request[namespaces.AssignCertificateToNamespaceRequest]) (*connect.Response[namespaces.AssignCertificateToNamespaceResponse], error) - RemoveCertificateFromNamespace(context.Context, *connect.Request[namespaces.RemoveCertificateFromNamespaceRequest]) (*connect.Response[namespaces.RemoveCertificateFromNamespaceResponse], error) } // NewNamespaceServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -300,72 +247,61 @@ type NamespaceServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewNamespaceServiceHandler(svc NamespaceServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + namespaceServiceMethods := namespaces.File_policy_namespaces_namespaces_proto.Services().ByName("NamespaceService").Methods() namespaceServiceGetNamespaceHandler := connect.NewUnaryHandler( NamespaceServiceGetNamespaceProcedure, svc.GetNamespace, - connect.WithSchema(namespaceServiceGetNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("GetNamespace")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) namespaceServiceListNamespacesHandler := connect.NewUnaryHandler( NamespaceServiceListNamespacesProcedure, svc.ListNamespaces, - connect.WithSchema(namespaceServiceListNamespacesMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("ListNamespaces")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) namespaceServiceCreateNamespaceHandler := connect.NewUnaryHandler( NamespaceServiceCreateNamespaceProcedure, svc.CreateNamespace, - connect.WithSchema(namespaceServiceCreateNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("CreateNamespace")), connect.WithHandlerOptions(opts...), ) namespaceServiceUpdateNamespaceHandler := connect.NewUnaryHandler( NamespaceServiceUpdateNamespaceProcedure, svc.UpdateNamespace, - connect.WithSchema(namespaceServiceUpdateNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("UpdateNamespace")), connect.WithHandlerOptions(opts...), ) namespaceServiceDeactivateNamespaceHandler := connect.NewUnaryHandler( NamespaceServiceDeactivateNamespaceProcedure, svc.DeactivateNamespace, - connect.WithSchema(namespaceServiceDeactivateNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("DeactivateNamespace")), connect.WithHandlerOptions(opts...), ) namespaceServiceAssignKeyAccessServerToNamespaceHandler := connect.NewUnaryHandler( NamespaceServiceAssignKeyAccessServerToNamespaceProcedure, svc.AssignKeyAccessServerToNamespace, - connect.WithSchema(namespaceServiceAssignKeyAccessServerToNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("AssignKeyAccessServerToNamespace")), connect.WithHandlerOptions(opts...), ) namespaceServiceRemoveKeyAccessServerFromNamespaceHandler := connect.NewUnaryHandler( NamespaceServiceRemoveKeyAccessServerFromNamespaceProcedure, svc.RemoveKeyAccessServerFromNamespace, - connect.WithSchema(namespaceServiceRemoveKeyAccessServerFromNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("RemoveKeyAccessServerFromNamespace")), connect.WithHandlerOptions(opts...), ) namespaceServiceAssignPublicKeyToNamespaceHandler := connect.NewUnaryHandler( NamespaceServiceAssignPublicKeyToNamespaceProcedure, svc.AssignPublicKeyToNamespace, - connect.WithSchema(namespaceServiceAssignPublicKeyToNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("AssignPublicKeyToNamespace")), connect.WithHandlerOptions(opts...), ) namespaceServiceRemovePublicKeyFromNamespaceHandler := connect.NewUnaryHandler( NamespaceServiceRemovePublicKeyFromNamespaceProcedure, svc.RemovePublicKeyFromNamespace, - connect.WithSchema(namespaceServiceRemovePublicKeyFromNamespaceMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - namespaceServiceAssignCertificateToNamespaceHandler := connect.NewUnaryHandler( - NamespaceServiceAssignCertificateToNamespaceProcedure, - svc.AssignCertificateToNamespace, - connect.WithSchema(namespaceServiceAssignCertificateToNamespaceMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - namespaceServiceRemoveCertificateFromNamespaceHandler := connect.NewUnaryHandler( - NamespaceServiceRemoveCertificateFromNamespaceProcedure, - svc.RemoveCertificateFromNamespace, - connect.WithSchema(namespaceServiceRemoveCertificateFromNamespaceMethodDescriptor), + connect.WithSchema(namespaceServiceMethods.ByName("RemovePublicKeyFromNamespace")), connect.WithHandlerOptions(opts...), ) return "/policy.namespaces.NamespaceService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -388,10 +324,6 @@ func NewNamespaceServiceHandler(svc NamespaceServiceHandler, opts ...connect.Han namespaceServiceAssignPublicKeyToNamespaceHandler.ServeHTTP(w, r) case NamespaceServiceRemovePublicKeyFromNamespaceProcedure: namespaceServiceRemovePublicKeyFromNamespaceHandler.ServeHTTP(w, r) - case NamespaceServiceAssignCertificateToNamespaceProcedure: - namespaceServiceAssignCertificateToNamespaceHandler.ServeHTTP(w, r) - case NamespaceServiceRemoveCertificateFromNamespaceProcedure: - namespaceServiceRemoveCertificateFromNamespaceHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -436,11 +368,3 @@ func (UnimplementedNamespaceServiceHandler) AssignPublicKeyToNamespace(context.C func (UnimplementedNamespaceServiceHandler) RemovePublicKeyFromNamespace(context.Context, *connect.Request[namespaces.RemovePublicKeyFromNamespaceRequest]) (*connect.Response[namespaces.RemovePublicKeyFromNamespaceResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("policy.namespaces.NamespaceService.RemovePublicKeyFromNamespace is not implemented")) } - -func (UnimplementedNamespaceServiceHandler) AssignCertificateToNamespace(context.Context, *connect.Request[namespaces.AssignCertificateToNamespaceRequest]) (*connect.Response[namespaces.AssignCertificateToNamespaceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("policy.namespaces.NamespaceService.AssignCertificateToNamespace is not implemented")) -} - -func (UnimplementedNamespaceServiceHandler) RemoveCertificateFromNamespace(context.Context, *connect.Request[namespaces.RemoveCertificateFromNamespaceRequest]) (*connect.Response[namespaces.RemoveCertificateFromNamespaceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("policy.namespaces.NamespaceService.RemoveCertificateFromNamespace is not implemented")) -} diff --git a/protocol/go/policy/objects.pb.go b/protocol/go/policy/objects.pb.go index e26f7c727e..b9eda84a37 100644 --- a/protocol/go/policy/objects.pb.go +++ b/protocol/go/policy/objects.pb.go @@ -237,31 +237,40 @@ func (SourceType) EnumDescriptor() ([]byte, []int) { type KasPublicKeyAlgEnum int32 const ( - KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED KasPublicKeyAlgEnum = 0 - KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 KasPublicKeyAlgEnum = 1 - KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 KasPublicKeyAlgEnum = 2 - KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 KasPublicKeyAlgEnum = 5 - KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 KasPublicKeyAlgEnum = 6 - KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 KasPublicKeyAlgEnum = 7 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED KasPublicKeyAlgEnum = 0 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 KasPublicKeyAlgEnum = 1 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 KasPublicKeyAlgEnum = 2 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 KasPublicKeyAlgEnum = 5 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 KasPublicKeyAlgEnum = 6 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 KasPublicKeyAlgEnum = 7 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING KasPublicKeyAlgEnum = 10 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 KasPublicKeyAlgEnum = 11 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 KasPublicKeyAlgEnum = 12 ) // Enum value maps for KasPublicKeyAlgEnum. var ( KasPublicKeyAlgEnum_name = map[int32]string{ - 0: "KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED", - 1: "KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048", - 2: "KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096", - 5: "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1", - 6: "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1", - 7: "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1", + 0: "KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED", + 1: "KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048", + 2: "KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096", + 5: "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1", + 6: "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1", + 7: "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1", + 10: "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING", + 11: "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768", + 12: "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024", } KasPublicKeyAlgEnum_value = map[string]int32{ - "KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED": 0, - "KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048": 1, - "KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096": 2, - "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1": 5, - "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1": 6, - "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1": 7, + "KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED": 0, + "KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048": 1, + "KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096": 2, + "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1": 5, + "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1": 6, + "KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1": 7, + "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING": 10, + "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768": 11, + "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024": 12, } ) @@ -296,12 +305,15 @@ func (KasPublicKeyAlgEnum) EnumDescriptor() ([]byte, []int) { type Algorithm int32 const ( - Algorithm_ALGORITHM_UNSPECIFIED Algorithm = 0 - Algorithm_ALGORITHM_RSA_2048 Algorithm = 1 - Algorithm_ALGORITHM_RSA_4096 Algorithm = 2 - Algorithm_ALGORITHM_EC_P256 Algorithm = 3 - Algorithm_ALGORITHM_EC_P384 Algorithm = 4 - Algorithm_ALGORITHM_EC_P521 Algorithm = 5 + Algorithm_ALGORITHM_UNSPECIFIED Algorithm = 0 + Algorithm_ALGORITHM_RSA_2048 Algorithm = 1 + Algorithm_ALGORITHM_RSA_4096 Algorithm = 2 + Algorithm_ALGORITHM_EC_P256 Algorithm = 3 + Algorithm_ALGORITHM_EC_P384 Algorithm = 4 + Algorithm_ALGORITHM_EC_P521 Algorithm = 5 + Algorithm_ALGORITHM_HPQT_XWING Algorithm = 6 + Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 Algorithm = 7 + Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 Algorithm = 8 ) // Enum value maps for Algorithm. @@ -313,14 +325,20 @@ var ( 3: "ALGORITHM_EC_P256", 4: "ALGORITHM_EC_P384", 5: "ALGORITHM_EC_P521", + 6: "ALGORITHM_HPQT_XWING", + 7: "ALGORITHM_HPQT_SECP256R1_MLKEM768", + 8: "ALGORITHM_HPQT_SECP384R1_MLKEM1024", } Algorithm_value = map[string]int32{ - "ALGORITHM_UNSPECIFIED": 0, - "ALGORITHM_RSA_2048": 1, - "ALGORITHM_RSA_4096": 2, - "ALGORITHM_EC_P256": 3, - "ALGORITHM_EC_P384": 4, - "ALGORITHM_EC_P521": 5, + "ALGORITHM_UNSPECIFIED": 0, + "ALGORITHM_RSA_2048": 1, + "ALGORITHM_RSA_4096": 2, + "ALGORITHM_EC_P256": 3, + "ALGORITHM_EC_P384": 4, + "ALGORITHM_EC_P521": 5, + "ALGORITHM_HPQT_XWING": 6, + "ALGORITHM_HPQT_SECP256R1_MLKEM768": 7, + "ALGORITHM_HPQT_SECP384R1_MLKEM1024": 8, } ) @@ -520,7 +538,7 @@ func (x Action_StandardAction) Number() protoreflect.EnumNumber { // Deprecated: Use Action_StandardAction.Descriptor instead. func (Action_StandardAction) EnumDescriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{7, 0} + return file_policy_objects_proto_rawDescGZIP(), []int{6, 0} } type SimpleKasPublicKey struct { @@ -747,8 +765,6 @@ type Namespace struct { Grants []*KeyAccessServer `protobuf:"bytes,6,rep,name=grants,proto3" json:"grants,omitempty"` // Keys for the namespace KasKeys []*SimpleKasKey `protobuf:"bytes,7,rep,name=kas_keys,json=kasKeys,proto3" json:"kas_keys,omitempty"` - // Root certificates for chain of trust - RootCerts []*Certificate `protobuf:"bytes,8,rep,name=root_certs,json=rootCerts,proto3" json:"root_certs,omitempty"` } func (x *Namespace) Reset() { @@ -832,79 +848,6 @@ func (x *Namespace) GetKasKeys() []*SimpleKasKey { return nil } -func (x *Namespace) GetRootCerts() []*Certificate { - if x != nil { - return x.RootCerts - } - return nil -} - -type Certificate struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // generated uuid in database - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // PEM format certificate - Pem string `protobuf:"bytes,2,opt,name=pem,proto3" json:"pem,omitempty"` - // Optional metadata. - Metadata *common.Metadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` -} - -func (x *Certificate) Reset() { - *x = Certificate{} - if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Certificate) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Certificate) ProtoMessage() {} - -func (x *Certificate) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Certificate.ProtoReflect.Descriptor instead. -func (*Certificate) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{4} -} - -func (x *Certificate) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *Certificate) GetPem() string { - if x != nil { - return x.Pem - } - return "" -} - -func (x *Certificate) GetMetadata() *common.Metadata { - if x != nil { - return x.Metadata - } - return nil -} - type Attribute struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -925,6 +868,9 @@ type Attribute struct { Active *wrapperspb.BoolValue `protobuf:"bytes,8,opt,name=active,proto3" json:"active,omitempty"` // Keys associated with the attribute KasKeys []*SimpleKasKey `protobuf:"bytes,9,rep,name=kas_keys,json=kasKeys,proto3" json:"kas_keys,omitempty"` + // Whether or not we will use the attribute definition during encryption + // if the attribute value is missing. + AllowTraversal *wrapperspb.BoolValue `protobuf:"bytes,10,opt,name=allow_traversal,json=allowTraversal,proto3" json:"allow_traversal,omitempty"` // Common metadata Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } @@ -932,7 +878,7 @@ type Attribute struct { func (x *Attribute) Reset() { *x = Attribute{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[5] + mi := &file_policy_objects_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -945,7 +891,7 @@ func (x *Attribute) String() string { func (*Attribute) ProtoMessage() {} func (x *Attribute) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[5] + mi := &file_policy_objects_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -958,7 +904,7 @@ func (x *Attribute) ProtoReflect() protoreflect.Message { // Deprecated: Use Attribute.ProtoReflect.Descriptor instead. func (*Attribute) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{5} + return file_policy_objects_proto_rawDescGZIP(), []int{4} } func (x *Attribute) GetId() string { @@ -1024,6 +970,13 @@ func (x *Attribute) GetKasKeys() []*SimpleKasKey { return nil } +func (x *Attribute) GetAllowTraversal() *wrapperspb.BoolValue { + if x != nil { + return x.AllowTraversal + } + return nil +} + func (x *Attribute) GetMetadata() *common.Metadata { if x != nil { return x.Metadata @@ -1057,7 +1010,7 @@ type Value struct { func (x *Value) Reset() { *x = Value{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[6] + mi := &file_policy_objects_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1070,7 +1023,7 @@ func (x *Value) String() string { func (*Value) ProtoMessage() {} func (x *Value) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[6] + mi := &file_policy_objects_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1083,7 +1036,7 @@ func (x *Value) ProtoReflect() protoreflect.Message { // Deprecated: Use Value.ProtoReflect.Descriptor instead. func (*Value) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{6} + return file_policy_objects_proto_rawDescGZIP(), []int{5} } func (x *Value) GetId() string { @@ -1177,15 +1130,17 @@ type Action struct { // // *Action_Standard // *Action_Custom - Value isAction_Value `protobuf_oneof:"value"` - Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` - Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` + Value isAction_Value `protobuf_oneof:"value"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + // Namespace context for this action + Namespace *Namespace `protobuf:"bytes,5,opt,name=namespace,proto3" json:"namespace,omitempty"` + Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *Action) Reset() { *x = Action{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[7] + mi := &file_policy_objects_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1198,7 +1153,7 @@ func (x *Action) String() string { func (*Action) ProtoMessage() {} func (x *Action) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[7] + mi := &file_policy_objects_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1211,7 +1166,7 @@ func (x *Action) ProtoReflect() protoreflect.Message { // Deprecated: Use Action.ProtoReflect.Descriptor instead. func (*Action) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{7} + return file_policy_objects_proto_rawDescGZIP(), []int{6} } func (x *Action) GetId() string { @@ -1249,6 +1204,13 @@ func (x *Action) GetName() string { return "" } +func (x *Action) GetNamespace() *Namespace { + if x != nil { + return x.Namespace + } + return nil +} + func (x *Action) GetMetadata() *common.Metadata { if x != nil { return x.Metadata @@ -1287,14 +1249,18 @@ type SubjectMapping struct { // the reusable SubjectConditionSet mapped to the given Attribute Value SubjectConditionSet *SubjectConditionSet `protobuf:"bytes,3,opt,name=subject_condition_set,json=subjectConditionSet,proto3" json:"subject_condition_set,omitempty"` // The actions permitted by subjects in this mapping - Actions []*Action `protobuf:"bytes,4,rep,name=actions,proto3" json:"actions,omitempty"` - Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` + Actions []*Action `protobuf:"bytes,4,rep,name=actions,proto3" json:"actions,omitempty"` + // the namespace containing this subject mapping + // possible this is empty. If so that means + // the Subject Mapping has not been migrated to a namespace. + Namespace *Namespace `protobuf:"bytes,5,opt,name=namespace,proto3" json:"namespace,omitempty"` + Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *SubjectMapping) Reset() { *x = SubjectMapping{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[8] + mi := &file_policy_objects_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1307,7 +1273,7 @@ func (x *SubjectMapping) String() string { func (*SubjectMapping) ProtoMessage() {} func (x *SubjectMapping) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[8] + mi := &file_policy_objects_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1320,7 +1286,7 @@ func (x *SubjectMapping) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectMapping.ProtoReflect.Descriptor instead. func (*SubjectMapping) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{8} + return file_policy_objects_proto_rawDescGZIP(), []int{7} } func (x *SubjectMapping) GetId() string { @@ -1351,6 +1317,13 @@ func (x *SubjectMapping) GetActions() []*Action { return nil } +func (x *SubjectMapping) GetNamespace() *Namespace { + if x != nil { + return x.Namespace + } + return nil +} + func (x *SubjectMapping) GetMetadata() *common.Metadata { if x != nil { return x.Metadata @@ -1380,7 +1353,7 @@ type Condition struct { func (x *Condition) Reset() { *x = Condition{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[9] + mi := &file_policy_objects_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1393,7 +1366,7 @@ func (x *Condition) String() string { func (*Condition) ProtoMessage() {} func (x *Condition) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[9] + mi := &file_policy_objects_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1406,7 +1379,7 @@ func (x *Condition) ProtoReflect() protoreflect.Message { // Deprecated: Use Condition.ProtoReflect.Descriptor instead. func (*Condition) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{9} + return file_policy_objects_proto_rawDescGZIP(), []int{8} } func (x *Condition) GetSubjectExternalSelectorValue() string { @@ -1444,7 +1417,7 @@ type ConditionGroup struct { func (x *ConditionGroup) Reset() { *x = ConditionGroup{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[10] + mi := &file_policy_objects_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1457,7 +1430,7 @@ func (x *ConditionGroup) String() string { func (*ConditionGroup) ProtoMessage() {} func (x *ConditionGroup) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[10] + mi := &file_policy_objects_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1470,7 +1443,7 @@ func (x *ConditionGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use ConditionGroup.ProtoReflect.Descriptor instead. func (*ConditionGroup) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{10} + return file_policy_objects_proto_rawDescGZIP(), []int{9} } func (x *ConditionGroup) GetConditions() []*Condition { @@ -1500,7 +1473,7 @@ type SubjectSet struct { func (x *SubjectSet) Reset() { *x = SubjectSet{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[11] + mi := &file_policy_objects_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1513,7 +1486,7 @@ func (x *SubjectSet) String() string { func (*SubjectSet) ProtoMessage() {} func (x *SubjectSet) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[11] + mi := &file_policy_objects_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1526,7 +1499,7 @@ func (x *SubjectSet) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectSet.ProtoReflect.Descriptor instead. func (*SubjectSet) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{11} + return file_policy_objects_proto_rawDescGZIP(), []int{10} } func (x *SubjectSet) GetConditionGroups() []*ConditionGroup { @@ -1546,7 +1519,11 @@ type SubjectConditionSet struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // the namespace containing this subject condition set + // possible this is empty in the case a subject condition set + // has not been migrated to a namespace. + Namespace *Namespace `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` SubjectSets []*SubjectSet `protobuf:"bytes,3,rep,name=subject_sets,json=subjectSets,proto3" json:"subject_sets,omitempty"` Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } @@ -1554,7 +1531,7 @@ type SubjectConditionSet struct { func (x *SubjectConditionSet) Reset() { *x = SubjectConditionSet{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[12] + mi := &file_policy_objects_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1567,7 +1544,7 @@ func (x *SubjectConditionSet) String() string { func (*SubjectConditionSet) ProtoMessage() {} func (x *SubjectConditionSet) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[12] + mi := &file_policy_objects_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1580,7 +1557,7 @@ func (x *SubjectConditionSet) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectConditionSet.ProtoReflect.Descriptor instead. func (*SubjectConditionSet) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{12} + return file_policy_objects_proto_rawDescGZIP(), []int{11} } func (x *SubjectConditionSet) GetId() string { @@ -1590,6 +1567,13 @@ func (x *SubjectConditionSet) GetId() string { return "" } +func (x *SubjectConditionSet) GetNamespace() *Namespace { + if x != nil { + return x.Namespace + } + return nil +} + func (x *SubjectConditionSet) GetSubjectSets() []*SubjectSet { if x != nil { return x.SubjectSets @@ -1626,7 +1610,7 @@ type SubjectProperty struct { func (x *SubjectProperty) Reset() { *x = SubjectProperty{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[13] + mi := &file_policy_objects_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1639,7 +1623,7 @@ func (x *SubjectProperty) String() string { func (*SubjectProperty) ProtoMessage() {} func (x *SubjectProperty) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[13] + mi := &file_policy_objects_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1652,7 +1636,7 @@ func (x *SubjectProperty) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectProperty.ProtoReflect.Descriptor instead. func (*SubjectProperty) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{13} + return file_policy_objects_proto_rawDescGZIP(), []int{12} } func (x *SubjectProperty) GetExternalSelectorValue() string { @@ -1682,6 +1666,8 @@ type ResourceMappingGroup struct { // the common name for the group of resource mappings, which must be unique // per namespace Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // the fully qualified name of the resource mapping group + Fqn string `protobuf:"bytes,4,opt,name=fqn,proto3" json:"fqn,omitempty"` // Common metadata Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } @@ -1689,7 +1675,7 @@ type ResourceMappingGroup struct { func (x *ResourceMappingGroup) Reset() { *x = ResourceMappingGroup{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[14] + mi := &file_policy_objects_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1702,7 +1688,7 @@ func (x *ResourceMappingGroup) String() string { func (*ResourceMappingGroup) ProtoMessage() {} func (x *ResourceMappingGroup) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[14] + mi := &file_policy_objects_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1715,7 +1701,7 @@ func (x *ResourceMappingGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceMappingGroup.ProtoReflect.Descriptor instead. func (*ResourceMappingGroup) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{14} + return file_policy_objects_proto_rawDescGZIP(), []int{13} } func (x *ResourceMappingGroup) GetId() string { @@ -1739,6 +1725,13 @@ func (x *ResourceMappingGroup) GetName() string { return "" } +func (x *ResourceMappingGroup) GetFqn() string { + if x != nil { + return x.Fqn + } + return "" +} + func (x *ResourceMappingGroup) GetMetadata() *common.Metadata { if x != nil { return x.Metadata @@ -1763,7 +1756,7 @@ type ResourceMapping struct { func (x *ResourceMapping) Reset() { *x = ResourceMapping{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[15] + mi := &file_policy_objects_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1776,7 +1769,7 @@ func (x *ResourceMapping) String() string { func (*ResourceMapping) ProtoMessage() {} func (x *ResourceMapping) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[15] + mi := &file_policy_objects_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1789,7 +1782,7 @@ func (x *ResourceMapping) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceMapping.ProtoReflect.Descriptor instead. func (*ResourceMapping) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{15} + return file_policy_objects_proto_rawDescGZIP(), []int{14} } func (x *ResourceMapping) GetId() string { @@ -1852,7 +1845,7 @@ type KeyAccessServer struct { func (x *KeyAccessServer) Reset() { *x = KeyAccessServer{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[16] + mi := &file_policy_objects_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1865,7 +1858,7 @@ func (x *KeyAccessServer) String() string { func (*KeyAccessServer) ProtoMessage() {} func (x *KeyAccessServer) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[16] + mi := &file_policy_objects_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1878,7 +1871,7 @@ func (x *KeyAccessServer) ProtoReflect() protoreflect.Message { // Deprecated: Use KeyAccessServer.ProtoReflect.Descriptor instead. func (*KeyAccessServer) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{16} + return file_policy_objects_proto_rawDescGZIP(), []int{15} } func (x *KeyAccessServer) GetId() string { @@ -1948,7 +1941,7 @@ type Key struct { func (x *Key) Reset() { *x = Key{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[17] + mi := &file_policy_objects_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1961,7 +1954,7 @@ func (x *Key) String() string { func (*Key) ProtoMessage() {} func (x *Key) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[17] + mi := &file_policy_objects_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1974,7 +1967,7 @@ func (x *Key) ProtoReflect() protoreflect.Message { // Deprecated: Use Key.ProtoReflect.Descriptor instead. func (*Key) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{17} + return file_policy_objects_proto_rawDescGZIP(), []int{16} } func (x *Key) GetId() string { @@ -2031,15 +2024,15 @@ type KasPublicKey struct { // A unique string identifier for this key Kid string `protobuf:"bytes,2,opt,name=kid,proto3" json:"kid,omitempty"` // A known algorithm type with any additional parameters encoded. - // To start, these may be `rsa:2048` for encrypting ZTDF files and - // `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + // To start, these may be `rsa:2048` for RSA-based wrapping and + // `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. Alg KasPublicKeyAlgEnum `protobuf:"varint,3,opt,name=alg,proto3,enum=policy.KasPublicKeyAlgEnum" json:"alg,omitempty"` } func (x *KasPublicKey) Reset() { *x = KasPublicKey{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[18] + mi := &file_policy_objects_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2052,7 +2045,7 @@ func (x *KasPublicKey) String() string { func (*KasPublicKey) ProtoMessage() {} func (x *KasPublicKey) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[18] + mi := &file_policy_objects_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2065,7 +2058,7 @@ func (x *KasPublicKey) ProtoReflect() protoreflect.Message { // Deprecated: Use KasPublicKey.ProtoReflect.Descriptor instead. func (*KasPublicKey) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{18} + return file_policy_objects_proto_rawDescGZIP(), []int{17} } func (x *KasPublicKey) GetPem() string { @@ -2102,7 +2095,7 @@ type KasPublicKeySet struct { func (x *KasPublicKeySet) Reset() { *x = KasPublicKeySet{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[19] + mi := &file_policy_objects_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2115,7 +2108,7 @@ func (x *KasPublicKeySet) String() string { func (*KasPublicKeySet) ProtoMessage() {} func (x *KasPublicKeySet) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[19] + mi := &file_policy_objects_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2128,7 +2121,7 @@ func (x *KasPublicKeySet) ProtoReflect() protoreflect.Message { // Deprecated: Use KasPublicKeySet.ProtoReflect.Descriptor instead. func (*KasPublicKeySet) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{19} + return file_policy_objects_proto_rawDescGZIP(), []int{18} } func (x *KasPublicKeySet) GetKeys() []*KasPublicKey { @@ -2154,7 +2147,7 @@ type PublicKey struct { func (x *PublicKey) Reset() { *x = PublicKey{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[20] + mi := &file_policy_objects_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2167,7 +2160,7 @@ func (x *PublicKey) String() string { func (*PublicKey) ProtoMessage() {} func (x *PublicKey) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[20] + mi := &file_policy_objects_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2180,7 +2173,7 @@ func (x *PublicKey) ProtoReflect() protoreflect.Message { // Deprecated: Use PublicKey.ProtoReflect.Descriptor instead. func (*PublicKey) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{20} + return file_policy_objects_proto_rawDescGZIP(), []int{19} } func (m *PublicKey) GetPublicKey() isPublicKey_PublicKey { @@ -2227,9 +2220,10 @@ type RegisteredResource struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Values []*RegisteredResourceValue `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Values []*RegisteredResourceValue `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty"` + Namespace *Namespace `protobuf:"bytes,4,opt,name=namespace,proto3" json:"namespace,omitempty"` // Common metadata Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } @@ -2237,7 +2231,7 @@ type RegisteredResource struct { func (x *RegisteredResource) Reset() { *x = RegisteredResource{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[21] + mi := &file_policy_objects_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2250,7 +2244,7 @@ func (x *RegisteredResource) String() string { func (*RegisteredResource) ProtoMessage() {} func (x *RegisteredResource) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[21] + mi := &file_policy_objects_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2263,7 +2257,7 @@ func (x *RegisteredResource) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisteredResource.ProtoReflect.Descriptor instead. func (*RegisteredResource) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{21} + return file_policy_objects_proto_rawDescGZIP(), []int{20} } func (x *RegisteredResource) GetId() string { @@ -2287,6 +2281,13 @@ func (x *RegisteredResource) GetValues() []*RegisteredResourceValue { return nil } +func (x *RegisteredResource) GetNamespace() *Namespace { + if x != nil { + return x.Namespace + } + return nil +} + func (x *RegisteredResource) GetMetadata() *common.Metadata { if x != nil { return x.Metadata @@ -2303,6 +2304,7 @@ type RegisteredResourceValue struct { Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` Resource *RegisteredResource `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"` ActionAttributeValues []*RegisteredResourceValue_ActionAttributeValue `protobuf:"bytes,4,rep,name=action_attribute_values,json=actionAttributeValues,proto3" json:"action_attribute_values,omitempty"` + Fqn string `protobuf:"bytes,5,opt,name=fqn,proto3" json:"fqn,omitempty"` // Common metadata Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } @@ -2310,7 +2312,7 @@ type RegisteredResourceValue struct { func (x *RegisteredResourceValue) Reset() { *x = RegisteredResourceValue{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[22] + mi := &file_policy_objects_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2323,7 +2325,7 @@ func (x *RegisteredResourceValue) String() string { func (*RegisteredResourceValue) ProtoMessage() {} func (x *RegisteredResourceValue) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[22] + mi := &file_policy_objects_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2336,7 +2338,7 @@ func (x *RegisteredResourceValue) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisteredResourceValue.ProtoReflect.Descriptor instead. func (*RegisteredResourceValue) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{22} + return file_policy_objects_proto_rawDescGZIP(), []int{21} } func (x *RegisteredResourceValue) GetId() string { @@ -2367,6 +2369,13 @@ func (x *RegisteredResourceValue) GetActionAttributeValues() []*RegisteredResour return nil } +func (x *RegisteredResourceValue) GetFqn() string { + if x != nil { + return x.Fqn + } + return "" +} + func (x *RegisteredResourceValue) GetMetadata() *common.Metadata { if x != nil { return x.Metadata @@ -2385,7 +2394,7 @@ type PolicyEnforcementPoint struct { func (x *PolicyEnforcementPoint) Reset() { *x = PolicyEnforcementPoint{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[23] + mi := &file_policy_objects_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2398,7 +2407,7 @@ func (x *PolicyEnforcementPoint) String() string { func (*PolicyEnforcementPoint) ProtoMessage() {} func (x *PolicyEnforcementPoint) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[23] + mi := &file_policy_objects_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2411,7 +2420,7 @@ func (x *PolicyEnforcementPoint) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyEnforcementPoint.ProtoReflect.Descriptor instead. func (*PolicyEnforcementPoint) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{23} + return file_policy_objects_proto_rawDescGZIP(), []int{22} } func (x *PolicyEnforcementPoint) GetClientId() string { @@ -2433,7 +2442,7 @@ type RequestContext struct { func (x *RequestContext) Reset() { *x = RequestContext{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[24] + mi := &file_policy_objects_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2446,7 +2455,7 @@ func (x *RequestContext) String() string { func (*RequestContext) ProtoMessage() {} func (x *RequestContext) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[24] + mi := &file_policy_objects_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2459,7 +2468,7 @@ func (x *RequestContext) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestContext.ProtoReflect.Descriptor instead. func (*RequestContext) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{24} + return file_policy_objects_proto_rawDescGZIP(), []int{23} } func (x *RequestContext) GetPep() *PolicyEnforcementPoint { @@ -2485,7 +2494,7 @@ type Obligation struct { func (x *Obligation) Reset() { *x = Obligation{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[25] + mi := &file_policy_objects_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2498,7 +2507,7 @@ func (x *Obligation) String() string { func (*Obligation) ProtoMessage() {} func (x *Obligation) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[25] + mi := &file_policy_objects_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2511,7 +2520,7 @@ func (x *Obligation) ProtoReflect() protoreflect.Message { // Deprecated: Use Obligation.ProtoReflect.Descriptor instead. func (*Obligation) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{25} + return file_policy_objects_proto_rawDescGZIP(), []int{24} } func (x *Obligation) GetId() string { @@ -2572,7 +2581,7 @@ type ObligationValue struct { func (x *ObligationValue) Reset() { *x = ObligationValue{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[26] + mi := &file_policy_objects_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2585,7 +2594,7 @@ func (x *ObligationValue) String() string { func (*ObligationValue) ProtoMessage() {} func (x *ObligationValue) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[26] + mi := &file_policy_objects_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2598,7 +2607,7 @@ func (x *ObligationValue) ProtoReflect() protoreflect.Message { // Deprecated: Use ObligationValue.ProtoReflect.Descriptor instead. func (*ObligationValue) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{26} + return file_policy_objects_proto_rawDescGZIP(), []int{25} } func (x *ObligationValue) GetId() string { @@ -2653,13 +2662,15 @@ type ObligationTrigger struct { Action *Action `protobuf:"bytes,3,opt,name=action,proto3" json:"action,omitempty"` AttributeValue *Value `protobuf:"bytes,4,opt,name=attribute_value,json=attributeValue,proto3" json:"attribute_value,omitempty"` Context []*RequestContext `protobuf:"bytes,5,rep,name=context,proto3" json:"context,omitempty"` - Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` + // The source namespace for this trigger, derived from the attribute value and action. + Namespace *Namespace `protobuf:"bytes,11,opt,name=namespace,proto3" json:"namespace,omitempty"` + Metadata *common.Metadata `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *ObligationTrigger) Reset() { *x = ObligationTrigger{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[27] + mi := &file_policy_objects_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2672,7 +2683,7 @@ func (x *ObligationTrigger) String() string { func (*ObligationTrigger) ProtoMessage() {} func (x *ObligationTrigger) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[27] + mi := &file_policy_objects_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2685,7 +2696,7 @@ func (x *ObligationTrigger) ProtoReflect() protoreflect.Message { // Deprecated: Use ObligationTrigger.ProtoReflect.Descriptor instead. func (*ObligationTrigger) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{27} + return file_policy_objects_proto_rawDescGZIP(), []int{26} } func (x *ObligationTrigger) GetId() string { @@ -2723,6 +2734,13 @@ func (x *ObligationTrigger) GetContext() []*RequestContext { return nil } +func (x *ObligationTrigger) GetNamespace() *Namespace { + if x != nil { + return x.Namespace + } + return nil +} + func (x *ObligationTrigger) GetMetadata() *common.Metadata { if x != nil { return x.Metadata @@ -2743,7 +2761,7 @@ type KasKey struct { func (x *KasKey) Reset() { *x = KasKey{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[28] + mi := &file_policy_objects_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2756,7 +2774,7 @@ func (x *KasKey) String() string { func (*KasKey) ProtoMessage() {} func (x *KasKey) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[28] + mi := &file_policy_objects_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2769,7 +2787,7 @@ func (x *KasKey) ProtoReflect() protoreflect.Message { // Deprecated: Use KasKey.ProtoReflect.Descriptor instead. func (*KasKey) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{28} + return file_policy_objects_proto_rawDescGZIP(), []int{27} } func (x *KasKey) GetKasId() string { @@ -2805,7 +2823,7 @@ type PublicKeyCtx struct { func (x *PublicKeyCtx) Reset() { *x = PublicKeyCtx{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[29] + mi := &file_policy_objects_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2818,7 +2836,7 @@ func (x *PublicKeyCtx) String() string { func (*PublicKeyCtx) ProtoMessage() {} func (x *PublicKeyCtx) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[29] + mi := &file_policy_objects_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2831,7 +2849,7 @@ func (x *PublicKeyCtx) ProtoReflect() protoreflect.Message { // Deprecated: Use PublicKeyCtx.ProtoReflect.Descriptor instead. func (*PublicKeyCtx) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{29} + return file_policy_objects_proto_rawDescGZIP(), []int{28} } func (x *PublicKeyCtx) GetPem() string { @@ -2855,7 +2873,7 @@ type PrivateKeyCtx struct { func (x *PrivateKeyCtx) Reset() { *x = PrivateKeyCtx{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[30] + mi := &file_policy_objects_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2868,7 +2886,7 @@ func (x *PrivateKeyCtx) String() string { func (*PrivateKeyCtx) ProtoMessage() {} func (x *PrivateKeyCtx) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[30] + mi := &file_policy_objects_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2881,7 +2899,7 @@ func (x *PrivateKeyCtx) ProtoReflect() protoreflect.Message { // Deprecated: Use PrivateKeyCtx.ProtoReflect.Descriptor instead. func (*PrivateKeyCtx) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{30} + return file_policy_objects_proto_rawDescGZIP(), []int{29} } func (x *PrivateKeyCtx) GetKeyId() string { @@ -2928,7 +2946,7 @@ type AsymmetricKey struct { func (x *AsymmetricKey) Reset() { *x = AsymmetricKey{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[31] + mi := &file_policy_objects_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2941,7 +2959,7 @@ func (x *AsymmetricKey) String() string { func (*AsymmetricKey) ProtoMessage() {} func (x *AsymmetricKey) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[31] + mi := &file_policy_objects_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2954,7 +2972,7 @@ func (x *AsymmetricKey) ProtoReflect() protoreflect.Message { // Deprecated: Use AsymmetricKey.ProtoReflect.Descriptor instead. func (*AsymmetricKey) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{31} + return file_policy_objects_proto_rawDescGZIP(), []int{30} } func (x *AsymmetricKey) GetId() string { @@ -3045,7 +3063,7 @@ type SymmetricKey struct { func (x *SymmetricKey) Reset() { *x = SymmetricKey{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[32] + mi := &file_policy_objects_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3058,7 +3076,7 @@ func (x *SymmetricKey) String() string { func (*SymmetricKey) ProtoMessage() {} func (x *SymmetricKey) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[32] + mi := &file_policy_objects_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3071,7 +3089,7 @@ func (x *SymmetricKey) ProtoReflect() protoreflect.Message { // Deprecated: Use SymmetricKey.ProtoReflect.Descriptor instead. func (*SymmetricKey) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{32} + return file_policy_objects_proto_rawDescGZIP(), []int{31} } func (x *SymmetricKey) GetId() string { @@ -3138,7 +3156,7 @@ type RegisteredResourceValue_ActionAttributeValue struct { func (x *RegisteredResourceValue_ActionAttributeValue) Reset() { *x = RegisteredResourceValue_ActionAttributeValue{} if protoimpl.UnsafeEnabled { - mi := &file_policy_objects_proto_msgTypes[33] + mi := &file_policy_objects_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3151,7 +3169,7 @@ func (x *RegisteredResourceValue_ActionAttributeValue) String() string { func (*RegisteredResourceValue_ActionAttributeValue) ProtoMessage() {} func (x *RegisteredResourceValue_ActionAttributeValue) ProtoReflect() protoreflect.Message { - mi := &file_policy_objects_proto_msgTypes[33] + mi := &file_policy_objects_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3164,7 +3182,7 @@ func (x *RegisteredResourceValue_ActionAttributeValue) ProtoReflect() protorefle // Deprecated: Use RegisteredResourceValue_ActionAttributeValue.ProtoReflect.Descriptor instead. func (*RegisteredResourceValue_ActionAttributeValue) Descriptor() ([]byte, []int) { - return file_policy_objects_proto_rawDescGZIP(), []int{22, 0} + return file_policy_objects_proto_rawDescGZIP(), []int{21, 0} } func (x *RegisteredResourceValue_ActionAttributeValue) GetId() string { @@ -3229,7 +3247,7 @@ var file_policy_objects_proto_rawDesc = []byte{ 0x28, 0x09, 0x52, 0x07, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb9, 0x02, 0x0a, 0x09, 0x4e, 0x61, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x85, 0x02, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x66, @@ -3246,526 +3264,555 @@ var file_policy_objects_proto_rawDesc = []byte{ 0x12, 0x2f, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, - 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x18, - 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x74, - 0x43, 0x65, 0x72, 0x74, 0x73, 0x22, 0x5d, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x70, 0x65, 0x6d, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x22, 0x9d, 0x03, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x45, 0x6e, 0x75, 0x6d, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, - 0x01, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2f, - 0x0a, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, - 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, - 0x6e, 0x12, 0x32, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x61, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, - 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, - 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x22, 0x82, 0x04, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2f, - 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, - 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x12, 0x32, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, - 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x41, 0x0a, 0x10, - 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, - 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0f, - 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, - 0x2f, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, - 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, - 0x12, 0x44, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, - 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x61, 0x70, - 0x70, 0x69, 0x6e, 0x67, 0x52, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, - 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x22, 0xa8, 0x02, 0x0a, 0x06, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x3b, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x08, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, - 0x64, 0x12, 0x18, 0x0a, 0x06, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x06, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x6c, 0x0a, - 0x0e, 0x53, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x1f, 0x0a, 0x1b, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x5f, 0x41, 0x43, 0x54, 0x49, - 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x5f, 0x41, 0x43, 0x54, - 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x43, 0x52, 0x59, 0x50, 0x54, 0x10, 0x01, 0x12, 0x1c, 0x0a, - 0x18, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x4d, 0x49, 0x54, 0x10, 0x02, 0x42, 0x07, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x22, 0x81, 0x02, 0x0a, 0x0e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x36, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x4f, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x13, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, - 0x12, 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xe9, 0x01, 0x0a, 0x09, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x1f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x1c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, - 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x45, 0x6e, 0x75, 0x6d, 0x42, 0x0b, 0xba, 0x48, 0x08, - 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x6f, 0x72, 0x12, 0x40, 0x0a, 0x17, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x15, 0x73, - 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x22, 0xa7, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x3b, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x58, 0x0a, 0x10, 0x62, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x5f, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x42, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x45, 0x6e, 0x75, 0x6d, - 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0f, 0x62, - 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x59, - 0x0a, 0x0a, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x12, 0x4b, 0x0a, 0x10, - 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x13, 0x53, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x3f, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x74, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, - 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x7c, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x50, 0x72, 0x6f, 0x70, 0x65, - 0x72, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, - 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x78, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x9b, - 0x01, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x29, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, - 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, + 0x73, 0x22, 0xe2, 0x03, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x45, 0x6e, 0x75, + 0x6d, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, + 0x72, 0x75, 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, + 0x66, 0x71, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x12, 0x32, + 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x09, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, + 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4b, + 0x65, 0x79, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x74, 0x72, 0x61, + 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, + 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x54, + 0x72, 0x61, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x82, 0x04, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x12, 0x32, 0x0a, 0x06, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, + 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x41, + 0x0a, 0x10, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x73, 0x12, 0x2f, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x09, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, + 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4b, 0x65, + 0x79, 0x73, 0x12, 0x44, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0b, 0x6f, 0x62, 0x6c, 0x69, + 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xd9, 0x01, 0x0a, - 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3e, - 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x74, - 0x65, 0x72, 0x6d, 0x73, 0x12, 0x32, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x85, 0x05, 0x0a, 0x0f, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x87, 0x03, 0x0a, - 0x03, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xf4, 0x02, 0xba, 0x48, 0xf0, - 0x02, 0xba, 0x01, 0xec, 0x02, 0x0a, 0x0a, 0x75, 0x72, 0x69, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x12, 0xcf, 0x01, 0x55, 0x52, 0x49, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, - 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x52, 0x4c, 0x20, 0x28, 0x65, 0x2e, 0x67, - 0x2e, 0x2c, 0x20, 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x27, 0x29, 0x20, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, - 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x73, - 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x20, 0x45, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, - 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, - 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, - 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2c, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, - 0x63, 0x74, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x73, 0x6c, 0x61, 0x73, 0x68, - 0x65, 0x73, 0x2e, 0x1a, 0x8b, 0x01, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, - 0x65, 0x73, 0x28, 0x27, 0x5e, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3f, 0x3a, 0x2f, 0x2f, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, - 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x28, 0x5c, 0x5c, 0x2e, 0x5b, - 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, - 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, - 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x29, 0x2a, 0x28, 0x3a, - 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x29, 0x3f, 0x28, 0x2f, 0x2e, 0x2a, 0x29, 0x3f, 0x24, 0x27, - 0x29, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x30, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, - 0x65, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, - 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, - 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, + 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x04, + 0x10, 0x05, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x22, 0xd9, 0x02, 0x0a, 0x06, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3b, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, + 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, + 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x08, 0x73, 0x74, 0x61, 0x6e, 0x64, + 0x61, 0x72, 0x64, 0x12, 0x18, 0x0a, 0x06, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x97, 0x02, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x37, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x61, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, - 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x73, 0x41, 0x63, 0x74, 0x69, 0x76, - 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x77, 0x61, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x09, 0x77, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x12, 0x33, 0x0a, 0x0a, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x12, 0x29, 0x0a, 0x03, 0x6b, 0x61, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x03, 0x6b, 0x61, 0x73, 0x12, 0x2c, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x84, 0x01, 0x0a, 0x0c, 0x4b, - 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x03, 0x70, - 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, - 0x01, 0x18, 0x80, 0x40, 0x52, 0x03, 0x70, 0x65, 0x6d, 0x12, 0x1b, 0x0a, 0x03, 0x6b, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xba, 0x48, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, - 0x20, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x03, 0x61, 0x6c, 0x67, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, - 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x45, 0x6e, 0x75, 0x6d, - 0x42, 0x0a, 0xba, 0x48, 0x07, 0x82, 0x01, 0x04, 0x10, 0x01, 0x20, 0x00, 0x52, 0x03, 0x61, 0x6c, - 0x67, 0x22, 0x3b, 0x0a, 0x0f, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x53, 0x65, 0x74, 0x12, 0x28, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x50, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0xe0, - 0x03, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x84, 0x03, 0x0a, - 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0xe9, 0x02, - 0xba, 0x48, 0xe5, 0x02, 0xba, 0x01, 0xe1, 0x02, 0x0a, 0x0a, 0x75, 0x72, 0x69, 0x5f, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x12, 0xcf, 0x01, 0x55, 0x52, 0x49, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, - 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x52, 0x4c, 0x20, 0x28, - 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, - 0x65, 0x6d, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x27, 0x29, 0x20, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, - 0x77, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x20, 0x45, 0x61, 0x63, 0x68, - 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, - 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, - 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2c, 0x20, 0x63, 0x61, 0x6e, 0x20, - 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, - 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, - 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x73, 0x6c, - 0x61, 0x73, 0x68, 0x65, 0x73, 0x2e, 0x1a, 0x80, 0x01, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, - 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x28, 0x5c, 0x5c, - 0x2e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, - 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, - 0x7d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x29, 0x2a, - 0x28, 0x2f, 0x2e, 0x2a, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, - 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x48, 0x00, 0x52, 0x06, - 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x5f, 0x6b, 0x65, 0x79, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x05, 0x6c, 0x6f, 0x63, 0x61, - 0x6c, 0x22, 0x9f, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x06, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x22, 0x6c, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x5f, 0x41, + 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x5f, + 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x43, 0x52, 0x59, 0x50, 0x54, 0x10, 0x01, + 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x5f, 0x41, 0x43, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x4d, 0x49, 0x54, 0x10, 0x02, 0x42, 0x07, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xb2, 0x02, 0x0a, 0x0e, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x36, 0x0a, 0x0f, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x4f, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x13, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x65, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2f, 0x0a, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2c, + 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xe9, 0x01, 0x0a, + 0x09, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x1f, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x73, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x1c, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x45, 0x6e, 0x75, 0x6d, 0x42, + 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x08, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x40, 0x0a, 0x17, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, + 0x01, 0x52, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0xa7, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x3b, 0x0a, 0x0a, 0x63, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0a, 0x63, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x58, 0x0a, 0x10, 0x62, 0x6f, 0x6f, 0x6c, + 0x65, 0x61, 0x6e, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x54, 0x79, 0x70, 0x65, + 0x45, 0x6e, 0x75, 0x6d, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, + 0x01, 0x52, 0x0f, 0x62, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x6f, 0x72, 0x22, 0x59, 0x0a, 0x0a, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, + 0x12, 0x4b, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0f, 0x63, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0xc5, 0x01, + 0x0a, 0x13, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x3f, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x7c, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, + 0x01, 0x72, 0x02, 0x10, 0x01, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0e, + 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x22, 0xad, 0x01, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x29, 0x0a, 0x0c, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x66, 0x71, 0x6e, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x22, 0xca, 0x03, 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x61, 0x74, 0x61, 0x22, 0xd9, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3e, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x06, 0xba, + 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x32, 0x0a, 0x05, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, + 0x85, 0x05, 0x0a, 0x0f, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x87, 0x03, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0xf4, 0x02, 0xba, 0x48, 0xf0, 0x02, 0xba, 0x01, 0xec, 0x02, 0x0a, 0x0a, 0x75, 0x72, + 0x69, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xcf, 0x01, 0x55, 0x52, 0x49, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, + 0x52, 0x4c, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, + 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x27, 0x29, 0x20, 0x66, + 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x20, + 0x45, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6d, 0x75, 0x73, + 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x64, 0x20, + 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, + 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2c, 0x20, + 0x63, 0x61, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x68, 0x79, 0x70, 0x68, + 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, + 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x61, 0x6e, + 0x64, 0x20, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x73, 0x2e, 0x1a, 0x8b, 0x01, 0x74, 0x68, 0x69, + 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x68, 0x74, 0x74, 0x70, + 0x73, 0x3f, 0x3a, 0x2f, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, + 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, 0x2d, 0x5d, 0x7b, + 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, + 0x29, 0x3f, 0x28, 0x5c, 0x5c, 0x2e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, + 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, 0x2d, 0x5d, + 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, + 0x5d, 0x29, 0x3f, 0x29, 0x2a, 0x28, 0x3a, 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x29, 0x3f, 0x28, + 0x2f, 0x2e, 0x2a, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x30, 0x0a, + 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, + 0x33, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, + 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x97, 0x02, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x6c, 0x0a, - 0x17, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x2e, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xb4, 0x01, 0x0a, 0x14, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x0f, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x3e, 0x0a, 0x16, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x09, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, - 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, - 0x22, 0x4a, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x12, 0x38, 0x0a, 0x03, 0x70, 0x65, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x45, - 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x42, - 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x03, 0x70, 0x65, 0x70, 0x22, 0xd2, 0x01, 0x0a, - 0x0a, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2f, 0x0a, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x2f, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x66, 0x71, 0x6e, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x37, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, + 0x69, 0x73, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x77, 0x61, 0x73, 0x5f, + 0x6d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, + 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x77, 0x61, 0x73, 0x4d, 0x61, 0x70, + 0x70, 0x65, 0x64, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x03, 0x6b, 0x61, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x03, + 0x6b, 0x61, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x22, 0xe2, 0x01, 0x0a, 0x0f, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x35, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x08, 0x74, 0x72, - 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xa7, 0x02, 0x0a, 0x11, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x42, 0x0a, 0x10, - 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x0f, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x05, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, + 0x61, 0x22, 0x84, 0x01, 0x0a, 0x0c, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x03, 0x70, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x40, 0x52, 0x03, 0x70, 0x65, 0x6d, + 0x12, 0x1b, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xba, + 0x48, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x20, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x12, 0x39, 0x0a, + 0x03, 0x61, 0x6c, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x41, 0x6c, 0x67, 0x45, 0x6e, 0x75, 0x6d, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x82, 0x01, 0x04, 0x10, + 0x01, 0x20, 0x00, 0x52, 0x03, 0x61, 0x6c, 0x67, 0x22, 0x3b, 0x0a, 0x0f, 0x4b, 0x61, 0x73, 0x50, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x12, 0x28, 0x0a, 0x04, 0x6b, + 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, + 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0xe0, 0x03, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x12, 0x84, 0x03, 0x0a, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0xe9, 0x02, 0xba, 0x48, 0xe5, 0x02, 0xba, 0x01, 0xe1, 0x02, 0x0a, + 0x0a, 0x75, 0x72, 0x69, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xcf, 0x01, 0x55, 0x52, + 0x49, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x20, 0x55, 0x52, 0x4c, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x68, 0x74, + 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x27, + 0x29, 0x20, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x20, 0x45, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x65, + 0x6e, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, + 0x72, 0x2c, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x68, + 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x2c, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, + 0x65, 0x72, 0x69, 0x63, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2c, + 0x20, 0x61, 0x6e, 0x64, 0x20, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x73, 0x2e, 0x1a, 0x80, 0x01, + 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x68, + 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, + 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, 0x2d, + 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, + 0x39, 0x5d, 0x29, 0x3f, 0x28, 0x5c, 0x5c, 0x2e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, + 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5c, 0x5c, + 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, + 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x29, 0x2a, 0x28, 0x2f, 0x2e, 0x2a, 0x29, 0x3f, 0x24, 0x27, 0x29, + 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x63, 0x61, + 0x63, 0x68, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x53, 0x65, 0x74, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x0c, 0x0a, + 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x4a, 0x04, 0x08, 0x02, 0x10, + 0x03, 0x52, 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x22, 0xd0, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2c, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xdc, 0x03, 0x0a, 0x17, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x36, 0x0a, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x6c, 0x0a, 0x17, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x66, 0x71, 0x6e, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x1a, 0xb4, 0x01, 0x0a, 0x14, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x06, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3e, 0x0a, 0x16, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x50, + 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, + 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x4a, 0x0a, 0x0e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x38, 0x0a, 0x03, + 0x70, 0x65, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, + 0x01, 0x52, 0x03, 0x70, 0x65, 0x70, 0x22, 0xd2, 0x01, 0x0a, 0x0a, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x66, + 0x71, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x12, 0x2c, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xe2, 0x01, 0x0a, 0x0f, + 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x74, 0x72, 0x69, + 0x67, 0x67, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, + 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, + 0x71, 0x6e, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x61, 0x0a, 0x06, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x61, - 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, - 0x64, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x73, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x61, - 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x61, 0x73, - 0x55, 0x72, 0x69, 0x22, 0x29, 0x0a, 0x0c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x43, 0x74, 0x78, 0x12, 0x19, 0x0a, 0x03, 0x70, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x03, 0x70, 0x65, 0x6d, 0x22, 0x50, - 0x0a, 0x0d, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, - 0x1e, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, - 0x1f, 0x0a, 0x0b, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, - 0x22, 0xd1, 0x03, 0x0a, 0x0d, 0x41, 0x73, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x36, 0x0a, 0x0d, 0x6b, 0x65, 0x79, - 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, - 0x74, 0x68, 0x6d, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, - 0x6d, 0x12, 0x30, 0x0a, 0x0a, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, - 0x65, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x09, 0x6b, 0x65, 0x79, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x2a, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, - 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, - 0x3a, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, - 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, 0x0c, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x3d, 0x0a, 0x0f, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, 0x0d, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x42, 0x0a, 0x0f, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, - 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, - 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x22, 0x9e, 0x02, 0x0a, 0x0c, 0x53, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x0a, - 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, + 0x22, 0xd8, 0x02, 0x0a, 0x11, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x42, 0x0a, 0x10, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x6f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x0a, 0x07, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x2f, 0x0a, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2c, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x61, 0x0a, 0x06, 0x4b, + 0x61, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x41, 0x73, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x22, 0x29, + 0x0a, 0x0c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x19, + 0x0a, 0x03, 0x70, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, + 0x72, 0x02, 0x10, 0x01, 0x52, 0x03, 0x70, 0x65, 0x6d, 0x22, 0x50, 0x0a, 0x0d, 0x50, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x1e, 0x0a, 0x06, 0x6b, 0x65, + 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, + 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x72, + 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x22, 0xd1, 0x03, 0x0a, 0x0d, + 0x41, 0x73, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x15, 0x0a, + 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, + 0x65, 0x79, 0x49, 0x64, 0x12, 0x36, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, + 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x52, 0x0c, + 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x30, 0x0a, 0x0a, + 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x09, 0x6b, 0x65, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2a, - 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, + 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, - 0x65, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x65, - 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6b, 0x65, 0x79, - 0x43, 0x74, 0x78, 0x12, 0x42, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x2a, 0xb3, 0x01, 0x0a, 0x15, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x12, - 0x28, 0x0a, 0x24, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4c, - 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x41, 0x54, 0x54, + 0x65, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x3a, 0x0a, 0x0e, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x3d, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x42, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x67, + 0x61, 0x63, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, + 0x79, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, + 0x9e, 0x02, 0x0a, 0x0c, 0x53, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x0a, 0x6b, 0x65, 0x79, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x09, + 0x6b, 0x65, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2a, 0x0a, 0x08, 0x6b, 0x65, 0x79, + 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, 0x6b, 0x65, + 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x42, + 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x2a, 0xb3, 0x01, 0x0a, 0x15, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x75, + 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x28, 0x0a, 0x24, 0x41, 0x54, + 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4c, 0x45, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, + 0x45, 0x5f, 0x52, 0x55, 0x4c, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, + 0x5f, 0x41, 0x4c, 0x4c, 0x5f, 0x4f, 0x46, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4c, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x41, 0x4c, 0x4c, 0x5f, 0x4f, 0x46, 0x10, 0x01, 0x12, 0x23, - 0x0a, 0x1f, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4c, 0x45, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x41, 0x4e, 0x59, 0x5f, 0x4f, - 0x46, 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, - 0x5f, 0x52, 0x55, 0x4c, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, - 0x48, 0x49, 0x45, 0x52, 0x41, 0x52, 0x43, 0x48, 0x59, 0x10, 0x03, 0x2a, 0xca, 0x01, 0x0a, 0x1a, - 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x2d, 0x0a, 0x29, 0x53, 0x55, + 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x41, 0x4e, 0x59, 0x5f, 0x4f, 0x46, 0x10, 0x02, 0x12, 0x26, + 0x0a, 0x22, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4c, 0x45, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x48, 0x49, 0x45, 0x52, 0x41, + 0x52, 0x43, 0x48, 0x59, 0x10, 0x03, 0x2a, 0xca, 0x01, 0x0a, 0x1a, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, + 0x72, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x2d, 0x0a, 0x29, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, + 0x5f, 0x4d, 0x41, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x4f, + 0x52, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, + 0x4d, 0x41, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52, + 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x28, 0x0a, 0x24, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x4d, 0x41, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x4f, 0x50, - 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x55, 0x42, - 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x4d, 0x41, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x4f, 0x50, 0x45, - 0x52, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x49, 0x4e, 0x10, 0x01, 0x12, - 0x28, 0x0a, 0x24, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x4d, 0x41, 0x50, 0x50, 0x49, - 0x4e, 0x47, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x45, 0x4e, 0x55, 0x4d, - 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x2d, 0x0a, 0x29, 0x53, 0x55, 0x42, - 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x4d, 0x41, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x4f, 0x50, 0x45, - 0x52, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x49, 0x4e, 0x5f, 0x43, 0x4f, - 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x53, 0x10, 0x03, 0x2a, 0x90, 0x01, 0x0a, 0x18, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x54, 0x79, 0x70, - 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x2b, 0x0a, 0x27, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, - 0x4f, 0x4e, 0x5f, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, + 0x49, 0x4e, 0x10, 0x02, 0x12, 0x2d, 0x0a, 0x29, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, + 0x4d, 0x41, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52, + 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x49, 0x4e, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, + 0x53, 0x10, 0x03, 0x2a, 0x90, 0x01, 0x0a, 0x18, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x42, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x45, 0x6e, 0x75, 0x6d, + 0x12, 0x2b, 0x0a, 0x27, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x4f, + 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, + 0x1f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x4f, 0x4f, 0x4c, 0x45, + 0x41, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x41, 0x4e, 0x44, + 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x55, - 0x4d, 0x5f, 0x41, 0x4e, 0x44, 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x4f, 0x52, 0x10, 0x02, 0x2a, 0x5d, 0x0a, 0x0a, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x4f, 0x55, - 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x01, - 0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x45, 0x58, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x2a, 0x88, 0x02, 0x0a, 0x13, 0x4b, - 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x45, 0x6e, - 0x75, 0x6d, 0x12, 0x27, 0x0a, 0x23, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, - 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x4b, - 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, - 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x32, 0x30, 0x34, 0x38, 0x10, - 0x01, 0x12, 0x24, 0x0a, 0x20, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, - 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x52, 0x53, 0x41, - 0x5f, 0x34, 0x30, 0x39, 0x36, 0x10, 0x02, 0x12, 0x28, 0x0a, 0x24, 0x4b, 0x41, 0x53, 0x5f, 0x50, - 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, - 0x55, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x32, 0x35, 0x36, 0x52, 0x31, 0x10, - 0x05, 0x12, 0x28, 0x0a, 0x24, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, - 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x45, 0x43, 0x5f, - 0x53, 0x45, 0x43, 0x50, 0x33, 0x38, 0x34, 0x52, 0x31, 0x10, 0x06, 0x12, 0x28, 0x0a, 0x24, 0x4b, - 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, - 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x35, 0x32, - 0x31, 0x52, 0x31, 0x10, 0x07, 0x2a, 0x9b, 0x01, 0x0a, 0x09, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, - 0x74, 0x68, 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, - 0x0a, 0x12, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, - 0x32, 0x30, 0x34, 0x38, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, - 0x54, 0x48, 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x34, 0x30, 0x39, 0x36, 0x10, 0x02, 0x12, 0x15, - 0x0a, 0x11, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, - 0x32, 0x35, 0x36, 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, - 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x33, 0x38, 0x34, 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, - 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x35, 0x32, - 0x31, 0x10, 0x05, 0x2a, 0x56, 0x0a, 0x09, 0x4b, 0x65, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x1a, 0x0a, 0x16, 0x4b, 0x45, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, - 0x4b, 0x45, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, - 0x45, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x4b, 0x45, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, - 0x53, 0x5f, 0x52, 0x4f, 0x54, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x94, 0x01, 0x0a, 0x07, - 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x4b, 0x45, 0x59, 0x5f, 0x4d, - 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, - 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, - 0x1e, 0x0a, 0x1a, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, - 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x02, 0x12, - 0x13, 0x0a, 0x0f, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, - 0x54, 0x45, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, - 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, - 0x10, 0x04, 0x42, 0x82, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x42, 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, - 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, - 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0xa2, 0x02, 0x03, 0x50, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0xca, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xe2, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, - 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4d, 0x5f, 0x4f, 0x52, 0x10, 0x02, 0x2a, 0x5d, 0x0a, 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x53, + 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x58, 0x54, 0x45, 0x52, + 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x2a, 0x9b, 0x03, 0x0a, 0x13, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x27, 0x0a, + 0x23, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, + 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, + 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, + 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x32, 0x30, 0x34, 0x38, 0x10, 0x01, 0x12, 0x24, 0x0a, 0x20, + 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, + 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x34, 0x30, 0x39, 0x36, + 0x10, 0x02, 0x12, 0x28, 0x0a, 0x24, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, + 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x45, 0x43, + 0x5f, 0x53, 0x45, 0x43, 0x50, 0x32, 0x35, 0x36, 0x52, 0x31, 0x10, 0x05, 0x12, 0x28, 0x0a, 0x24, + 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, + 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x33, + 0x38, 0x34, 0x52, 0x31, 0x10, 0x06, 0x12, 0x28, 0x0a, 0x24, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, + 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, + 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x35, 0x32, 0x31, 0x52, 0x31, 0x10, 0x07, + 0x12, 0x26, 0x0a, 0x22, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, + 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x48, 0x50, 0x51, 0x54, + 0x5f, 0x58, 0x57, 0x49, 0x4e, 0x47, 0x10, 0x0a, 0x12, 0x33, 0x0a, 0x2f, 0x4b, 0x41, 0x53, 0x5f, + 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, + 0x4e, 0x55, 0x4d, 0x5f, 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x32, 0x35, 0x36, + 0x52, 0x31, 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x37, 0x36, 0x38, 0x10, 0x0b, 0x12, 0x34, 0x0a, + 0x30, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, + 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, 0x45, + 0x43, 0x50, 0x33, 0x38, 0x34, 0x52, 0x31, 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x31, 0x30, 0x32, + 0x34, 0x10, 0x0c, 0x2a, 0x84, 0x02, 0x0a, 0x09, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, + 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, + 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x32, 0x30, + 0x34, 0x38, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, + 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x34, 0x30, 0x39, 0x36, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, + 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x32, 0x35, + 0x36, 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, + 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x33, 0x38, 0x34, 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x4c, + 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x35, 0x32, 0x31, 0x10, + 0x05, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x48, + 0x50, 0x51, 0x54, 0x5f, 0x58, 0x57, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x25, 0x0a, 0x21, 0x41, + 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, 0x45, + 0x43, 0x50, 0x32, 0x35, 0x36, 0x52, 0x31, 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x37, 0x36, 0x38, + 0x10, 0x07, 0x12, 0x26, 0x0a, 0x22, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, + 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x33, 0x38, 0x34, 0x52, 0x31, 0x5f, 0x4d, + 0x4c, 0x4b, 0x45, 0x4d, 0x31, 0x30, 0x32, 0x34, 0x10, 0x08, 0x2a, 0x56, 0x0a, 0x09, 0x4b, 0x65, + 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, 0x16, 0x4b, 0x45, 0x59, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x4b, 0x45, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x4b, 0x45, + 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x4f, 0x54, 0x41, 0x54, 0x45, 0x44, + 0x10, 0x02, 0x2a, 0x94, 0x01, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, + 0x0a, 0x14, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x4b, 0x45, 0x59, 0x5f, + 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, + 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, + 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, + 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, + 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x4b, + 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, + 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x04, 0x42, 0x82, 0x01, 0x0a, 0x0a, 0x63, 0x6f, + 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, + 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, + 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x58, 0x58, 0xaa, 0x02, + 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xca, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0xe2, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3781,7 +3828,7 @@ func file_policy_objects_proto_rawDescGZIP() []byte { } var file_policy_objects_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_policy_objects_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_policy_objects_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_policy_objects_proto_goTypes = []interface{}{ (AttributeRuleTypeEnum)(0), // 0: policy.AttributeRuleTypeEnum (SubjectMappingOperatorEnum)(0), // 1: policy.SubjectMappingOperatorEnum @@ -3796,129 +3843,132 @@ var file_policy_objects_proto_goTypes = []interface{}{ (*SimpleKasKey)(nil), // 10: policy.SimpleKasKey (*KeyProviderConfig)(nil), // 11: policy.KeyProviderConfig (*Namespace)(nil), // 12: policy.Namespace - (*Certificate)(nil), // 13: policy.Certificate - (*Attribute)(nil), // 14: policy.Attribute - (*Value)(nil), // 15: policy.Value - (*Action)(nil), // 16: policy.Action - (*SubjectMapping)(nil), // 17: policy.SubjectMapping - (*Condition)(nil), // 18: policy.Condition - (*ConditionGroup)(nil), // 19: policy.ConditionGroup - (*SubjectSet)(nil), // 20: policy.SubjectSet - (*SubjectConditionSet)(nil), // 21: policy.SubjectConditionSet - (*SubjectProperty)(nil), // 22: policy.SubjectProperty - (*ResourceMappingGroup)(nil), // 23: policy.ResourceMappingGroup - (*ResourceMapping)(nil), // 24: policy.ResourceMapping - (*KeyAccessServer)(nil), // 25: policy.KeyAccessServer - (*Key)(nil), // 26: policy.Key - (*KasPublicKey)(nil), // 27: policy.KasPublicKey - (*KasPublicKeySet)(nil), // 28: policy.KasPublicKeySet - (*PublicKey)(nil), // 29: policy.PublicKey - (*RegisteredResource)(nil), // 30: policy.RegisteredResource - (*RegisteredResourceValue)(nil), // 31: policy.RegisteredResourceValue - (*PolicyEnforcementPoint)(nil), // 32: policy.PolicyEnforcementPoint - (*RequestContext)(nil), // 33: policy.RequestContext - (*Obligation)(nil), // 34: policy.Obligation - (*ObligationValue)(nil), // 35: policy.ObligationValue - (*ObligationTrigger)(nil), // 36: policy.ObligationTrigger - (*KasKey)(nil), // 37: policy.KasKey - (*PublicKeyCtx)(nil), // 38: policy.PublicKeyCtx - (*PrivateKeyCtx)(nil), // 39: policy.PrivateKeyCtx - (*AsymmetricKey)(nil), // 40: policy.AsymmetricKey - (*SymmetricKey)(nil), // 41: policy.SymmetricKey - (*RegisteredResourceValue_ActionAttributeValue)(nil), // 42: policy.RegisteredResourceValue.ActionAttributeValue - (*common.Metadata)(nil), // 43: common.Metadata - (*wrapperspb.BoolValue)(nil), // 44: google.protobuf.BoolValue + (*Attribute)(nil), // 13: policy.Attribute + (*Value)(nil), // 14: policy.Value + (*Action)(nil), // 15: policy.Action + (*SubjectMapping)(nil), // 16: policy.SubjectMapping + (*Condition)(nil), // 17: policy.Condition + (*ConditionGroup)(nil), // 18: policy.ConditionGroup + (*SubjectSet)(nil), // 19: policy.SubjectSet + (*SubjectConditionSet)(nil), // 20: policy.SubjectConditionSet + (*SubjectProperty)(nil), // 21: policy.SubjectProperty + (*ResourceMappingGroup)(nil), // 22: policy.ResourceMappingGroup + (*ResourceMapping)(nil), // 23: policy.ResourceMapping + (*KeyAccessServer)(nil), // 24: policy.KeyAccessServer + (*Key)(nil), // 25: policy.Key + (*KasPublicKey)(nil), // 26: policy.KasPublicKey + (*KasPublicKeySet)(nil), // 27: policy.KasPublicKeySet + (*PublicKey)(nil), // 28: policy.PublicKey + (*RegisteredResource)(nil), // 29: policy.RegisteredResource + (*RegisteredResourceValue)(nil), // 30: policy.RegisteredResourceValue + (*PolicyEnforcementPoint)(nil), // 31: policy.PolicyEnforcementPoint + (*RequestContext)(nil), // 32: policy.RequestContext + (*Obligation)(nil), // 33: policy.Obligation + (*ObligationValue)(nil), // 34: policy.ObligationValue + (*ObligationTrigger)(nil), // 35: policy.ObligationTrigger + (*KasKey)(nil), // 36: policy.KasKey + (*PublicKeyCtx)(nil), // 37: policy.PublicKeyCtx + (*PrivateKeyCtx)(nil), // 38: policy.PrivateKeyCtx + (*AsymmetricKey)(nil), // 39: policy.AsymmetricKey + (*SymmetricKey)(nil), // 40: policy.SymmetricKey + (*RegisteredResourceValue_ActionAttributeValue)(nil), // 41: policy.RegisteredResourceValue.ActionAttributeValue + (*common.Metadata)(nil), // 42: common.Metadata + (*wrapperspb.BoolValue)(nil), // 43: google.protobuf.BoolValue } var file_policy_objects_proto_depIdxs = []int32{ 5, // 0: policy.SimpleKasPublicKey.algorithm:type_name -> policy.Algorithm 9, // 1: policy.SimpleKasKey.public_key:type_name -> policy.SimpleKasPublicKey - 43, // 2: policy.KeyProviderConfig.metadata:type_name -> common.Metadata - 44, // 3: policy.Namespace.active:type_name -> google.protobuf.BoolValue - 43, // 4: policy.Namespace.metadata:type_name -> common.Metadata - 25, // 5: policy.Namespace.grants:type_name -> policy.KeyAccessServer + 42, // 2: policy.KeyProviderConfig.metadata:type_name -> common.Metadata + 43, // 3: policy.Namespace.active:type_name -> google.protobuf.BoolValue + 42, // 4: policy.Namespace.metadata:type_name -> common.Metadata + 24, // 5: policy.Namespace.grants:type_name -> policy.KeyAccessServer 10, // 6: policy.Namespace.kas_keys:type_name -> policy.SimpleKasKey - 13, // 7: policy.Namespace.root_certs:type_name -> policy.Certificate - 43, // 8: policy.Certificate.metadata:type_name -> common.Metadata - 12, // 9: policy.Attribute.namespace:type_name -> policy.Namespace - 0, // 10: policy.Attribute.rule:type_name -> policy.AttributeRuleTypeEnum - 15, // 11: policy.Attribute.values:type_name -> policy.Value - 25, // 12: policy.Attribute.grants:type_name -> policy.KeyAccessServer - 44, // 13: policy.Attribute.active:type_name -> google.protobuf.BoolValue - 10, // 14: policy.Attribute.kas_keys:type_name -> policy.SimpleKasKey - 43, // 15: policy.Attribute.metadata:type_name -> common.Metadata - 14, // 16: policy.Value.attribute:type_name -> policy.Attribute - 25, // 17: policy.Value.grants:type_name -> policy.KeyAccessServer - 44, // 18: policy.Value.active:type_name -> google.protobuf.BoolValue - 17, // 19: policy.Value.subject_mappings:type_name -> policy.SubjectMapping - 10, // 20: policy.Value.kas_keys:type_name -> policy.SimpleKasKey - 24, // 21: policy.Value.resource_mappings:type_name -> policy.ResourceMapping - 34, // 22: policy.Value.obligations:type_name -> policy.Obligation - 43, // 23: policy.Value.metadata:type_name -> common.Metadata - 8, // 24: policy.Action.standard:type_name -> policy.Action.StandardAction - 43, // 25: policy.Action.metadata:type_name -> common.Metadata - 15, // 26: policy.SubjectMapping.attribute_value:type_name -> policy.Value - 21, // 27: policy.SubjectMapping.subject_condition_set:type_name -> policy.SubjectConditionSet - 16, // 28: policy.SubjectMapping.actions:type_name -> policy.Action - 43, // 29: policy.SubjectMapping.metadata:type_name -> common.Metadata - 1, // 30: policy.Condition.operator:type_name -> policy.SubjectMappingOperatorEnum - 18, // 31: policy.ConditionGroup.conditions:type_name -> policy.Condition - 2, // 32: policy.ConditionGroup.boolean_operator:type_name -> policy.ConditionBooleanTypeEnum - 19, // 33: policy.SubjectSet.condition_groups:type_name -> policy.ConditionGroup - 20, // 34: policy.SubjectConditionSet.subject_sets:type_name -> policy.SubjectSet - 43, // 35: policy.SubjectConditionSet.metadata:type_name -> common.Metadata - 43, // 36: policy.ResourceMappingGroup.metadata:type_name -> common.Metadata - 43, // 37: policy.ResourceMapping.metadata:type_name -> common.Metadata - 15, // 38: policy.ResourceMapping.attribute_value:type_name -> policy.Value - 23, // 39: policy.ResourceMapping.group:type_name -> policy.ResourceMappingGroup - 29, // 40: policy.KeyAccessServer.public_key:type_name -> policy.PublicKey - 3, // 41: policy.KeyAccessServer.source_type:type_name -> policy.SourceType - 10, // 42: policy.KeyAccessServer.kas_keys:type_name -> policy.SimpleKasKey - 43, // 43: policy.KeyAccessServer.metadata:type_name -> common.Metadata - 44, // 44: policy.Key.is_active:type_name -> google.protobuf.BoolValue - 44, // 45: policy.Key.was_mapped:type_name -> google.protobuf.BoolValue - 27, // 46: policy.Key.public_key:type_name -> policy.KasPublicKey - 25, // 47: policy.Key.kas:type_name -> policy.KeyAccessServer - 43, // 48: policy.Key.metadata:type_name -> common.Metadata - 4, // 49: policy.KasPublicKey.alg:type_name -> policy.KasPublicKeyAlgEnum - 27, // 50: policy.KasPublicKeySet.keys:type_name -> policy.KasPublicKey - 28, // 51: policy.PublicKey.cached:type_name -> policy.KasPublicKeySet - 31, // 52: policy.RegisteredResource.values:type_name -> policy.RegisteredResourceValue - 43, // 53: policy.RegisteredResource.metadata:type_name -> common.Metadata - 30, // 54: policy.RegisteredResourceValue.resource:type_name -> policy.RegisteredResource - 42, // 55: policy.RegisteredResourceValue.action_attribute_values:type_name -> policy.RegisteredResourceValue.ActionAttributeValue - 43, // 56: policy.RegisteredResourceValue.metadata:type_name -> common.Metadata - 32, // 57: policy.RequestContext.pep:type_name -> policy.PolicyEnforcementPoint - 12, // 58: policy.Obligation.namespace:type_name -> policy.Namespace - 35, // 59: policy.Obligation.values:type_name -> policy.ObligationValue - 43, // 60: policy.Obligation.metadata:type_name -> common.Metadata - 34, // 61: policy.ObligationValue.obligation:type_name -> policy.Obligation - 36, // 62: policy.ObligationValue.triggers:type_name -> policy.ObligationTrigger - 43, // 63: policy.ObligationValue.metadata:type_name -> common.Metadata - 35, // 64: policy.ObligationTrigger.obligation_value:type_name -> policy.ObligationValue - 16, // 65: policy.ObligationTrigger.action:type_name -> policy.Action - 15, // 66: policy.ObligationTrigger.attribute_value:type_name -> policy.Value - 33, // 67: policy.ObligationTrigger.context:type_name -> policy.RequestContext - 43, // 68: policy.ObligationTrigger.metadata:type_name -> common.Metadata - 40, // 69: policy.KasKey.key:type_name -> policy.AsymmetricKey - 5, // 70: policy.AsymmetricKey.key_algorithm:type_name -> policy.Algorithm - 6, // 71: policy.AsymmetricKey.key_status:type_name -> policy.KeyStatus - 7, // 72: policy.AsymmetricKey.key_mode:type_name -> policy.KeyMode - 38, // 73: policy.AsymmetricKey.public_key_ctx:type_name -> policy.PublicKeyCtx - 39, // 74: policy.AsymmetricKey.private_key_ctx:type_name -> policy.PrivateKeyCtx - 11, // 75: policy.AsymmetricKey.provider_config:type_name -> policy.KeyProviderConfig - 43, // 76: policy.AsymmetricKey.metadata:type_name -> common.Metadata - 6, // 77: policy.SymmetricKey.key_status:type_name -> policy.KeyStatus - 7, // 78: policy.SymmetricKey.key_mode:type_name -> policy.KeyMode - 11, // 79: policy.SymmetricKey.provider_config:type_name -> policy.KeyProviderConfig - 43, // 80: policy.SymmetricKey.metadata:type_name -> common.Metadata - 16, // 81: policy.RegisteredResourceValue.ActionAttributeValue.action:type_name -> policy.Action - 15, // 82: policy.RegisteredResourceValue.ActionAttributeValue.attribute_value:type_name -> policy.Value - 43, // 83: policy.RegisteredResourceValue.ActionAttributeValue.metadata:type_name -> common.Metadata - 84, // [84:84] is the sub-list for method output_type - 84, // [84:84] is the sub-list for method input_type - 84, // [84:84] is the sub-list for extension type_name - 84, // [84:84] is the sub-list for extension extendee - 0, // [0:84] is the sub-list for field type_name + 12, // 7: policy.Attribute.namespace:type_name -> policy.Namespace + 0, // 8: policy.Attribute.rule:type_name -> policy.AttributeRuleTypeEnum + 14, // 9: policy.Attribute.values:type_name -> policy.Value + 24, // 10: policy.Attribute.grants:type_name -> policy.KeyAccessServer + 43, // 11: policy.Attribute.active:type_name -> google.protobuf.BoolValue + 10, // 12: policy.Attribute.kas_keys:type_name -> policy.SimpleKasKey + 43, // 13: policy.Attribute.allow_traversal:type_name -> google.protobuf.BoolValue + 42, // 14: policy.Attribute.metadata:type_name -> common.Metadata + 13, // 15: policy.Value.attribute:type_name -> policy.Attribute + 24, // 16: policy.Value.grants:type_name -> policy.KeyAccessServer + 43, // 17: policy.Value.active:type_name -> google.protobuf.BoolValue + 16, // 18: policy.Value.subject_mappings:type_name -> policy.SubjectMapping + 10, // 19: policy.Value.kas_keys:type_name -> policy.SimpleKasKey + 23, // 20: policy.Value.resource_mappings:type_name -> policy.ResourceMapping + 33, // 21: policy.Value.obligations:type_name -> policy.Obligation + 42, // 22: policy.Value.metadata:type_name -> common.Metadata + 8, // 23: policy.Action.standard:type_name -> policy.Action.StandardAction + 12, // 24: policy.Action.namespace:type_name -> policy.Namespace + 42, // 25: policy.Action.metadata:type_name -> common.Metadata + 14, // 26: policy.SubjectMapping.attribute_value:type_name -> policy.Value + 20, // 27: policy.SubjectMapping.subject_condition_set:type_name -> policy.SubjectConditionSet + 15, // 28: policy.SubjectMapping.actions:type_name -> policy.Action + 12, // 29: policy.SubjectMapping.namespace:type_name -> policy.Namespace + 42, // 30: policy.SubjectMapping.metadata:type_name -> common.Metadata + 1, // 31: policy.Condition.operator:type_name -> policy.SubjectMappingOperatorEnum + 17, // 32: policy.ConditionGroup.conditions:type_name -> policy.Condition + 2, // 33: policy.ConditionGroup.boolean_operator:type_name -> policy.ConditionBooleanTypeEnum + 18, // 34: policy.SubjectSet.condition_groups:type_name -> policy.ConditionGroup + 12, // 35: policy.SubjectConditionSet.namespace:type_name -> policy.Namespace + 19, // 36: policy.SubjectConditionSet.subject_sets:type_name -> policy.SubjectSet + 42, // 37: policy.SubjectConditionSet.metadata:type_name -> common.Metadata + 42, // 38: policy.ResourceMappingGroup.metadata:type_name -> common.Metadata + 42, // 39: policy.ResourceMapping.metadata:type_name -> common.Metadata + 14, // 40: policy.ResourceMapping.attribute_value:type_name -> policy.Value + 22, // 41: policy.ResourceMapping.group:type_name -> policy.ResourceMappingGroup + 28, // 42: policy.KeyAccessServer.public_key:type_name -> policy.PublicKey + 3, // 43: policy.KeyAccessServer.source_type:type_name -> policy.SourceType + 10, // 44: policy.KeyAccessServer.kas_keys:type_name -> policy.SimpleKasKey + 42, // 45: policy.KeyAccessServer.metadata:type_name -> common.Metadata + 43, // 46: policy.Key.is_active:type_name -> google.protobuf.BoolValue + 43, // 47: policy.Key.was_mapped:type_name -> google.protobuf.BoolValue + 26, // 48: policy.Key.public_key:type_name -> policy.KasPublicKey + 24, // 49: policy.Key.kas:type_name -> policy.KeyAccessServer + 42, // 50: policy.Key.metadata:type_name -> common.Metadata + 4, // 51: policy.KasPublicKey.alg:type_name -> policy.KasPublicKeyAlgEnum + 26, // 52: policy.KasPublicKeySet.keys:type_name -> policy.KasPublicKey + 27, // 53: policy.PublicKey.cached:type_name -> policy.KasPublicKeySet + 30, // 54: policy.RegisteredResource.values:type_name -> policy.RegisteredResourceValue + 12, // 55: policy.RegisteredResource.namespace:type_name -> policy.Namespace + 42, // 56: policy.RegisteredResource.metadata:type_name -> common.Metadata + 29, // 57: policy.RegisteredResourceValue.resource:type_name -> policy.RegisteredResource + 41, // 58: policy.RegisteredResourceValue.action_attribute_values:type_name -> policy.RegisteredResourceValue.ActionAttributeValue + 42, // 59: policy.RegisteredResourceValue.metadata:type_name -> common.Metadata + 31, // 60: policy.RequestContext.pep:type_name -> policy.PolicyEnforcementPoint + 12, // 61: policy.Obligation.namespace:type_name -> policy.Namespace + 34, // 62: policy.Obligation.values:type_name -> policy.ObligationValue + 42, // 63: policy.Obligation.metadata:type_name -> common.Metadata + 33, // 64: policy.ObligationValue.obligation:type_name -> policy.Obligation + 35, // 65: policy.ObligationValue.triggers:type_name -> policy.ObligationTrigger + 42, // 66: policy.ObligationValue.metadata:type_name -> common.Metadata + 34, // 67: policy.ObligationTrigger.obligation_value:type_name -> policy.ObligationValue + 15, // 68: policy.ObligationTrigger.action:type_name -> policy.Action + 14, // 69: policy.ObligationTrigger.attribute_value:type_name -> policy.Value + 32, // 70: policy.ObligationTrigger.context:type_name -> policy.RequestContext + 12, // 71: policy.ObligationTrigger.namespace:type_name -> policy.Namespace + 42, // 72: policy.ObligationTrigger.metadata:type_name -> common.Metadata + 39, // 73: policy.KasKey.key:type_name -> policy.AsymmetricKey + 5, // 74: policy.AsymmetricKey.key_algorithm:type_name -> policy.Algorithm + 6, // 75: policy.AsymmetricKey.key_status:type_name -> policy.KeyStatus + 7, // 76: policy.AsymmetricKey.key_mode:type_name -> policy.KeyMode + 37, // 77: policy.AsymmetricKey.public_key_ctx:type_name -> policy.PublicKeyCtx + 38, // 78: policy.AsymmetricKey.private_key_ctx:type_name -> policy.PrivateKeyCtx + 11, // 79: policy.AsymmetricKey.provider_config:type_name -> policy.KeyProviderConfig + 42, // 80: policy.AsymmetricKey.metadata:type_name -> common.Metadata + 6, // 81: policy.SymmetricKey.key_status:type_name -> policy.KeyStatus + 7, // 82: policy.SymmetricKey.key_mode:type_name -> policy.KeyMode + 11, // 83: policy.SymmetricKey.provider_config:type_name -> policy.KeyProviderConfig + 42, // 84: policy.SymmetricKey.metadata:type_name -> common.Metadata + 15, // 85: policy.RegisteredResourceValue.ActionAttributeValue.action:type_name -> policy.Action + 14, // 86: policy.RegisteredResourceValue.ActionAttributeValue.attribute_value:type_name -> policy.Value + 42, // 87: policy.RegisteredResourceValue.ActionAttributeValue.metadata:type_name -> common.Metadata + 88, // [88:88] is the sub-list for method output_type + 88, // [88:88] is the sub-list for method input_type + 88, // [88:88] is the sub-list for extension type_name + 88, // [88:88] is the sub-list for extension extendee + 0, // [0:88] is the sub-list for field type_name } func init() { file_policy_objects_proto_init() } @@ -3976,18 +4026,6 @@ func file_policy_objects_proto_init() { } } file_policy_objects_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Certificate); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_policy_objects_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Attribute); i { case 0: return &v.state @@ -3999,7 +4037,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Value); i { case 0: return &v.state @@ -4011,7 +4049,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Action); i { case 0: return &v.state @@ -4023,7 +4061,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SubjectMapping); i { case 0: return &v.state @@ -4035,7 +4073,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Condition); i { case 0: return &v.state @@ -4047,7 +4085,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ConditionGroup); i { case 0: return &v.state @@ -4059,7 +4097,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SubjectSet); i { case 0: return &v.state @@ -4071,7 +4109,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SubjectConditionSet); i { case 0: return &v.state @@ -4083,7 +4121,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SubjectProperty); i { case 0: return &v.state @@ -4095,7 +4133,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ResourceMappingGroup); i { case 0: return &v.state @@ -4107,7 +4145,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ResourceMapping); i { case 0: return &v.state @@ -4119,7 +4157,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KeyAccessServer); i { case 0: return &v.state @@ -4131,7 +4169,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Key); i { case 0: return &v.state @@ -4143,7 +4181,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KasPublicKey); i { case 0: return &v.state @@ -4155,7 +4193,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KasPublicKeySet); i { case 0: return &v.state @@ -4167,7 +4205,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PublicKey); i { case 0: return &v.state @@ -4179,7 +4217,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RegisteredResource); i { case 0: return &v.state @@ -4191,7 +4229,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RegisteredResourceValue); i { case 0: return &v.state @@ -4203,7 +4241,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PolicyEnforcementPoint); i { case 0: return &v.state @@ -4215,7 +4253,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RequestContext); i { case 0: return &v.state @@ -4227,7 +4265,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Obligation); i { case 0: return &v.state @@ -4239,7 +4277,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ObligationValue); i { case 0: return &v.state @@ -4251,7 +4289,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ObligationTrigger); i { case 0: return &v.state @@ -4263,7 +4301,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KasKey); i { case 0: return &v.state @@ -4275,7 +4313,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PublicKeyCtx); i { case 0: return &v.state @@ -4287,7 +4325,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PrivateKeyCtx); i { case 0: return &v.state @@ -4299,7 +4337,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AsymmetricKey); i { case 0: return &v.state @@ -4311,7 +4349,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SymmetricKey); i { case 0: return &v.state @@ -4323,7 +4361,7 @@ func file_policy_objects_proto_init() { return nil } } - file_policy_objects_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { + file_policy_objects_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RegisteredResourceValue_ActionAttributeValue); i { case 0: return &v.state @@ -4336,11 +4374,11 @@ func file_policy_objects_proto_init() { } } } - file_policy_objects_proto_msgTypes[7].OneofWrappers = []interface{}{ + file_policy_objects_proto_msgTypes[6].OneofWrappers = []interface{}{ (*Action_Standard)(nil), (*Action_Custom)(nil), } - file_policy_objects_proto_msgTypes[20].OneofWrappers = []interface{}{ + file_policy_objects_proto_msgTypes[19].OneofWrappers = []interface{}{ (*PublicKey_Remote)(nil), (*PublicKey_Cached)(nil), } @@ -4350,7 +4388,7 @@ func file_policy_objects_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_policy_objects_proto_rawDesc, NumEnums: 9, - NumMessages: 34, + NumMessages: 33, NumExtensions: 0, NumServices: 0, }, diff --git a/protocol/go/policy/obligations/obligations.pb.go b/protocol/go/policy/obligations/obligations.pb.go index 0d25666cc2..0216912107 100644 --- a/protocol/go/policy/obligations/obligations.pb.go +++ b/protocol/go/policy/obligations/obligations.pb.go @@ -23,7 +23,116 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// Definitions +type SortObligationsType int32 + +const ( + SortObligationsType_SORT_OBLIGATIONS_TYPE_UNSPECIFIED SortObligationsType = 0 + SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME SortObligationsType = 1 + SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN SortObligationsType = 2 + SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT SortObligationsType = 3 + SortObligationsType_SORT_OBLIGATIONS_TYPE_UPDATED_AT SortObligationsType = 4 +) + +// Enum value maps for SortObligationsType. +var ( + SortObligationsType_name = map[int32]string{ + 0: "SORT_OBLIGATIONS_TYPE_UNSPECIFIED", + 1: "SORT_OBLIGATIONS_TYPE_NAME", + 2: "SORT_OBLIGATIONS_TYPE_FQN", + 3: "SORT_OBLIGATIONS_TYPE_CREATED_AT", + 4: "SORT_OBLIGATIONS_TYPE_UPDATED_AT", + } + SortObligationsType_value = map[string]int32{ + "SORT_OBLIGATIONS_TYPE_UNSPECIFIED": 0, + "SORT_OBLIGATIONS_TYPE_NAME": 1, + "SORT_OBLIGATIONS_TYPE_FQN": 2, + "SORT_OBLIGATIONS_TYPE_CREATED_AT": 3, + "SORT_OBLIGATIONS_TYPE_UPDATED_AT": 4, + } +) + +func (x SortObligationsType) Enum() *SortObligationsType { + p := new(SortObligationsType) + *p = x + return p +} + +func (x SortObligationsType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortObligationsType) Descriptor() protoreflect.EnumDescriptor { + return file_policy_obligations_obligations_proto_enumTypes[0].Descriptor() +} + +func (SortObligationsType) Type() protoreflect.EnumType { + return &file_policy_obligations_obligations_proto_enumTypes[0] +} + +func (x SortObligationsType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortObligationsType.Descriptor instead. +func (SortObligationsType) EnumDescriptor() ([]byte, []int) { + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{0} +} + +type ObligationsSort struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field SortObligationsType `protobuf:"varint,1,opt,name=field,proto3,enum=policy.obligations.SortObligationsType" json:"field,omitempty"` + Direction policy.SortDirection `protobuf:"varint,2,opt,name=direction,proto3,enum=policy.SortDirection" json:"direction,omitempty"` +} + +func (x *ObligationsSort) Reset() { + *x = ObligationsSort{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_obligations_obligations_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ObligationsSort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObligationsSort) ProtoMessage() {} + +func (x *ObligationsSort) ProtoReflect() protoreflect.Message { + mi := &file_policy_obligations_obligations_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ObligationsSort.ProtoReflect.Descriptor instead. +func (*ObligationsSort) Descriptor() ([]byte, []int) { + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{0} +} + +func (x *ObligationsSort) GetField() SortObligationsType { + if x != nil { + return x.Field + } + return SortObligationsType_SORT_OBLIGATIONS_TYPE_UNSPECIFIED +} + +func (x *ObligationsSort) GetDirection() policy.SortDirection { + if x != nil { + return x.Direction + } + return policy.SortDirection(0) +} + type GetObligationRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -36,7 +145,7 @@ type GetObligationRequest struct { func (x *GetObligationRequest) Reset() { *x = GetObligationRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[0] + mi := &file_policy_obligations_obligations_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -49,7 +158,7 @@ func (x *GetObligationRequest) String() string { func (*GetObligationRequest) ProtoMessage() {} func (x *GetObligationRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[0] + mi := &file_policy_obligations_obligations_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -62,7 +171,7 @@ func (x *GetObligationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetObligationRequest.ProtoReflect.Descriptor instead. func (*GetObligationRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{0} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{1} } func (x *GetObligationRequest) GetId() string { @@ -95,7 +204,7 @@ type ValueTriggerRequest struct { func (x *ValueTriggerRequest) Reset() { *x = ValueTriggerRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[1] + mi := &file_policy_obligations_obligations_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -108,7 +217,7 @@ func (x *ValueTriggerRequest) String() string { func (*ValueTriggerRequest) ProtoMessage() {} func (x *ValueTriggerRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[1] + mi := &file_policy_obligations_obligations_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -121,7 +230,7 @@ func (x *ValueTriggerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValueTriggerRequest.ProtoReflect.Descriptor instead. func (*ValueTriggerRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{1} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{2} } func (x *ValueTriggerRequest) GetAction() *common.IdNameIdentifier { @@ -156,7 +265,7 @@ type GetObligationResponse struct { func (x *GetObligationResponse) Reset() { *x = GetObligationResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[2] + mi := &file_policy_obligations_obligations_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -169,7 +278,7 @@ func (x *GetObligationResponse) String() string { func (*GetObligationResponse) ProtoMessage() {} func (x *GetObligationResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[2] + mi := &file_policy_obligations_obligations_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -182,7 +291,7 @@ func (x *GetObligationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetObligationResponse.ProtoReflect.Descriptor instead. func (*GetObligationResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{2} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{3} } func (x *GetObligationResponse) GetObligation() *policy.Obligation { @@ -203,7 +312,7 @@ type GetObligationsByFQNsRequest struct { func (x *GetObligationsByFQNsRequest) Reset() { *x = GetObligationsByFQNsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[3] + mi := &file_policy_obligations_obligations_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -216,7 +325,7 @@ func (x *GetObligationsByFQNsRequest) String() string { func (*GetObligationsByFQNsRequest) ProtoMessage() {} func (x *GetObligationsByFQNsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[3] + mi := &file_policy_obligations_obligations_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -229,7 +338,7 @@ func (x *GetObligationsByFQNsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetObligationsByFQNsRequest.ProtoReflect.Descriptor instead. func (*GetObligationsByFQNsRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{3} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{4} } func (x *GetObligationsByFQNsRequest) GetFqns() []string { @@ -250,7 +359,7 @@ type GetObligationsByFQNsResponse struct { func (x *GetObligationsByFQNsResponse) Reset() { *x = GetObligationsByFQNsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[4] + mi := &file_policy_obligations_obligations_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -263,7 +372,7 @@ func (x *GetObligationsByFQNsResponse) String() string { func (*GetObligationsByFQNsResponse) ProtoMessage() {} func (x *GetObligationsByFQNsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[4] + mi := &file_policy_obligations_obligations_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -276,7 +385,7 @@ func (x *GetObligationsByFQNsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetObligationsByFQNsResponse.ProtoReflect.Descriptor instead. func (*GetObligationsByFQNsResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{4} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{5} } func (x *GetObligationsByFQNsResponse) GetFqnObligationMap() map[string]*policy.Obligation { @@ -304,7 +413,7 @@ type CreateObligationRequest struct { func (x *CreateObligationRequest) Reset() { *x = CreateObligationRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[5] + mi := &file_policy_obligations_obligations_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -317,7 +426,7 @@ func (x *CreateObligationRequest) String() string { func (*CreateObligationRequest) ProtoMessage() {} func (x *CreateObligationRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[5] + mi := &file_policy_obligations_obligations_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -330,7 +439,7 @@ func (x *CreateObligationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateObligationRequest.ProtoReflect.Descriptor instead. func (*CreateObligationRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{5} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{6} } func (x *CreateObligationRequest) GetNamespaceId() string { @@ -379,7 +488,7 @@ type CreateObligationResponse struct { func (x *CreateObligationResponse) Reset() { *x = CreateObligationResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[6] + mi := &file_policy_obligations_obligations_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -392,7 +501,7 @@ func (x *CreateObligationResponse) String() string { func (*CreateObligationResponse) ProtoMessage() {} func (x *CreateObligationResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[6] + mi := &file_policy_obligations_obligations_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -405,7 +514,7 @@ func (x *CreateObligationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateObligationResponse.ProtoReflect.Descriptor instead. func (*CreateObligationResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{6} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{7} } func (x *CreateObligationResponse) GetObligation() *policy.Obligation { @@ -431,7 +540,7 @@ type UpdateObligationRequest struct { func (x *UpdateObligationRequest) Reset() { *x = UpdateObligationRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[7] + mi := &file_policy_obligations_obligations_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -444,7 +553,7 @@ func (x *UpdateObligationRequest) String() string { func (*UpdateObligationRequest) ProtoMessage() {} func (x *UpdateObligationRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[7] + mi := &file_policy_obligations_obligations_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -457,7 +566,7 @@ func (x *UpdateObligationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateObligationRequest.ProtoReflect.Descriptor instead. func (*UpdateObligationRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{7} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{8} } func (x *UpdateObligationRequest) GetId() string { @@ -499,7 +608,7 @@ type UpdateObligationResponse struct { func (x *UpdateObligationResponse) Reset() { *x = UpdateObligationResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[8] + mi := &file_policy_obligations_obligations_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -512,7 +621,7 @@ func (x *UpdateObligationResponse) String() string { func (*UpdateObligationResponse) ProtoMessage() {} func (x *UpdateObligationResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[8] + mi := &file_policy_obligations_obligations_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -525,7 +634,7 @@ func (x *UpdateObligationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateObligationResponse.ProtoReflect.Descriptor instead. func (*UpdateObligationResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{8} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{9} } func (x *UpdateObligationResponse) GetObligation() *policy.Obligation { @@ -547,7 +656,7 @@ type DeleteObligationRequest struct { func (x *DeleteObligationRequest) Reset() { *x = DeleteObligationRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[9] + mi := &file_policy_obligations_obligations_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -560,7 +669,7 @@ func (x *DeleteObligationRequest) String() string { func (*DeleteObligationRequest) ProtoMessage() {} func (x *DeleteObligationRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[9] + mi := &file_policy_obligations_obligations_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -573,7 +682,7 @@ func (x *DeleteObligationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteObligationRequest.ProtoReflect.Descriptor instead. func (*DeleteObligationRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{9} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{10} } func (x *DeleteObligationRequest) GetId() string { @@ -601,7 +710,7 @@ type DeleteObligationResponse struct { func (x *DeleteObligationResponse) Reset() { *x = DeleteObligationResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[10] + mi := &file_policy_obligations_obligations_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -614,7 +723,7 @@ func (x *DeleteObligationResponse) String() string { func (*DeleteObligationResponse) ProtoMessage() {} func (x *DeleteObligationResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[10] + mi := &file_policy_obligations_obligations_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -627,7 +736,7 @@ func (x *DeleteObligationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteObligationResponse.ProtoReflect.Descriptor instead. func (*DeleteObligationResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{10} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{11} } func (x *DeleteObligationResponse) GetObligation() *policy.Obligation { @@ -646,12 +755,18 @@ type ListObligationsRequest struct { NamespaceFqn string `protobuf:"bytes,2,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + Sort []*ObligationsSort `protobuf:"bytes,11,rep,name=sort,proto3" json:"sort,omitempty"` } func (x *ListObligationsRequest) Reset() { *x = ListObligationsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[11] + mi := &file_policy_obligations_obligations_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -664,7 +779,7 @@ func (x *ListObligationsRequest) String() string { func (*ListObligationsRequest) ProtoMessage() {} func (x *ListObligationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[11] + mi := &file_policy_obligations_obligations_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -677,7 +792,7 @@ func (x *ListObligationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListObligationsRequest.ProtoReflect.Descriptor instead. func (*ListObligationsRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{11} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{12} } func (x *ListObligationsRequest) GetNamespaceId() string { @@ -701,6 +816,13 @@ func (x *ListObligationsRequest) GetPagination() *policy.PageRequest { return nil } +func (x *ListObligationsRequest) GetSort() []*ObligationsSort { + if x != nil { + return x.Sort + } + return nil +} + type ListObligationsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -713,7 +835,7 @@ type ListObligationsResponse struct { func (x *ListObligationsResponse) Reset() { *x = ListObligationsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[12] + mi := &file_policy_obligations_obligations_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -726,7 +848,7 @@ func (x *ListObligationsResponse) String() string { func (*ListObligationsResponse) ProtoMessage() {} func (x *ListObligationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[12] + mi := &file_policy_obligations_obligations_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -739,7 +861,7 @@ func (x *ListObligationsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListObligationsResponse.ProtoReflect.Descriptor instead. func (*ListObligationsResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{12} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{13} } func (x *ListObligationsResponse) GetObligations() []*policy.Obligation { @@ -769,7 +891,7 @@ type GetObligationValueRequest struct { func (x *GetObligationValueRequest) Reset() { *x = GetObligationValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[13] + mi := &file_policy_obligations_obligations_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -782,7 +904,7 @@ func (x *GetObligationValueRequest) String() string { func (*GetObligationValueRequest) ProtoMessage() {} func (x *GetObligationValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[13] + mi := &file_policy_obligations_obligations_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -795,7 +917,7 @@ func (x *GetObligationValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetObligationValueRequest.ProtoReflect.Descriptor instead. func (*GetObligationValueRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{13} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{14} } func (x *GetObligationValueRequest) GetId() string { @@ -823,7 +945,7 @@ type GetObligationValueResponse struct { func (x *GetObligationValueResponse) Reset() { *x = GetObligationValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[14] + mi := &file_policy_obligations_obligations_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -836,7 +958,7 @@ func (x *GetObligationValueResponse) String() string { func (*GetObligationValueResponse) ProtoMessage() {} func (x *GetObligationValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[14] + mi := &file_policy_obligations_obligations_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -849,7 +971,7 @@ func (x *GetObligationValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetObligationValueResponse.ProtoReflect.Descriptor instead. func (*GetObligationValueResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{14} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{15} } func (x *GetObligationValueResponse) GetValue() *policy.ObligationValue { @@ -870,7 +992,7 @@ type GetObligationValuesByFQNsRequest struct { func (x *GetObligationValuesByFQNsRequest) Reset() { *x = GetObligationValuesByFQNsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[15] + mi := &file_policy_obligations_obligations_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -883,7 +1005,7 @@ func (x *GetObligationValuesByFQNsRequest) String() string { func (*GetObligationValuesByFQNsRequest) ProtoMessage() {} func (x *GetObligationValuesByFQNsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[15] + mi := &file_policy_obligations_obligations_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -896,7 +1018,7 @@ func (x *GetObligationValuesByFQNsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetObligationValuesByFQNsRequest.ProtoReflect.Descriptor instead. func (*GetObligationValuesByFQNsRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{15} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{16} } func (x *GetObligationValuesByFQNsRequest) GetFqns() []string { @@ -917,7 +1039,7 @@ type GetObligationValuesByFQNsResponse struct { func (x *GetObligationValuesByFQNsResponse) Reset() { *x = GetObligationValuesByFQNsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[16] + mi := &file_policy_obligations_obligations_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -930,7 +1052,7 @@ func (x *GetObligationValuesByFQNsResponse) String() string { func (*GetObligationValuesByFQNsResponse) ProtoMessage() {} func (x *GetObligationValuesByFQNsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[16] + mi := &file_policy_obligations_obligations_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -943,7 +1065,7 @@ func (x *GetObligationValuesByFQNsResponse) ProtoReflect() protoreflect.Message // Deprecated: Use GetObligationValuesByFQNsResponse.ProtoReflect.Descriptor instead. func (*GetObligationValuesByFQNsResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{16} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{17} } func (x *GetObligationValuesByFQNsResponse) GetFqnValueMap() map[string]*policy.ObligationValue { @@ -972,7 +1094,7 @@ type CreateObligationValueRequest struct { func (x *CreateObligationValueRequest) Reset() { *x = CreateObligationValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[17] + mi := &file_policy_obligations_obligations_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -985,7 +1107,7 @@ func (x *CreateObligationValueRequest) String() string { func (*CreateObligationValueRequest) ProtoMessage() {} func (x *CreateObligationValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[17] + mi := &file_policy_obligations_obligations_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -998,7 +1120,7 @@ func (x *CreateObligationValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateObligationValueRequest.ProtoReflect.Descriptor instead. func (*CreateObligationValueRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{17} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{18} } func (x *CreateObligationValueRequest) GetObligationId() string { @@ -1047,7 +1169,7 @@ type CreateObligationValueResponse struct { func (x *CreateObligationValueResponse) Reset() { *x = CreateObligationValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[18] + mi := &file_policy_obligations_obligations_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1060,7 +1182,7 @@ func (x *CreateObligationValueResponse) String() string { func (*CreateObligationValueResponse) ProtoMessage() {} func (x *CreateObligationValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[18] + mi := &file_policy_obligations_obligations_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1073,7 +1195,7 @@ func (x *CreateObligationValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateObligationValueResponse.ProtoReflect.Descriptor instead. func (*CreateObligationValueResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{18} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{19} } func (x *CreateObligationValueResponse) GetValue() *policy.ObligationValue { @@ -1104,7 +1226,7 @@ type UpdateObligationValueRequest struct { func (x *UpdateObligationValueRequest) Reset() { *x = UpdateObligationValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[19] + mi := &file_policy_obligations_obligations_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1117,7 +1239,7 @@ func (x *UpdateObligationValueRequest) String() string { func (*UpdateObligationValueRequest) ProtoMessage() {} func (x *UpdateObligationValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[19] + mi := &file_policy_obligations_obligations_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1130,7 +1252,7 @@ func (x *UpdateObligationValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateObligationValueRequest.ProtoReflect.Descriptor instead. func (*UpdateObligationValueRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{19} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{20} } func (x *UpdateObligationValueRequest) GetId() string { @@ -1179,7 +1301,7 @@ type UpdateObligationValueResponse struct { func (x *UpdateObligationValueResponse) Reset() { *x = UpdateObligationValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[20] + mi := &file_policy_obligations_obligations_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1192,7 +1314,7 @@ func (x *UpdateObligationValueResponse) String() string { func (*UpdateObligationValueResponse) ProtoMessage() {} func (x *UpdateObligationValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[20] + mi := &file_policy_obligations_obligations_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1205,7 +1327,7 @@ func (x *UpdateObligationValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateObligationValueResponse.ProtoReflect.Descriptor instead. func (*UpdateObligationValueResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{20} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{21} } func (x *UpdateObligationValueResponse) GetValue() *policy.ObligationValue { @@ -1227,7 +1349,7 @@ type DeleteObligationValueRequest struct { func (x *DeleteObligationValueRequest) Reset() { *x = DeleteObligationValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[21] + mi := &file_policy_obligations_obligations_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1240,7 +1362,7 @@ func (x *DeleteObligationValueRequest) String() string { func (*DeleteObligationValueRequest) ProtoMessage() {} func (x *DeleteObligationValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[21] + mi := &file_policy_obligations_obligations_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1253,7 +1375,7 @@ func (x *DeleteObligationValueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteObligationValueRequest.ProtoReflect.Descriptor instead. func (*DeleteObligationValueRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{21} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{22} } func (x *DeleteObligationValueRequest) GetId() string { @@ -1281,7 +1403,7 @@ type DeleteObligationValueResponse struct { func (x *DeleteObligationValueResponse) Reset() { *x = DeleteObligationValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[22] + mi := &file_policy_obligations_obligations_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1294,7 +1416,7 @@ func (x *DeleteObligationValueResponse) String() string { func (*DeleteObligationValueResponse) ProtoMessage() {} func (x *DeleteObligationValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[22] + mi := &file_policy_obligations_obligations_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1307,7 +1429,7 @@ func (x *DeleteObligationValueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteObligationValueResponse.ProtoReflect.Descriptor instead. func (*DeleteObligationValueResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{22} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{23} } func (x *DeleteObligationValueResponse) GetValue() *policy.ObligationValue { @@ -1318,6 +1440,104 @@ func (x *DeleteObligationValueResponse) GetValue() *policy.ObligationValue { } // Triggers +type GetObligationTriggerRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Required + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *GetObligationTriggerRequest) Reset() { + *x = GetObligationTriggerRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_obligations_obligations_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetObligationTriggerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetObligationTriggerRequest) ProtoMessage() {} + +func (x *GetObligationTriggerRequest) ProtoReflect() protoreflect.Message { + mi := &file_policy_obligations_obligations_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetObligationTriggerRequest.ProtoReflect.Descriptor instead. +func (*GetObligationTriggerRequest) Descriptor() ([]byte, []int) { + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{24} +} + +func (x *GetObligationTriggerRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type GetObligationTriggerResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Trigger *policy.ObligationTrigger `protobuf:"bytes,1,opt,name=trigger,proto3" json:"trigger,omitempty"` +} + +func (x *GetObligationTriggerResponse) Reset() { + *x = GetObligationTriggerResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_obligations_obligations_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetObligationTriggerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetObligationTriggerResponse) ProtoMessage() {} + +func (x *GetObligationTriggerResponse) ProtoReflect() protoreflect.Message { + mi := &file_policy_obligations_obligations_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetObligationTriggerResponse.ProtoReflect.Descriptor instead. +func (*GetObligationTriggerResponse) Descriptor() ([]byte, []int) { + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{25} +} + +func (x *GetObligationTriggerResponse) GetTrigger() *policy.ObligationTrigger { + if x != nil { + return x.Trigger + } + return nil +} + +// Obligation Triggers are owned by the namespace that owns the action and attribute value, which must +// be the same. In this way, a trigger can intentionally cross namespace boundaries: associating +// obligation values of a different namespace than the one that owns the action being taken or the attribute value. type AddObligationTriggerRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1340,7 +1560,7 @@ type AddObligationTriggerRequest struct { func (x *AddObligationTriggerRequest) Reset() { *x = AddObligationTriggerRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[23] + mi := &file_policy_obligations_obligations_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1353,7 +1573,7 @@ func (x *AddObligationTriggerRequest) String() string { func (*AddObligationTriggerRequest) ProtoMessage() {} func (x *AddObligationTriggerRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[23] + mi := &file_policy_obligations_obligations_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1366,7 +1586,7 @@ func (x *AddObligationTriggerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddObligationTriggerRequest.ProtoReflect.Descriptor instead. func (*AddObligationTriggerRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{23} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{26} } func (x *AddObligationTriggerRequest) GetObligationValue() *common.IdFqnIdentifier { @@ -1415,7 +1635,7 @@ type AddObligationTriggerResponse struct { func (x *AddObligationTriggerResponse) Reset() { *x = AddObligationTriggerResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[24] + mi := &file_policy_obligations_obligations_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1428,7 +1648,7 @@ func (x *AddObligationTriggerResponse) String() string { func (*AddObligationTriggerResponse) ProtoMessage() {} func (x *AddObligationTriggerResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[24] + mi := &file_policy_obligations_obligations_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1441,7 +1661,7 @@ func (x *AddObligationTriggerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddObligationTriggerResponse.ProtoReflect.Descriptor instead. func (*AddObligationTriggerResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{24} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{27} } func (x *AddObligationTriggerResponse) GetTrigger() *policy.ObligationTrigger { @@ -1463,7 +1683,7 @@ type RemoveObligationTriggerRequest struct { func (x *RemoveObligationTriggerRequest) Reset() { *x = RemoveObligationTriggerRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[25] + mi := &file_policy_obligations_obligations_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1476,7 +1696,7 @@ func (x *RemoveObligationTriggerRequest) String() string { func (*RemoveObligationTriggerRequest) ProtoMessage() {} func (x *RemoveObligationTriggerRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[25] + mi := &file_policy_obligations_obligations_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1489,7 +1709,7 @@ func (x *RemoveObligationTriggerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveObligationTriggerRequest.ProtoReflect.Descriptor instead. func (*RemoveObligationTriggerRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{25} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{28} } func (x *RemoveObligationTriggerRequest) GetId() string { @@ -1510,7 +1730,7 @@ type RemoveObligationTriggerResponse struct { func (x *RemoveObligationTriggerResponse) Reset() { *x = RemoveObligationTriggerResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[26] + mi := &file_policy_obligations_obligations_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1523,7 +1743,7 @@ func (x *RemoveObligationTriggerResponse) String() string { func (*RemoveObligationTriggerResponse) ProtoMessage() {} func (x *RemoveObligationTriggerResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[26] + mi := &file_policy_obligations_obligations_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1536,7 +1756,7 @@ func (x *RemoveObligationTriggerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveObligationTriggerResponse.ProtoReflect.Descriptor instead. func (*RemoveObligationTriggerResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{26} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{29} } func (x *RemoveObligationTriggerResponse) GetTrigger() *policy.ObligationTrigger { @@ -1560,7 +1780,7 @@ type ListObligationTriggersRequest struct { func (x *ListObligationTriggersRequest) Reset() { *x = ListObligationTriggersRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[27] + mi := &file_policy_obligations_obligations_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1573,7 +1793,7 @@ func (x *ListObligationTriggersRequest) String() string { func (*ListObligationTriggersRequest) ProtoMessage() {} func (x *ListObligationTriggersRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[27] + mi := &file_policy_obligations_obligations_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1586,7 +1806,7 @@ func (x *ListObligationTriggersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListObligationTriggersRequest.ProtoReflect.Descriptor instead. func (*ListObligationTriggersRequest) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{27} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{30} } func (x *ListObligationTriggersRequest) GetNamespaceId() string { @@ -1622,7 +1842,7 @@ type ListObligationTriggersResponse struct { func (x *ListObligationTriggersResponse) Reset() { *x = ListObligationTriggersResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_obligations_obligations_proto_msgTypes[28] + mi := &file_policy_obligations_obligations_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1635,7 +1855,7 @@ func (x *ListObligationTriggersResponse) String() string { func (*ListObligationTriggersResponse) ProtoMessage() {} func (x *ListObligationTriggersResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_obligations_obligations_proto_msgTypes[28] + mi := &file_policy_obligations_obligations_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1648,7 +1868,7 @@ func (x *ListObligationTriggersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListObligationTriggersResponse.ProtoReflect.Descriptor instead. func (*ListObligationTriggersResponse) Descriptor() ([]byte, []int) { - return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{28} + return file_policy_obligations_obligations_proto_rawDescGZIP(), []int{31} } func (x *ListObligationTriggersResponse) GetTriggers() []*policy.ObligationTrigger { @@ -1677,472 +1897,515 @@ var file_policy_obligations_obligations_proto_rawDesc = []byte{ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x16, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x60, 0x0a, 0x14, 0x47, 0x65, - 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x03, - 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, - 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0x10, 0xba, 0x48, 0x0d, 0x22, - 0x0b, 0x0a, 0x02, 0x69, 0x64, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x10, 0x01, 0x22, 0xcb, 0x01, 0x0a, - 0x13, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x64, - 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, - 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x48, - 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x49, 0x64, 0x46, 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x4b, 0x0a, 0x15, 0x47, 0x65, - 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x6c, - 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x49, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x09, 0x42, 0x16, 0xba, 0x48, 0x13, 0x92, 0x01, 0x10, 0x08, 0x01, 0x10, 0xfa, - 0x01, 0x18, 0x01, 0x22, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x04, 0x66, 0x71, - 0x6e, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x74, 0x0a, 0x12, 0x66, 0x71, 0x6e, 0x5f, 0x6f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x46, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x46, 0x71, 0x6e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, - 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x10, 0x66, 0x71, 0x6e, 0x4f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x1a, 0x57, 0x0a, 0x15, 0x46, 0x71, 0x6e, - 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x99, 0x01, 0x0a, 0x0f, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x47, + 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, + 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x64, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x60, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, + 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0x10, 0xba, 0x48, 0x0d, 0x22, 0x0b, 0x0a, 0x02, 0x69, + 0x64, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x10, 0x01, 0x22, 0xcb, 0x01, 0x0a, 0x13, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x38, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x64, 0x4e, 0x61, 0x6d, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, + 0x01, 0x01, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x0f, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x64, 0x46, + 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, + 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x4b, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, - 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0xd4, 0x04, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, - 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, - 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0xa7, 0x02, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x92, 0x02, 0xba, 0x48, - 0x8e, 0x02, 0xba, 0x01, 0x82, 0x02, 0x0a, 0x16, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xaa, - 0x01, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, - 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, - 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, - 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, - 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, - 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, - 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x64, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, - 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, - 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x49, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, + 0x42, 0x16, 0xba, 0x48, 0x13, 0x92, 0x01, 0x10, 0x08, 0x01, 0x10, 0xfa, 0x01, 0x18, 0x01, 0x22, + 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x22, 0xed, + 0x01, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x74, 0x0a, 0x12, 0x66, 0x71, 0x6e, 0x5f, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x46, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, + 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x71, + 0x6e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x10, 0x66, 0x71, 0x6e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x1a, 0x57, 0x0a, 0x15, 0x46, 0x71, 0x6e, 0x4f, 0x62, 0x6c, 0x69, + 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd4, + 0x04, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0xa7, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x92, 0x02, 0xba, 0x48, 0x8e, 0x02, 0xba, 0x01, + 0x82, 0x02, 0x0a, 0x16, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xaa, 0x01, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, + 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, + 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, + 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, + 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, + 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, + 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, + 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, + 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, + 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, + 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, + 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, + 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, + 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, + 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x56, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x09, 0x42, 0x3e, 0xba, 0x48, 0x3b, 0x92, 0x01, 0x38, 0x08, 0x00, 0x18, 0x01, 0x22, 0x32, + 0x72, 0x30, 0x18, 0xfd, 0x01, 0x32, 0x2b, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, + 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, + 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, + 0x3f, 0x24, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, + 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x66, 0x71, 0x6e, 0x10, 0x01, 0x22, 0x4e, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x04, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xbf, 0x02, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xaa, 0x02, 0xba, 0x48, 0xa6, + 0x02, 0xba, 0x01, 0x9a, 0x02, 0x0a, 0x16, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xaa, 0x01, + 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, + 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, + 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, + 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, + 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x64, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, + 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, + 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x53, 0x73, 0x69, 0x7a, 0x65, + 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, - 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x56, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x42, 0x3e, 0xba, 0x48, 0x3b, 0x92, 0x01, 0x38, 0x08, 0x00, - 0x18, 0x01, 0x22, 0x32, 0x72, 0x30, 0x18, 0xfd, 0x01, 0x32, 0x2b, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, - 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, - 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x33, - 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x3a, 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x01, 0x22, 0x4e, 0x0a, 0x18, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x04, 0x0a, 0x17, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, - 0xbf, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xaa, - 0x02, 0xba, 0x48, 0xa6, 0x02, 0xba, 0x01, 0x9a, 0x02, 0x0a, 0x16, 0x6f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x12, 0xaa, 0x01, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, - 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, - 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, - 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, - 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, - 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, - 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, - 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, - 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x53, - 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, - 0x72, 0x75, 0x65, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, - 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x4e, 0x0a, 0x18, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x63, 0x0a, 0x17, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x1c, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, - 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, - 0x10, 0xba, 0x48, 0x0d, 0x22, 0x0b, 0x0a, 0x02, 0x69, 0x64, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x10, - 0x01, 0x22, 0x4e, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, - 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0xd1, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, - 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3a, - 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x66, 0x71, 0x6e, 0x10, 0x00, 0x22, 0x85, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x34, 0x0a, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x6f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x65, 0x0a, - 0x19, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x03, 0x66, - 0x71, 0x6e, 0x3a, 0x10, 0xba, 0x48, 0x0d, 0x22, 0x0b, 0x0a, 0x02, 0x69, 0x64, 0x0a, 0x03, 0x66, - 0x71, 0x6e, 0x10, 0x01, 0x22, 0x4b, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0x4e, 0x0a, 0x20, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x09, 0x42, 0x16, 0xba, 0x48, 0x13, 0x92, 0x01, 0x10, 0x08, 0x01, 0x10, 0xfa, 0x01, - 0x18, 0x01, 0x22, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x04, 0x66, 0x71, 0x6e, - 0x73, 0x22, 0xe8, 0x01, 0x0a, 0x21, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x0d, 0x66, 0x71, 0x6e, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x46, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, - 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x66, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x4d, 0x61, 0x70, 0x1a, 0x57, 0x0a, 0x10, 0x46, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, - 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd1, 0x04, 0x0a, - 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, - 0x0d, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0c, - 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x0e, - 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, - 0x52, 0x0d, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x71, 0x6e, 0x12, - 0xac, 0x02, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x95, 0x02, 0xba, 0x48, 0x91, 0x02, 0xba, 0x01, 0x85, 0x02, 0x0a, 0x17, 0x6f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x12, 0xac, 0x01, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, - 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, - 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, - 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, - 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, - 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, - 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, - 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, - 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, - 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, - 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, - 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, - 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, - 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x43, - 0x0a, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x72, 0x69, 0x67, 0x67, - 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, - 0x65, 0x72, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x26, 0xba, 0x48, 0x23, 0x22, 0x21, 0x0a, - 0x0d, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x0a, 0x0e, - 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x01, - 0x22, 0x4e, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0xc8, + 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, + 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x4e, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, + 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x63, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, + 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, + 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0x10, 0xba, 0x48, 0x0d, + 0x22, 0x0b, 0x0a, 0x02, 0x69, 0x64, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x10, 0x01, 0x22, 0x4e, 0x0a, + 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x6f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x94, 0x02, + 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, + 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, + 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x04, 0x73, + 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x3a, 0x24, + 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, + 0x71, 0x6e, 0x10, 0x00, 0x22, 0x85, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x34, 0x0a, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x6f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x65, 0x0a, 0x19, + 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, + 0x6e, 0x3a, 0x10, 0xba, 0x48, 0x0d, 0x22, 0x0b, 0x0a, 0x02, 0x69, 0x64, 0x0a, 0x03, 0x66, 0x71, + 0x6e, 0x10, 0x01, 0x22, 0x4b, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x22, 0xcf, 0x04, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x22, 0x4e, 0x0a, 0x20, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x42, 0x16, 0xba, 0x48, 0x13, 0x92, 0x01, 0x10, 0x08, 0x01, 0x10, 0xfa, 0x01, 0x18, + 0x01, 0x22, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x04, 0x66, 0x71, 0x6e, 0x73, + 0x22, 0xe8, 0x01, 0x0a, 0x21, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x0d, 0x66, 0x71, 0x6e, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x46, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x66, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, + 0x61, 0x70, 0x1a, 0x57, 0x0a, 0x10, 0x46, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, + 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd1, 0x04, 0x0a, 0x1c, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x0d, + 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0c, 0x6f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x0e, 0x6f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, + 0x0d, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x71, 0x6e, 0x12, 0xac, + 0x02, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x95, + 0x02, 0xba, 0x48, 0x91, 0x02, 0xba, 0x01, 0x85, 0x02, 0x0a, 0x17, 0x6f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x12, 0xac, 0x01, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, + 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, + 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, + 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, + 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, + 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, + 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, + 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, + 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, + 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, + 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, + 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, + 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, + 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x43, 0x0a, + 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, + 0x72, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x26, 0xba, 0x48, 0x23, 0x22, 0x21, 0x0a, 0x0d, + 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x0a, 0x0e, 0x6f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x01, 0x22, + 0x4e, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, + 0xcf, 0x04, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, + 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xc4, 0x02, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xad, 0x02, 0xba, 0x48, 0xa9, + 0x02, 0xba, 0x01, 0x9d, 0x02, 0x0a, 0x17, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xac, + 0x01, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, + 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, + 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, + 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, + 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, + 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, + 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x53, 0x73, + 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, + 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, + 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, + 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, + 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, 0x72, + 0x75, 0x65, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x43, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x72, + 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x74, 0x72, + 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, + 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, + 0x72, 0x22, 0x4e, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x68, 0x0a, 0x1c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, - 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xc4, 0x02, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xad, 0x02, 0xba, 0x48, - 0xa9, 0x02, 0xba, 0x01, 0x9d, 0x02, 0x0a, 0x17, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, - 0xac, 0x01, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, - 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, - 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, - 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x77, 0x69, 0x6c, - 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, - 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x53, - 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, - 0x72, 0x75, 0x65, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x74, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, - 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, - 0x6f, 0x72, 0x22, 0x4e, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x22, 0x68, 0x0a, 0x1c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x03, - 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, - 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0x10, 0xba, 0x48, 0x0d, 0x22, - 0x0b, 0x0a, 0x02, 0x69, 0x64, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x10, 0x01, 0x22, 0x4e, 0x0a, 0x1d, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd4, 0x02, 0x0a, - 0x1b, 0x41, 0x64, 0x64, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, - 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x10, - 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x49, 0x64, 0x46, 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, - 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0f, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x49, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x64, 0x46, 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x0a, 0x07, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x33, - 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x22, 0x53, 0x0a, 0x1c, 0x41, 0x64, 0x64, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, - 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x22, 0x3a, 0x0a, 0x1e, 0x52, 0x65, 0x6d, 0x6f, - 0x76, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x03, 0x66, + 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, + 0x01, 0x88, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x3a, 0x10, 0xba, 0x48, 0x0d, 0x22, 0x0b, + 0x0a, 0x02, 0x69, 0x64, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x10, 0x01, 0x22, 0x4e, 0x0a, 0x1d, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x37, 0x0a, 0x1b, 0x47, + 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x52, 0x02, 0x69, 0x64, 0x22, 0x56, 0x0a, 0x1f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, - 0x67, 0x65, 0x72, 0x52, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x22, 0xd8, 0x01, 0x0a, - 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, - 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, 0x0a, - 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x3a, 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x00, 0x22, 0x8d, 0x01, 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, - 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x74, 0x72, - 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, - 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xcd, 0x0d, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x6f, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x03, 0x90, 0x02, 0x01, 0x12, 0x69, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x52, 0x02, 0x69, 0x64, 0x22, 0x53, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, + 0x52, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x22, 0xd4, 0x02, 0x0a, 0x1b, 0x41, 0x64, + 0x64, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x10, 0x6f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x64, 0x46, + 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0xba, 0x48, + 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0f, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, + 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x48, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x49, 0x64, 0x46, 0x71, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x33, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, + 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x22, 0x53, 0x0a, 0x1c, 0x41, 0x64, 0x64, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x33, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x07, 0x74, 0x72, + 0x69, 0x67, 0x67, 0x65, 0x72, 0x22, 0x3a, 0x0a, 0x1e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, + 0x64, 0x22, 0x56, 0x0a, 0x1f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, + 0x52, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x22, 0xd8, 0x01, 0x0a, 0x1d, 0x4c, 0x69, + 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, + 0x67, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x24, + 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, + 0x71, 0x6e, 0x10, 0x00, 0x22, 0x8d, 0x01, 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, + 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, + 0x67, 0x67, 0x65, 0x72, 0x52, 0x08, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x12, 0x34, + 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0xc7, 0x01, 0x0a, 0x13, 0x53, 0x6f, 0x72, 0x74, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x21, + 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4f, 0x42, 0x4c, 0x49, 0x47, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x53, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4f, 0x42, 0x4c, 0x49, + 0x47, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x41, 0x4d, + 0x45, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4f, 0x42, 0x4c, 0x49, + 0x47, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x51, 0x4e, + 0x10, 0x02, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4f, 0x42, 0x4c, 0x49, 0x47, + 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, + 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x4f, 0x52, 0x54, + 0x5f, 0x4f, 0x42, 0x4c, 0x49, 0x47, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x04, 0x32, 0xcd, + 0x0e, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6f, 0x0a, 0x0f, 0x4c, 0x69, + 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x69, 0x0a, 0x0d, 0x47, + 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x7e, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x12, 0x2f, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, - 0x7e, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, - 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, - 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, - 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, - 0x6f, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, - 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x6f, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, - 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x6f, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x78, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, - 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, - 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x8d, 0x01, 0x0a, - 0x19, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, + 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x6f, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, - 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7b, 0x0a, 0x14, - 0x41, 0x64, 0x64, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, - 0x67, 0x67, 0x65, 0x72, 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x4f, 0x62, 0x6c, - 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x4f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x84, 0x01, 0x0a, 0x17, 0x52, 0x65, - 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, - 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, - 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, - 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, - 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x84, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x12, 0x31, 0x2e, 0x70, 0x6f, + 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x78, 0x0a, 0x12, 0x47, 0x65, 0x74, + 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x2d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x42, 0xcf, 0x01, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x42, 0x10, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, - 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0xa2, 0x02, 0x03, 0x50, 0x4f, 0x58, 0xaa, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0xca, 0x02, 0x12, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0xe2, 0x02, 0x1e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4f, 0x62, 0x6c, 0x69, - 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x4f, 0x62, - 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, + 0x90, 0x02, 0x01, 0x12, 0x8d, 0x01, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, + 0x73, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, + 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, + 0x90, 0x02, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x2f, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, + 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, + 0x90, 0x02, 0x01, 0x12, 0x7b, 0x0a, 0x14, 0x41, 0x64, 0x64, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x2f, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x41, 0x64, 0x64, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, + 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x41, 0x64, 0x64, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x84, 0x01, 0x0a, 0x17, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x32, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x62, 0x6c, 0x69, + 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x84, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, + 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, + 0x72, 0x73, 0x12, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, 0x6c, 0x69, + 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6c, 0x69, + 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, + 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x42, 0xcf, + 0x01, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6f, 0x62, + 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x10, 0x4f, 0x62, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, + 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6f, 0x62, + 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0xa2, 0x02, 0x03, 0x50, 0x4f, 0x58, 0xaa, + 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0xca, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4f, 0x62, + 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0xe2, 0x02, 0x1e, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x5c, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5c, 0x47, + 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x4f, 0x62, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2157,120 +2420,132 @@ func file_policy_obligations_obligations_proto_rawDescGZIP() []byte { return file_policy_obligations_obligations_proto_rawDescData } -var file_policy_obligations_obligations_proto_msgTypes = make([]protoimpl.MessageInfo, 31) +var file_policy_obligations_obligations_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_policy_obligations_obligations_proto_msgTypes = make([]protoimpl.MessageInfo, 34) var file_policy_obligations_obligations_proto_goTypes = []interface{}{ - (*GetObligationRequest)(nil), // 0: policy.obligations.GetObligationRequest - (*ValueTriggerRequest)(nil), // 1: policy.obligations.ValueTriggerRequest - (*GetObligationResponse)(nil), // 2: policy.obligations.GetObligationResponse - (*GetObligationsByFQNsRequest)(nil), // 3: policy.obligations.GetObligationsByFQNsRequest - (*GetObligationsByFQNsResponse)(nil), // 4: policy.obligations.GetObligationsByFQNsResponse - (*CreateObligationRequest)(nil), // 5: policy.obligations.CreateObligationRequest - (*CreateObligationResponse)(nil), // 6: policy.obligations.CreateObligationResponse - (*UpdateObligationRequest)(nil), // 7: policy.obligations.UpdateObligationRequest - (*UpdateObligationResponse)(nil), // 8: policy.obligations.UpdateObligationResponse - (*DeleteObligationRequest)(nil), // 9: policy.obligations.DeleteObligationRequest - (*DeleteObligationResponse)(nil), // 10: policy.obligations.DeleteObligationResponse - (*ListObligationsRequest)(nil), // 11: policy.obligations.ListObligationsRequest - (*ListObligationsResponse)(nil), // 12: policy.obligations.ListObligationsResponse - (*GetObligationValueRequest)(nil), // 13: policy.obligations.GetObligationValueRequest - (*GetObligationValueResponse)(nil), // 14: policy.obligations.GetObligationValueResponse - (*GetObligationValuesByFQNsRequest)(nil), // 15: policy.obligations.GetObligationValuesByFQNsRequest - (*GetObligationValuesByFQNsResponse)(nil), // 16: policy.obligations.GetObligationValuesByFQNsResponse - (*CreateObligationValueRequest)(nil), // 17: policy.obligations.CreateObligationValueRequest - (*CreateObligationValueResponse)(nil), // 18: policy.obligations.CreateObligationValueResponse - (*UpdateObligationValueRequest)(nil), // 19: policy.obligations.UpdateObligationValueRequest - (*UpdateObligationValueResponse)(nil), // 20: policy.obligations.UpdateObligationValueResponse - (*DeleteObligationValueRequest)(nil), // 21: policy.obligations.DeleteObligationValueRequest - (*DeleteObligationValueResponse)(nil), // 22: policy.obligations.DeleteObligationValueResponse - (*AddObligationTriggerRequest)(nil), // 23: policy.obligations.AddObligationTriggerRequest - (*AddObligationTriggerResponse)(nil), // 24: policy.obligations.AddObligationTriggerResponse - (*RemoveObligationTriggerRequest)(nil), // 25: policy.obligations.RemoveObligationTriggerRequest - (*RemoveObligationTriggerResponse)(nil), // 26: policy.obligations.RemoveObligationTriggerResponse - (*ListObligationTriggersRequest)(nil), // 27: policy.obligations.ListObligationTriggersRequest - (*ListObligationTriggersResponse)(nil), // 28: policy.obligations.ListObligationTriggersResponse - nil, // 29: policy.obligations.GetObligationsByFQNsResponse.FqnObligationMapEntry - nil, // 30: policy.obligations.GetObligationValuesByFQNsResponse.FqnValueMapEntry - (*common.IdNameIdentifier)(nil), // 31: common.IdNameIdentifier - (*common.IdFqnIdentifier)(nil), // 32: common.IdFqnIdentifier - (*policy.RequestContext)(nil), // 33: policy.RequestContext - (*policy.Obligation)(nil), // 34: policy.Obligation - (*common.MetadataMutable)(nil), // 35: common.MetadataMutable - (common.MetadataUpdateEnum)(0), // 36: common.MetadataUpdateEnum - (*policy.PageRequest)(nil), // 37: policy.PageRequest - (*policy.PageResponse)(nil), // 38: policy.PageResponse - (*policy.ObligationValue)(nil), // 39: policy.ObligationValue - (*policy.ObligationTrigger)(nil), // 40: policy.ObligationTrigger + (SortObligationsType)(0), // 0: policy.obligations.SortObligationsType + (*ObligationsSort)(nil), // 1: policy.obligations.ObligationsSort + (*GetObligationRequest)(nil), // 2: policy.obligations.GetObligationRequest + (*ValueTriggerRequest)(nil), // 3: policy.obligations.ValueTriggerRequest + (*GetObligationResponse)(nil), // 4: policy.obligations.GetObligationResponse + (*GetObligationsByFQNsRequest)(nil), // 5: policy.obligations.GetObligationsByFQNsRequest + (*GetObligationsByFQNsResponse)(nil), // 6: policy.obligations.GetObligationsByFQNsResponse + (*CreateObligationRequest)(nil), // 7: policy.obligations.CreateObligationRequest + (*CreateObligationResponse)(nil), // 8: policy.obligations.CreateObligationResponse + (*UpdateObligationRequest)(nil), // 9: policy.obligations.UpdateObligationRequest + (*UpdateObligationResponse)(nil), // 10: policy.obligations.UpdateObligationResponse + (*DeleteObligationRequest)(nil), // 11: policy.obligations.DeleteObligationRequest + (*DeleteObligationResponse)(nil), // 12: policy.obligations.DeleteObligationResponse + (*ListObligationsRequest)(nil), // 13: policy.obligations.ListObligationsRequest + (*ListObligationsResponse)(nil), // 14: policy.obligations.ListObligationsResponse + (*GetObligationValueRequest)(nil), // 15: policy.obligations.GetObligationValueRequest + (*GetObligationValueResponse)(nil), // 16: policy.obligations.GetObligationValueResponse + (*GetObligationValuesByFQNsRequest)(nil), // 17: policy.obligations.GetObligationValuesByFQNsRequest + (*GetObligationValuesByFQNsResponse)(nil), // 18: policy.obligations.GetObligationValuesByFQNsResponse + (*CreateObligationValueRequest)(nil), // 19: policy.obligations.CreateObligationValueRequest + (*CreateObligationValueResponse)(nil), // 20: policy.obligations.CreateObligationValueResponse + (*UpdateObligationValueRequest)(nil), // 21: policy.obligations.UpdateObligationValueRequest + (*UpdateObligationValueResponse)(nil), // 22: policy.obligations.UpdateObligationValueResponse + (*DeleteObligationValueRequest)(nil), // 23: policy.obligations.DeleteObligationValueRequest + (*DeleteObligationValueResponse)(nil), // 24: policy.obligations.DeleteObligationValueResponse + (*GetObligationTriggerRequest)(nil), // 25: policy.obligations.GetObligationTriggerRequest + (*GetObligationTriggerResponse)(nil), // 26: policy.obligations.GetObligationTriggerResponse + (*AddObligationTriggerRequest)(nil), // 27: policy.obligations.AddObligationTriggerRequest + (*AddObligationTriggerResponse)(nil), // 28: policy.obligations.AddObligationTriggerResponse + (*RemoveObligationTriggerRequest)(nil), // 29: policy.obligations.RemoveObligationTriggerRequest + (*RemoveObligationTriggerResponse)(nil), // 30: policy.obligations.RemoveObligationTriggerResponse + (*ListObligationTriggersRequest)(nil), // 31: policy.obligations.ListObligationTriggersRequest + (*ListObligationTriggersResponse)(nil), // 32: policy.obligations.ListObligationTriggersResponse + nil, // 33: policy.obligations.GetObligationsByFQNsResponse.FqnObligationMapEntry + nil, // 34: policy.obligations.GetObligationValuesByFQNsResponse.FqnValueMapEntry + (policy.SortDirection)(0), // 35: policy.SortDirection + (*common.IdNameIdentifier)(nil), // 36: common.IdNameIdentifier + (*common.IdFqnIdentifier)(nil), // 37: common.IdFqnIdentifier + (*policy.RequestContext)(nil), // 38: policy.RequestContext + (*policy.Obligation)(nil), // 39: policy.Obligation + (*common.MetadataMutable)(nil), // 40: common.MetadataMutable + (common.MetadataUpdateEnum)(0), // 41: common.MetadataUpdateEnum + (*policy.PageRequest)(nil), // 42: policy.PageRequest + (*policy.PageResponse)(nil), // 43: policy.PageResponse + (*policy.ObligationValue)(nil), // 44: policy.ObligationValue + (*policy.ObligationTrigger)(nil), // 45: policy.ObligationTrigger } var file_policy_obligations_obligations_proto_depIdxs = []int32{ - 31, // 0: policy.obligations.ValueTriggerRequest.action:type_name -> common.IdNameIdentifier - 32, // 1: policy.obligations.ValueTriggerRequest.attribute_value:type_name -> common.IdFqnIdentifier - 33, // 2: policy.obligations.ValueTriggerRequest.context:type_name -> policy.RequestContext - 34, // 3: policy.obligations.GetObligationResponse.obligation:type_name -> policy.Obligation - 29, // 4: policy.obligations.GetObligationsByFQNsResponse.fqn_obligation_map:type_name -> policy.obligations.GetObligationsByFQNsResponse.FqnObligationMapEntry - 35, // 5: policy.obligations.CreateObligationRequest.metadata:type_name -> common.MetadataMutable - 34, // 6: policy.obligations.CreateObligationResponse.obligation:type_name -> policy.Obligation - 35, // 7: policy.obligations.UpdateObligationRequest.metadata:type_name -> common.MetadataMutable - 36, // 8: policy.obligations.UpdateObligationRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 34, // 9: policy.obligations.UpdateObligationResponse.obligation:type_name -> policy.Obligation - 34, // 10: policy.obligations.DeleteObligationResponse.obligation:type_name -> policy.Obligation - 37, // 11: policy.obligations.ListObligationsRequest.pagination:type_name -> policy.PageRequest - 34, // 12: policy.obligations.ListObligationsResponse.obligations:type_name -> policy.Obligation - 38, // 13: policy.obligations.ListObligationsResponse.pagination:type_name -> policy.PageResponse - 39, // 14: policy.obligations.GetObligationValueResponse.value:type_name -> policy.ObligationValue - 30, // 15: policy.obligations.GetObligationValuesByFQNsResponse.fqn_value_map:type_name -> policy.obligations.GetObligationValuesByFQNsResponse.FqnValueMapEntry - 1, // 16: policy.obligations.CreateObligationValueRequest.triggers:type_name -> policy.obligations.ValueTriggerRequest - 35, // 17: policy.obligations.CreateObligationValueRequest.metadata:type_name -> common.MetadataMutable - 39, // 18: policy.obligations.CreateObligationValueResponse.value:type_name -> policy.ObligationValue - 1, // 19: policy.obligations.UpdateObligationValueRequest.triggers:type_name -> policy.obligations.ValueTriggerRequest - 35, // 20: policy.obligations.UpdateObligationValueRequest.metadata:type_name -> common.MetadataMutable - 36, // 21: policy.obligations.UpdateObligationValueRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 39, // 22: policy.obligations.UpdateObligationValueResponse.value:type_name -> policy.ObligationValue - 39, // 23: policy.obligations.DeleteObligationValueResponse.value:type_name -> policy.ObligationValue - 32, // 24: policy.obligations.AddObligationTriggerRequest.obligation_value:type_name -> common.IdFqnIdentifier - 31, // 25: policy.obligations.AddObligationTriggerRequest.action:type_name -> common.IdNameIdentifier - 32, // 26: policy.obligations.AddObligationTriggerRequest.attribute_value:type_name -> common.IdFqnIdentifier - 33, // 27: policy.obligations.AddObligationTriggerRequest.context:type_name -> policy.RequestContext - 35, // 28: policy.obligations.AddObligationTriggerRequest.metadata:type_name -> common.MetadataMutable - 40, // 29: policy.obligations.AddObligationTriggerResponse.trigger:type_name -> policy.ObligationTrigger - 40, // 30: policy.obligations.RemoveObligationTriggerResponse.trigger:type_name -> policy.ObligationTrigger - 37, // 31: policy.obligations.ListObligationTriggersRequest.pagination:type_name -> policy.PageRequest - 40, // 32: policy.obligations.ListObligationTriggersResponse.triggers:type_name -> policy.ObligationTrigger - 38, // 33: policy.obligations.ListObligationTriggersResponse.pagination:type_name -> policy.PageResponse - 34, // 34: policy.obligations.GetObligationsByFQNsResponse.FqnObligationMapEntry.value:type_name -> policy.Obligation - 39, // 35: policy.obligations.GetObligationValuesByFQNsResponse.FqnValueMapEntry.value:type_name -> policy.ObligationValue - 11, // 36: policy.obligations.Service.ListObligations:input_type -> policy.obligations.ListObligationsRequest - 0, // 37: policy.obligations.Service.GetObligation:input_type -> policy.obligations.GetObligationRequest - 3, // 38: policy.obligations.Service.GetObligationsByFQNs:input_type -> policy.obligations.GetObligationsByFQNsRequest - 5, // 39: policy.obligations.Service.CreateObligation:input_type -> policy.obligations.CreateObligationRequest - 7, // 40: policy.obligations.Service.UpdateObligation:input_type -> policy.obligations.UpdateObligationRequest - 9, // 41: policy.obligations.Service.DeleteObligation:input_type -> policy.obligations.DeleteObligationRequest - 13, // 42: policy.obligations.Service.GetObligationValue:input_type -> policy.obligations.GetObligationValueRequest - 15, // 43: policy.obligations.Service.GetObligationValuesByFQNs:input_type -> policy.obligations.GetObligationValuesByFQNsRequest - 17, // 44: policy.obligations.Service.CreateObligationValue:input_type -> policy.obligations.CreateObligationValueRequest - 19, // 45: policy.obligations.Service.UpdateObligationValue:input_type -> policy.obligations.UpdateObligationValueRequest - 21, // 46: policy.obligations.Service.DeleteObligationValue:input_type -> policy.obligations.DeleteObligationValueRequest - 23, // 47: policy.obligations.Service.AddObligationTrigger:input_type -> policy.obligations.AddObligationTriggerRequest - 25, // 48: policy.obligations.Service.RemoveObligationTrigger:input_type -> policy.obligations.RemoveObligationTriggerRequest - 27, // 49: policy.obligations.Service.ListObligationTriggers:input_type -> policy.obligations.ListObligationTriggersRequest - 12, // 50: policy.obligations.Service.ListObligations:output_type -> policy.obligations.ListObligationsResponse - 2, // 51: policy.obligations.Service.GetObligation:output_type -> policy.obligations.GetObligationResponse - 4, // 52: policy.obligations.Service.GetObligationsByFQNs:output_type -> policy.obligations.GetObligationsByFQNsResponse - 6, // 53: policy.obligations.Service.CreateObligation:output_type -> policy.obligations.CreateObligationResponse - 8, // 54: policy.obligations.Service.UpdateObligation:output_type -> policy.obligations.UpdateObligationResponse - 10, // 55: policy.obligations.Service.DeleteObligation:output_type -> policy.obligations.DeleteObligationResponse - 14, // 56: policy.obligations.Service.GetObligationValue:output_type -> policy.obligations.GetObligationValueResponse - 16, // 57: policy.obligations.Service.GetObligationValuesByFQNs:output_type -> policy.obligations.GetObligationValuesByFQNsResponse - 18, // 58: policy.obligations.Service.CreateObligationValue:output_type -> policy.obligations.CreateObligationValueResponse - 20, // 59: policy.obligations.Service.UpdateObligationValue:output_type -> policy.obligations.UpdateObligationValueResponse - 22, // 60: policy.obligations.Service.DeleteObligationValue:output_type -> policy.obligations.DeleteObligationValueResponse - 24, // 61: policy.obligations.Service.AddObligationTrigger:output_type -> policy.obligations.AddObligationTriggerResponse - 26, // 62: policy.obligations.Service.RemoveObligationTrigger:output_type -> policy.obligations.RemoveObligationTriggerResponse - 28, // 63: policy.obligations.Service.ListObligationTriggers:output_type -> policy.obligations.ListObligationTriggersResponse - 50, // [50:64] is the sub-list for method output_type - 36, // [36:50] is the sub-list for method input_type - 36, // [36:36] is the sub-list for extension type_name - 36, // [36:36] is the sub-list for extension extendee - 0, // [0:36] is the sub-list for field type_name + 0, // 0: policy.obligations.ObligationsSort.field:type_name -> policy.obligations.SortObligationsType + 35, // 1: policy.obligations.ObligationsSort.direction:type_name -> policy.SortDirection + 36, // 2: policy.obligations.ValueTriggerRequest.action:type_name -> common.IdNameIdentifier + 37, // 3: policy.obligations.ValueTriggerRequest.attribute_value:type_name -> common.IdFqnIdentifier + 38, // 4: policy.obligations.ValueTriggerRequest.context:type_name -> policy.RequestContext + 39, // 5: policy.obligations.GetObligationResponse.obligation:type_name -> policy.Obligation + 33, // 6: policy.obligations.GetObligationsByFQNsResponse.fqn_obligation_map:type_name -> policy.obligations.GetObligationsByFQNsResponse.FqnObligationMapEntry + 40, // 7: policy.obligations.CreateObligationRequest.metadata:type_name -> common.MetadataMutable + 39, // 8: policy.obligations.CreateObligationResponse.obligation:type_name -> policy.Obligation + 40, // 9: policy.obligations.UpdateObligationRequest.metadata:type_name -> common.MetadataMutable + 41, // 10: policy.obligations.UpdateObligationRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 39, // 11: policy.obligations.UpdateObligationResponse.obligation:type_name -> policy.Obligation + 39, // 12: policy.obligations.DeleteObligationResponse.obligation:type_name -> policy.Obligation + 42, // 13: policy.obligations.ListObligationsRequest.pagination:type_name -> policy.PageRequest + 1, // 14: policy.obligations.ListObligationsRequest.sort:type_name -> policy.obligations.ObligationsSort + 39, // 15: policy.obligations.ListObligationsResponse.obligations:type_name -> policy.Obligation + 43, // 16: policy.obligations.ListObligationsResponse.pagination:type_name -> policy.PageResponse + 44, // 17: policy.obligations.GetObligationValueResponse.value:type_name -> policy.ObligationValue + 34, // 18: policy.obligations.GetObligationValuesByFQNsResponse.fqn_value_map:type_name -> policy.obligations.GetObligationValuesByFQNsResponse.FqnValueMapEntry + 3, // 19: policy.obligations.CreateObligationValueRequest.triggers:type_name -> policy.obligations.ValueTriggerRequest + 40, // 20: policy.obligations.CreateObligationValueRequest.metadata:type_name -> common.MetadataMutable + 44, // 21: policy.obligations.CreateObligationValueResponse.value:type_name -> policy.ObligationValue + 3, // 22: policy.obligations.UpdateObligationValueRequest.triggers:type_name -> policy.obligations.ValueTriggerRequest + 40, // 23: policy.obligations.UpdateObligationValueRequest.metadata:type_name -> common.MetadataMutable + 41, // 24: policy.obligations.UpdateObligationValueRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 44, // 25: policy.obligations.UpdateObligationValueResponse.value:type_name -> policy.ObligationValue + 44, // 26: policy.obligations.DeleteObligationValueResponse.value:type_name -> policy.ObligationValue + 45, // 27: policy.obligations.GetObligationTriggerResponse.trigger:type_name -> policy.ObligationTrigger + 37, // 28: policy.obligations.AddObligationTriggerRequest.obligation_value:type_name -> common.IdFqnIdentifier + 36, // 29: policy.obligations.AddObligationTriggerRequest.action:type_name -> common.IdNameIdentifier + 37, // 30: policy.obligations.AddObligationTriggerRequest.attribute_value:type_name -> common.IdFqnIdentifier + 38, // 31: policy.obligations.AddObligationTriggerRequest.context:type_name -> policy.RequestContext + 40, // 32: policy.obligations.AddObligationTriggerRequest.metadata:type_name -> common.MetadataMutable + 45, // 33: policy.obligations.AddObligationTriggerResponse.trigger:type_name -> policy.ObligationTrigger + 45, // 34: policy.obligations.RemoveObligationTriggerResponse.trigger:type_name -> policy.ObligationTrigger + 42, // 35: policy.obligations.ListObligationTriggersRequest.pagination:type_name -> policy.PageRequest + 45, // 36: policy.obligations.ListObligationTriggersResponse.triggers:type_name -> policy.ObligationTrigger + 43, // 37: policy.obligations.ListObligationTriggersResponse.pagination:type_name -> policy.PageResponse + 39, // 38: policy.obligations.GetObligationsByFQNsResponse.FqnObligationMapEntry.value:type_name -> policy.Obligation + 44, // 39: policy.obligations.GetObligationValuesByFQNsResponse.FqnValueMapEntry.value:type_name -> policy.ObligationValue + 13, // 40: policy.obligations.Service.ListObligations:input_type -> policy.obligations.ListObligationsRequest + 2, // 41: policy.obligations.Service.GetObligation:input_type -> policy.obligations.GetObligationRequest + 5, // 42: policy.obligations.Service.GetObligationsByFQNs:input_type -> policy.obligations.GetObligationsByFQNsRequest + 7, // 43: policy.obligations.Service.CreateObligation:input_type -> policy.obligations.CreateObligationRequest + 9, // 44: policy.obligations.Service.UpdateObligation:input_type -> policy.obligations.UpdateObligationRequest + 11, // 45: policy.obligations.Service.DeleteObligation:input_type -> policy.obligations.DeleteObligationRequest + 15, // 46: policy.obligations.Service.GetObligationValue:input_type -> policy.obligations.GetObligationValueRequest + 17, // 47: policy.obligations.Service.GetObligationValuesByFQNs:input_type -> policy.obligations.GetObligationValuesByFQNsRequest + 19, // 48: policy.obligations.Service.CreateObligationValue:input_type -> policy.obligations.CreateObligationValueRequest + 21, // 49: policy.obligations.Service.UpdateObligationValue:input_type -> policy.obligations.UpdateObligationValueRequest + 23, // 50: policy.obligations.Service.DeleteObligationValue:input_type -> policy.obligations.DeleteObligationValueRequest + 25, // 51: policy.obligations.Service.GetObligationTrigger:input_type -> policy.obligations.GetObligationTriggerRequest + 27, // 52: policy.obligations.Service.AddObligationTrigger:input_type -> policy.obligations.AddObligationTriggerRequest + 29, // 53: policy.obligations.Service.RemoveObligationTrigger:input_type -> policy.obligations.RemoveObligationTriggerRequest + 31, // 54: policy.obligations.Service.ListObligationTriggers:input_type -> policy.obligations.ListObligationTriggersRequest + 14, // 55: policy.obligations.Service.ListObligations:output_type -> policy.obligations.ListObligationsResponse + 4, // 56: policy.obligations.Service.GetObligation:output_type -> policy.obligations.GetObligationResponse + 6, // 57: policy.obligations.Service.GetObligationsByFQNs:output_type -> policy.obligations.GetObligationsByFQNsResponse + 8, // 58: policy.obligations.Service.CreateObligation:output_type -> policy.obligations.CreateObligationResponse + 10, // 59: policy.obligations.Service.UpdateObligation:output_type -> policy.obligations.UpdateObligationResponse + 12, // 60: policy.obligations.Service.DeleteObligation:output_type -> policy.obligations.DeleteObligationResponse + 16, // 61: policy.obligations.Service.GetObligationValue:output_type -> policy.obligations.GetObligationValueResponse + 18, // 62: policy.obligations.Service.GetObligationValuesByFQNs:output_type -> policy.obligations.GetObligationValuesByFQNsResponse + 20, // 63: policy.obligations.Service.CreateObligationValue:output_type -> policy.obligations.CreateObligationValueResponse + 22, // 64: policy.obligations.Service.UpdateObligationValue:output_type -> policy.obligations.UpdateObligationValueResponse + 24, // 65: policy.obligations.Service.DeleteObligationValue:output_type -> policy.obligations.DeleteObligationValueResponse + 26, // 66: policy.obligations.Service.GetObligationTrigger:output_type -> policy.obligations.GetObligationTriggerResponse + 28, // 67: policy.obligations.Service.AddObligationTrigger:output_type -> policy.obligations.AddObligationTriggerResponse + 30, // 68: policy.obligations.Service.RemoveObligationTrigger:output_type -> policy.obligations.RemoveObligationTriggerResponse + 32, // 69: policy.obligations.Service.ListObligationTriggers:output_type -> policy.obligations.ListObligationTriggersResponse + 55, // [55:70] is the sub-list for method output_type + 40, // [40:55] is the sub-list for method input_type + 40, // [40:40] is the sub-list for extension type_name + 40, // [40:40] is the sub-list for extension extendee + 0, // [0:40] is the sub-list for field type_name } func init() { file_policy_obligations_obligations_proto_init() } @@ -2280,7 +2555,7 @@ func file_policy_obligations_obligations_proto_init() { } if !protoimpl.UnsafeEnabled { file_policy_obligations_obligations_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObligationRequest); i { + switch v := v.(*ObligationsSort); i { case 0: return &v.state case 1: @@ -2292,7 +2567,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValueTriggerRequest); i { + switch v := v.(*GetObligationRequest); i { case 0: return &v.state case 1: @@ -2304,7 +2579,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObligationResponse); i { + switch v := v.(*ValueTriggerRequest); i { case 0: return &v.state case 1: @@ -2316,7 +2591,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObligationsByFQNsRequest); i { + switch v := v.(*GetObligationResponse); i { case 0: return &v.state case 1: @@ -2328,7 +2603,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObligationsByFQNsResponse); i { + switch v := v.(*GetObligationsByFQNsRequest); i { case 0: return &v.state case 1: @@ -2340,7 +2615,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateObligationRequest); i { + switch v := v.(*GetObligationsByFQNsResponse); i { case 0: return &v.state case 1: @@ -2352,7 +2627,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateObligationResponse); i { + switch v := v.(*CreateObligationRequest); i { case 0: return &v.state case 1: @@ -2364,7 +2639,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateObligationRequest); i { + switch v := v.(*CreateObligationResponse); i { case 0: return &v.state case 1: @@ -2376,7 +2651,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateObligationResponse); i { + switch v := v.(*UpdateObligationRequest); i { case 0: return &v.state case 1: @@ -2388,7 +2663,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteObligationRequest); i { + switch v := v.(*UpdateObligationResponse); i { case 0: return &v.state case 1: @@ -2400,7 +2675,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteObligationResponse); i { + switch v := v.(*DeleteObligationRequest); i { case 0: return &v.state case 1: @@ -2412,7 +2687,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListObligationsRequest); i { + switch v := v.(*DeleteObligationResponse); i { case 0: return &v.state case 1: @@ -2424,7 +2699,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListObligationsResponse); i { + switch v := v.(*ListObligationsRequest); i { case 0: return &v.state case 1: @@ -2436,7 +2711,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObligationValueRequest); i { + switch v := v.(*ListObligationsResponse); i { case 0: return &v.state case 1: @@ -2448,7 +2723,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObligationValueResponse); i { + switch v := v.(*GetObligationValueRequest); i { case 0: return &v.state case 1: @@ -2460,7 +2735,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObligationValuesByFQNsRequest); i { + switch v := v.(*GetObligationValueResponse); i { case 0: return &v.state case 1: @@ -2472,7 +2747,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObligationValuesByFQNsResponse); i { + switch v := v.(*GetObligationValuesByFQNsRequest); i { case 0: return &v.state case 1: @@ -2484,7 +2759,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateObligationValueRequest); i { + switch v := v.(*GetObligationValuesByFQNsResponse); i { case 0: return &v.state case 1: @@ -2496,7 +2771,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateObligationValueResponse); i { + switch v := v.(*CreateObligationValueRequest); i { case 0: return &v.state case 1: @@ -2508,7 +2783,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateObligationValueRequest); i { + switch v := v.(*CreateObligationValueResponse); i { case 0: return &v.state case 1: @@ -2520,7 +2795,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateObligationValueResponse); i { + switch v := v.(*UpdateObligationValueRequest); i { case 0: return &v.state case 1: @@ -2532,7 +2807,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteObligationValueRequest); i { + switch v := v.(*UpdateObligationValueResponse); i { case 0: return &v.state case 1: @@ -2544,7 +2819,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteObligationValueResponse); i { + switch v := v.(*DeleteObligationValueRequest); i { case 0: return &v.state case 1: @@ -2556,7 +2831,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AddObligationTriggerRequest); i { + switch v := v.(*DeleteObligationValueResponse); i { case 0: return &v.state case 1: @@ -2568,7 +2843,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AddObligationTriggerResponse); i { + switch v := v.(*GetObligationTriggerRequest); i { case 0: return &v.state case 1: @@ -2580,7 +2855,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveObligationTriggerRequest); i { + switch v := v.(*GetObligationTriggerResponse); i { case 0: return &v.state case 1: @@ -2592,7 +2867,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveObligationTriggerResponse); i { + switch v := v.(*AddObligationTriggerRequest); i { case 0: return &v.state case 1: @@ -2604,7 +2879,7 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListObligationTriggersRequest); i { + switch v := v.(*AddObligationTriggerResponse); i { case 0: return &v.state case 1: @@ -2616,6 +2891,42 @@ func file_policy_obligations_obligations_proto_init() { } } file_policy_obligations_obligations_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemoveObligationTriggerRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_obligations_obligations_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemoveObligationTriggerResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_obligations_obligations_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListObligationTriggersRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_obligations_obligations_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ListObligationTriggersResponse); i { case 0: return &v.state @@ -2633,13 +2944,14 @@ func file_policy_obligations_obligations_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_policy_obligations_obligations_proto_rawDesc, - NumEnums: 0, - NumMessages: 31, + NumEnums: 1, + NumMessages: 34, NumExtensions: 0, NumServices: 1, }, GoTypes: file_policy_obligations_obligations_proto_goTypes, DependencyIndexes: file_policy_obligations_obligations_proto_depIdxs, + EnumInfos: file_policy_obligations_obligations_proto_enumTypes, MessageInfos: file_policy_obligations_obligations_proto_msgTypes, }.Build() File_policy_obligations_obligations_proto = out.File diff --git a/protocol/go/policy/obligations/obligations_grpc.pb.go b/protocol/go/policy/obligations/obligations_grpc.pb.go index 947a537e87..918625f22d 100644 --- a/protocol/go/policy/obligations/obligations_grpc.pb.go +++ b/protocol/go/policy/obligations/obligations_grpc.pb.go @@ -30,6 +30,7 @@ const ( Service_CreateObligationValue_FullMethodName = "/policy.obligations.Service/CreateObligationValue" Service_UpdateObligationValue_FullMethodName = "/policy.obligations.Service/UpdateObligationValue" Service_DeleteObligationValue_FullMethodName = "/policy.obligations.Service/DeleteObligationValue" + Service_GetObligationTrigger_FullMethodName = "/policy.obligations.Service/GetObligationTrigger" Service_AddObligationTrigger_FullMethodName = "/policy.obligations.Service/AddObligationTrigger" Service_RemoveObligationTrigger_FullMethodName = "/policy.obligations.Service/RemoveObligationTrigger" Service_ListObligationTriggers_FullMethodName = "/policy.obligations.Service/ListObligationTriggers" @@ -50,6 +51,7 @@ type ServiceClient interface { CreateObligationValue(ctx context.Context, in *CreateObligationValueRequest, opts ...grpc.CallOption) (*CreateObligationValueResponse, error) UpdateObligationValue(ctx context.Context, in *UpdateObligationValueRequest, opts ...grpc.CallOption) (*UpdateObligationValueResponse, error) DeleteObligationValue(ctx context.Context, in *DeleteObligationValueRequest, opts ...grpc.CallOption) (*DeleteObligationValueResponse, error) + GetObligationTrigger(ctx context.Context, in *GetObligationTriggerRequest, opts ...grpc.CallOption) (*GetObligationTriggerResponse, error) AddObligationTrigger(ctx context.Context, in *AddObligationTriggerRequest, opts ...grpc.CallOption) (*AddObligationTriggerResponse, error) RemoveObligationTrigger(ctx context.Context, in *RemoveObligationTriggerRequest, opts ...grpc.CallOption) (*RemoveObligationTriggerResponse, error) ListObligationTriggers(ctx context.Context, in *ListObligationTriggersRequest, opts ...grpc.CallOption) (*ListObligationTriggersResponse, error) @@ -162,6 +164,15 @@ func (c *serviceClient) DeleteObligationValue(ctx context.Context, in *DeleteObl return out, nil } +func (c *serviceClient) GetObligationTrigger(ctx context.Context, in *GetObligationTriggerRequest, opts ...grpc.CallOption) (*GetObligationTriggerResponse, error) { + out := new(GetObligationTriggerResponse) + err := c.cc.Invoke(ctx, Service_GetObligationTrigger_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *serviceClient) AddObligationTrigger(ctx context.Context, in *AddObligationTriggerRequest, opts ...grpc.CallOption) (*AddObligationTriggerResponse, error) { out := new(AddObligationTriggerResponse) err := c.cc.Invoke(ctx, Service_AddObligationTrigger_FullMethodName, in, out, opts...) @@ -204,6 +215,7 @@ type ServiceServer interface { CreateObligationValue(context.Context, *CreateObligationValueRequest) (*CreateObligationValueResponse, error) UpdateObligationValue(context.Context, *UpdateObligationValueRequest) (*UpdateObligationValueResponse, error) DeleteObligationValue(context.Context, *DeleteObligationValueRequest) (*DeleteObligationValueResponse, error) + GetObligationTrigger(context.Context, *GetObligationTriggerRequest) (*GetObligationTriggerResponse, error) AddObligationTrigger(context.Context, *AddObligationTriggerRequest) (*AddObligationTriggerResponse, error) RemoveObligationTrigger(context.Context, *RemoveObligationTriggerRequest) (*RemoveObligationTriggerResponse, error) ListObligationTriggers(context.Context, *ListObligationTriggersRequest) (*ListObligationTriggersResponse, error) @@ -247,6 +259,9 @@ func (UnimplementedServiceServer) UpdateObligationValue(context.Context, *Update func (UnimplementedServiceServer) DeleteObligationValue(context.Context, *DeleteObligationValueRequest) (*DeleteObligationValueResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteObligationValue not implemented") } +func (UnimplementedServiceServer) GetObligationTrigger(context.Context, *GetObligationTriggerRequest) (*GetObligationTriggerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetObligationTrigger not implemented") +} func (UnimplementedServiceServer) AddObligationTrigger(context.Context, *AddObligationTriggerRequest) (*AddObligationTriggerResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AddObligationTrigger not implemented") } @@ -467,6 +482,24 @@ func _Service_DeleteObligationValue_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _Service_GetObligationTrigger_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetObligationTriggerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).GetObligationTrigger(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Service_GetObligationTrigger_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).GetObligationTrigger(ctx, req.(*GetObligationTriggerRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Service_AddObligationTrigger_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AddObligationTriggerRequest) if err := dec(in); err != nil { @@ -572,6 +605,10 @@ var Service_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteObligationValue", Handler: _Service_DeleteObligationValue_Handler, }, + { + MethodName: "GetObligationTrigger", + Handler: _Service_GetObligationTrigger_Handler, + }, { MethodName: "AddObligationTrigger", Handler: _Service_AddObligationTrigger_Handler, diff --git a/protocol/go/policy/obligations/obligationsconnect/obligations.connect.go b/protocol/go/policy/obligations/obligationsconnect/obligations.connect.go index 24efee7a4e..2650d8e094 100644 --- a/protocol/go/policy/obligations/obligationsconnect/obligations.connect.go +++ b/protocol/go/policy/obligations/obligationsconnect/obligations.connect.go @@ -64,6 +64,9 @@ const ( // ServiceDeleteObligationValueProcedure is the fully-qualified name of the Service's // DeleteObligationValue RPC. ServiceDeleteObligationValueProcedure = "/policy.obligations.Service/DeleteObligationValue" + // ServiceGetObligationTriggerProcedure is the fully-qualified name of the Service's + // GetObligationTrigger RPC. + ServiceGetObligationTriggerProcedure = "/policy.obligations.Service/GetObligationTrigger" // ServiceAddObligationTriggerProcedure is the fully-qualified name of the Service's // AddObligationTrigger RPC. ServiceAddObligationTriggerProcedure = "/policy.obligations.Service/AddObligationTrigger" @@ -75,25 +78,6 @@ const ( ServiceListObligationTriggersProcedure = "/policy.obligations.Service/ListObligationTriggers" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - serviceServiceDescriptor = obligations.File_policy_obligations_obligations_proto.Services().ByName("Service") - serviceListObligationsMethodDescriptor = serviceServiceDescriptor.Methods().ByName("ListObligations") - serviceGetObligationMethodDescriptor = serviceServiceDescriptor.Methods().ByName("GetObligation") - serviceGetObligationsByFQNsMethodDescriptor = serviceServiceDescriptor.Methods().ByName("GetObligationsByFQNs") - serviceCreateObligationMethodDescriptor = serviceServiceDescriptor.Methods().ByName("CreateObligation") - serviceUpdateObligationMethodDescriptor = serviceServiceDescriptor.Methods().ByName("UpdateObligation") - serviceDeleteObligationMethodDescriptor = serviceServiceDescriptor.Methods().ByName("DeleteObligation") - serviceGetObligationValueMethodDescriptor = serviceServiceDescriptor.Methods().ByName("GetObligationValue") - serviceGetObligationValuesByFQNsMethodDescriptor = serviceServiceDescriptor.Methods().ByName("GetObligationValuesByFQNs") - serviceCreateObligationValueMethodDescriptor = serviceServiceDescriptor.Methods().ByName("CreateObligationValue") - serviceUpdateObligationValueMethodDescriptor = serviceServiceDescriptor.Methods().ByName("UpdateObligationValue") - serviceDeleteObligationValueMethodDescriptor = serviceServiceDescriptor.Methods().ByName("DeleteObligationValue") - serviceAddObligationTriggerMethodDescriptor = serviceServiceDescriptor.Methods().ByName("AddObligationTrigger") - serviceRemoveObligationTriggerMethodDescriptor = serviceServiceDescriptor.Methods().ByName("RemoveObligationTrigger") - serviceListObligationTriggersMethodDescriptor = serviceServiceDescriptor.Methods().ByName("ListObligationTriggers") -) - // ServiceClient is a client for the policy.obligations.Service service. type ServiceClient interface { ListObligations(context.Context, *connect.Request[obligations.ListObligationsRequest]) (*connect.Response[obligations.ListObligationsResponse], error) @@ -107,6 +91,7 @@ type ServiceClient interface { CreateObligationValue(context.Context, *connect.Request[obligations.CreateObligationValueRequest]) (*connect.Response[obligations.CreateObligationValueResponse], error) UpdateObligationValue(context.Context, *connect.Request[obligations.UpdateObligationValueRequest]) (*connect.Response[obligations.UpdateObligationValueResponse], error) DeleteObligationValue(context.Context, *connect.Request[obligations.DeleteObligationValueRequest]) (*connect.Response[obligations.DeleteObligationValueResponse], error) + GetObligationTrigger(context.Context, *connect.Request[obligations.GetObligationTriggerRequest]) (*connect.Response[obligations.GetObligationTriggerResponse], error) AddObligationTrigger(context.Context, *connect.Request[obligations.AddObligationTriggerRequest]) (*connect.Response[obligations.AddObligationTriggerResponse], error) RemoveObligationTrigger(context.Context, *connect.Request[obligations.RemoveObligationTriggerRequest]) (*connect.Response[obligations.RemoveObligationTriggerResponse], error) ListObligationTriggers(context.Context, *connect.Request[obligations.ListObligationTriggersRequest]) (*connect.Response[obligations.ListObligationTriggersResponse], error) @@ -121,94 +106,102 @@ type ServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ServiceClient { baseURL = strings.TrimRight(baseURL, "/") + serviceMethods := obligations.File_policy_obligations_obligations_proto.Services().ByName("Service").Methods() return &serviceClient{ listObligations: connect.NewClient[obligations.ListObligationsRequest, obligations.ListObligationsResponse]( httpClient, baseURL+ServiceListObligationsProcedure, - connect.WithSchema(serviceListObligationsMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("ListObligations")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getObligation: connect.NewClient[obligations.GetObligationRequest, obligations.GetObligationResponse]( httpClient, baseURL+ServiceGetObligationProcedure, - connect.WithSchema(serviceGetObligationMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("GetObligation")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getObligationsByFQNs: connect.NewClient[obligations.GetObligationsByFQNsRequest, obligations.GetObligationsByFQNsResponse]( httpClient, baseURL+ServiceGetObligationsByFQNsProcedure, - connect.WithSchema(serviceGetObligationsByFQNsMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("GetObligationsByFQNs")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createObligation: connect.NewClient[obligations.CreateObligationRequest, obligations.CreateObligationResponse]( httpClient, baseURL+ServiceCreateObligationProcedure, - connect.WithSchema(serviceCreateObligationMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("CreateObligation")), connect.WithClientOptions(opts...), ), updateObligation: connect.NewClient[obligations.UpdateObligationRequest, obligations.UpdateObligationResponse]( httpClient, baseURL+ServiceUpdateObligationProcedure, - connect.WithSchema(serviceUpdateObligationMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("UpdateObligation")), connect.WithClientOptions(opts...), ), deleteObligation: connect.NewClient[obligations.DeleteObligationRequest, obligations.DeleteObligationResponse]( httpClient, baseURL+ServiceDeleteObligationProcedure, - connect.WithSchema(serviceDeleteObligationMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("DeleteObligation")), connect.WithClientOptions(opts...), ), getObligationValue: connect.NewClient[obligations.GetObligationValueRequest, obligations.GetObligationValueResponse]( httpClient, baseURL+ServiceGetObligationValueProcedure, - connect.WithSchema(serviceGetObligationValueMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("GetObligationValue")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getObligationValuesByFQNs: connect.NewClient[obligations.GetObligationValuesByFQNsRequest, obligations.GetObligationValuesByFQNsResponse]( httpClient, baseURL+ServiceGetObligationValuesByFQNsProcedure, - connect.WithSchema(serviceGetObligationValuesByFQNsMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("GetObligationValuesByFQNs")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createObligationValue: connect.NewClient[obligations.CreateObligationValueRequest, obligations.CreateObligationValueResponse]( httpClient, baseURL+ServiceCreateObligationValueProcedure, - connect.WithSchema(serviceCreateObligationValueMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("CreateObligationValue")), connect.WithClientOptions(opts...), ), updateObligationValue: connect.NewClient[obligations.UpdateObligationValueRequest, obligations.UpdateObligationValueResponse]( httpClient, baseURL+ServiceUpdateObligationValueProcedure, - connect.WithSchema(serviceUpdateObligationValueMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("UpdateObligationValue")), connect.WithClientOptions(opts...), ), deleteObligationValue: connect.NewClient[obligations.DeleteObligationValueRequest, obligations.DeleteObligationValueResponse]( httpClient, baseURL+ServiceDeleteObligationValueProcedure, - connect.WithSchema(serviceDeleteObligationValueMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("DeleteObligationValue")), + connect.WithClientOptions(opts...), + ), + getObligationTrigger: connect.NewClient[obligations.GetObligationTriggerRequest, obligations.GetObligationTriggerResponse]( + httpClient, + baseURL+ServiceGetObligationTriggerProcedure, + connect.WithSchema(serviceMethods.ByName("GetObligationTrigger")), + connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), addObligationTrigger: connect.NewClient[obligations.AddObligationTriggerRequest, obligations.AddObligationTriggerResponse]( httpClient, baseURL+ServiceAddObligationTriggerProcedure, - connect.WithSchema(serviceAddObligationTriggerMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("AddObligationTrigger")), connect.WithClientOptions(opts...), ), removeObligationTrigger: connect.NewClient[obligations.RemoveObligationTriggerRequest, obligations.RemoveObligationTriggerResponse]( httpClient, baseURL+ServiceRemoveObligationTriggerProcedure, - connect.WithSchema(serviceRemoveObligationTriggerMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("RemoveObligationTrigger")), connect.WithClientOptions(opts...), ), listObligationTriggers: connect.NewClient[obligations.ListObligationTriggersRequest, obligations.ListObligationTriggersResponse]( httpClient, baseURL+ServiceListObligationTriggersProcedure, - connect.WithSchema(serviceListObligationTriggersMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("ListObligationTriggers")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), @@ -228,6 +221,7 @@ type serviceClient struct { createObligationValue *connect.Client[obligations.CreateObligationValueRequest, obligations.CreateObligationValueResponse] updateObligationValue *connect.Client[obligations.UpdateObligationValueRequest, obligations.UpdateObligationValueResponse] deleteObligationValue *connect.Client[obligations.DeleteObligationValueRequest, obligations.DeleteObligationValueResponse] + getObligationTrigger *connect.Client[obligations.GetObligationTriggerRequest, obligations.GetObligationTriggerResponse] addObligationTrigger *connect.Client[obligations.AddObligationTriggerRequest, obligations.AddObligationTriggerResponse] removeObligationTrigger *connect.Client[obligations.RemoveObligationTriggerRequest, obligations.RemoveObligationTriggerResponse] listObligationTriggers *connect.Client[obligations.ListObligationTriggersRequest, obligations.ListObligationTriggersResponse] @@ -288,6 +282,11 @@ func (c *serviceClient) DeleteObligationValue(ctx context.Context, req *connect. return c.deleteObligationValue.CallUnary(ctx, req) } +// GetObligationTrigger calls policy.obligations.Service.GetObligationTrigger. +func (c *serviceClient) GetObligationTrigger(ctx context.Context, req *connect.Request[obligations.GetObligationTriggerRequest]) (*connect.Response[obligations.GetObligationTriggerResponse], error) { + return c.getObligationTrigger.CallUnary(ctx, req) +} + // AddObligationTrigger calls policy.obligations.Service.AddObligationTrigger. func (c *serviceClient) AddObligationTrigger(ctx context.Context, req *connect.Request[obligations.AddObligationTriggerRequest]) (*connect.Response[obligations.AddObligationTriggerResponse], error) { return c.addObligationTrigger.CallUnary(ctx, req) @@ -316,6 +315,7 @@ type ServiceHandler interface { CreateObligationValue(context.Context, *connect.Request[obligations.CreateObligationValueRequest]) (*connect.Response[obligations.CreateObligationValueResponse], error) UpdateObligationValue(context.Context, *connect.Request[obligations.UpdateObligationValueRequest]) (*connect.Response[obligations.UpdateObligationValueResponse], error) DeleteObligationValue(context.Context, *connect.Request[obligations.DeleteObligationValueRequest]) (*connect.Response[obligations.DeleteObligationValueResponse], error) + GetObligationTrigger(context.Context, *connect.Request[obligations.GetObligationTriggerRequest]) (*connect.Response[obligations.GetObligationTriggerResponse], error) AddObligationTrigger(context.Context, *connect.Request[obligations.AddObligationTriggerRequest]) (*connect.Response[obligations.AddObligationTriggerResponse], error) RemoveObligationTrigger(context.Context, *connect.Request[obligations.RemoveObligationTriggerRequest]) (*connect.Response[obligations.RemoveObligationTriggerResponse], error) ListObligationTriggers(context.Context, *connect.Request[obligations.ListObligationTriggersRequest]) (*connect.Response[obligations.ListObligationTriggersResponse], error) @@ -327,93 +327,101 @@ type ServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewServiceHandler(svc ServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + serviceMethods := obligations.File_policy_obligations_obligations_proto.Services().ByName("Service").Methods() serviceListObligationsHandler := connect.NewUnaryHandler( ServiceListObligationsProcedure, svc.ListObligations, - connect.WithSchema(serviceListObligationsMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("ListObligations")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) serviceGetObligationHandler := connect.NewUnaryHandler( ServiceGetObligationProcedure, svc.GetObligation, - connect.WithSchema(serviceGetObligationMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("GetObligation")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) serviceGetObligationsByFQNsHandler := connect.NewUnaryHandler( ServiceGetObligationsByFQNsProcedure, svc.GetObligationsByFQNs, - connect.WithSchema(serviceGetObligationsByFQNsMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("GetObligationsByFQNs")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) serviceCreateObligationHandler := connect.NewUnaryHandler( ServiceCreateObligationProcedure, svc.CreateObligation, - connect.WithSchema(serviceCreateObligationMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("CreateObligation")), connect.WithHandlerOptions(opts...), ) serviceUpdateObligationHandler := connect.NewUnaryHandler( ServiceUpdateObligationProcedure, svc.UpdateObligation, - connect.WithSchema(serviceUpdateObligationMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("UpdateObligation")), connect.WithHandlerOptions(opts...), ) serviceDeleteObligationHandler := connect.NewUnaryHandler( ServiceDeleteObligationProcedure, svc.DeleteObligation, - connect.WithSchema(serviceDeleteObligationMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("DeleteObligation")), connect.WithHandlerOptions(opts...), ) serviceGetObligationValueHandler := connect.NewUnaryHandler( ServiceGetObligationValueProcedure, svc.GetObligationValue, - connect.WithSchema(serviceGetObligationValueMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("GetObligationValue")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) serviceGetObligationValuesByFQNsHandler := connect.NewUnaryHandler( ServiceGetObligationValuesByFQNsProcedure, svc.GetObligationValuesByFQNs, - connect.WithSchema(serviceGetObligationValuesByFQNsMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("GetObligationValuesByFQNs")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) serviceCreateObligationValueHandler := connect.NewUnaryHandler( ServiceCreateObligationValueProcedure, svc.CreateObligationValue, - connect.WithSchema(serviceCreateObligationValueMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("CreateObligationValue")), connect.WithHandlerOptions(opts...), ) serviceUpdateObligationValueHandler := connect.NewUnaryHandler( ServiceUpdateObligationValueProcedure, svc.UpdateObligationValue, - connect.WithSchema(serviceUpdateObligationValueMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("UpdateObligationValue")), connect.WithHandlerOptions(opts...), ) serviceDeleteObligationValueHandler := connect.NewUnaryHandler( ServiceDeleteObligationValueProcedure, svc.DeleteObligationValue, - connect.WithSchema(serviceDeleteObligationValueMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("DeleteObligationValue")), + connect.WithHandlerOptions(opts...), + ) + serviceGetObligationTriggerHandler := connect.NewUnaryHandler( + ServiceGetObligationTriggerProcedure, + svc.GetObligationTrigger, + connect.WithSchema(serviceMethods.ByName("GetObligationTrigger")), + connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) serviceAddObligationTriggerHandler := connect.NewUnaryHandler( ServiceAddObligationTriggerProcedure, svc.AddObligationTrigger, - connect.WithSchema(serviceAddObligationTriggerMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("AddObligationTrigger")), connect.WithHandlerOptions(opts...), ) serviceRemoveObligationTriggerHandler := connect.NewUnaryHandler( ServiceRemoveObligationTriggerProcedure, svc.RemoveObligationTrigger, - connect.WithSchema(serviceRemoveObligationTriggerMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("RemoveObligationTrigger")), connect.WithHandlerOptions(opts...), ) serviceListObligationTriggersHandler := connect.NewUnaryHandler( ServiceListObligationTriggersProcedure, svc.ListObligationTriggers, - connect.WithSchema(serviceListObligationTriggersMethodDescriptor), + connect.WithSchema(serviceMethods.ByName("ListObligationTriggers")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) @@ -441,6 +449,8 @@ func NewServiceHandler(svc ServiceHandler, opts ...connect.HandlerOption) (strin serviceUpdateObligationValueHandler.ServeHTTP(w, r) case ServiceDeleteObligationValueProcedure: serviceDeleteObligationValueHandler.ServeHTTP(w, r) + case ServiceGetObligationTriggerProcedure: + serviceGetObligationTriggerHandler.ServeHTTP(w, r) case ServiceAddObligationTriggerProcedure: serviceAddObligationTriggerHandler.ServeHTTP(w, r) case ServiceRemoveObligationTriggerProcedure: @@ -500,6 +510,10 @@ func (UnimplementedServiceHandler) DeleteObligationValue(context.Context, *conne return nil, connect.NewError(connect.CodeUnimplemented, errors.New("policy.obligations.Service.DeleteObligationValue is not implemented")) } +func (UnimplementedServiceHandler) GetObligationTrigger(context.Context, *connect.Request[obligations.GetObligationTriggerRequest]) (*connect.Response[obligations.GetObligationTriggerResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("policy.obligations.Service.GetObligationTrigger is not implemented")) +} + func (UnimplementedServiceHandler) AddObligationTrigger(context.Context, *connect.Request[obligations.AddObligationTriggerRequest]) (*connect.Response[obligations.AddObligationTriggerResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("policy.obligations.Service.AddObligationTrigger is not implemented")) } diff --git a/protocol/go/policy/registeredresources/registered_resources.pb.go b/protocol/go/policy/registeredresources/registered_resources.pb.go index f124880fe2..5662a78482 100644 --- a/protocol/go/policy/registeredresources/registered_resources.pb.go +++ b/protocol/go/policy/registeredresources/registered_resources.pb.go @@ -23,6 +23,58 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type SortRegisteredResourcesType int32 + +const ( + SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED SortRegisteredResourcesType = 0 + SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME SortRegisteredResourcesType = 1 + SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT SortRegisteredResourcesType = 2 + SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT SortRegisteredResourcesType = 3 +) + +// Enum value maps for SortRegisteredResourcesType. +var ( + SortRegisteredResourcesType_name = map[int32]string{ + 0: "SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED", + 1: "SORT_REGISTERED_RESOURCES_TYPE_NAME", + 2: "SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT", + 3: "SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT", + } + SortRegisteredResourcesType_value = map[string]int32{ + "SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED": 0, + "SORT_REGISTERED_RESOURCES_TYPE_NAME": 1, + "SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT": 2, + "SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT": 3, + } +) + +func (x SortRegisteredResourcesType) Enum() *SortRegisteredResourcesType { + p := new(SortRegisteredResourcesType) + *p = x + return p +} + +func (x SortRegisteredResourcesType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortRegisteredResourcesType) Descriptor() protoreflect.EnumDescriptor { + return file_policy_registeredresources_registered_resources_proto_enumTypes[0].Descriptor() +} + +func (SortRegisteredResourcesType) Type() protoreflect.EnumType { + return &file_policy_registeredresources_registered_resources_proto_enumTypes[0] +} + +func (x SortRegisteredResourcesType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortRegisteredResourcesType.Descriptor instead. +func (SortRegisteredResourcesType) EnumDescriptor() ([]byte, []int) { + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{0} +} + type CreateRegisteredResourceRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -33,7 +85,9 @@ type CreateRegisteredResourceRequest struct { // Optional // Registered Resource Values (when provided) must be alphanumeric strings, allowing hyphens and underscores but not as the first or last character. // The stored value will be normalized to lower case. - Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"` + Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"` + NamespaceId string `protobuf:"bytes,3,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + NamespaceFqn string `protobuf:"bytes,4,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` // Optional // Common metadata Metadata *common.MetadataMutable `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` @@ -85,6 +139,20 @@ func (x *CreateRegisteredResourceRequest) GetValues() []string { return nil } +func (x *CreateRegisteredResourceRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CreateRegisteredResourceRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" +} + func (x *CreateRegisteredResourceRequest) GetMetadata() *common.MetadataMutable { if x != nil { return x.Metadata @@ -148,7 +216,9 @@ type GetRegisteredResourceRequest struct { // // *GetRegisteredResourceRequest_Id // *GetRegisteredResourceRequest_Name - Identifier isGetRegisteredResourceRequest_Identifier `protobuf_oneof:"identifier"` + Identifier isGetRegisteredResourceRequest_Identifier `protobuf_oneof:"identifier"` + NamespaceFqn string `protobuf:"bytes,3,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` + NamespaceId string `protobuf:"bytes,4,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` } func (x *GetRegisteredResourceRequest) Reset() { @@ -204,6 +274,20 @@ func (x *GetRegisteredResourceRequest) GetName() string { return "" } +func (x *GetRegisteredResourceRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" +} + +func (x *GetRegisteredResourceRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + type isGetRegisteredResourceRequest_Identifier interface { isGetRegisteredResourceRequest_Identifier() } @@ -267,19 +351,82 @@ func (x *GetRegisteredResourceResponse) GetResource() *policy.RegisteredResource return nil } +type RegisteredResourcesSort struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field SortRegisteredResourcesType `protobuf:"varint,1,opt,name=field,proto3,enum=policy.registeredresources.SortRegisteredResourcesType" json:"field,omitempty"` + Direction policy.SortDirection `protobuf:"varint,2,opt,name=direction,proto3,enum=policy.SortDirection" json:"direction,omitempty"` +} + +func (x *RegisteredResourcesSort) Reset() { + *x = RegisteredResourcesSort{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisteredResourcesSort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisteredResourcesSort) ProtoMessage() {} + +func (x *RegisteredResourcesSort) ProtoReflect() protoreflect.Message { + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisteredResourcesSort.ProtoReflect.Descriptor instead. +func (*RegisteredResourcesSort) Descriptor() ([]byte, []int) { + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{4} +} + +func (x *RegisteredResourcesSort) GetField() SortRegisteredResourcesType { + if x != nil { + return x.Field + } + return SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED +} + +func (x *RegisteredResourcesSort) GetDirection() policy.SortDirection { + if x != nil { + return x.Direction + } + return policy.SortDirection(0) +} + type ListRegisteredResourcesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + NamespaceFqn string `protobuf:"bytes,2,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + Sort []*RegisteredResourcesSort `protobuf:"bytes,11,rep,name=sort,proto3" json:"sort,omitempty"` } func (x *ListRegisteredResourcesRequest) Reset() { *x = ListRegisteredResourcesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[4] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -292,7 +439,7 @@ func (x *ListRegisteredResourcesRequest) String() string { func (*ListRegisteredResourcesRequest) ProtoMessage() {} func (x *ListRegisteredResourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[4] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -305,7 +452,21 @@ func (x *ListRegisteredResourcesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRegisteredResourcesRequest.ProtoReflect.Descriptor instead. func (*ListRegisteredResourcesRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{4} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{5} +} + +func (x *ListRegisteredResourcesRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *ListRegisteredResourcesRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" } func (x *ListRegisteredResourcesRequest) GetPagination() *policy.PageRequest { @@ -315,6 +476,13 @@ func (x *ListRegisteredResourcesRequest) GetPagination() *policy.PageRequest { return nil } +func (x *ListRegisteredResourcesRequest) GetSort() []*RegisteredResourcesSort { + if x != nil { + return x.Sort + } + return nil +} + type ListRegisteredResourcesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -327,7 +495,7 @@ type ListRegisteredResourcesResponse struct { func (x *ListRegisteredResourcesResponse) Reset() { *x = ListRegisteredResourcesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[5] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -340,7 +508,7 @@ func (x *ListRegisteredResourcesResponse) String() string { func (*ListRegisteredResourcesResponse) ProtoMessage() {} func (x *ListRegisteredResourcesResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[5] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -353,7 +521,7 @@ func (x *ListRegisteredResourcesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRegisteredResourcesResponse.ProtoReflect.Descriptor instead. func (*ListRegisteredResourcesResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{5} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{6} } func (x *ListRegisteredResourcesResponse) GetResources() []*policy.RegisteredResource { @@ -388,7 +556,7 @@ type UpdateRegisteredResourceRequest struct { func (x *UpdateRegisteredResourceRequest) Reset() { *x = UpdateRegisteredResourceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[6] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -401,7 +569,7 @@ func (x *UpdateRegisteredResourceRequest) String() string { func (*UpdateRegisteredResourceRequest) ProtoMessage() {} func (x *UpdateRegisteredResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[6] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -414,7 +582,7 @@ func (x *UpdateRegisteredResourceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRegisteredResourceRequest.ProtoReflect.Descriptor instead. func (*UpdateRegisteredResourceRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{6} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{7} } func (x *UpdateRegisteredResourceRequest) GetId() string { @@ -456,7 +624,7 @@ type UpdateRegisteredResourceResponse struct { func (x *UpdateRegisteredResourceResponse) Reset() { *x = UpdateRegisteredResourceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[7] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -469,7 +637,7 @@ func (x *UpdateRegisteredResourceResponse) String() string { func (*UpdateRegisteredResourceResponse) ProtoMessage() {} func (x *UpdateRegisteredResourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[7] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -482,7 +650,7 @@ func (x *UpdateRegisteredResourceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRegisteredResourceResponse.ProtoReflect.Descriptor instead. func (*UpdateRegisteredResourceResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{7} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{8} } func (x *UpdateRegisteredResourceResponse) GetResource() *policy.RegisteredResource { @@ -504,7 +672,7 @@ type DeleteRegisteredResourceRequest struct { func (x *DeleteRegisteredResourceRequest) Reset() { *x = DeleteRegisteredResourceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[8] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -517,7 +685,7 @@ func (x *DeleteRegisteredResourceRequest) String() string { func (*DeleteRegisteredResourceRequest) ProtoMessage() {} func (x *DeleteRegisteredResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[8] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -530,7 +698,7 @@ func (x *DeleteRegisteredResourceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteRegisteredResourceRequest.ProtoReflect.Descriptor instead. func (*DeleteRegisteredResourceRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{8} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{9} } func (x *DeleteRegisteredResourceRequest) GetId() string { @@ -551,7 +719,7 @@ type DeleteRegisteredResourceResponse struct { func (x *DeleteRegisteredResourceResponse) Reset() { *x = DeleteRegisteredResourceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[9] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -564,7 +732,7 @@ func (x *DeleteRegisteredResourceResponse) String() string { func (*DeleteRegisteredResourceResponse) ProtoMessage() {} func (x *DeleteRegisteredResourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[9] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -577,7 +745,7 @@ func (x *DeleteRegisteredResourceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteRegisteredResourceResponse.ProtoReflect.Descriptor instead. func (*DeleteRegisteredResourceResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{9} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{10} } func (x *DeleteRegisteredResourceResponse) GetResource() *policy.RegisteredResource { @@ -611,7 +779,7 @@ type ActionAttributeValue struct { func (x *ActionAttributeValue) Reset() { *x = ActionAttributeValue{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[10] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -624,7 +792,7 @@ func (x *ActionAttributeValue) String() string { func (*ActionAttributeValue) ProtoMessage() {} func (x *ActionAttributeValue) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[10] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -637,7 +805,7 @@ func (x *ActionAttributeValue) ProtoReflect() protoreflect.Message { // Deprecated: Use ActionAttributeValue.ProtoReflect.Descriptor instead. func (*ActionAttributeValue) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{10} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{11} } func (m *ActionAttributeValue) GetActionIdentifier() isActionAttributeValue_ActionIdentifier { @@ -735,7 +903,7 @@ type CreateRegisteredResourceValueRequest struct { func (x *CreateRegisteredResourceValueRequest) Reset() { *x = CreateRegisteredResourceValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[11] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -748,7 +916,7 @@ func (x *CreateRegisteredResourceValueRequest) String() string { func (*CreateRegisteredResourceValueRequest) ProtoMessage() {} func (x *CreateRegisteredResourceValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[11] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -761,7 +929,7 @@ func (x *CreateRegisteredResourceValueRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use CreateRegisteredResourceValueRequest.ProtoReflect.Descriptor instead. func (*CreateRegisteredResourceValueRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{11} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{12} } func (x *CreateRegisteredResourceValueRequest) GetResourceId() string { @@ -803,7 +971,7 @@ type CreateRegisteredResourceValueResponse struct { func (x *CreateRegisteredResourceValueResponse) Reset() { *x = CreateRegisteredResourceValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[12] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -816,7 +984,7 @@ func (x *CreateRegisteredResourceValueResponse) String() string { func (*CreateRegisteredResourceValueResponse) ProtoMessage() {} func (x *CreateRegisteredResourceValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[12] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -829,7 +997,7 @@ func (x *CreateRegisteredResourceValueResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use CreateRegisteredResourceValueResponse.ProtoReflect.Descriptor instead. func (*CreateRegisteredResourceValueResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{12} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{13} } func (x *CreateRegisteredResourceValueResponse) GetValue() *policy.RegisteredResourceValue { @@ -854,7 +1022,7 @@ type GetRegisteredResourceValueRequest struct { func (x *GetRegisteredResourceValueRequest) Reset() { *x = GetRegisteredResourceValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[13] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -867,7 +1035,7 @@ func (x *GetRegisteredResourceValueRequest) String() string { func (*GetRegisteredResourceValueRequest) ProtoMessage() {} func (x *GetRegisteredResourceValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[13] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -880,7 +1048,7 @@ func (x *GetRegisteredResourceValueRequest) ProtoReflect() protoreflect.Message // Deprecated: Use GetRegisteredResourceValueRequest.ProtoReflect.Descriptor instead. func (*GetRegisteredResourceValueRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{13} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{14} } func (m *GetRegisteredResourceValueRequest) GetIdentifier() isGetRegisteredResourceValueRequest_Identifier { @@ -931,7 +1099,7 @@ type GetRegisteredResourceValueResponse struct { func (x *GetRegisteredResourceValueResponse) Reset() { *x = GetRegisteredResourceValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[14] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -944,7 +1112,7 @@ func (x *GetRegisteredResourceValueResponse) String() string { func (*GetRegisteredResourceValueResponse) ProtoMessage() {} func (x *GetRegisteredResourceValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[14] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -957,7 +1125,7 @@ func (x *GetRegisteredResourceValueResponse) ProtoReflect() protoreflect.Message // Deprecated: Use GetRegisteredResourceValueResponse.ProtoReflect.Descriptor instead. func (*GetRegisteredResourceValueResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{14} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{15} } func (x *GetRegisteredResourceValueResponse) GetValue() *policy.RegisteredResourceValue { @@ -979,7 +1147,7 @@ type GetRegisteredResourceValuesByFQNsRequest struct { func (x *GetRegisteredResourceValuesByFQNsRequest) Reset() { *x = GetRegisteredResourceValuesByFQNsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[15] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -992,7 +1160,7 @@ func (x *GetRegisteredResourceValuesByFQNsRequest) String() string { func (*GetRegisteredResourceValuesByFQNsRequest) ProtoMessage() {} func (x *GetRegisteredResourceValuesByFQNsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[15] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1005,7 +1173,7 @@ func (x *GetRegisteredResourceValuesByFQNsRequest) ProtoReflect() protoreflect.M // Deprecated: Use GetRegisteredResourceValuesByFQNsRequest.ProtoReflect.Descriptor instead. func (*GetRegisteredResourceValuesByFQNsRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{15} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{16} } func (x *GetRegisteredResourceValuesByFQNsRequest) GetFqns() []string { @@ -1026,7 +1194,7 @@ type GetRegisteredResourceValuesByFQNsResponse struct { func (x *GetRegisteredResourceValuesByFQNsResponse) Reset() { *x = GetRegisteredResourceValuesByFQNsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[16] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1039,7 +1207,7 @@ func (x *GetRegisteredResourceValuesByFQNsResponse) String() string { func (*GetRegisteredResourceValuesByFQNsResponse) ProtoMessage() {} func (x *GetRegisteredResourceValuesByFQNsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[16] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1052,7 +1220,7 @@ func (x *GetRegisteredResourceValuesByFQNsResponse) ProtoReflect() protoreflect. // Deprecated: Use GetRegisteredResourceValuesByFQNsResponse.ProtoReflect.Descriptor instead. func (*GetRegisteredResourceValuesByFQNsResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{16} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{17} } func (x *GetRegisteredResourceValuesByFQNsResponse) GetFqnValueMap() map[string]*policy.RegisteredResourceValue { @@ -1076,7 +1244,7 @@ type ListRegisteredResourceValuesRequest struct { func (x *ListRegisteredResourceValuesRequest) Reset() { *x = ListRegisteredResourceValuesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[17] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1089,7 +1257,7 @@ func (x *ListRegisteredResourceValuesRequest) String() string { func (*ListRegisteredResourceValuesRequest) ProtoMessage() {} func (x *ListRegisteredResourceValuesRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[17] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1102,7 +1270,7 @@ func (x *ListRegisteredResourceValuesRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use ListRegisteredResourceValuesRequest.ProtoReflect.Descriptor instead. func (*ListRegisteredResourceValuesRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{17} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{18} } func (x *ListRegisteredResourceValuesRequest) GetResourceId() string { @@ -1131,7 +1299,7 @@ type ListRegisteredResourceValuesResponse struct { func (x *ListRegisteredResourceValuesResponse) Reset() { *x = ListRegisteredResourceValuesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[18] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1144,7 +1312,7 @@ func (x *ListRegisteredResourceValuesResponse) String() string { func (*ListRegisteredResourceValuesResponse) ProtoMessage() {} func (x *ListRegisteredResourceValuesResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[18] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1157,7 +1325,7 @@ func (x *ListRegisteredResourceValuesResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use ListRegisteredResourceValuesResponse.ProtoReflect.Descriptor instead. func (*ListRegisteredResourceValuesResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{18} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{19} } func (x *ListRegisteredResourceValuesResponse) GetValues() []*policy.RegisteredResourceValue { @@ -1195,7 +1363,7 @@ type UpdateRegisteredResourceValueRequest struct { func (x *UpdateRegisteredResourceValueRequest) Reset() { *x = UpdateRegisteredResourceValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[19] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1208,7 +1376,7 @@ func (x *UpdateRegisteredResourceValueRequest) String() string { func (*UpdateRegisteredResourceValueRequest) ProtoMessage() {} func (x *UpdateRegisteredResourceValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[19] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1221,7 +1389,7 @@ func (x *UpdateRegisteredResourceValueRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use UpdateRegisteredResourceValueRequest.ProtoReflect.Descriptor instead. func (*UpdateRegisteredResourceValueRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{19} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{20} } func (x *UpdateRegisteredResourceValueRequest) GetId() string { @@ -1270,7 +1438,7 @@ type UpdateRegisteredResourceValueResponse struct { func (x *UpdateRegisteredResourceValueResponse) Reset() { *x = UpdateRegisteredResourceValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[20] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1283,7 +1451,7 @@ func (x *UpdateRegisteredResourceValueResponse) String() string { func (*UpdateRegisteredResourceValueResponse) ProtoMessage() {} func (x *UpdateRegisteredResourceValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[20] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1296,7 +1464,7 @@ func (x *UpdateRegisteredResourceValueResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use UpdateRegisteredResourceValueResponse.ProtoReflect.Descriptor instead. func (*UpdateRegisteredResourceValueResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{20} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{21} } func (x *UpdateRegisteredResourceValueResponse) GetValue() *policy.RegisteredResourceValue { @@ -1318,7 +1486,7 @@ type DeleteRegisteredResourceValueRequest struct { func (x *DeleteRegisteredResourceValueRequest) Reset() { *x = DeleteRegisteredResourceValueRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[21] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1331,7 +1499,7 @@ func (x *DeleteRegisteredResourceValueRequest) String() string { func (*DeleteRegisteredResourceValueRequest) ProtoMessage() {} func (x *DeleteRegisteredResourceValueRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[21] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1344,7 +1512,7 @@ func (x *DeleteRegisteredResourceValueRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use DeleteRegisteredResourceValueRequest.ProtoReflect.Descriptor instead. func (*DeleteRegisteredResourceValueRequest) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{21} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{22} } func (x *DeleteRegisteredResourceValueRequest) GetId() string { @@ -1365,7 +1533,7 @@ type DeleteRegisteredResourceValueResponse struct { func (x *DeleteRegisteredResourceValueResponse) Reset() { *x = DeleteRegisteredResourceValueResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[22] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1378,7 +1546,7 @@ func (x *DeleteRegisteredResourceValueResponse) String() string { func (*DeleteRegisteredResourceValueResponse) ProtoMessage() {} func (x *DeleteRegisteredResourceValueResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[22] + mi := &file_policy_registeredresources_registered_resources_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1391,7 +1559,7 @@ func (x *DeleteRegisteredResourceValueResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use DeleteRegisteredResourceValueResponse.ProtoReflect.Descriptor instead. func (*DeleteRegisteredResourceValueResponse) Descriptor() ([]byte, []int) { - return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{22} + return file_policy_registeredresources_registered_resources_proto_rawDescGZIP(), []int{23} } func (x *DeleteRegisteredResourceValueResponse) GetValue() *policy.RegisteredResourceValue { @@ -1415,7 +1583,7 @@ var file_policy_registeredresources_registered_resources_proto_rawDesc = []byte{ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x16, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0xd9, 0x03, 0x0a, 0x1f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x6f, 0x74, 0x6f, 0x22, 0xdd, 0x04, 0x0a, 0x1f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xa8, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x93, 0x02, 0xba, 0x48, 0x8f, 0x02, 0xba, 0x01, 0x83, @@ -1441,20 +1609,107 @@ var file_policy_registeredresources_registered_resources_proto_rawDesc = []byte{ 0x72, 0x30, 0x18, 0xfd, 0x01, 0x32, 0x2b, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, - 0x3f, 0x24, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, - 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, - 0x5a, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, + 0x3f, 0x24, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, + 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x24, 0xba, + 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, + 0x6e, 0x10, 0x00, 0x22, 0x5a, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, + 0x98, 0x04, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, + 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0xc2, 0x02, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xab, 0x02, 0xba, 0x48, + 0xa7, 0x02, 0xba, 0x01, 0x9b, 0x02, 0x0a, 0x0e, 0x72, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb3, 0x01, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x20, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x20, 0x4e, 0x61, 0x6d, + 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, + 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, + 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, + 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, + 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, + 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, + 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x53, 0x73, 0x69, + 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x74, + 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, + 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, + 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, + 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, 0x72, 0x75, + 0x65, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, + 0x71, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, + 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, + 0x71, 0x6e, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, + 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x3a, + 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x66, 0x71, 0x6e, 0x10, 0x00, 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x57, 0x0a, 0x1d, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x22, 0xb1, 0x01, 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x12, + 0x57, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x37, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x53, 0x6f, 0x72, 0x74, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, + 0x01, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xac, 0x02, 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x51, 0x0a, + 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x53, 0x6f, 0x72, 0x74, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, + 0x3a, 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x00, 0x22, 0x91, 0x01, 0x0a, 0x1f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x94, 0x03, 0x0a, 0x1c, - 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, - 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0xc2, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, + 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x89, 0x04, 0x0a, 0x1f, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xc0, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xab, 0x02, 0xba, 0x48, 0xa7, 0x02, 0xba, 0x01, 0x9b, 0x02, 0x0a, 0x0e, 0x72, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb3, 0x01, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, @@ -1474,402 +1729,370 @@ var file_policy_registeredresources_registered_resources_proto_rawDesc = []byte{ 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0xc8, 0x01, 0x00, - 0x72, 0x03, 0x18, 0xfd, 0x01, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x13, 0x0a, - 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, - 0x08, 0x01, 0x22, 0x57, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, + 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, + 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x5a, 0x0a, 0x20, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x22, 0x3b, 0x0a, 0x1f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, + 0x5a, 0x0a, 0x20, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x55, 0x0a, 0x1e, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, - 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x91, 0x01, 0x0a, 0x1f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xad, 0x04, 0x0a, 0x14, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x27, 0x0a, 0x09, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, + 0x01, 0x48, 0x00, 0x52, 0x08, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0xb2, 0x02, + 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x8e, 0x02, 0xba, 0x48, 0x8a, 0x02, 0xba, 0x01, 0x81, 0x02, 0x0a, 0x12, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x12, 0xad, 0x01, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, + 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, + 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, + 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, + 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, + 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, + 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x64, 0x20, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, + 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, + 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, + 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, + 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, + 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, + 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x72, + 0x03, 0x18, 0xfd, 0x01, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x01, 0x52, 0x10, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x13, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, + 0x66, 0x71, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, + 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x01, 0x52, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x71, 0x6e, 0x42, 0x1a, 0x0a, 0x11, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, + 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x42, 0x23, 0x0a, 0x1a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0xa0, 0x04, 0x0a, 0x24, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, + 0xb0, 0x01, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, + 0xad, 0x02, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x96, 0x02, 0xba, 0x48, 0x92, 0x02, 0xba, 0x01, 0x86, 0x02, 0x0a, 0x0f, 0x72, 0x72, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb5, 0x01, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x20, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, + 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, + 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, + 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, + 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, + 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, + 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, + 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, + 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, + 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, + 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, + 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, + 0xc8, 0x01, 0x01, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x68, 0x0a, 0x17, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x15, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x5e, + 0x0a, 0x25, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x74, + 0x0a, 0x21, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x1e, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, + 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x42, + 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, + 0x48, 0x02, 0x08, 0x01, 0x22, 0x5b, 0x0a, 0x22, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x89, 0x04, 0x0a, 0x1f, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x52, 0x02, 0x69, 0x64, 0x12, 0xc0, 0x02, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0xab, 0x02, 0xba, 0x48, 0xa7, 0x02, 0xba, 0x01, 0x9b, 0x02, 0x0a, 0x0e, - 0x72, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb3, - 0x01, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x20, 0x4e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x53, 0x0a, 0x28, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, + 0x04, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x13, 0xba, 0x48, 0x10, + 0x92, 0x01, 0x0d, 0x08, 0x01, 0x18, 0x01, 0x22, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, + 0x52, 0x04, 0x66, 0x71, 0x6e, 0x73, 0x22, 0x88, 0x02, 0x0a, 0x29, 0x47, 0x65, 0x74, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x0d, 0x66, 0x71, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x56, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x46, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x0b, 0x66, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, + 0x1a, 0x5f, 0x0a, 0x10, 0x46, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0xb2, 0x02, 0x0a, 0x23, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xd5, 0x01, 0x0a, 0x0b, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0xb3, 0x01, 0xba, 0x48, 0xaf, 0x01, 0xba, 0x01, 0xab, 0x01, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x12, 0x23, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x20, 0x55, 0x55, 0x49, 0x44, 0x1a, 0x6e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, + 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, + 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, + 0x2d, 0x46, 0x5d, 0x7b, 0x38, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, + 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, + 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, + 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, + 0x31, 0x32, 0x7d, 0x27, 0x29, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x24, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x37, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xfd, + 0x04, 0x0a, 0x24, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, + 0x64, 0x12, 0xc5, 0x02, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0xae, 0x02, 0xba, 0x48, 0xaa, 0x02, 0xba, 0x01, 0x9e, 0x02, 0x0a, 0x0f, 0x72, 0x72, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb5, 0x01, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x20, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, - 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x6e, 0x61, - 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, - 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, - 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x53, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, - 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, - 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, - 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, - 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, - 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, - 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, - 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, - 0x6f, 0x72, 0x22, 0x5a, 0x0a, 0x20, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, + 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, + 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x53, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, + 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, + 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, + 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, + 0x24, 0x27, 0x29, 0x20, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, + 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x68, 0x0a, 0x17, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, + 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x5e, + 0x0a, 0x25, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x40, + 0x0a, 0x24, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, + 0x22, 0x5e, 0x0a, 0x25, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x3b, - 0x0a, 0x1f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, - 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x5a, 0x0a, 0x20, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x36, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xad, 0x04, 0x0a, 0x14, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x27, 0x0a, 0x09, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, - 0x08, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0xb2, 0x02, 0x0a, 0x0b, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x8e, 0x02, 0xba, 0x48, 0x8a, 0x02, 0xba, 0x01, 0x81, 0x02, 0x0a, 0x12, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xad, - 0x01, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, - 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, - 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, - 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, - 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, - 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, - 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, - 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, - 0x20, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x77, 0x69, 0x6c, - 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, - 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, - 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, - 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, - 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, - 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x72, 0x03, 0x18, 0xfd, 0x01, - 0x48, 0x00, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x38, - 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, - 0x03, 0xb0, 0x01, 0x01, 0x48, 0x01, 0x52, 0x10, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x13, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, - 0x01, 0x48, 0x01, 0x52, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x46, 0x71, 0x6e, 0x42, 0x1a, 0x0a, 0x11, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, - 0x08, 0x01, 0x42, 0x23, 0x0a, 0x1a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0xa0, 0x04, 0x0a, 0x24, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x29, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, - 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0xad, 0x02, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x96, 0x02, 0xba, 0x48, - 0x92, 0x02, 0xba, 0x01, 0x86, 0x02, 0x0a, 0x0f, 0x72, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb5, 0x01, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x65, 0x64, 0x20, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x20, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, - 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, - 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, - 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, - 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, - 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x77, 0x69, - 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, - 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, - 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, - 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0xc8, 0x01, 0x01, 0x72, - 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x68, 0x0a, 0x17, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, - 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x5e, 0x0a, 0x25, 0x43, 0x72, + 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x2a, 0xd4, 0x01, 0x0a, 0x1b, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x2e, 0x0a, 0x2a, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, + 0x52, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x27, 0x0a, 0x23, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, + 0x52, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x01, 0x12, 0x2d, 0x0a, 0x29, 0x53, 0x4f, 0x52, + 0x54, 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x53, + 0x4f, 0x55, 0x52, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, + 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x02, 0x12, 0x2d, 0x0a, 0x29, 0x53, 0x4f, 0x52, 0x54, + 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x53, 0x4f, + 0x55, 0x52, 0x43, 0x45, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, + 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x32, 0x88, 0x0e, 0x0a, 0x1a, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x97, 0x01, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x74, 0x0a, 0x21, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x03, 0x66, - 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, - 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x42, 0x13, 0x0a, 0x0a, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, - 0x22, 0x5b, 0x0a, 0x22, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x8e, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x38, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x94, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, - 0x28, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, - 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x04, 0x66, 0x71, 0x6e, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x13, 0xba, 0x48, 0x10, 0x92, 0x01, 0x0d, 0x08, - 0x01, 0x18, 0x01, 0x22, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x04, 0x66, 0x71, - 0x6e, 0x73, 0x22, 0x88, 0x02, 0x0a, 0x29, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x7a, 0x0a, 0x0d, 0x66, 0x71, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, - 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x56, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, - 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0b, 0x66, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, 0x1a, 0x5f, 0x0a, 0x10, - 0x46, 0x71, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb2, 0x02, - 0x0a, 0x23, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xd5, 0x01, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0xb3, 0x01, 0xba, 0x48, - 0xaf, 0x01, 0xba, 0x01, 0xab, 0x01, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x5f, 0x75, 0x75, 0x69, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x23, 0x4f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6d, 0x75, 0x73, - 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x55, 0x49, - 0x44, 0x1a, 0x6e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, - 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, - 0x65, 0x73, 0x28, 0x27, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, - 0x38, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, - 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, - 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, - 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x31, 0x32, 0x7d, 0x27, - 0x29, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x33, 0x0a, - 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x24, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, - 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xfd, 0x04, 0x0a, 0x24, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xc5, 0x02, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xae, 0x02, - 0xba, 0x48, 0xaa, 0x02, 0xba, 0x01, 0x9e, 0x02, 0x0a, 0x0f, 0x72, 0x72, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb5, 0x01, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x20, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, - 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, - 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, - 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, - 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, - 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, - 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, - 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, - 0x2e, 0x1a, 0x53, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3e, 0x20, - 0x30, 0x20, 0x3f, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, - 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, - 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, - 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x20, - 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0xc8, 0x01, 0x00, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x68, 0x0a, 0x17, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, - 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, - 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, - 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x5e, 0x0a, 0x25, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x40, 0x0a, 0x24, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x5e, 0x0a, 0x25, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x88, 0x0e, 0x0a, - 0x1a, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x97, 0x01, 0x0a, 0x18, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x97, 0x01, 0x0a, 0x18, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8e, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, + 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x97, 0x01, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, - 0x38, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, + 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x94, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x12, 0x3a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa6, 0x01, 0x0a, + 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x40, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x97, 0x01, - 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x97, 0x01, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, + 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x12, 0x3b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, + 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x41, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x9d, 0x01, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x3c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0xa6, 0x01, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x40, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, + 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x9d, 0x01, 0x0a, 0x1a, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xb2, 0x01, 0x0a, 0x21, 0x47, + 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xb2, 0x01, 0x0a, 0x21, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x12, 0x44, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x45, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, - 0x12, 0x44, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x45, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, - 0x79, 0x46, 0x51, 0x4e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0xa3, 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x12, 0x3f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x40, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x4c, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa3, 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa6, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x40, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3f, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa6, - 0x01, 0x0a, 0x1d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x40, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x40, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0xa6, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x40, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x87, 0x02, 0x0a, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, - 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0x18, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, - 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, - 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0xa2, 0x02, 0x03, 0x50, 0x52, 0x58, - 0xaa, 0x02, 0x1a, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0xca, 0x02, 0x1a, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, - 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0xe2, 0x02, 0x26, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x5c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x52, 0x65, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa6, 0x01, 0x0a, 0x1d, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x40, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x42, 0x87, 0x02, 0x0a, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0x18, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, + 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0xa2, 0x02, 0x03, 0x50, 0x52, 0x58, 0xaa, 0x02, 0x1a, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0xca, 0x02, 0x1a, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x5c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0xe2, 0x02, 0x26, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x1b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1884,91 +2107,98 @@ func file_policy_registeredresources_registered_resources_proto_rawDescGZIP() [] return file_policy_registeredresources_registered_resources_proto_rawDescData } -var file_policy_registeredresources_registered_resources_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_policy_registeredresources_registered_resources_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_policy_registeredresources_registered_resources_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_policy_registeredresources_registered_resources_proto_goTypes = []interface{}{ - (*CreateRegisteredResourceRequest)(nil), // 0: policy.registeredresources.CreateRegisteredResourceRequest - (*CreateRegisteredResourceResponse)(nil), // 1: policy.registeredresources.CreateRegisteredResourceResponse - (*GetRegisteredResourceRequest)(nil), // 2: policy.registeredresources.GetRegisteredResourceRequest - (*GetRegisteredResourceResponse)(nil), // 3: policy.registeredresources.GetRegisteredResourceResponse - (*ListRegisteredResourcesRequest)(nil), // 4: policy.registeredresources.ListRegisteredResourcesRequest - (*ListRegisteredResourcesResponse)(nil), // 5: policy.registeredresources.ListRegisteredResourcesResponse - (*UpdateRegisteredResourceRequest)(nil), // 6: policy.registeredresources.UpdateRegisteredResourceRequest - (*UpdateRegisteredResourceResponse)(nil), // 7: policy.registeredresources.UpdateRegisteredResourceResponse - (*DeleteRegisteredResourceRequest)(nil), // 8: policy.registeredresources.DeleteRegisteredResourceRequest - (*DeleteRegisteredResourceResponse)(nil), // 9: policy.registeredresources.DeleteRegisteredResourceResponse - (*ActionAttributeValue)(nil), // 10: policy.registeredresources.ActionAttributeValue - (*CreateRegisteredResourceValueRequest)(nil), // 11: policy.registeredresources.CreateRegisteredResourceValueRequest - (*CreateRegisteredResourceValueResponse)(nil), // 12: policy.registeredresources.CreateRegisteredResourceValueResponse - (*GetRegisteredResourceValueRequest)(nil), // 13: policy.registeredresources.GetRegisteredResourceValueRequest - (*GetRegisteredResourceValueResponse)(nil), // 14: policy.registeredresources.GetRegisteredResourceValueResponse - (*GetRegisteredResourceValuesByFQNsRequest)(nil), // 15: policy.registeredresources.GetRegisteredResourceValuesByFQNsRequest - (*GetRegisteredResourceValuesByFQNsResponse)(nil), // 16: policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse - (*ListRegisteredResourceValuesRequest)(nil), // 17: policy.registeredresources.ListRegisteredResourceValuesRequest - (*ListRegisteredResourceValuesResponse)(nil), // 18: policy.registeredresources.ListRegisteredResourceValuesResponse - (*UpdateRegisteredResourceValueRequest)(nil), // 19: policy.registeredresources.UpdateRegisteredResourceValueRequest - (*UpdateRegisteredResourceValueResponse)(nil), // 20: policy.registeredresources.UpdateRegisteredResourceValueResponse - (*DeleteRegisteredResourceValueRequest)(nil), // 21: policy.registeredresources.DeleteRegisteredResourceValueRequest - (*DeleteRegisteredResourceValueResponse)(nil), // 22: policy.registeredresources.DeleteRegisteredResourceValueResponse - nil, // 23: policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse.FqnValueMapEntry - (*common.MetadataMutable)(nil), // 24: common.MetadataMutable - (*policy.RegisteredResource)(nil), // 25: policy.RegisteredResource - (*policy.PageRequest)(nil), // 26: policy.PageRequest - (*policy.PageResponse)(nil), // 27: policy.PageResponse - (common.MetadataUpdateEnum)(0), // 28: common.MetadataUpdateEnum - (*policy.RegisteredResourceValue)(nil), // 29: policy.RegisteredResourceValue + (SortRegisteredResourcesType)(0), // 0: policy.registeredresources.SortRegisteredResourcesType + (*CreateRegisteredResourceRequest)(nil), // 1: policy.registeredresources.CreateRegisteredResourceRequest + (*CreateRegisteredResourceResponse)(nil), // 2: policy.registeredresources.CreateRegisteredResourceResponse + (*GetRegisteredResourceRequest)(nil), // 3: policy.registeredresources.GetRegisteredResourceRequest + (*GetRegisteredResourceResponse)(nil), // 4: policy.registeredresources.GetRegisteredResourceResponse + (*RegisteredResourcesSort)(nil), // 5: policy.registeredresources.RegisteredResourcesSort + (*ListRegisteredResourcesRequest)(nil), // 6: policy.registeredresources.ListRegisteredResourcesRequest + (*ListRegisteredResourcesResponse)(nil), // 7: policy.registeredresources.ListRegisteredResourcesResponse + (*UpdateRegisteredResourceRequest)(nil), // 8: policy.registeredresources.UpdateRegisteredResourceRequest + (*UpdateRegisteredResourceResponse)(nil), // 9: policy.registeredresources.UpdateRegisteredResourceResponse + (*DeleteRegisteredResourceRequest)(nil), // 10: policy.registeredresources.DeleteRegisteredResourceRequest + (*DeleteRegisteredResourceResponse)(nil), // 11: policy.registeredresources.DeleteRegisteredResourceResponse + (*ActionAttributeValue)(nil), // 12: policy.registeredresources.ActionAttributeValue + (*CreateRegisteredResourceValueRequest)(nil), // 13: policy.registeredresources.CreateRegisteredResourceValueRequest + (*CreateRegisteredResourceValueResponse)(nil), // 14: policy.registeredresources.CreateRegisteredResourceValueResponse + (*GetRegisteredResourceValueRequest)(nil), // 15: policy.registeredresources.GetRegisteredResourceValueRequest + (*GetRegisteredResourceValueResponse)(nil), // 16: policy.registeredresources.GetRegisteredResourceValueResponse + (*GetRegisteredResourceValuesByFQNsRequest)(nil), // 17: policy.registeredresources.GetRegisteredResourceValuesByFQNsRequest + (*GetRegisteredResourceValuesByFQNsResponse)(nil), // 18: policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse + (*ListRegisteredResourceValuesRequest)(nil), // 19: policy.registeredresources.ListRegisteredResourceValuesRequest + (*ListRegisteredResourceValuesResponse)(nil), // 20: policy.registeredresources.ListRegisteredResourceValuesResponse + (*UpdateRegisteredResourceValueRequest)(nil), // 21: policy.registeredresources.UpdateRegisteredResourceValueRequest + (*UpdateRegisteredResourceValueResponse)(nil), // 22: policy.registeredresources.UpdateRegisteredResourceValueResponse + (*DeleteRegisteredResourceValueRequest)(nil), // 23: policy.registeredresources.DeleteRegisteredResourceValueRequest + (*DeleteRegisteredResourceValueResponse)(nil), // 24: policy.registeredresources.DeleteRegisteredResourceValueResponse + nil, // 25: policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse.FqnValueMapEntry + (*common.MetadataMutable)(nil), // 26: common.MetadataMutable + (*policy.RegisteredResource)(nil), // 27: policy.RegisteredResource + (policy.SortDirection)(0), // 28: policy.SortDirection + (*policy.PageRequest)(nil), // 29: policy.PageRequest + (*policy.PageResponse)(nil), // 30: policy.PageResponse + (common.MetadataUpdateEnum)(0), // 31: common.MetadataUpdateEnum + (*policy.RegisteredResourceValue)(nil), // 32: policy.RegisteredResourceValue } var file_policy_registeredresources_registered_resources_proto_depIdxs = []int32{ - 24, // 0: policy.registeredresources.CreateRegisteredResourceRequest.metadata:type_name -> common.MetadataMutable - 25, // 1: policy.registeredresources.CreateRegisteredResourceResponse.resource:type_name -> policy.RegisteredResource - 25, // 2: policy.registeredresources.GetRegisteredResourceResponse.resource:type_name -> policy.RegisteredResource - 26, // 3: policy.registeredresources.ListRegisteredResourcesRequest.pagination:type_name -> policy.PageRequest - 25, // 4: policy.registeredresources.ListRegisteredResourcesResponse.resources:type_name -> policy.RegisteredResource - 27, // 5: policy.registeredresources.ListRegisteredResourcesResponse.pagination:type_name -> policy.PageResponse - 24, // 6: policy.registeredresources.UpdateRegisteredResourceRequest.metadata:type_name -> common.MetadataMutable - 28, // 7: policy.registeredresources.UpdateRegisteredResourceRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 25, // 8: policy.registeredresources.UpdateRegisteredResourceResponse.resource:type_name -> policy.RegisteredResource - 25, // 9: policy.registeredresources.DeleteRegisteredResourceResponse.resource:type_name -> policy.RegisteredResource - 10, // 10: policy.registeredresources.CreateRegisteredResourceValueRequest.action_attribute_values:type_name -> policy.registeredresources.ActionAttributeValue - 24, // 11: policy.registeredresources.CreateRegisteredResourceValueRequest.metadata:type_name -> common.MetadataMutable - 29, // 12: policy.registeredresources.CreateRegisteredResourceValueResponse.value:type_name -> policy.RegisteredResourceValue - 29, // 13: policy.registeredresources.GetRegisteredResourceValueResponse.value:type_name -> policy.RegisteredResourceValue - 23, // 14: policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse.fqn_value_map:type_name -> policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse.FqnValueMapEntry - 26, // 15: policy.registeredresources.ListRegisteredResourceValuesRequest.pagination:type_name -> policy.PageRequest - 29, // 16: policy.registeredresources.ListRegisteredResourceValuesResponse.values:type_name -> policy.RegisteredResourceValue - 27, // 17: policy.registeredresources.ListRegisteredResourceValuesResponse.pagination:type_name -> policy.PageResponse - 10, // 18: policy.registeredresources.UpdateRegisteredResourceValueRequest.action_attribute_values:type_name -> policy.registeredresources.ActionAttributeValue - 24, // 19: policy.registeredresources.UpdateRegisteredResourceValueRequest.metadata:type_name -> common.MetadataMutable - 28, // 20: policy.registeredresources.UpdateRegisteredResourceValueRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 29, // 21: policy.registeredresources.UpdateRegisteredResourceValueResponse.value:type_name -> policy.RegisteredResourceValue - 29, // 22: policy.registeredresources.DeleteRegisteredResourceValueResponse.value:type_name -> policy.RegisteredResourceValue - 29, // 23: policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse.FqnValueMapEntry.value:type_name -> policy.RegisteredResourceValue - 0, // 24: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResource:input_type -> policy.registeredresources.CreateRegisteredResourceRequest - 2, // 25: policy.registeredresources.RegisteredResourcesService.GetRegisteredResource:input_type -> policy.registeredresources.GetRegisteredResourceRequest - 4, // 26: policy.registeredresources.RegisteredResourcesService.ListRegisteredResources:input_type -> policy.registeredresources.ListRegisteredResourcesRequest - 6, // 27: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResource:input_type -> policy.registeredresources.UpdateRegisteredResourceRequest - 8, // 28: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResource:input_type -> policy.registeredresources.DeleteRegisteredResourceRequest - 11, // 29: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResourceValue:input_type -> policy.registeredresources.CreateRegisteredResourceValueRequest - 13, // 30: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValue:input_type -> policy.registeredresources.GetRegisteredResourceValueRequest - 15, // 31: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValuesByFQNs:input_type -> policy.registeredresources.GetRegisteredResourceValuesByFQNsRequest - 17, // 32: policy.registeredresources.RegisteredResourcesService.ListRegisteredResourceValues:input_type -> policy.registeredresources.ListRegisteredResourceValuesRequest - 19, // 33: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResourceValue:input_type -> policy.registeredresources.UpdateRegisteredResourceValueRequest - 21, // 34: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResourceValue:input_type -> policy.registeredresources.DeleteRegisteredResourceValueRequest - 1, // 35: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResource:output_type -> policy.registeredresources.CreateRegisteredResourceResponse - 3, // 36: policy.registeredresources.RegisteredResourcesService.GetRegisteredResource:output_type -> policy.registeredresources.GetRegisteredResourceResponse - 5, // 37: policy.registeredresources.RegisteredResourcesService.ListRegisteredResources:output_type -> policy.registeredresources.ListRegisteredResourcesResponse - 7, // 38: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResource:output_type -> policy.registeredresources.UpdateRegisteredResourceResponse - 9, // 39: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResource:output_type -> policy.registeredresources.DeleteRegisteredResourceResponse - 12, // 40: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResourceValue:output_type -> policy.registeredresources.CreateRegisteredResourceValueResponse - 14, // 41: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValue:output_type -> policy.registeredresources.GetRegisteredResourceValueResponse - 16, // 42: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValuesByFQNs:output_type -> policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse - 18, // 43: policy.registeredresources.RegisteredResourcesService.ListRegisteredResourceValues:output_type -> policy.registeredresources.ListRegisteredResourceValuesResponse - 20, // 44: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResourceValue:output_type -> policy.registeredresources.UpdateRegisteredResourceValueResponse - 22, // 45: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResourceValue:output_type -> policy.registeredresources.DeleteRegisteredResourceValueResponse - 35, // [35:46] is the sub-list for method output_type - 24, // [24:35] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 26, // 0: policy.registeredresources.CreateRegisteredResourceRequest.metadata:type_name -> common.MetadataMutable + 27, // 1: policy.registeredresources.CreateRegisteredResourceResponse.resource:type_name -> policy.RegisteredResource + 27, // 2: policy.registeredresources.GetRegisteredResourceResponse.resource:type_name -> policy.RegisteredResource + 0, // 3: policy.registeredresources.RegisteredResourcesSort.field:type_name -> policy.registeredresources.SortRegisteredResourcesType + 28, // 4: policy.registeredresources.RegisteredResourcesSort.direction:type_name -> policy.SortDirection + 29, // 5: policy.registeredresources.ListRegisteredResourcesRequest.pagination:type_name -> policy.PageRequest + 5, // 6: policy.registeredresources.ListRegisteredResourcesRequest.sort:type_name -> policy.registeredresources.RegisteredResourcesSort + 27, // 7: policy.registeredresources.ListRegisteredResourcesResponse.resources:type_name -> policy.RegisteredResource + 30, // 8: policy.registeredresources.ListRegisteredResourcesResponse.pagination:type_name -> policy.PageResponse + 26, // 9: policy.registeredresources.UpdateRegisteredResourceRequest.metadata:type_name -> common.MetadataMutable + 31, // 10: policy.registeredresources.UpdateRegisteredResourceRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 27, // 11: policy.registeredresources.UpdateRegisteredResourceResponse.resource:type_name -> policy.RegisteredResource + 27, // 12: policy.registeredresources.DeleteRegisteredResourceResponse.resource:type_name -> policy.RegisteredResource + 12, // 13: policy.registeredresources.CreateRegisteredResourceValueRequest.action_attribute_values:type_name -> policy.registeredresources.ActionAttributeValue + 26, // 14: policy.registeredresources.CreateRegisteredResourceValueRequest.metadata:type_name -> common.MetadataMutable + 32, // 15: policy.registeredresources.CreateRegisteredResourceValueResponse.value:type_name -> policy.RegisteredResourceValue + 32, // 16: policy.registeredresources.GetRegisteredResourceValueResponse.value:type_name -> policy.RegisteredResourceValue + 25, // 17: policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse.fqn_value_map:type_name -> policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse.FqnValueMapEntry + 29, // 18: policy.registeredresources.ListRegisteredResourceValuesRequest.pagination:type_name -> policy.PageRequest + 32, // 19: policy.registeredresources.ListRegisteredResourceValuesResponse.values:type_name -> policy.RegisteredResourceValue + 30, // 20: policy.registeredresources.ListRegisteredResourceValuesResponse.pagination:type_name -> policy.PageResponse + 12, // 21: policy.registeredresources.UpdateRegisteredResourceValueRequest.action_attribute_values:type_name -> policy.registeredresources.ActionAttributeValue + 26, // 22: policy.registeredresources.UpdateRegisteredResourceValueRequest.metadata:type_name -> common.MetadataMutable + 31, // 23: policy.registeredresources.UpdateRegisteredResourceValueRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 32, // 24: policy.registeredresources.UpdateRegisteredResourceValueResponse.value:type_name -> policy.RegisteredResourceValue + 32, // 25: policy.registeredresources.DeleteRegisteredResourceValueResponse.value:type_name -> policy.RegisteredResourceValue + 32, // 26: policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse.FqnValueMapEntry.value:type_name -> policy.RegisteredResourceValue + 1, // 27: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResource:input_type -> policy.registeredresources.CreateRegisteredResourceRequest + 3, // 28: policy.registeredresources.RegisteredResourcesService.GetRegisteredResource:input_type -> policy.registeredresources.GetRegisteredResourceRequest + 6, // 29: policy.registeredresources.RegisteredResourcesService.ListRegisteredResources:input_type -> policy.registeredresources.ListRegisteredResourcesRequest + 8, // 30: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResource:input_type -> policy.registeredresources.UpdateRegisteredResourceRequest + 10, // 31: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResource:input_type -> policy.registeredresources.DeleteRegisteredResourceRequest + 13, // 32: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResourceValue:input_type -> policy.registeredresources.CreateRegisteredResourceValueRequest + 15, // 33: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValue:input_type -> policy.registeredresources.GetRegisteredResourceValueRequest + 17, // 34: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValuesByFQNs:input_type -> policy.registeredresources.GetRegisteredResourceValuesByFQNsRequest + 19, // 35: policy.registeredresources.RegisteredResourcesService.ListRegisteredResourceValues:input_type -> policy.registeredresources.ListRegisteredResourceValuesRequest + 21, // 36: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResourceValue:input_type -> policy.registeredresources.UpdateRegisteredResourceValueRequest + 23, // 37: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResourceValue:input_type -> policy.registeredresources.DeleteRegisteredResourceValueRequest + 2, // 38: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResource:output_type -> policy.registeredresources.CreateRegisteredResourceResponse + 4, // 39: policy.registeredresources.RegisteredResourcesService.GetRegisteredResource:output_type -> policy.registeredresources.GetRegisteredResourceResponse + 7, // 40: policy.registeredresources.RegisteredResourcesService.ListRegisteredResources:output_type -> policy.registeredresources.ListRegisteredResourcesResponse + 9, // 41: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResource:output_type -> policy.registeredresources.UpdateRegisteredResourceResponse + 11, // 42: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResource:output_type -> policy.registeredresources.DeleteRegisteredResourceResponse + 14, // 43: policy.registeredresources.RegisteredResourcesService.CreateRegisteredResourceValue:output_type -> policy.registeredresources.CreateRegisteredResourceValueResponse + 16, // 44: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValue:output_type -> policy.registeredresources.GetRegisteredResourceValueResponse + 18, // 45: policy.registeredresources.RegisteredResourcesService.GetRegisteredResourceValuesByFQNs:output_type -> policy.registeredresources.GetRegisteredResourceValuesByFQNsResponse + 20, // 46: policy.registeredresources.RegisteredResourcesService.ListRegisteredResourceValues:output_type -> policy.registeredresources.ListRegisteredResourceValuesResponse + 22, // 47: policy.registeredresources.RegisteredResourcesService.UpdateRegisteredResourceValue:output_type -> policy.registeredresources.UpdateRegisteredResourceValueResponse + 24, // 48: policy.registeredresources.RegisteredResourcesService.DeleteRegisteredResourceValue:output_type -> policy.registeredresources.DeleteRegisteredResourceValueResponse + 38, // [38:49] is the sub-list for method output_type + 27, // [27:38] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_policy_registeredresources_registered_resources_proto_init() } @@ -2026,7 +2256,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRegisteredResourcesRequest); i { + switch v := v.(*RegisteredResourcesSort); i { case 0: return &v.state case 1: @@ -2038,7 +2268,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRegisteredResourcesResponse); i { + switch v := v.(*ListRegisteredResourcesRequest); i { case 0: return &v.state case 1: @@ -2050,7 +2280,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateRegisteredResourceRequest); i { + switch v := v.(*ListRegisteredResourcesResponse); i { case 0: return &v.state case 1: @@ -2062,7 +2292,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateRegisteredResourceResponse); i { + switch v := v.(*UpdateRegisteredResourceRequest); i { case 0: return &v.state case 1: @@ -2074,7 +2304,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteRegisteredResourceRequest); i { + switch v := v.(*UpdateRegisteredResourceResponse); i { case 0: return &v.state case 1: @@ -2086,7 +2316,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteRegisteredResourceResponse); i { + switch v := v.(*DeleteRegisteredResourceRequest); i { case 0: return &v.state case 1: @@ -2098,7 +2328,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ActionAttributeValue); i { + switch v := v.(*DeleteRegisteredResourceResponse); i { case 0: return &v.state case 1: @@ -2110,7 +2340,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateRegisteredResourceValueRequest); i { + switch v := v.(*ActionAttributeValue); i { case 0: return &v.state case 1: @@ -2122,7 +2352,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateRegisteredResourceValueResponse); i { + switch v := v.(*CreateRegisteredResourceValueRequest); i { case 0: return &v.state case 1: @@ -2134,7 +2364,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetRegisteredResourceValueRequest); i { + switch v := v.(*CreateRegisteredResourceValueResponse); i { case 0: return &v.state case 1: @@ -2146,7 +2376,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetRegisteredResourceValueResponse); i { + switch v := v.(*GetRegisteredResourceValueRequest); i { case 0: return &v.state case 1: @@ -2158,7 +2388,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetRegisteredResourceValuesByFQNsRequest); i { + switch v := v.(*GetRegisteredResourceValueResponse); i { case 0: return &v.state case 1: @@ -2170,7 +2400,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetRegisteredResourceValuesByFQNsResponse); i { + switch v := v.(*GetRegisteredResourceValuesByFQNsRequest); i { case 0: return &v.state case 1: @@ -2182,7 +2412,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRegisteredResourceValuesRequest); i { + switch v := v.(*GetRegisteredResourceValuesByFQNsResponse); i { case 0: return &v.state case 1: @@ -2194,7 +2424,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRegisteredResourceValuesResponse); i { + switch v := v.(*ListRegisteredResourceValuesRequest); i { case 0: return &v.state case 1: @@ -2206,7 +2436,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateRegisteredResourceValueRequest); i { + switch v := v.(*ListRegisteredResourceValuesResponse); i { case 0: return &v.state case 1: @@ -2218,7 +2448,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateRegisteredResourceValueResponse); i { + switch v := v.(*UpdateRegisteredResourceValueRequest); i { case 0: return &v.state case 1: @@ -2230,7 +2460,7 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteRegisteredResourceValueRequest); i { + switch v := v.(*UpdateRegisteredResourceValueResponse); i { case 0: return &v.state case 1: @@ -2242,6 +2472,18 @@ func file_policy_registeredresources_registered_resources_proto_init() { } } file_policy_registeredresources_registered_resources_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteRegisteredResourceValueRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_registeredresources_registered_resources_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteRegisteredResourceValueResponse); i { case 0: return &v.state @@ -2258,13 +2500,13 @@ func file_policy_registeredresources_registered_resources_proto_init() { (*GetRegisteredResourceRequest_Id)(nil), (*GetRegisteredResourceRequest_Name)(nil), } - file_policy_registeredresources_registered_resources_proto_msgTypes[10].OneofWrappers = []interface{}{ + file_policy_registeredresources_registered_resources_proto_msgTypes[11].OneofWrappers = []interface{}{ (*ActionAttributeValue_ActionId)(nil), (*ActionAttributeValue_ActionName)(nil), (*ActionAttributeValue_AttributeValueId)(nil), (*ActionAttributeValue_AttributeValueFqn)(nil), } - file_policy_registeredresources_registered_resources_proto_msgTypes[13].OneofWrappers = []interface{}{ + file_policy_registeredresources_registered_resources_proto_msgTypes[14].OneofWrappers = []interface{}{ (*GetRegisteredResourceValueRequest_Id)(nil), (*GetRegisteredResourceValueRequest_Fqn)(nil), } @@ -2273,13 +2515,14 @@ func file_policy_registeredresources_registered_resources_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_policy_registeredresources_registered_resources_proto_rawDesc, - NumEnums: 0, - NumMessages: 24, + NumEnums: 1, + NumMessages: 25, NumExtensions: 0, NumServices: 1, }, GoTypes: file_policy_registeredresources_registered_resources_proto_goTypes, DependencyIndexes: file_policy_registeredresources_registered_resources_proto_depIdxs, + EnumInfos: file_policy_registeredresources_registered_resources_proto_enumTypes, MessageInfos: file_policy_registeredresources_registered_resources_proto_msgTypes, }.Build() File_policy_registeredresources_registered_resources_proto = out.File diff --git a/protocol/go/policy/registeredresources/registeredresourcesconnect/registered_resources.connect.go b/protocol/go/policy/registeredresources/registeredresourcesconnect/registered_resources.connect.go index d35ac7a546..ae58b17b7f 100644 --- a/protocol/go/policy/registeredresources/registeredresourcesconnect/registered_resources.connect.go +++ b/protocol/go/policy/registeredresources/registeredresourcesconnect/registered_resources.connect.go @@ -69,22 +69,6 @@ const ( RegisteredResourcesServiceDeleteRegisteredResourceValueProcedure = "/policy.registeredresources.RegisteredResourcesService/DeleteRegisteredResourceValue" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - registeredResourcesServiceServiceDescriptor = registeredresources.File_policy_registeredresources_registered_resources_proto.Services().ByName("RegisteredResourcesService") - registeredResourcesServiceCreateRegisteredResourceMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("CreateRegisteredResource") - registeredResourcesServiceGetRegisteredResourceMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("GetRegisteredResource") - registeredResourcesServiceListRegisteredResourcesMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("ListRegisteredResources") - registeredResourcesServiceUpdateRegisteredResourceMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("UpdateRegisteredResource") - registeredResourcesServiceDeleteRegisteredResourceMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("DeleteRegisteredResource") - registeredResourcesServiceCreateRegisteredResourceValueMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("CreateRegisteredResourceValue") - registeredResourcesServiceGetRegisteredResourceValueMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("GetRegisteredResourceValue") - registeredResourcesServiceGetRegisteredResourceValuesByFQNsMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("GetRegisteredResourceValuesByFQNs") - registeredResourcesServiceListRegisteredResourceValuesMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("ListRegisteredResourceValues") - registeredResourcesServiceUpdateRegisteredResourceValueMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("UpdateRegisteredResourceValue") - registeredResourcesServiceDeleteRegisteredResourceValueMethodDescriptor = registeredResourcesServiceServiceDescriptor.Methods().ByName("DeleteRegisteredResourceValue") -) - // RegisteredResourcesServiceClient is a client for the // policy.registeredresources.RegisteredResourcesService service. type RegisteredResourcesServiceClient interface { @@ -111,71 +95,72 @@ type RegisteredResourcesServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewRegisteredResourcesServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RegisteredResourcesServiceClient { baseURL = strings.TrimRight(baseURL, "/") + registeredResourcesServiceMethods := registeredresources.File_policy_registeredresources_registered_resources_proto.Services().ByName("RegisteredResourcesService").Methods() return ®isteredResourcesServiceClient{ createRegisteredResource: connect.NewClient[registeredresources.CreateRegisteredResourceRequest, registeredresources.CreateRegisteredResourceResponse]( httpClient, baseURL+RegisteredResourcesServiceCreateRegisteredResourceProcedure, - connect.WithSchema(registeredResourcesServiceCreateRegisteredResourceMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("CreateRegisteredResource")), connect.WithClientOptions(opts...), ), getRegisteredResource: connect.NewClient[registeredresources.GetRegisteredResourceRequest, registeredresources.GetRegisteredResourceResponse]( httpClient, baseURL+RegisteredResourcesServiceGetRegisteredResourceProcedure, - connect.WithSchema(registeredResourcesServiceGetRegisteredResourceMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("GetRegisteredResource")), connect.WithClientOptions(opts...), ), listRegisteredResources: connect.NewClient[registeredresources.ListRegisteredResourcesRequest, registeredresources.ListRegisteredResourcesResponse]( httpClient, baseURL+RegisteredResourcesServiceListRegisteredResourcesProcedure, - connect.WithSchema(registeredResourcesServiceListRegisteredResourcesMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("ListRegisteredResources")), connect.WithClientOptions(opts...), ), updateRegisteredResource: connect.NewClient[registeredresources.UpdateRegisteredResourceRequest, registeredresources.UpdateRegisteredResourceResponse]( httpClient, baseURL+RegisteredResourcesServiceUpdateRegisteredResourceProcedure, - connect.WithSchema(registeredResourcesServiceUpdateRegisteredResourceMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("UpdateRegisteredResource")), connect.WithClientOptions(opts...), ), deleteRegisteredResource: connect.NewClient[registeredresources.DeleteRegisteredResourceRequest, registeredresources.DeleteRegisteredResourceResponse]( httpClient, baseURL+RegisteredResourcesServiceDeleteRegisteredResourceProcedure, - connect.WithSchema(registeredResourcesServiceDeleteRegisteredResourceMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("DeleteRegisteredResource")), connect.WithClientOptions(opts...), ), createRegisteredResourceValue: connect.NewClient[registeredresources.CreateRegisteredResourceValueRequest, registeredresources.CreateRegisteredResourceValueResponse]( httpClient, baseURL+RegisteredResourcesServiceCreateRegisteredResourceValueProcedure, - connect.WithSchema(registeredResourcesServiceCreateRegisteredResourceValueMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("CreateRegisteredResourceValue")), connect.WithClientOptions(opts...), ), getRegisteredResourceValue: connect.NewClient[registeredresources.GetRegisteredResourceValueRequest, registeredresources.GetRegisteredResourceValueResponse]( httpClient, baseURL+RegisteredResourcesServiceGetRegisteredResourceValueProcedure, - connect.WithSchema(registeredResourcesServiceGetRegisteredResourceValueMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("GetRegisteredResourceValue")), connect.WithClientOptions(opts...), ), getRegisteredResourceValuesByFQNs: connect.NewClient[registeredresources.GetRegisteredResourceValuesByFQNsRequest, registeredresources.GetRegisteredResourceValuesByFQNsResponse]( httpClient, baseURL+RegisteredResourcesServiceGetRegisteredResourceValuesByFQNsProcedure, - connect.WithSchema(registeredResourcesServiceGetRegisteredResourceValuesByFQNsMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("GetRegisteredResourceValuesByFQNs")), connect.WithClientOptions(opts...), ), listRegisteredResourceValues: connect.NewClient[registeredresources.ListRegisteredResourceValuesRequest, registeredresources.ListRegisteredResourceValuesResponse]( httpClient, baseURL+RegisteredResourcesServiceListRegisteredResourceValuesProcedure, - connect.WithSchema(registeredResourcesServiceListRegisteredResourceValuesMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("ListRegisteredResourceValues")), connect.WithClientOptions(opts...), ), updateRegisteredResourceValue: connect.NewClient[registeredresources.UpdateRegisteredResourceValueRequest, registeredresources.UpdateRegisteredResourceValueResponse]( httpClient, baseURL+RegisteredResourcesServiceUpdateRegisteredResourceValueProcedure, - connect.WithSchema(registeredResourcesServiceUpdateRegisteredResourceValueMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("UpdateRegisteredResourceValue")), connect.WithClientOptions(opts...), ), deleteRegisteredResourceValue: connect.NewClient[registeredresources.DeleteRegisteredResourceValueRequest, registeredresources.DeleteRegisteredResourceValueResponse]( httpClient, baseURL+RegisteredResourcesServiceDeleteRegisteredResourceValueProcedure, - connect.WithSchema(registeredResourcesServiceDeleteRegisteredResourceValueMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("DeleteRegisteredResourceValue")), connect.WithClientOptions(opts...), ), } @@ -284,70 +269,71 @@ type RegisteredResourcesServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewRegisteredResourcesServiceHandler(svc RegisteredResourcesServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + registeredResourcesServiceMethods := registeredresources.File_policy_registeredresources_registered_resources_proto.Services().ByName("RegisteredResourcesService").Methods() registeredResourcesServiceCreateRegisteredResourceHandler := connect.NewUnaryHandler( RegisteredResourcesServiceCreateRegisteredResourceProcedure, svc.CreateRegisteredResource, - connect.WithSchema(registeredResourcesServiceCreateRegisteredResourceMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("CreateRegisteredResource")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceGetRegisteredResourceHandler := connect.NewUnaryHandler( RegisteredResourcesServiceGetRegisteredResourceProcedure, svc.GetRegisteredResource, - connect.WithSchema(registeredResourcesServiceGetRegisteredResourceMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("GetRegisteredResource")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceListRegisteredResourcesHandler := connect.NewUnaryHandler( RegisteredResourcesServiceListRegisteredResourcesProcedure, svc.ListRegisteredResources, - connect.WithSchema(registeredResourcesServiceListRegisteredResourcesMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("ListRegisteredResources")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceUpdateRegisteredResourceHandler := connect.NewUnaryHandler( RegisteredResourcesServiceUpdateRegisteredResourceProcedure, svc.UpdateRegisteredResource, - connect.WithSchema(registeredResourcesServiceUpdateRegisteredResourceMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("UpdateRegisteredResource")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceDeleteRegisteredResourceHandler := connect.NewUnaryHandler( RegisteredResourcesServiceDeleteRegisteredResourceProcedure, svc.DeleteRegisteredResource, - connect.WithSchema(registeredResourcesServiceDeleteRegisteredResourceMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("DeleteRegisteredResource")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceCreateRegisteredResourceValueHandler := connect.NewUnaryHandler( RegisteredResourcesServiceCreateRegisteredResourceValueProcedure, svc.CreateRegisteredResourceValue, - connect.WithSchema(registeredResourcesServiceCreateRegisteredResourceValueMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("CreateRegisteredResourceValue")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceGetRegisteredResourceValueHandler := connect.NewUnaryHandler( RegisteredResourcesServiceGetRegisteredResourceValueProcedure, svc.GetRegisteredResourceValue, - connect.WithSchema(registeredResourcesServiceGetRegisteredResourceValueMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("GetRegisteredResourceValue")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceGetRegisteredResourceValuesByFQNsHandler := connect.NewUnaryHandler( RegisteredResourcesServiceGetRegisteredResourceValuesByFQNsProcedure, svc.GetRegisteredResourceValuesByFQNs, - connect.WithSchema(registeredResourcesServiceGetRegisteredResourceValuesByFQNsMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("GetRegisteredResourceValuesByFQNs")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceListRegisteredResourceValuesHandler := connect.NewUnaryHandler( RegisteredResourcesServiceListRegisteredResourceValuesProcedure, svc.ListRegisteredResourceValues, - connect.WithSchema(registeredResourcesServiceListRegisteredResourceValuesMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("ListRegisteredResourceValues")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceUpdateRegisteredResourceValueHandler := connect.NewUnaryHandler( RegisteredResourcesServiceUpdateRegisteredResourceValueProcedure, svc.UpdateRegisteredResourceValue, - connect.WithSchema(registeredResourcesServiceUpdateRegisteredResourceValueMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("UpdateRegisteredResourceValue")), connect.WithHandlerOptions(opts...), ) registeredResourcesServiceDeleteRegisteredResourceValueHandler := connect.NewUnaryHandler( RegisteredResourcesServiceDeleteRegisteredResourceValueProcedure, svc.DeleteRegisteredResourceValue, - connect.WithSchema(registeredResourcesServiceDeleteRegisteredResourceValueMethodDescriptor), + connect.WithSchema(registeredResourcesServiceMethods.ByName("DeleteRegisteredResourceValue")), connect.WithHandlerOptions(opts...), ) return "/policy.registeredresources.RegisteredResourcesService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/policy/resourcemapping/resourcemappingconnect/resource_mapping.connect.go b/protocol/go/policy/resourcemapping/resourcemappingconnect/resource_mapping.connect.go index c5589908e9..06de25357b 100644 --- a/protocol/go/policy/resourcemapping/resourcemappingconnect/resource_mapping.connect.go +++ b/protocol/go/policy/resourcemapping/resourcemappingconnect/resource_mapping.connect.go @@ -68,22 +68,6 @@ const ( ResourceMappingServiceDeleteResourceMappingProcedure = "/policy.resourcemapping.ResourceMappingService/DeleteResourceMapping" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - resourceMappingServiceServiceDescriptor = resourcemapping.File_policy_resourcemapping_resource_mapping_proto.Services().ByName("ResourceMappingService") - resourceMappingServiceListResourceMappingGroupsMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("ListResourceMappingGroups") - resourceMappingServiceGetResourceMappingGroupMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("GetResourceMappingGroup") - resourceMappingServiceCreateResourceMappingGroupMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("CreateResourceMappingGroup") - resourceMappingServiceUpdateResourceMappingGroupMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("UpdateResourceMappingGroup") - resourceMappingServiceDeleteResourceMappingGroupMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("DeleteResourceMappingGroup") - resourceMappingServiceListResourceMappingsMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("ListResourceMappings") - resourceMappingServiceListResourceMappingsByGroupFqnsMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("ListResourceMappingsByGroupFqns") - resourceMappingServiceGetResourceMappingMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("GetResourceMapping") - resourceMappingServiceCreateResourceMappingMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("CreateResourceMapping") - resourceMappingServiceUpdateResourceMappingMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("UpdateResourceMapping") - resourceMappingServiceDeleteResourceMappingMethodDescriptor = resourceMappingServiceServiceDescriptor.Methods().ByName("DeleteResourceMapping") -) - // ResourceMappingServiceClient is a client for the policy.resourcemapping.ResourceMappingService // service. type ResourceMappingServiceClient interface { @@ -110,76 +94,77 @@ type ResourceMappingServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewResourceMappingServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ResourceMappingServiceClient { baseURL = strings.TrimRight(baseURL, "/") + resourceMappingServiceMethods := resourcemapping.File_policy_resourcemapping_resource_mapping_proto.Services().ByName("ResourceMappingService").Methods() return &resourceMappingServiceClient{ listResourceMappingGroups: connect.NewClient[resourcemapping.ListResourceMappingGroupsRequest, resourcemapping.ListResourceMappingGroupsResponse]( httpClient, baseURL+ResourceMappingServiceListResourceMappingGroupsProcedure, - connect.WithSchema(resourceMappingServiceListResourceMappingGroupsMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("ListResourceMappingGroups")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getResourceMappingGroup: connect.NewClient[resourcemapping.GetResourceMappingGroupRequest, resourcemapping.GetResourceMappingGroupResponse]( httpClient, baseURL+ResourceMappingServiceGetResourceMappingGroupProcedure, - connect.WithSchema(resourceMappingServiceGetResourceMappingGroupMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("GetResourceMappingGroup")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createResourceMappingGroup: connect.NewClient[resourcemapping.CreateResourceMappingGroupRequest, resourcemapping.CreateResourceMappingGroupResponse]( httpClient, baseURL+ResourceMappingServiceCreateResourceMappingGroupProcedure, - connect.WithSchema(resourceMappingServiceCreateResourceMappingGroupMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("CreateResourceMappingGroup")), connect.WithClientOptions(opts...), ), updateResourceMappingGroup: connect.NewClient[resourcemapping.UpdateResourceMappingGroupRequest, resourcemapping.UpdateResourceMappingGroupResponse]( httpClient, baseURL+ResourceMappingServiceUpdateResourceMappingGroupProcedure, - connect.WithSchema(resourceMappingServiceUpdateResourceMappingGroupMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("UpdateResourceMappingGroup")), connect.WithClientOptions(opts...), ), deleteResourceMappingGroup: connect.NewClient[resourcemapping.DeleteResourceMappingGroupRequest, resourcemapping.DeleteResourceMappingGroupResponse]( httpClient, baseURL+ResourceMappingServiceDeleteResourceMappingGroupProcedure, - connect.WithSchema(resourceMappingServiceDeleteResourceMappingGroupMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("DeleteResourceMappingGroup")), connect.WithClientOptions(opts...), ), listResourceMappings: connect.NewClient[resourcemapping.ListResourceMappingsRequest, resourcemapping.ListResourceMappingsResponse]( httpClient, baseURL+ResourceMappingServiceListResourceMappingsProcedure, - connect.WithSchema(resourceMappingServiceListResourceMappingsMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("ListResourceMappings")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), listResourceMappingsByGroupFqns: connect.NewClient[resourcemapping.ListResourceMappingsByGroupFqnsRequest, resourcemapping.ListResourceMappingsByGroupFqnsResponse]( httpClient, baseURL+ResourceMappingServiceListResourceMappingsByGroupFqnsProcedure, - connect.WithSchema(resourceMappingServiceListResourceMappingsByGroupFqnsMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("ListResourceMappingsByGroupFqns")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getResourceMapping: connect.NewClient[resourcemapping.GetResourceMappingRequest, resourcemapping.GetResourceMappingResponse]( httpClient, baseURL+ResourceMappingServiceGetResourceMappingProcedure, - connect.WithSchema(resourceMappingServiceGetResourceMappingMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("GetResourceMapping")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createResourceMapping: connect.NewClient[resourcemapping.CreateResourceMappingRequest, resourcemapping.CreateResourceMappingResponse]( httpClient, baseURL+ResourceMappingServiceCreateResourceMappingProcedure, - connect.WithSchema(resourceMappingServiceCreateResourceMappingMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("CreateResourceMapping")), connect.WithClientOptions(opts...), ), updateResourceMapping: connect.NewClient[resourcemapping.UpdateResourceMappingRequest, resourcemapping.UpdateResourceMappingResponse]( httpClient, baseURL+ResourceMappingServiceUpdateResourceMappingProcedure, - connect.WithSchema(resourceMappingServiceUpdateResourceMappingMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("UpdateResourceMapping")), connect.WithClientOptions(opts...), ), deleteResourceMapping: connect.NewClient[resourcemapping.DeleteResourceMappingRequest, resourcemapping.DeleteResourceMappingResponse]( httpClient, baseURL+ResourceMappingServiceDeleteResourceMappingProcedure, - connect.WithSchema(resourceMappingServiceDeleteResourceMappingMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("DeleteResourceMapping")), connect.WithClientOptions(opts...), ), } @@ -283,75 +268,76 @@ type ResourceMappingServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewResourceMappingServiceHandler(svc ResourceMappingServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + resourceMappingServiceMethods := resourcemapping.File_policy_resourcemapping_resource_mapping_proto.Services().ByName("ResourceMappingService").Methods() resourceMappingServiceListResourceMappingGroupsHandler := connect.NewUnaryHandler( ResourceMappingServiceListResourceMappingGroupsProcedure, svc.ListResourceMappingGroups, - connect.WithSchema(resourceMappingServiceListResourceMappingGroupsMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("ListResourceMappingGroups")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) resourceMappingServiceGetResourceMappingGroupHandler := connect.NewUnaryHandler( ResourceMappingServiceGetResourceMappingGroupProcedure, svc.GetResourceMappingGroup, - connect.WithSchema(resourceMappingServiceGetResourceMappingGroupMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("GetResourceMappingGroup")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) resourceMappingServiceCreateResourceMappingGroupHandler := connect.NewUnaryHandler( ResourceMappingServiceCreateResourceMappingGroupProcedure, svc.CreateResourceMappingGroup, - connect.WithSchema(resourceMappingServiceCreateResourceMappingGroupMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("CreateResourceMappingGroup")), connect.WithHandlerOptions(opts...), ) resourceMappingServiceUpdateResourceMappingGroupHandler := connect.NewUnaryHandler( ResourceMappingServiceUpdateResourceMappingGroupProcedure, svc.UpdateResourceMappingGroup, - connect.WithSchema(resourceMappingServiceUpdateResourceMappingGroupMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("UpdateResourceMappingGroup")), connect.WithHandlerOptions(opts...), ) resourceMappingServiceDeleteResourceMappingGroupHandler := connect.NewUnaryHandler( ResourceMappingServiceDeleteResourceMappingGroupProcedure, svc.DeleteResourceMappingGroup, - connect.WithSchema(resourceMappingServiceDeleteResourceMappingGroupMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("DeleteResourceMappingGroup")), connect.WithHandlerOptions(opts...), ) resourceMappingServiceListResourceMappingsHandler := connect.NewUnaryHandler( ResourceMappingServiceListResourceMappingsProcedure, svc.ListResourceMappings, - connect.WithSchema(resourceMappingServiceListResourceMappingsMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("ListResourceMappings")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) resourceMappingServiceListResourceMappingsByGroupFqnsHandler := connect.NewUnaryHandler( ResourceMappingServiceListResourceMappingsByGroupFqnsProcedure, svc.ListResourceMappingsByGroupFqns, - connect.WithSchema(resourceMappingServiceListResourceMappingsByGroupFqnsMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("ListResourceMappingsByGroupFqns")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) resourceMappingServiceGetResourceMappingHandler := connect.NewUnaryHandler( ResourceMappingServiceGetResourceMappingProcedure, svc.GetResourceMapping, - connect.WithSchema(resourceMappingServiceGetResourceMappingMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("GetResourceMapping")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) resourceMappingServiceCreateResourceMappingHandler := connect.NewUnaryHandler( ResourceMappingServiceCreateResourceMappingProcedure, svc.CreateResourceMapping, - connect.WithSchema(resourceMappingServiceCreateResourceMappingMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("CreateResourceMapping")), connect.WithHandlerOptions(opts...), ) resourceMappingServiceUpdateResourceMappingHandler := connect.NewUnaryHandler( ResourceMappingServiceUpdateResourceMappingProcedure, svc.UpdateResourceMapping, - connect.WithSchema(resourceMappingServiceUpdateResourceMappingMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("UpdateResourceMapping")), connect.WithHandlerOptions(opts...), ) resourceMappingServiceDeleteResourceMappingHandler := connect.NewUnaryHandler( ResourceMappingServiceDeleteResourceMappingProcedure, svc.DeleteResourceMapping, - connect.WithSchema(resourceMappingServiceDeleteResourceMappingMethodDescriptor), + connect.WithSchema(resourceMappingServiceMethods.ByName("DeleteResourceMapping")), connect.WithHandlerOptions(opts...), ) return "/policy.resourcemapping.ResourceMappingService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/policy/selectors.pb.go b/protocol/go/policy/selectors.pb.go index f0e5289146..0449d03658 100644 --- a/protocol/go/policy/selectors.pb.go +++ b/protocol/go/policy/selectors.pb.go @@ -20,6 +20,59 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// Sorting direction shared across list APIs. +// When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, +// the endpoint's request message defines the default ordering; see the +// specific List* request docs. +type SortDirection int32 + +const ( + SortDirection_SORT_DIRECTION_UNSPECIFIED SortDirection = 0 + SortDirection_SORT_DIRECTION_ASC SortDirection = 1 + SortDirection_SORT_DIRECTION_DESC SortDirection = 2 +) + +// Enum value maps for SortDirection. +var ( + SortDirection_name = map[int32]string{ + 0: "SORT_DIRECTION_UNSPECIFIED", + 1: "SORT_DIRECTION_ASC", + 2: "SORT_DIRECTION_DESC", + } + SortDirection_value = map[string]int32{ + "SORT_DIRECTION_UNSPECIFIED": 0, + "SORT_DIRECTION_ASC": 1, + "SORT_DIRECTION_DESC": 2, + } +) + +func (x SortDirection) Enum() *SortDirection { + p := new(SortDirection) + *p = x + return p +} + +func (x SortDirection) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortDirection) Descriptor() protoreflect.EnumDescriptor { + return file_policy_selectors_proto_enumTypes[0].Descriptor() +} + +func (SortDirection) Type() protoreflect.EnumType { + return &file_policy_selectors_proto_enumTypes[0] +} + +func (x SortDirection) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortDirection.Descriptor instead. +func (SortDirection) EnumDescriptor() ([]byte, []int) { + return file_policy_selectors_proto_rawDescGZIP(), []int{0} +} + // Deprecated: never utilized type AttributeNamespaceSelector struct { state protoimpl.MessageState @@ -750,16 +803,22 @@ var file_policy_selectors_proto_rawDesc = []byte{ 0x0a, 0x0b, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x84, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x42, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, - 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xca, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xe2, 0x02, - 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x2a, 0x60, 0x0a, 0x0d, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, + 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, + 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x53, 0x43, 0x10, 0x01, 0x12, 0x17, + 0x0a, 0x13, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x44, 0x45, 0x53, 0x43, 0x10, 0x02, 0x42, 0x84, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, + 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, + 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x58, 0x58, 0xaa, 0x02, + 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xca, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0xe2, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -774,27 +833,29 @@ func file_policy_selectors_proto_rawDescGZIP() []byte { return file_policy_selectors_proto_rawDescData } +var file_policy_selectors_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_policy_selectors_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_policy_selectors_proto_goTypes = []interface{}{ - (*AttributeNamespaceSelector)(nil), // 0: policy.AttributeNamespaceSelector - (*AttributeDefinitionSelector)(nil), // 1: policy.AttributeDefinitionSelector - (*AttributeValueSelector)(nil), // 2: policy.AttributeValueSelector - (*PageRequest)(nil), // 3: policy.PageRequest - (*PageResponse)(nil), // 4: policy.PageResponse - (*AttributeNamespaceSelector_AttributeSelector)(nil), // 5: policy.AttributeNamespaceSelector.AttributeSelector - (*AttributeNamespaceSelector_AttributeSelector_ValueSelector)(nil), // 6: policy.AttributeNamespaceSelector.AttributeSelector.ValueSelector - (*AttributeDefinitionSelector_NamespaceSelector)(nil), // 7: policy.AttributeDefinitionSelector.NamespaceSelector - (*AttributeDefinitionSelector_ValueSelector)(nil), // 8: policy.AttributeDefinitionSelector.ValueSelector - (*AttributeValueSelector_AttributeSelector)(nil), // 9: policy.AttributeValueSelector.AttributeSelector - (*AttributeValueSelector_AttributeSelector_NamespaceSelector)(nil), // 10: policy.AttributeValueSelector.AttributeSelector.NamespaceSelector + (SortDirection)(0), // 0: policy.SortDirection + (*AttributeNamespaceSelector)(nil), // 1: policy.AttributeNamespaceSelector + (*AttributeDefinitionSelector)(nil), // 2: policy.AttributeDefinitionSelector + (*AttributeValueSelector)(nil), // 3: policy.AttributeValueSelector + (*PageRequest)(nil), // 4: policy.PageRequest + (*PageResponse)(nil), // 5: policy.PageResponse + (*AttributeNamespaceSelector_AttributeSelector)(nil), // 6: policy.AttributeNamespaceSelector.AttributeSelector + (*AttributeNamespaceSelector_AttributeSelector_ValueSelector)(nil), // 7: policy.AttributeNamespaceSelector.AttributeSelector.ValueSelector + (*AttributeDefinitionSelector_NamespaceSelector)(nil), // 8: policy.AttributeDefinitionSelector.NamespaceSelector + (*AttributeDefinitionSelector_ValueSelector)(nil), // 9: policy.AttributeDefinitionSelector.ValueSelector + (*AttributeValueSelector_AttributeSelector)(nil), // 10: policy.AttributeValueSelector.AttributeSelector + (*AttributeValueSelector_AttributeSelector_NamespaceSelector)(nil), // 11: policy.AttributeValueSelector.AttributeSelector.NamespaceSelector } var file_policy_selectors_proto_depIdxs = []int32{ - 5, // 0: policy.AttributeNamespaceSelector.with_attributes:type_name -> policy.AttributeNamespaceSelector.AttributeSelector - 7, // 1: policy.AttributeDefinitionSelector.with_namespace:type_name -> policy.AttributeDefinitionSelector.NamespaceSelector - 8, // 2: policy.AttributeDefinitionSelector.with_values:type_name -> policy.AttributeDefinitionSelector.ValueSelector - 9, // 3: policy.AttributeValueSelector.with_attribute:type_name -> policy.AttributeValueSelector.AttributeSelector - 6, // 4: policy.AttributeNamespaceSelector.AttributeSelector.with_values:type_name -> policy.AttributeNamespaceSelector.AttributeSelector.ValueSelector - 10, // 5: policy.AttributeValueSelector.AttributeSelector.with_namespace:type_name -> policy.AttributeValueSelector.AttributeSelector.NamespaceSelector + 6, // 0: policy.AttributeNamespaceSelector.with_attributes:type_name -> policy.AttributeNamespaceSelector.AttributeSelector + 8, // 1: policy.AttributeDefinitionSelector.with_namespace:type_name -> policy.AttributeDefinitionSelector.NamespaceSelector + 9, // 2: policy.AttributeDefinitionSelector.with_values:type_name -> policy.AttributeDefinitionSelector.ValueSelector + 10, // 3: policy.AttributeValueSelector.with_attribute:type_name -> policy.AttributeValueSelector.AttributeSelector + 7, // 4: policy.AttributeNamespaceSelector.AttributeSelector.with_values:type_name -> policy.AttributeNamespaceSelector.AttributeSelector.ValueSelector + 11, // 5: policy.AttributeValueSelector.AttributeSelector.with_namespace:type_name -> policy.AttributeValueSelector.AttributeSelector.NamespaceSelector 6, // [6:6] is the sub-list for method output_type 6, // [6:6] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name @@ -946,13 +1007,14 @@ func file_policy_selectors_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_policy_selectors_proto_rawDesc, - NumEnums: 0, + NumEnums: 1, NumMessages: 11, NumExtensions: 0, NumServices: 0, }, GoTypes: file_policy_selectors_proto_goTypes, DependencyIndexes: file_policy_selectors_proto_depIdxs, + EnumInfos: file_policy_selectors_proto_enumTypes, MessageInfos: file_policy_selectors_proto_msgTypes, }.Build() File_policy_selectors_proto = out.File diff --git a/protocol/go/policy/subjectmapping/subject_mapping.pb.go b/protocol/go/policy/subjectmapping/subject_mapping.pb.go index e17519bbcb..3c1bf6c2f1 100644 --- a/protocol/go/policy/subjectmapping/subject_mapping.pb.go +++ b/protocol/go/policy/subjectmapping/subject_mapping.pb.go @@ -23,6 +23,104 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type SortSubjectMappingsType int32 + +const ( + SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED SortSubjectMappingsType = 0 + SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT SortSubjectMappingsType = 1 + SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT SortSubjectMappingsType = 2 +) + +// Enum value maps for SortSubjectMappingsType. +var ( + SortSubjectMappingsType_name = map[int32]string{ + 0: "SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED", + 1: "SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT", + 2: "SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT", + } + SortSubjectMappingsType_value = map[string]int32{ + "SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED": 0, + "SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT": 1, + "SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT": 2, + } +) + +func (x SortSubjectMappingsType) Enum() *SortSubjectMappingsType { + p := new(SortSubjectMappingsType) + *p = x + return p +} + +func (x SortSubjectMappingsType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortSubjectMappingsType) Descriptor() protoreflect.EnumDescriptor { + return file_policy_subjectmapping_subject_mapping_proto_enumTypes[0].Descriptor() +} + +func (SortSubjectMappingsType) Type() protoreflect.EnumType { + return &file_policy_subjectmapping_subject_mapping_proto_enumTypes[0] +} + +func (x SortSubjectMappingsType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortSubjectMappingsType.Descriptor instead. +func (SortSubjectMappingsType) EnumDescriptor() ([]byte, []int) { + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{0} +} + +type SortSubjectConditionSetsType int32 + +const ( + SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED SortSubjectConditionSetsType = 0 + SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT SortSubjectConditionSetsType = 1 + SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT SortSubjectConditionSetsType = 2 +) + +// Enum value maps for SortSubjectConditionSetsType. +var ( + SortSubjectConditionSetsType_name = map[int32]string{ + 0: "SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED", + 1: "SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT", + 2: "SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT", + } + SortSubjectConditionSetsType_value = map[string]int32{ + "SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED": 0, + "SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT": 1, + "SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT": 2, + } +) + +func (x SortSubjectConditionSetsType) Enum() *SortSubjectConditionSetsType { + p := new(SortSubjectConditionSetsType) + *p = x + return p +} + +func (x SortSubjectConditionSetsType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortSubjectConditionSetsType) Descriptor() protoreflect.EnumDescriptor { + return file_policy_subjectmapping_subject_mapping_proto_enumTypes[1].Descriptor() +} + +func (SortSubjectConditionSetsType) Type() protoreflect.EnumType { + return &file_policy_subjectmapping_subject_mapping_proto_enumTypes[1] +} + +func (x SortSubjectConditionSetsType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortSubjectConditionSetsType.Descriptor instead. +func (SortSubjectConditionSetsType) EnumDescriptor() ([]byte, []int) { + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{1} +} + // MatchSubjectMappingsRequest liberally returns a list of SubjectMappings based on the provided SubjectProperties. // The SubjectMappings are returned if an external selector field matches. type MatchSubjectMappingsRequest struct { @@ -214,19 +312,82 @@ func (x *GetSubjectMappingResponse) GetSubjectMapping() *policy.SubjectMapping { return nil } +type SubjectMappingsSort struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field SortSubjectMappingsType `protobuf:"varint,1,opt,name=field,proto3,enum=policy.subjectmapping.SortSubjectMappingsType" json:"field,omitempty"` + Direction policy.SortDirection `protobuf:"varint,2,opt,name=direction,proto3,enum=policy.SortDirection" json:"direction,omitempty"` +} + +func (x *SubjectMappingsSort) Reset() { + *x = SubjectMappingsSort{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubjectMappingsSort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubjectMappingsSort) ProtoMessage() {} + +func (x *SubjectMappingsSort) ProtoReflect() protoreflect.Message { + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubjectMappingsSort.ProtoReflect.Descriptor instead. +func (*SubjectMappingsSort) Descriptor() ([]byte, []int) { + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{4} +} + +func (x *SubjectMappingsSort) GetField() SortSubjectMappingsType { + if x != nil { + return x.Field + } + return SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED +} + +func (x *SubjectMappingsSort) GetDirection() policy.SortDirection { + if x != nil { + return x.Direction + } + return policy.SortDirection(0) +} + type ListSubjectMappingsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + NamespaceFqn string `protobuf:"bytes,2,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + Sort []*SubjectMappingsSort `protobuf:"bytes,11,rep,name=sort,proto3" json:"sort,omitempty"` } func (x *ListSubjectMappingsRequest) Reset() { *x = ListSubjectMappingsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[4] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -239,7 +400,7 @@ func (x *ListSubjectMappingsRequest) String() string { func (*ListSubjectMappingsRequest) ProtoMessage() {} func (x *ListSubjectMappingsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[4] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -252,7 +413,21 @@ func (x *ListSubjectMappingsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSubjectMappingsRequest.ProtoReflect.Descriptor instead. func (*ListSubjectMappingsRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{4} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{5} +} + +func (x *ListSubjectMappingsRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *ListSubjectMappingsRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" } func (x *ListSubjectMappingsRequest) GetPagination() *policy.PageRequest { @@ -262,6 +437,13 @@ func (x *ListSubjectMappingsRequest) GetPagination() *policy.PageRequest { return nil } +func (x *ListSubjectMappingsRequest) GetSort() []*SubjectMappingsSort { + if x != nil { + return x.Sort + } + return nil +} + type ListSubjectMappingsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -274,7 +456,7 @@ type ListSubjectMappingsResponse struct { func (x *ListSubjectMappingsResponse) Reset() { *x = ListSubjectMappingsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[5] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -287,7 +469,7 @@ func (x *ListSubjectMappingsResponse) String() string { func (*ListSubjectMappingsResponse) ProtoMessage() {} func (x *ListSubjectMappingsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[5] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -300,7 +482,7 @@ func (x *ListSubjectMappingsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSubjectMappingsResponse.ProtoReflect.Descriptor instead. func (*ListSubjectMappingsResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{5} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{6} } func (x *ListSubjectMappingsResponse) GetSubjectMappings() []*policy.SubjectMapping { @@ -334,13 +516,17 @@ type CreateSubjectMappingRequest struct { // Create new SubjectConditionSet (NOTE: ignored if existing_subject_condition_set_id is provided) NewSubjectConditionSet *SubjectConditionSetCreate `protobuf:"bytes,4,opt,name=new_subject_condition_set,json=newSubjectConditionSet,proto3" json:"new_subject_condition_set,omitempty"` // Optional + // Namespace ID or FQN for the subject mapping + NamespaceId string `protobuf:"bytes,5,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + NamespaceFqn string `protobuf:"bytes,6,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` + // Optional Metadata *common.MetadataMutable `protobuf:"bytes,100,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *CreateSubjectMappingRequest) Reset() { *x = CreateSubjectMappingRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[6] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -353,7 +539,7 @@ func (x *CreateSubjectMappingRequest) String() string { func (*CreateSubjectMappingRequest) ProtoMessage() {} func (x *CreateSubjectMappingRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[6] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -366,7 +552,7 @@ func (x *CreateSubjectMappingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSubjectMappingRequest.ProtoReflect.Descriptor instead. func (*CreateSubjectMappingRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{6} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{7} } func (x *CreateSubjectMappingRequest) GetAttributeValueId() string { @@ -397,6 +583,20 @@ func (x *CreateSubjectMappingRequest) GetNewSubjectConditionSet() *SubjectCondit return nil } +func (x *CreateSubjectMappingRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CreateSubjectMappingRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" +} + func (x *CreateSubjectMappingRequest) GetMetadata() *common.MetadataMutable { if x != nil { return x.Metadata @@ -415,7 +615,7 @@ type CreateSubjectMappingResponse struct { func (x *CreateSubjectMappingResponse) Reset() { *x = CreateSubjectMappingResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[7] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -428,7 +628,7 @@ func (x *CreateSubjectMappingResponse) String() string { func (*CreateSubjectMappingResponse) ProtoMessage() {} func (x *CreateSubjectMappingResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[7] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -441,7 +641,7 @@ func (x *CreateSubjectMappingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSubjectMappingResponse.ProtoReflect.Descriptor instead. func (*CreateSubjectMappingResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{7} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{8} } func (x *CreateSubjectMappingResponse) GetSubjectMapping() *policy.SubjectMapping { @@ -472,7 +672,7 @@ type UpdateSubjectMappingRequest struct { func (x *UpdateSubjectMappingRequest) Reset() { *x = UpdateSubjectMappingRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[8] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -485,7 +685,7 @@ func (x *UpdateSubjectMappingRequest) String() string { func (*UpdateSubjectMappingRequest) ProtoMessage() {} func (x *UpdateSubjectMappingRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[8] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -498,7 +698,7 @@ func (x *UpdateSubjectMappingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSubjectMappingRequest.ProtoReflect.Descriptor instead. func (*UpdateSubjectMappingRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{8} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{9} } func (x *UpdateSubjectMappingRequest) GetId() string { @@ -548,7 +748,7 @@ type UpdateSubjectMappingResponse struct { func (x *UpdateSubjectMappingResponse) Reset() { *x = UpdateSubjectMappingResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[9] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -561,7 +761,7 @@ func (x *UpdateSubjectMappingResponse) String() string { func (*UpdateSubjectMappingResponse) ProtoMessage() {} func (x *UpdateSubjectMappingResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[9] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -574,7 +774,7 @@ func (x *UpdateSubjectMappingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSubjectMappingResponse.ProtoReflect.Descriptor instead. func (*UpdateSubjectMappingResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{9} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{10} } func (x *UpdateSubjectMappingResponse) GetSubjectMapping() *policy.SubjectMapping { @@ -596,7 +796,7 @@ type DeleteSubjectMappingRequest struct { func (x *DeleteSubjectMappingRequest) Reset() { *x = DeleteSubjectMappingRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[10] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -609,7 +809,7 @@ func (x *DeleteSubjectMappingRequest) String() string { func (*DeleteSubjectMappingRequest) ProtoMessage() {} func (x *DeleteSubjectMappingRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[10] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -622,7 +822,7 @@ func (x *DeleteSubjectMappingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSubjectMappingRequest.ProtoReflect.Descriptor instead. func (*DeleteSubjectMappingRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{10} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{11} } func (x *DeleteSubjectMappingRequest) GetId() string { @@ -644,7 +844,7 @@ type DeleteSubjectMappingResponse struct { func (x *DeleteSubjectMappingResponse) Reset() { *x = DeleteSubjectMappingResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[11] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -657,7 +857,7 @@ func (x *DeleteSubjectMappingResponse) String() string { func (*DeleteSubjectMappingResponse) ProtoMessage() {} func (x *DeleteSubjectMappingResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[11] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -670,7 +870,7 @@ func (x *DeleteSubjectMappingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSubjectMappingResponse.ProtoReflect.Descriptor instead. func (*DeleteSubjectMappingResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{11} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{12} } func (x *DeleteSubjectMappingResponse) GetSubjectMapping() *policy.SubjectMapping { @@ -692,7 +892,7 @@ type GetSubjectConditionSetRequest struct { func (x *GetSubjectConditionSetRequest) Reset() { *x = GetSubjectConditionSetRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[12] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -705,7 +905,7 @@ func (x *GetSubjectConditionSetRequest) String() string { func (*GetSubjectConditionSetRequest) ProtoMessage() {} func (x *GetSubjectConditionSetRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[12] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -718,7 +918,7 @@ func (x *GetSubjectConditionSetRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSubjectConditionSetRequest.ProtoReflect.Descriptor instead. func (*GetSubjectConditionSetRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{12} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{13} } func (x *GetSubjectConditionSetRequest) GetId() string { @@ -741,7 +941,7 @@ type GetSubjectConditionSetResponse struct { func (x *GetSubjectConditionSetResponse) Reset() { *x = GetSubjectConditionSetResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[13] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -754,7 +954,7 @@ func (x *GetSubjectConditionSetResponse) String() string { func (*GetSubjectConditionSetResponse) ProtoMessage() {} func (x *GetSubjectConditionSetResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[13] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -767,7 +967,7 @@ func (x *GetSubjectConditionSetResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSubjectConditionSetResponse.ProtoReflect.Descriptor instead. func (*GetSubjectConditionSetResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{13} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{14} } func (x *GetSubjectConditionSetResponse) GetSubjectConditionSet() *policy.SubjectConditionSet { @@ -784,19 +984,82 @@ func (x *GetSubjectConditionSetResponse) GetAssociatedSubjectMappings() []*polic return nil } +type SubjectConditionSetsSort struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field SortSubjectConditionSetsType `protobuf:"varint,1,opt,name=field,proto3,enum=policy.subjectmapping.SortSubjectConditionSetsType" json:"field,omitempty"` + Direction policy.SortDirection `protobuf:"varint,2,opt,name=direction,proto3,enum=policy.SortDirection" json:"direction,omitempty"` +} + +func (x *SubjectConditionSetsSort) Reset() { + *x = SubjectConditionSetsSort{} + if protoimpl.UnsafeEnabled { + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubjectConditionSetsSort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubjectConditionSetsSort) ProtoMessage() {} + +func (x *SubjectConditionSetsSort) ProtoReflect() protoreflect.Message { + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubjectConditionSetsSort.ProtoReflect.Descriptor instead. +func (*SubjectConditionSetsSort) Descriptor() ([]byte, []int) { + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{15} +} + +func (x *SubjectConditionSetsSort) GetField() SortSubjectConditionSetsType { + if x != nil { + return x.Field + } + return SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED +} + +func (x *SubjectConditionSetsSort) GetDirection() policy.SortDirection { + if x != nil { + return x.Direction + } + return policy.SortDirection(0) +} + type ListSubjectConditionSetsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + NamespaceFqn string `protobuf:"bytes,2,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` // Optional Pagination *policy.PageRequest `protobuf:"bytes,10,opt,name=pagination,proto3" json:"pagination,omitempty"` + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + Sort []*SubjectConditionSetsSort `protobuf:"bytes,11,rep,name=sort,proto3" json:"sort,omitempty"` } func (x *ListSubjectConditionSetsRequest) Reset() { *x = ListSubjectConditionSetsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[14] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -809,7 +1072,7 @@ func (x *ListSubjectConditionSetsRequest) String() string { func (*ListSubjectConditionSetsRequest) ProtoMessage() {} func (x *ListSubjectConditionSetsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[14] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -822,7 +1085,21 @@ func (x *ListSubjectConditionSetsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSubjectConditionSetsRequest.ProtoReflect.Descriptor instead. func (*ListSubjectConditionSetsRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{14} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{16} +} + +func (x *ListSubjectConditionSetsRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *ListSubjectConditionSetsRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" } func (x *ListSubjectConditionSetsRequest) GetPagination() *policy.PageRequest { @@ -832,6 +1109,13 @@ func (x *ListSubjectConditionSetsRequest) GetPagination() *policy.PageRequest { return nil } +func (x *ListSubjectConditionSetsRequest) GetSort() []*SubjectConditionSetsSort { + if x != nil { + return x.Sort + } + return nil +} + type ListSubjectConditionSetsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -844,7 +1128,7 @@ type ListSubjectConditionSetsResponse struct { func (x *ListSubjectConditionSetsResponse) Reset() { *x = ListSubjectConditionSetsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[15] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -857,7 +1141,7 @@ func (x *ListSubjectConditionSetsResponse) String() string { func (*ListSubjectConditionSetsResponse) ProtoMessage() {} func (x *ListSubjectConditionSetsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[15] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -870,7 +1154,7 @@ func (x *ListSubjectConditionSetsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSubjectConditionSetsResponse.ProtoReflect.Descriptor instead. func (*ListSubjectConditionSetsResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{15} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{17} } func (x *ListSubjectConditionSetsResponse) GetSubjectConditionSets() []*policy.SubjectConditionSet { @@ -902,7 +1186,7 @@ type SubjectConditionSetCreate struct { func (x *SubjectConditionSetCreate) Reset() { *x = SubjectConditionSetCreate{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[16] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -915,7 +1199,7 @@ func (x *SubjectConditionSetCreate) String() string { func (*SubjectConditionSetCreate) ProtoMessage() {} func (x *SubjectConditionSetCreate) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[16] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -928,7 +1212,7 @@ func (x *SubjectConditionSetCreate) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectConditionSetCreate.ProtoReflect.Descriptor instead. func (*SubjectConditionSetCreate) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{16} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{18} } func (x *SubjectConditionSetCreate) GetSubjectSets() []*policy.SubjectSet { @@ -951,12 +1235,14 @@ type CreateSubjectConditionSetRequest struct { unknownFields protoimpl.UnknownFields SubjectConditionSet *SubjectConditionSetCreate `protobuf:"bytes,1,opt,name=subject_condition_set,json=subjectConditionSet,proto3" json:"subject_condition_set,omitempty"` + NamespaceId string `protobuf:"bytes,2,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + NamespaceFqn string `protobuf:"bytes,3,opt,name=namespace_fqn,json=namespaceFqn,proto3" json:"namespace_fqn,omitempty"` } func (x *CreateSubjectConditionSetRequest) Reset() { *x = CreateSubjectConditionSetRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[17] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -969,7 +1255,7 @@ func (x *CreateSubjectConditionSetRequest) String() string { func (*CreateSubjectConditionSetRequest) ProtoMessage() {} func (x *CreateSubjectConditionSetRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[17] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -982,7 +1268,7 @@ func (x *CreateSubjectConditionSetRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSubjectConditionSetRequest.ProtoReflect.Descriptor instead. func (*CreateSubjectConditionSetRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{17} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{19} } func (x *CreateSubjectConditionSetRequest) GetSubjectConditionSet() *SubjectConditionSetCreate { @@ -992,6 +1278,20 @@ func (x *CreateSubjectConditionSetRequest) GetSubjectConditionSet() *SubjectCond return nil } +func (x *CreateSubjectConditionSetRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CreateSubjectConditionSetRequest) GetNamespaceFqn() string { + if x != nil { + return x.NamespaceFqn + } + return "" +} + type CreateSubjectConditionSetResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1003,7 +1303,7 @@ type CreateSubjectConditionSetResponse struct { func (x *CreateSubjectConditionSetResponse) Reset() { *x = CreateSubjectConditionSetResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[18] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1016,7 +1316,7 @@ func (x *CreateSubjectConditionSetResponse) String() string { func (*CreateSubjectConditionSetResponse) ProtoMessage() {} func (x *CreateSubjectConditionSetResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[18] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1029,7 +1329,7 @@ func (x *CreateSubjectConditionSetResponse) ProtoReflect() protoreflect.Message // Deprecated: Use CreateSubjectConditionSetResponse.ProtoReflect.Descriptor instead. func (*CreateSubjectConditionSetResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{18} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{20} } func (x *CreateSubjectConditionSetResponse) GetSubjectConditionSet() *policy.SubjectConditionSet { @@ -1057,7 +1357,7 @@ type UpdateSubjectConditionSetRequest struct { func (x *UpdateSubjectConditionSetRequest) Reset() { *x = UpdateSubjectConditionSetRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[19] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1070,7 +1370,7 @@ func (x *UpdateSubjectConditionSetRequest) String() string { func (*UpdateSubjectConditionSetRequest) ProtoMessage() {} func (x *UpdateSubjectConditionSetRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[19] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1083,7 +1383,7 @@ func (x *UpdateSubjectConditionSetRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSubjectConditionSetRequest.ProtoReflect.Descriptor instead. func (*UpdateSubjectConditionSetRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{19} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{21} } func (x *UpdateSubjectConditionSetRequest) GetId() string { @@ -1126,7 +1426,7 @@ type UpdateSubjectConditionSetResponse struct { func (x *UpdateSubjectConditionSetResponse) Reset() { *x = UpdateSubjectConditionSetResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[20] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1139,7 +1439,7 @@ func (x *UpdateSubjectConditionSetResponse) String() string { func (*UpdateSubjectConditionSetResponse) ProtoMessage() {} func (x *UpdateSubjectConditionSetResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[20] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1152,7 +1452,7 @@ func (x *UpdateSubjectConditionSetResponse) ProtoReflect() protoreflect.Message // Deprecated: Use UpdateSubjectConditionSetResponse.ProtoReflect.Descriptor instead. func (*UpdateSubjectConditionSetResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{20} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{22} } func (x *UpdateSubjectConditionSetResponse) GetSubjectConditionSet() *policy.SubjectConditionSet { @@ -1174,7 +1474,7 @@ type DeleteSubjectConditionSetRequest struct { func (x *DeleteSubjectConditionSetRequest) Reset() { *x = DeleteSubjectConditionSetRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[21] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1187,7 +1487,7 @@ func (x *DeleteSubjectConditionSetRequest) String() string { func (*DeleteSubjectConditionSetRequest) ProtoMessage() {} func (x *DeleteSubjectConditionSetRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[21] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1200,7 +1500,7 @@ func (x *DeleteSubjectConditionSetRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSubjectConditionSetRequest.ProtoReflect.Descriptor instead. func (*DeleteSubjectConditionSetRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{21} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{23} } func (x *DeleteSubjectConditionSetRequest) GetId() string { @@ -1222,7 +1522,7 @@ type DeleteSubjectConditionSetResponse struct { func (x *DeleteSubjectConditionSetResponse) Reset() { *x = DeleteSubjectConditionSetResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[22] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1235,7 +1535,7 @@ func (x *DeleteSubjectConditionSetResponse) String() string { func (*DeleteSubjectConditionSetResponse) ProtoMessage() {} func (x *DeleteSubjectConditionSetResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[22] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1248,7 +1548,7 @@ func (x *DeleteSubjectConditionSetResponse) ProtoReflect() protoreflect.Message // Deprecated: Use DeleteSubjectConditionSetResponse.ProtoReflect.Descriptor instead. func (*DeleteSubjectConditionSetResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{22} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{24} } func (x *DeleteSubjectConditionSetResponse) GetSubjectConditionSet() *policy.SubjectConditionSet { @@ -1268,7 +1568,7 @@ type DeleteAllUnmappedSubjectConditionSetsRequest struct { func (x *DeleteAllUnmappedSubjectConditionSetsRequest) Reset() { *x = DeleteAllUnmappedSubjectConditionSetsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[23] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1281,7 +1581,7 @@ func (x *DeleteAllUnmappedSubjectConditionSetsRequest) String() string { func (*DeleteAllUnmappedSubjectConditionSetsRequest) ProtoMessage() {} func (x *DeleteAllUnmappedSubjectConditionSetsRequest) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[23] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1294,7 +1594,7 @@ func (x *DeleteAllUnmappedSubjectConditionSetsRequest) ProtoReflect() protorefle // Deprecated: Use DeleteAllUnmappedSubjectConditionSetsRequest.ProtoReflect.Descriptor instead. func (*DeleteAllUnmappedSubjectConditionSetsRequest) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{23} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{25} } type DeleteAllUnmappedSubjectConditionSetsResponse struct { @@ -1309,7 +1609,7 @@ type DeleteAllUnmappedSubjectConditionSetsResponse struct { func (x *DeleteAllUnmappedSubjectConditionSetsResponse) Reset() { *x = DeleteAllUnmappedSubjectConditionSetsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[24] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1322,7 +1622,7 @@ func (x *DeleteAllUnmappedSubjectConditionSetsResponse) String() string { func (*DeleteAllUnmappedSubjectConditionSetsResponse) ProtoMessage() {} func (x *DeleteAllUnmappedSubjectConditionSetsResponse) ProtoReflect() protoreflect.Message { - mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[24] + mi := &file_policy_subjectmapping_subject_mapping_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1335,7 +1635,7 @@ func (x *DeleteAllUnmappedSubjectConditionSetsResponse) ProtoReflect() protorefl // Deprecated: Use DeleteAllUnmappedSubjectConditionSetsResponse.ProtoReflect.Descriptor instead. func (*DeleteAllUnmappedSubjectConditionSetsResponse) Descriptor() ([]byte, []int) { - return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{24} + return file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP(), []int{26} } func (x *DeleteAllUnmappedSubjectConditionSetsResponse) GetSubjectConditionSets() []*policy.SubjectConditionSet { @@ -1380,354 +1680,440 @@ var file_policy_subjectmapping_subject_mapping_proto_rawDesc = []byte{ 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0e, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, 0x51, 0x0a, 0x1a, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, - 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0x96, 0x01, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x41, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, - 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb3, 0x05, 0x0a, 0x1b, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x36, 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x10, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, - 0x12, 0xb8, 0x01, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x42, 0x8d, 0x01, 0xba, 0x48, 0x89, 0x01, 0xba, 0x01, 0x80, 0x01, 0x0a, 0x1b, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6f, 0x72, 0x5f, 0x69, 0x64, - 0x5f, 0x6e, 0x6f, 0x74, 0x5f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2f, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x72, 0x20, 0x49, 0x44, 0x20, 0x6d, 0x75, - 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, - 0x69, 0x66, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x1a, 0x30, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x61, 0x6c, 0x6c, 0x28, 0x69, 0x74, 0x65, 0x6d, 0x2c, 0x20, 0x69, 0x74, 0x65, 0x6d, - 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x69, - 0x74, 0x65, 0x6d, 0x2e, 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x92, 0x01, 0x02, - 0x08, 0x01, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0xfe, 0x01, 0x0a, 0x21, - 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0xb3, 0x01, 0xba, 0x48, 0xaf, 0x01, 0xba, 0x01, - 0xab, 0x01, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x75, 0x69, - 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x23, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, - 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x55, 0x49, 0x44, 0x1a, 0x6e, 0x73, - 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, - 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, - 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x38, 0x7d, 0x2d, 0x5b, - 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, - 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, - 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, - 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x31, 0x32, 0x7d, 0x27, 0x29, 0x52, 0x1d, 0x65, - 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x6b, 0x0a, 0x19, - 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x52, 0x16, 0x6e, 0x65, 0x77, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, - 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x5f, - 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, - 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, - 0x0e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, - 0xfc, 0x04, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xed, 0x01, 0x0a, 0x18, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xb3, 0x01, 0xba, - 0x48, 0xaf, 0x01, 0xba, 0x01, 0xab, 0x01, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x23, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6d, 0x75, - 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x55, - 0x49, 0x44, 0x1a, 0x6e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, - 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, - 0x68, 0x65, 0x73, 0x28, 0x27, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, - 0x7b, 0x38, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, 0xa4, 0x01, 0x0a, + 0x13, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, + 0x53, 0x6f, 0x72, 0x74, 0x12, 0x4e, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x6f, 0x72, 0x74, + 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x54, + 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x05, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x9f, 0x02, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, + 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, + 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, + 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, + 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x42, + 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x3a, + 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x66, 0x71, 0x6e, 0x10, 0x00, 0x22, 0x96, 0x01, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb7, + 0x06, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x36, + 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x10, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, 0x12, 0xb8, 0x01, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x8d, 0x01, 0xba, 0x48, 0x89, 0x01, 0xba, + 0x01, 0x80, 0x01, 0x0a, 0x1b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x5f, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x6e, 0x6f, 0x74, 0x5f, 0x65, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x2f, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x72, + 0x20, 0x49, 0x44, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, + 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x64, 0x1a, 0x30, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x6c, 0x6c, 0x28, 0x69, 0x74, 0x65, 0x6d, + 0x2c, 0x20, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x21, 0x3d, 0x20, 0x27, + 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, + 0x27, 0x27, 0x29, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0xfe, 0x01, 0x0a, 0x21, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0xb3, 0x01, + 0xba, 0x48, 0xaf, 0x01, 0xba, 0x01, 0xab, 0x01, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x61, 0x6c, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x23, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, + 0x55, 0x49, 0x44, 0x1a, 0x6e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, + 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, + 0x5d, 0x7b, 0x38, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, + 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, - 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, - 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x31, 0x32, 0x7d, - 0x27, 0x29, 0x52, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0xc7, 0x01, 0x0a, 0x07, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x9c, 0x01, 0xba, 0x48, - 0x98, 0x01, 0xba, 0x01, 0x94, 0x01, 0x0a, 0x1b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x5f, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x6e, 0x6f, 0x74, 0x5f, 0x65, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x2f, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, - 0x20, 0x6f, 0x72, 0x20, 0x49, 0x44, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, - 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x64, 0x1a, 0x44, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x73, 0x69, 0x7a, 0x65, 0x28, - 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, - 0x6c, 0x6c, 0x28, 0x69, 0x74, 0x65, 0x6d, 0x2c, 0x20, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x6e, 0x61, - 0x6d, 0x65, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x69, 0x74, 0x65, 0x6d, - 0x2e, 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, - 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x5f, - 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, - 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, - 0x0e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, - 0x37, 0x0a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, - 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x5f, 0x0a, 0x1c, 0x44, 0x65, 0x6c, 0x65, + 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x31, 0x32, + 0x7d, 0x27, 0x29, 0x52, 0x1d, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, + 0x49, 0x64, 0x12, 0x6b, 0x0a, 0x19, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x74, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x16, 0x6e, 0x65, 0x77, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, + 0x2b, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, + 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, + 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x3a, 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x00, 0x22, 0x5f, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, 0x39, 0x0a, 0x1d, 0x47, 0x65, 0x74, - 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x52, 0x02, 0x69, 0x64, 0x22, 0xc9, 0x01, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x74, 0x52, 0x13, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x56, 0x0a, 0x1b, 0x61, 0x73, 0x73, 0x6f, - 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x19, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, - 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, - 0x22, 0x56, 0x0a, 0x1f, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, - 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xab, 0x01, 0x0a, 0x20, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, - 0x16, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x14, 0x73, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, - 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x91, 0x01, 0x0a, 0x19, 0x53, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x12, 0x3f, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, - 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x53, 0x65, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, - 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x90, 0x01, 0x0a, 0x20, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x6c, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x13, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x22, 0x74, 0x0a, - 0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x13, - 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x74, 0x22, 0xfe, 0x01, 0x0a, 0x20, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, + 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, 0xfc, 0x04, 0x0a, 0x1b, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, + 0x02, 0x69, 0x64, 0x12, 0xed, 0x01, 0x0a, 0x18, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xb3, 0x01, 0xba, 0x48, 0xaf, 0x01, 0xba, 0x01, 0xab, + 0x01, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x75, 0x69, 0x64, + 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x23, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, + 0x6c, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, + 0x61, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x55, 0x55, 0x49, 0x44, 0x1a, 0x6e, 0x73, 0x69, + 0x7a, 0x65, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x20, 0x7c, 0x7c, + 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5b, + 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x38, 0x7d, 0x2d, 0x5b, 0x30, + 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, + 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, + 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x34, 0x7d, 0x2d, 0x5b, 0x30, 0x2d, 0x39, 0x61, + 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x31, 0x32, 0x7d, 0x27, 0x29, 0x52, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x74, 0x49, 0x64, 0x12, 0xc7, 0x01, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x9c, 0x01, 0xba, 0x48, 0x98, 0x01, 0xba, 0x01, 0x94, 0x01, + 0x0a, 0x1b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6f, 0x72, + 0x5f, 0x69, 0x64, 0x5f, 0x6e, 0x6f, 0x74, 0x5f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2f, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x72, 0x20, 0x49, 0x44, + 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, + 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x1a, 0x44, + 0x74, 0x68, 0x69, 0x73, 0x2e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x30, + 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x6c, 0x6c, 0x28, 0x69, 0x74, 0x65, + 0x6d, 0x2c, 0x20, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x21, 0x3d, 0x20, + 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x69, 0x64, 0x20, 0x21, 0x3d, + 0x20, 0x27, 0x27, 0x29, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x33, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, + 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x5f, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, 0x37, 0x0a, 0x1b, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x35, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x65, - 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x52, 0x0b, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, - 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, - 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, - 0x76, 0x69, 0x6f, 0x72, 0x22, 0x74, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x15, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, - 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x13, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x22, 0x3c, 0x0a, 0x20, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, - 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x74, 0x0a, 0x21, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, - 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x13, 0x73, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x22, 0x2e, - 0x0a, 0x2c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, 0x6e, 0x6d, 0x61, 0x70, - 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x82, - 0x01, 0x0a, 0x2d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, 0x6e, 0x6d, 0x61, - 0x70, 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x51, 0x0a, 0x16, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x69, 0x64, 0x22, 0x5f, 0x0a, 0x1c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x22, 0x39, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0xc9, + 0x01, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x4f, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x14, 0x73, + 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x13, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x65, 0x74, 0x73, 0x32, 0xb8, 0x0d, 0x0a, 0x15, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x81, 0x01, - 0x0a, 0x14, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x4d, - 0x61, 0x74, 0x63, 0x68, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x81, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, - 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, - 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x7b, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, - 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, - 0x02, 0x01, 0x12, 0x81, 0x01, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x32, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, - 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, 0x14, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, - 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, - 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x12, 0x36, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x65, 0x74, 0x12, 0x56, 0x0a, 0x1b, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, + 0x19, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xae, 0x01, 0x0a, 0x18, 0x53, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x65, 0x74, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x53, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x53, + 0x6f, 0x72, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, + 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x09, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, + 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa9, 0x02, 0x0a, 0x1f, + 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2b, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, + 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, + 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x12, 0x33, 0x0a, + 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x53, 0x6f, 0x72, + 0x74, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x73, 0x6f, 0x72, + 0x74, 0x3a, 0x24, 0xba, 0x48, 0x21, 0x22, 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x00, 0x22, 0xab, 0x01, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, - 0x01, 0x12, 0x8a, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x34, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, - 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, + 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x16, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x14, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x12, + 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x91, 0x01, 0x0a, 0x19, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x12, 0x3f, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x73, + 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x53, 0x65, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x94, 0x02, 0x0a, 0x20, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x6c, + 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x13, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x0c, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0b, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x52, 0x0c, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x71, 0x6e, 0x3a, 0x24, 0xba, 0x48, 0x21, 0x22, + 0x1f, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x0a, + 0x0d, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x10, 0x00, + 0x22, 0x74, 0x0a, 0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x90, - 0x01, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x37, 0x2e, 0x70, + 0x74, 0x52, 0x13, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x22, 0xfe, 0x01, 0x0a, 0x20, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, + 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x52, + 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, + 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, + 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x22, 0x74, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x15, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x13, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x22, 0x3c, 0x0a, + 0x20, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x74, 0x0a, 0x21, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4f, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x13, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x74, 0x22, 0x2e, 0x0a, 0x2c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, 0x6e, + 0x6d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x82, 0x01, 0x0a, 0x2d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, + 0x6e, 0x6d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x16, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x63, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, + 0x52, 0x14, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x2a, 0x9b, 0x01, 0x0a, 0x17, 0x53, 0x6f, 0x72, 0x74, 0x53, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x26, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x53, 0x55, 0x42, 0x4a, 0x45, + 0x43, 0x54, 0x5f, 0x4d, 0x41, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x29, + 0x0a, 0x25, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x4d, + 0x41, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, + 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x01, 0x12, 0x29, 0x0a, 0x25, 0x53, 0x4f, 0x52, + 0x54, 0x5f, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x4d, 0x41, 0x50, 0x50, 0x49, 0x4e, + 0x47, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x5f, + 0x41, 0x54, 0x10, 0x02, 0x2a, 0xb2, 0x01, 0x0a, 0x1c, 0x53, 0x6f, 0x72, 0x74, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, + 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x30, 0x0a, 0x2c, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x53, 0x55, + 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x53, 0x45, 0x54, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2f, 0x0a, 0x2b, 0x53, 0x4f, 0x52, 0x54, 0x5f, + 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x53, 0x45, 0x54, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, + 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x01, 0x12, 0x2f, 0x0a, 0x2b, 0x53, 0x4f, 0x52, 0x54, + 0x5f, 0x53, 0x55, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, + 0x4f, 0x4e, 0x5f, 0x53, 0x45, 0x54, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, + 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x02, 0x32, 0xb8, 0x0d, 0x0a, 0x15, 0x53, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x14, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, - 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, + 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x53, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, + 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x7b, 0x0a, 0x11, 0x47, + 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x81, 0x01, 0x0a, 0x14, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x90, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, - 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, + 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x81, 0x01, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x32, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, + 0x73, 0x12, 0x36, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x8a, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x53, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x65, 0x74, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, + 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x03, 0x90, 0x02, 0x01, 0x12, 0x90, 0x01, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xb4, 0x01, 0x0a, 0x25, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, 0x6e, 0x6d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, - 0x73, 0x12, 0x43, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x19, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x37, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x38, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x41, 0x6c, 0x6c, 0x55, 0x6e, 0x6d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x44, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, 0x6e, 0x6d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xe4, - 0x01, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x42, 0x13, 0x53, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0xa2, 0x02, 0x03, 0x50, 0x53, 0x58, 0xaa, 0x02, 0x15, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0xca, 0x02, 0x15, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0xe2, 0x02, 0x21, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x5c, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x16, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xb4, 0x01, + 0x0a, 0x25, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, 0x6e, 0x6d, 0x61, 0x70, + 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x12, 0x43, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, 0x6e, 0x6d, 0x61, 0x70, 0x70, 0x65, + 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x44, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, + 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x55, 0x6e, + 0x6d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x42, 0xe4, 0x01, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x42, 0x13, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, + 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, + 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0xa2, 0x02, 0x03, 0x50, 0x53, 0x58, 0xaa, 0x02, + 0x15, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0xca, 0x02, 0x15, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, + 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0xe2, 0x02, + 0x21, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x6d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0xea, 0x02, 0x16, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -1742,103 +2128,115 @@ func file_policy_subjectmapping_subject_mapping_proto_rawDescGZIP() []byte { return file_policy_subjectmapping_subject_mapping_proto_rawDescData } -var file_policy_subjectmapping_subject_mapping_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_policy_subjectmapping_subject_mapping_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_policy_subjectmapping_subject_mapping_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_policy_subjectmapping_subject_mapping_proto_goTypes = []interface{}{ - (*MatchSubjectMappingsRequest)(nil), // 0: policy.subjectmapping.MatchSubjectMappingsRequest - (*MatchSubjectMappingsResponse)(nil), // 1: policy.subjectmapping.MatchSubjectMappingsResponse - (*GetSubjectMappingRequest)(nil), // 2: policy.subjectmapping.GetSubjectMappingRequest - (*GetSubjectMappingResponse)(nil), // 3: policy.subjectmapping.GetSubjectMappingResponse - (*ListSubjectMappingsRequest)(nil), // 4: policy.subjectmapping.ListSubjectMappingsRequest - (*ListSubjectMappingsResponse)(nil), // 5: policy.subjectmapping.ListSubjectMappingsResponse - (*CreateSubjectMappingRequest)(nil), // 6: policy.subjectmapping.CreateSubjectMappingRequest - (*CreateSubjectMappingResponse)(nil), // 7: policy.subjectmapping.CreateSubjectMappingResponse - (*UpdateSubjectMappingRequest)(nil), // 8: policy.subjectmapping.UpdateSubjectMappingRequest - (*UpdateSubjectMappingResponse)(nil), // 9: policy.subjectmapping.UpdateSubjectMappingResponse - (*DeleteSubjectMappingRequest)(nil), // 10: policy.subjectmapping.DeleteSubjectMappingRequest - (*DeleteSubjectMappingResponse)(nil), // 11: policy.subjectmapping.DeleteSubjectMappingResponse - (*GetSubjectConditionSetRequest)(nil), // 12: policy.subjectmapping.GetSubjectConditionSetRequest - (*GetSubjectConditionSetResponse)(nil), // 13: policy.subjectmapping.GetSubjectConditionSetResponse - (*ListSubjectConditionSetsRequest)(nil), // 14: policy.subjectmapping.ListSubjectConditionSetsRequest - (*ListSubjectConditionSetsResponse)(nil), // 15: policy.subjectmapping.ListSubjectConditionSetsResponse - (*SubjectConditionSetCreate)(nil), // 16: policy.subjectmapping.SubjectConditionSetCreate - (*CreateSubjectConditionSetRequest)(nil), // 17: policy.subjectmapping.CreateSubjectConditionSetRequest - (*CreateSubjectConditionSetResponse)(nil), // 18: policy.subjectmapping.CreateSubjectConditionSetResponse - (*UpdateSubjectConditionSetRequest)(nil), // 19: policy.subjectmapping.UpdateSubjectConditionSetRequest - (*UpdateSubjectConditionSetResponse)(nil), // 20: policy.subjectmapping.UpdateSubjectConditionSetResponse - (*DeleteSubjectConditionSetRequest)(nil), // 21: policy.subjectmapping.DeleteSubjectConditionSetRequest - (*DeleteSubjectConditionSetResponse)(nil), // 22: policy.subjectmapping.DeleteSubjectConditionSetResponse - (*DeleteAllUnmappedSubjectConditionSetsRequest)(nil), // 23: policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsRequest - (*DeleteAllUnmappedSubjectConditionSetsResponse)(nil), // 24: policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse - (*policy.SubjectProperty)(nil), // 25: policy.SubjectProperty - (*policy.SubjectMapping)(nil), // 26: policy.SubjectMapping - (*policy.PageRequest)(nil), // 27: policy.PageRequest - (*policy.PageResponse)(nil), // 28: policy.PageResponse - (*policy.Action)(nil), // 29: policy.Action - (*common.MetadataMutable)(nil), // 30: common.MetadataMutable - (common.MetadataUpdateEnum)(0), // 31: common.MetadataUpdateEnum - (*policy.SubjectConditionSet)(nil), // 32: policy.SubjectConditionSet - (*policy.SubjectSet)(nil), // 33: policy.SubjectSet + (SortSubjectMappingsType)(0), // 0: policy.subjectmapping.SortSubjectMappingsType + (SortSubjectConditionSetsType)(0), // 1: policy.subjectmapping.SortSubjectConditionSetsType + (*MatchSubjectMappingsRequest)(nil), // 2: policy.subjectmapping.MatchSubjectMappingsRequest + (*MatchSubjectMappingsResponse)(nil), // 3: policy.subjectmapping.MatchSubjectMappingsResponse + (*GetSubjectMappingRequest)(nil), // 4: policy.subjectmapping.GetSubjectMappingRequest + (*GetSubjectMappingResponse)(nil), // 5: policy.subjectmapping.GetSubjectMappingResponse + (*SubjectMappingsSort)(nil), // 6: policy.subjectmapping.SubjectMappingsSort + (*ListSubjectMappingsRequest)(nil), // 7: policy.subjectmapping.ListSubjectMappingsRequest + (*ListSubjectMappingsResponse)(nil), // 8: policy.subjectmapping.ListSubjectMappingsResponse + (*CreateSubjectMappingRequest)(nil), // 9: policy.subjectmapping.CreateSubjectMappingRequest + (*CreateSubjectMappingResponse)(nil), // 10: policy.subjectmapping.CreateSubjectMappingResponse + (*UpdateSubjectMappingRequest)(nil), // 11: policy.subjectmapping.UpdateSubjectMappingRequest + (*UpdateSubjectMappingResponse)(nil), // 12: policy.subjectmapping.UpdateSubjectMappingResponse + (*DeleteSubjectMappingRequest)(nil), // 13: policy.subjectmapping.DeleteSubjectMappingRequest + (*DeleteSubjectMappingResponse)(nil), // 14: policy.subjectmapping.DeleteSubjectMappingResponse + (*GetSubjectConditionSetRequest)(nil), // 15: policy.subjectmapping.GetSubjectConditionSetRequest + (*GetSubjectConditionSetResponse)(nil), // 16: policy.subjectmapping.GetSubjectConditionSetResponse + (*SubjectConditionSetsSort)(nil), // 17: policy.subjectmapping.SubjectConditionSetsSort + (*ListSubjectConditionSetsRequest)(nil), // 18: policy.subjectmapping.ListSubjectConditionSetsRequest + (*ListSubjectConditionSetsResponse)(nil), // 19: policy.subjectmapping.ListSubjectConditionSetsResponse + (*SubjectConditionSetCreate)(nil), // 20: policy.subjectmapping.SubjectConditionSetCreate + (*CreateSubjectConditionSetRequest)(nil), // 21: policy.subjectmapping.CreateSubjectConditionSetRequest + (*CreateSubjectConditionSetResponse)(nil), // 22: policy.subjectmapping.CreateSubjectConditionSetResponse + (*UpdateSubjectConditionSetRequest)(nil), // 23: policy.subjectmapping.UpdateSubjectConditionSetRequest + (*UpdateSubjectConditionSetResponse)(nil), // 24: policy.subjectmapping.UpdateSubjectConditionSetResponse + (*DeleteSubjectConditionSetRequest)(nil), // 25: policy.subjectmapping.DeleteSubjectConditionSetRequest + (*DeleteSubjectConditionSetResponse)(nil), // 26: policy.subjectmapping.DeleteSubjectConditionSetResponse + (*DeleteAllUnmappedSubjectConditionSetsRequest)(nil), // 27: policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsRequest + (*DeleteAllUnmappedSubjectConditionSetsResponse)(nil), // 28: policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse + (*policy.SubjectProperty)(nil), // 29: policy.SubjectProperty + (*policy.SubjectMapping)(nil), // 30: policy.SubjectMapping + (policy.SortDirection)(0), // 31: policy.SortDirection + (*policy.PageRequest)(nil), // 32: policy.PageRequest + (*policy.PageResponse)(nil), // 33: policy.PageResponse + (*policy.Action)(nil), // 34: policy.Action + (*common.MetadataMutable)(nil), // 35: common.MetadataMutable + (common.MetadataUpdateEnum)(0), // 36: common.MetadataUpdateEnum + (*policy.SubjectConditionSet)(nil), // 37: policy.SubjectConditionSet + (*policy.SubjectSet)(nil), // 38: policy.SubjectSet } var file_policy_subjectmapping_subject_mapping_proto_depIdxs = []int32{ - 25, // 0: policy.subjectmapping.MatchSubjectMappingsRequest.subject_properties:type_name -> policy.SubjectProperty - 26, // 1: policy.subjectmapping.MatchSubjectMappingsResponse.subject_mappings:type_name -> policy.SubjectMapping - 26, // 2: policy.subjectmapping.GetSubjectMappingResponse.subject_mapping:type_name -> policy.SubjectMapping - 27, // 3: policy.subjectmapping.ListSubjectMappingsRequest.pagination:type_name -> policy.PageRequest - 26, // 4: policy.subjectmapping.ListSubjectMappingsResponse.subject_mappings:type_name -> policy.SubjectMapping - 28, // 5: policy.subjectmapping.ListSubjectMappingsResponse.pagination:type_name -> policy.PageResponse - 29, // 6: policy.subjectmapping.CreateSubjectMappingRequest.actions:type_name -> policy.Action - 16, // 7: policy.subjectmapping.CreateSubjectMappingRequest.new_subject_condition_set:type_name -> policy.subjectmapping.SubjectConditionSetCreate - 30, // 8: policy.subjectmapping.CreateSubjectMappingRequest.metadata:type_name -> common.MetadataMutable - 26, // 9: policy.subjectmapping.CreateSubjectMappingResponse.subject_mapping:type_name -> policy.SubjectMapping - 29, // 10: policy.subjectmapping.UpdateSubjectMappingRequest.actions:type_name -> policy.Action - 30, // 11: policy.subjectmapping.UpdateSubjectMappingRequest.metadata:type_name -> common.MetadataMutable - 31, // 12: policy.subjectmapping.UpdateSubjectMappingRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 26, // 13: policy.subjectmapping.UpdateSubjectMappingResponse.subject_mapping:type_name -> policy.SubjectMapping - 26, // 14: policy.subjectmapping.DeleteSubjectMappingResponse.subject_mapping:type_name -> policy.SubjectMapping - 32, // 15: policy.subjectmapping.GetSubjectConditionSetResponse.subject_condition_set:type_name -> policy.SubjectConditionSet - 26, // 16: policy.subjectmapping.GetSubjectConditionSetResponse.associated_subject_mappings:type_name -> policy.SubjectMapping - 27, // 17: policy.subjectmapping.ListSubjectConditionSetsRequest.pagination:type_name -> policy.PageRequest - 32, // 18: policy.subjectmapping.ListSubjectConditionSetsResponse.subject_condition_sets:type_name -> policy.SubjectConditionSet - 28, // 19: policy.subjectmapping.ListSubjectConditionSetsResponse.pagination:type_name -> policy.PageResponse - 33, // 20: policy.subjectmapping.SubjectConditionSetCreate.subject_sets:type_name -> policy.SubjectSet - 30, // 21: policy.subjectmapping.SubjectConditionSetCreate.metadata:type_name -> common.MetadataMutable - 16, // 22: policy.subjectmapping.CreateSubjectConditionSetRequest.subject_condition_set:type_name -> policy.subjectmapping.SubjectConditionSetCreate - 32, // 23: policy.subjectmapping.CreateSubjectConditionSetResponse.subject_condition_set:type_name -> policy.SubjectConditionSet - 33, // 24: policy.subjectmapping.UpdateSubjectConditionSetRequest.subject_sets:type_name -> policy.SubjectSet - 30, // 25: policy.subjectmapping.UpdateSubjectConditionSetRequest.metadata:type_name -> common.MetadataMutable - 31, // 26: policy.subjectmapping.UpdateSubjectConditionSetRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum - 32, // 27: policy.subjectmapping.UpdateSubjectConditionSetResponse.subject_condition_set:type_name -> policy.SubjectConditionSet - 32, // 28: policy.subjectmapping.DeleteSubjectConditionSetResponse.subject_condition_set:type_name -> policy.SubjectConditionSet - 32, // 29: policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse.subject_condition_sets:type_name -> policy.SubjectConditionSet - 0, // 30: policy.subjectmapping.SubjectMappingService.MatchSubjectMappings:input_type -> policy.subjectmapping.MatchSubjectMappingsRequest - 4, // 31: policy.subjectmapping.SubjectMappingService.ListSubjectMappings:input_type -> policy.subjectmapping.ListSubjectMappingsRequest - 2, // 32: policy.subjectmapping.SubjectMappingService.GetSubjectMapping:input_type -> policy.subjectmapping.GetSubjectMappingRequest - 6, // 33: policy.subjectmapping.SubjectMappingService.CreateSubjectMapping:input_type -> policy.subjectmapping.CreateSubjectMappingRequest - 8, // 34: policy.subjectmapping.SubjectMappingService.UpdateSubjectMapping:input_type -> policy.subjectmapping.UpdateSubjectMappingRequest - 10, // 35: policy.subjectmapping.SubjectMappingService.DeleteSubjectMapping:input_type -> policy.subjectmapping.DeleteSubjectMappingRequest - 14, // 36: policy.subjectmapping.SubjectMappingService.ListSubjectConditionSets:input_type -> policy.subjectmapping.ListSubjectConditionSetsRequest - 12, // 37: policy.subjectmapping.SubjectMappingService.GetSubjectConditionSet:input_type -> policy.subjectmapping.GetSubjectConditionSetRequest - 17, // 38: policy.subjectmapping.SubjectMappingService.CreateSubjectConditionSet:input_type -> policy.subjectmapping.CreateSubjectConditionSetRequest - 19, // 39: policy.subjectmapping.SubjectMappingService.UpdateSubjectConditionSet:input_type -> policy.subjectmapping.UpdateSubjectConditionSetRequest - 21, // 40: policy.subjectmapping.SubjectMappingService.DeleteSubjectConditionSet:input_type -> policy.subjectmapping.DeleteSubjectConditionSetRequest - 23, // 41: policy.subjectmapping.SubjectMappingService.DeleteAllUnmappedSubjectConditionSets:input_type -> policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsRequest - 1, // 42: policy.subjectmapping.SubjectMappingService.MatchSubjectMappings:output_type -> policy.subjectmapping.MatchSubjectMappingsResponse - 5, // 43: policy.subjectmapping.SubjectMappingService.ListSubjectMappings:output_type -> policy.subjectmapping.ListSubjectMappingsResponse - 3, // 44: policy.subjectmapping.SubjectMappingService.GetSubjectMapping:output_type -> policy.subjectmapping.GetSubjectMappingResponse - 7, // 45: policy.subjectmapping.SubjectMappingService.CreateSubjectMapping:output_type -> policy.subjectmapping.CreateSubjectMappingResponse - 9, // 46: policy.subjectmapping.SubjectMappingService.UpdateSubjectMapping:output_type -> policy.subjectmapping.UpdateSubjectMappingResponse - 11, // 47: policy.subjectmapping.SubjectMappingService.DeleteSubjectMapping:output_type -> policy.subjectmapping.DeleteSubjectMappingResponse - 15, // 48: policy.subjectmapping.SubjectMappingService.ListSubjectConditionSets:output_type -> policy.subjectmapping.ListSubjectConditionSetsResponse - 13, // 49: policy.subjectmapping.SubjectMappingService.GetSubjectConditionSet:output_type -> policy.subjectmapping.GetSubjectConditionSetResponse - 18, // 50: policy.subjectmapping.SubjectMappingService.CreateSubjectConditionSet:output_type -> policy.subjectmapping.CreateSubjectConditionSetResponse - 20, // 51: policy.subjectmapping.SubjectMappingService.UpdateSubjectConditionSet:output_type -> policy.subjectmapping.UpdateSubjectConditionSetResponse - 22, // 52: policy.subjectmapping.SubjectMappingService.DeleteSubjectConditionSet:output_type -> policy.subjectmapping.DeleteSubjectConditionSetResponse - 24, // 53: policy.subjectmapping.SubjectMappingService.DeleteAllUnmappedSubjectConditionSets:output_type -> policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse - 42, // [42:54] is the sub-list for method output_type - 30, // [30:42] is the sub-list for method input_type - 30, // [30:30] is the sub-list for extension type_name - 30, // [30:30] is the sub-list for extension extendee - 0, // [0:30] is the sub-list for field type_name + 29, // 0: policy.subjectmapping.MatchSubjectMappingsRequest.subject_properties:type_name -> policy.SubjectProperty + 30, // 1: policy.subjectmapping.MatchSubjectMappingsResponse.subject_mappings:type_name -> policy.SubjectMapping + 30, // 2: policy.subjectmapping.GetSubjectMappingResponse.subject_mapping:type_name -> policy.SubjectMapping + 0, // 3: policy.subjectmapping.SubjectMappingsSort.field:type_name -> policy.subjectmapping.SortSubjectMappingsType + 31, // 4: policy.subjectmapping.SubjectMappingsSort.direction:type_name -> policy.SortDirection + 32, // 5: policy.subjectmapping.ListSubjectMappingsRequest.pagination:type_name -> policy.PageRequest + 6, // 6: policy.subjectmapping.ListSubjectMappingsRequest.sort:type_name -> policy.subjectmapping.SubjectMappingsSort + 30, // 7: policy.subjectmapping.ListSubjectMappingsResponse.subject_mappings:type_name -> policy.SubjectMapping + 33, // 8: policy.subjectmapping.ListSubjectMappingsResponse.pagination:type_name -> policy.PageResponse + 34, // 9: policy.subjectmapping.CreateSubjectMappingRequest.actions:type_name -> policy.Action + 20, // 10: policy.subjectmapping.CreateSubjectMappingRequest.new_subject_condition_set:type_name -> policy.subjectmapping.SubjectConditionSetCreate + 35, // 11: policy.subjectmapping.CreateSubjectMappingRequest.metadata:type_name -> common.MetadataMutable + 30, // 12: policy.subjectmapping.CreateSubjectMappingResponse.subject_mapping:type_name -> policy.SubjectMapping + 34, // 13: policy.subjectmapping.UpdateSubjectMappingRequest.actions:type_name -> policy.Action + 35, // 14: policy.subjectmapping.UpdateSubjectMappingRequest.metadata:type_name -> common.MetadataMutable + 36, // 15: policy.subjectmapping.UpdateSubjectMappingRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 30, // 16: policy.subjectmapping.UpdateSubjectMappingResponse.subject_mapping:type_name -> policy.SubjectMapping + 30, // 17: policy.subjectmapping.DeleteSubjectMappingResponse.subject_mapping:type_name -> policy.SubjectMapping + 37, // 18: policy.subjectmapping.GetSubjectConditionSetResponse.subject_condition_set:type_name -> policy.SubjectConditionSet + 30, // 19: policy.subjectmapping.GetSubjectConditionSetResponse.associated_subject_mappings:type_name -> policy.SubjectMapping + 1, // 20: policy.subjectmapping.SubjectConditionSetsSort.field:type_name -> policy.subjectmapping.SortSubjectConditionSetsType + 31, // 21: policy.subjectmapping.SubjectConditionSetsSort.direction:type_name -> policy.SortDirection + 32, // 22: policy.subjectmapping.ListSubjectConditionSetsRequest.pagination:type_name -> policy.PageRequest + 17, // 23: policy.subjectmapping.ListSubjectConditionSetsRequest.sort:type_name -> policy.subjectmapping.SubjectConditionSetsSort + 37, // 24: policy.subjectmapping.ListSubjectConditionSetsResponse.subject_condition_sets:type_name -> policy.SubjectConditionSet + 33, // 25: policy.subjectmapping.ListSubjectConditionSetsResponse.pagination:type_name -> policy.PageResponse + 38, // 26: policy.subjectmapping.SubjectConditionSetCreate.subject_sets:type_name -> policy.SubjectSet + 35, // 27: policy.subjectmapping.SubjectConditionSetCreate.metadata:type_name -> common.MetadataMutable + 20, // 28: policy.subjectmapping.CreateSubjectConditionSetRequest.subject_condition_set:type_name -> policy.subjectmapping.SubjectConditionSetCreate + 37, // 29: policy.subjectmapping.CreateSubjectConditionSetResponse.subject_condition_set:type_name -> policy.SubjectConditionSet + 38, // 30: policy.subjectmapping.UpdateSubjectConditionSetRequest.subject_sets:type_name -> policy.SubjectSet + 35, // 31: policy.subjectmapping.UpdateSubjectConditionSetRequest.metadata:type_name -> common.MetadataMutable + 36, // 32: policy.subjectmapping.UpdateSubjectConditionSetRequest.metadata_update_behavior:type_name -> common.MetadataUpdateEnum + 37, // 33: policy.subjectmapping.UpdateSubjectConditionSetResponse.subject_condition_set:type_name -> policy.SubjectConditionSet + 37, // 34: policy.subjectmapping.DeleteSubjectConditionSetResponse.subject_condition_set:type_name -> policy.SubjectConditionSet + 37, // 35: policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse.subject_condition_sets:type_name -> policy.SubjectConditionSet + 2, // 36: policy.subjectmapping.SubjectMappingService.MatchSubjectMappings:input_type -> policy.subjectmapping.MatchSubjectMappingsRequest + 7, // 37: policy.subjectmapping.SubjectMappingService.ListSubjectMappings:input_type -> policy.subjectmapping.ListSubjectMappingsRequest + 4, // 38: policy.subjectmapping.SubjectMappingService.GetSubjectMapping:input_type -> policy.subjectmapping.GetSubjectMappingRequest + 9, // 39: policy.subjectmapping.SubjectMappingService.CreateSubjectMapping:input_type -> policy.subjectmapping.CreateSubjectMappingRequest + 11, // 40: policy.subjectmapping.SubjectMappingService.UpdateSubjectMapping:input_type -> policy.subjectmapping.UpdateSubjectMappingRequest + 13, // 41: policy.subjectmapping.SubjectMappingService.DeleteSubjectMapping:input_type -> policy.subjectmapping.DeleteSubjectMappingRequest + 18, // 42: policy.subjectmapping.SubjectMappingService.ListSubjectConditionSets:input_type -> policy.subjectmapping.ListSubjectConditionSetsRequest + 15, // 43: policy.subjectmapping.SubjectMappingService.GetSubjectConditionSet:input_type -> policy.subjectmapping.GetSubjectConditionSetRequest + 21, // 44: policy.subjectmapping.SubjectMappingService.CreateSubjectConditionSet:input_type -> policy.subjectmapping.CreateSubjectConditionSetRequest + 23, // 45: policy.subjectmapping.SubjectMappingService.UpdateSubjectConditionSet:input_type -> policy.subjectmapping.UpdateSubjectConditionSetRequest + 25, // 46: policy.subjectmapping.SubjectMappingService.DeleteSubjectConditionSet:input_type -> policy.subjectmapping.DeleteSubjectConditionSetRequest + 27, // 47: policy.subjectmapping.SubjectMappingService.DeleteAllUnmappedSubjectConditionSets:input_type -> policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsRequest + 3, // 48: policy.subjectmapping.SubjectMappingService.MatchSubjectMappings:output_type -> policy.subjectmapping.MatchSubjectMappingsResponse + 8, // 49: policy.subjectmapping.SubjectMappingService.ListSubjectMappings:output_type -> policy.subjectmapping.ListSubjectMappingsResponse + 5, // 50: policy.subjectmapping.SubjectMappingService.GetSubjectMapping:output_type -> policy.subjectmapping.GetSubjectMappingResponse + 10, // 51: policy.subjectmapping.SubjectMappingService.CreateSubjectMapping:output_type -> policy.subjectmapping.CreateSubjectMappingResponse + 12, // 52: policy.subjectmapping.SubjectMappingService.UpdateSubjectMapping:output_type -> policy.subjectmapping.UpdateSubjectMappingResponse + 14, // 53: policy.subjectmapping.SubjectMappingService.DeleteSubjectMapping:output_type -> policy.subjectmapping.DeleteSubjectMappingResponse + 19, // 54: policy.subjectmapping.SubjectMappingService.ListSubjectConditionSets:output_type -> policy.subjectmapping.ListSubjectConditionSetsResponse + 16, // 55: policy.subjectmapping.SubjectMappingService.GetSubjectConditionSet:output_type -> policy.subjectmapping.GetSubjectConditionSetResponse + 22, // 56: policy.subjectmapping.SubjectMappingService.CreateSubjectConditionSet:output_type -> policy.subjectmapping.CreateSubjectConditionSetResponse + 24, // 57: policy.subjectmapping.SubjectMappingService.UpdateSubjectConditionSet:output_type -> policy.subjectmapping.UpdateSubjectConditionSetResponse + 26, // 58: policy.subjectmapping.SubjectMappingService.DeleteSubjectConditionSet:output_type -> policy.subjectmapping.DeleteSubjectConditionSetResponse + 28, // 59: policy.subjectmapping.SubjectMappingService.DeleteAllUnmappedSubjectConditionSets:output_type -> policy.subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse + 48, // [48:60] is the sub-list for method output_type + 36, // [36:48] is the sub-list for method input_type + 36, // [36:36] is the sub-list for extension type_name + 36, // [36:36] is the sub-list for extension extendee + 0, // [0:36] is the sub-list for field type_name } func init() { file_policy_subjectmapping_subject_mapping_proto_init() } @@ -1896,7 +2294,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListSubjectMappingsRequest); i { + switch v := v.(*SubjectMappingsSort); i { case 0: return &v.state case 1: @@ -1908,7 +2306,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListSubjectMappingsResponse); i { + switch v := v.(*ListSubjectMappingsRequest); i { case 0: return &v.state case 1: @@ -1920,7 +2318,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateSubjectMappingRequest); i { + switch v := v.(*ListSubjectMappingsResponse); i { case 0: return &v.state case 1: @@ -1932,7 +2330,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateSubjectMappingResponse); i { + switch v := v.(*CreateSubjectMappingRequest); i { case 0: return &v.state case 1: @@ -1944,7 +2342,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateSubjectMappingRequest); i { + switch v := v.(*CreateSubjectMappingResponse); i { case 0: return &v.state case 1: @@ -1956,7 +2354,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateSubjectMappingResponse); i { + switch v := v.(*UpdateSubjectMappingRequest); i { case 0: return &v.state case 1: @@ -1968,7 +2366,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteSubjectMappingRequest); i { + switch v := v.(*UpdateSubjectMappingResponse); i { case 0: return &v.state case 1: @@ -1980,7 +2378,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteSubjectMappingResponse); i { + switch v := v.(*DeleteSubjectMappingRequest); i { case 0: return &v.state case 1: @@ -1992,7 +2390,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetSubjectConditionSetRequest); i { + switch v := v.(*DeleteSubjectMappingResponse); i { case 0: return &v.state case 1: @@ -2004,7 +2402,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetSubjectConditionSetResponse); i { + switch v := v.(*GetSubjectConditionSetRequest); i { case 0: return &v.state case 1: @@ -2016,7 +2414,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListSubjectConditionSetsRequest); i { + switch v := v.(*GetSubjectConditionSetResponse); i { case 0: return &v.state case 1: @@ -2028,7 +2426,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListSubjectConditionSetsResponse); i { + switch v := v.(*SubjectConditionSetsSort); i { case 0: return &v.state case 1: @@ -2040,7 +2438,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SubjectConditionSetCreate); i { + switch v := v.(*ListSubjectConditionSetsRequest); i { case 0: return &v.state case 1: @@ -2052,7 +2450,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateSubjectConditionSetRequest); i { + switch v := v.(*ListSubjectConditionSetsResponse); i { case 0: return &v.state case 1: @@ -2064,7 +2462,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateSubjectConditionSetResponse); i { + switch v := v.(*SubjectConditionSetCreate); i { case 0: return &v.state case 1: @@ -2076,7 +2474,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateSubjectConditionSetRequest); i { + switch v := v.(*CreateSubjectConditionSetRequest); i { case 0: return &v.state case 1: @@ -2088,7 +2486,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateSubjectConditionSetResponse); i { + switch v := v.(*CreateSubjectConditionSetResponse); i { case 0: return &v.state case 1: @@ -2100,7 +2498,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteSubjectConditionSetRequest); i { + switch v := v.(*UpdateSubjectConditionSetRequest); i { case 0: return &v.state case 1: @@ -2112,7 +2510,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteSubjectConditionSetResponse); i { + switch v := v.(*UpdateSubjectConditionSetResponse); i { case 0: return &v.state case 1: @@ -2124,7 +2522,7 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteAllUnmappedSubjectConditionSetsRequest); i { + switch v := v.(*DeleteSubjectConditionSetRequest); i { case 0: return &v.state case 1: @@ -2136,6 +2534,30 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { } } file_policy_subjectmapping_subject_mapping_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteSubjectConditionSetResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_subjectmapping_subject_mapping_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteAllUnmappedSubjectConditionSetsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_policy_subjectmapping_subject_mapping_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteAllUnmappedSubjectConditionSetsResponse); i { case 0: return &v.state @@ -2153,13 +2575,14 @@ func file_policy_subjectmapping_subject_mapping_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_policy_subjectmapping_subject_mapping_proto_rawDesc, - NumEnums: 0, - NumMessages: 25, + NumEnums: 2, + NumMessages: 27, NumExtensions: 0, NumServices: 1, }, GoTypes: file_policy_subjectmapping_subject_mapping_proto_goTypes, DependencyIndexes: file_policy_subjectmapping_subject_mapping_proto_depIdxs, + EnumInfos: file_policy_subjectmapping_subject_mapping_proto_enumTypes, MessageInfos: file_policy_subjectmapping_subject_mapping_proto_msgTypes, }.Build() File_policy_subjectmapping_subject_mapping_proto = out.File diff --git a/protocol/go/policy/subjectmapping/subjectmappingconnect/subject_mapping.connect.go b/protocol/go/policy/subjectmapping/subjectmappingconnect/subject_mapping.connect.go index a4ca383de9..0838829b23 100644 --- a/protocol/go/policy/subjectmapping/subjectmappingconnect/subject_mapping.connect.go +++ b/protocol/go/policy/subjectmapping/subjectmappingconnect/subject_mapping.connect.go @@ -71,23 +71,6 @@ const ( SubjectMappingServiceDeleteAllUnmappedSubjectConditionSetsProcedure = "/policy.subjectmapping.SubjectMappingService/DeleteAllUnmappedSubjectConditionSets" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - subjectMappingServiceServiceDescriptor = subjectmapping.File_policy_subjectmapping_subject_mapping_proto.Services().ByName("SubjectMappingService") - subjectMappingServiceMatchSubjectMappingsMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("MatchSubjectMappings") - subjectMappingServiceListSubjectMappingsMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("ListSubjectMappings") - subjectMappingServiceGetSubjectMappingMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("GetSubjectMapping") - subjectMappingServiceCreateSubjectMappingMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("CreateSubjectMapping") - subjectMappingServiceUpdateSubjectMappingMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("UpdateSubjectMapping") - subjectMappingServiceDeleteSubjectMappingMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("DeleteSubjectMapping") - subjectMappingServiceListSubjectConditionSetsMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("ListSubjectConditionSets") - subjectMappingServiceGetSubjectConditionSetMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("GetSubjectConditionSet") - subjectMappingServiceCreateSubjectConditionSetMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("CreateSubjectConditionSet") - subjectMappingServiceUpdateSubjectConditionSetMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("UpdateSubjectConditionSet") - subjectMappingServiceDeleteSubjectConditionSetMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("DeleteSubjectConditionSet") - subjectMappingServiceDeleteAllUnmappedSubjectConditionSetsMethodDescriptor = subjectMappingServiceServiceDescriptor.Methods().ByName("DeleteAllUnmappedSubjectConditionSets") -) - // SubjectMappingServiceClient is a client for the policy.subjectmapping.SubjectMappingService // service. type SubjectMappingServiceClient interface { @@ -116,81 +99,82 @@ type SubjectMappingServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewSubjectMappingServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SubjectMappingServiceClient { baseURL = strings.TrimRight(baseURL, "/") + subjectMappingServiceMethods := subjectmapping.File_policy_subjectmapping_subject_mapping_proto.Services().ByName("SubjectMappingService").Methods() return &subjectMappingServiceClient{ matchSubjectMappings: connect.NewClient[subjectmapping.MatchSubjectMappingsRequest, subjectmapping.MatchSubjectMappingsResponse]( httpClient, baseURL+SubjectMappingServiceMatchSubjectMappingsProcedure, - connect.WithSchema(subjectMappingServiceMatchSubjectMappingsMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("MatchSubjectMappings")), connect.WithClientOptions(opts...), ), listSubjectMappings: connect.NewClient[subjectmapping.ListSubjectMappingsRequest, subjectmapping.ListSubjectMappingsResponse]( httpClient, baseURL+SubjectMappingServiceListSubjectMappingsProcedure, - connect.WithSchema(subjectMappingServiceListSubjectMappingsMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("ListSubjectMappings")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getSubjectMapping: connect.NewClient[subjectmapping.GetSubjectMappingRequest, subjectmapping.GetSubjectMappingResponse]( httpClient, baseURL+SubjectMappingServiceGetSubjectMappingProcedure, - connect.WithSchema(subjectMappingServiceGetSubjectMappingMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("GetSubjectMapping")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createSubjectMapping: connect.NewClient[subjectmapping.CreateSubjectMappingRequest, subjectmapping.CreateSubjectMappingResponse]( httpClient, baseURL+SubjectMappingServiceCreateSubjectMappingProcedure, - connect.WithSchema(subjectMappingServiceCreateSubjectMappingMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("CreateSubjectMapping")), connect.WithClientOptions(opts...), ), updateSubjectMapping: connect.NewClient[subjectmapping.UpdateSubjectMappingRequest, subjectmapping.UpdateSubjectMappingResponse]( httpClient, baseURL+SubjectMappingServiceUpdateSubjectMappingProcedure, - connect.WithSchema(subjectMappingServiceUpdateSubjectMappingMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("UpdateSubjectMapping")), connect.WithClientOptions(opts...), ), deleteSubjectMapping: connect.NewClient[subjectmapping.DeleteSubjectMappingRequest, subjectmapping.DeleteSubjectMappingResponse]( httpClient, baseURL+SubjectMappingServiceDeleteSubjectMappingProcedure, - connect.WithSchema(subjectMappingServiceDeleteSubjectMappingMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("DeleteSubjectMapping")), connect.WithClientOptions(opts...), ), listSubjectConditionSets: connect.NewClient[subjectmapping.ListSubjectConditionSetsRequest, subjectmapping.ListSubjectConditionSetsResponse]( httpClient, baseURL+SubjectMappingServiceListSubjectConditionSetsProcedure, - connect.WithSchema(subjectMappingServiceListSubjectConditionSetsMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("ListSubjectConditionSets")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), getSubjectConditionSet: connect.NewClient[subjectmapping.GetSubjectConditionSetRequest, subjectmapping.GetSubjectConditionSetResponse]( httpClient, baseURL+SubjectMappingServiceGetSubjectConditionSetProcedure, - connect.WithSchema(subjectMappingServiceGetSubjectConditionSetMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("GetSubjectConditionSet")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), createSubjectConditionSet: connect.NewClient[subjectmapping.CreateSubjectConditionSetRequest, subjectmapping.CreateSubjectConditionSetResponse]( httpClient, baseURL+SubjectMappingServiceCreateSubjectConditionSetProcedure, - connect.WithSchema(subjectMappingServiceCreateSubjectConditionSetMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("CreateSubjectConditionSet")), connect.WithClientOptions(opts...), ), updateSubjectConditionSet: connect.NewClient[subjectmapping.UpdateSubjectConditionSetRequest, subjectmapping.UpdateSubjectConditionSetResponse]( httpClient, baseURL+SubjectMappingServiceUpdateSubjectConditionSetProcedure, - connect.WithSchema(subjectMappingServiceUpdateSubjectConditionSetMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("UpdateSubjectConditionSet")), connect.WithClientOptions(opts...), ), deleteSubjectConditionSet: connect.NewClient[subjectmapping.DeleteSubjectConditionSetRequest, subjectmapping.DeleteSubjectConditionSetResponse]( httpClient, baseURL+SubjectMappingServiceDeleteSubjectConditionSetProcedure, - connect.WithSchema(subjectMappingServiceDeleteSubjectConditionSetMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("DeleteSubjectConditionSet")), connect.WithClientOptions(opts...), ), deleteAllUnmappedSubjectConditionSets: connect.NewClient[subjectmapping.DeleteAllUnmappedSubjectConditionSetsRequest, subjectmapping.DeleteAllUnmappedSubjectConditionSetsResponse]( httpClient, baseURL+SubjectMappingServiceDeleteAllUnmappedSubjectConditionSetsProcedure, - connect.WithSchema(subjectMappingServiceDeleteAllUnmappedSubjectConditionSetsMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("DeleteAllUnmappedSubjectConditionSets")), connect.WithClientOptions(opts...), ), } @@ -301,80 +285,81 @@ type SubjectMappingServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewSubjectMappingServiceHandler(svc SubjectMappingServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + subjectMappingServiceMethods := subjectmapping.File_policy_subjectmapping_subject_mapping_proto.Services().ByName("SubjectMappingService").Methods() subjectMappingServiceMatchSubjectMappingsHandler := connect.NewUnaryHandler( SubjectMappingServiceMatchSubjectMappingsProcedure, svc.MatchSubjectMappings, - connect.WithSchema(subjectMappingServiceMatchSubjectMappingsMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("MatchSubjectMappings")), connect.WithHandlerOptions(opts...), ) subjectMappingServiceListSubjectMappingsHandler := connect.NewUnaryHandler( SubjectMappingServiceListSubjectMappingsProcedure, svc.ListSubjectMappings, - connect.WithSchema(subjectMappingServiceListSubjectMappingsMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("ListSubjectMappings")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) subjectMappingServiceGetSubjectMappingHandler := connect.NewUnaryHandler( SubjectMappingServiceGetSubjectMappingProcedure, svc.GetSubjectMapping, - connect.WithSchema(subjectMappingServiceGetSubjectMappingMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("GetSubjectMapping")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) subjectMappingServiceCreateSubjectMappingHandler := connect.NewUnaryHandler( SubjectMappingServiceCreateSubjectMappingProcedure, svc.CreateSubjectMapping, - connect.WithSchema(subjectMappingServiceCreateSubjectMappingMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("CreateSubjectMapping")), connect.WithHandlerOptions(opts...), ) subjectMappingServiceUpdateSubjectMappingHandler := connect.NewUnaryHandler( SubjectMappingServiceUpdateSubjectMappingProcedure, svc.UpdateSubjectMapping, - connect.WithSchema(subjectMappingServiceUpdateSubjectMappingMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("UpdateSubjectMapping")), connect.WithHandlerOptions(opts...), ) subjectMappingServiceDeleteSubjectMappingHandler := connect.NewUnaryHandler( SubjectMappingServiceDeleteSubjectMappingProcedure, svc.DeleteSubjectMapping, - connect.WithSchema(subjectMappingServiceDeleteSubjectMappingMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("DeleteSubjectMapping")), connect.WithHandlerOptions(opts...), ) subjectMappingServiceListSubjectConditionSetsHandler := connect.NewUnaryHandler( SubjectMappingServiceListSubjectConditionSetsProcedure, svc.ListSubjectConditionSets, - connect.WithSchema(subjectMappingServiceListSubjectConditionSetsMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("ListSubjectConditionSets")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) subjectMappingServiceGetSubjectConditionSetHandler := connect.NewUnaryHandler( SubjectMappingServiceGetSubjectConditionSetProcedure, svc.GetSubjectConditionSet, - connect.WithSchema(subjectMappingServiceGetSubjectConditionSetMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("GetSubjectConditionSet")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) subjectMappingServiceCreateSubjectConditionSetHandler := connect.NewUnaryHandler( SubjectMappingServiceCreateSubjectConditionSetProcedure, svc.CreateSubjectConditionSet, - connect.WithSchema(subjectMappingServiceCreateSubjectConditionSetMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("CreateSubjectConditionSet")), connect.WithHandlerOptions(opts...), ) subjectMappingServiceUpdateSubjectConditionSetHandler := connect.NewUnaryHandler( SubjectMappingServiceUpdateSubjectConditionSetProcedure, svc.UpdateSubjectConditionSet, - connect.WithSchema(subjectMappingServiceUpdateSubjectConditionSetMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("UpdateSubjectConditionSet")), connect.WithHandlerOptions(opts...), ) subjectMappingServiceDeleteSubjectConditionSetHandler := connect.NewUnaryHandler( SubjectMappingServiceDeleteSubjectConditionSetProcedure, svc.DeleteSubjectConditionSet, - connect.WithSchema(subjectMappingServiceDeleteSubjectConditionSetMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("DeleteSubjectConditionSet")), connect.WithHandlerOptions(opts...), ) subjectMappingServiceDeleteAllUnmappedSubjectConditionSetsHandler := connect.NewUnaryHandler( SubjectMappingServiceDeleteAllUnmappedSubjectConditionSetsProcedure, svc.DeleteAllUnmappedSubjectConditionSets, - connect.WithSchema(subjectMappingServiceDeleteAllUnmappedSubjectConditionSetsMethodDescriptor), + connect.WithSchema(subjectMappingServiceMethods.ByName("DeleteAllUnmappedSubjectConditionSets")), connect.WithHandlerOptions(opts...), ) return "/policy.subjectmapping.SubjectMappingService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/policy/unsafe/unsafe.pb.go b/protocol/go/policy/unsafe/unsafe.pb.go index b21c8887b1..31aefb9a4c 100644 --- a/protocol/go/policy/unsafe/unsafe.pb.go +++ b/protocol/go/policy/unsafe/unsafe.pb.go @@ -11,6 +11,7 @@ import ( policy "github.com/opentdf/platform/protocol/go/policy" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" reflect "reflect" sync "sync" ) @@ -355,6 +356,12 @@ type UnsafeUpdateAttributeRequest struct { Rule policy.AttributeRuleTypeEnum `protobuf:"varint,3,opt,name=rule,proto3,enum=policy.AttributeRuleTypeEnum" json:"rule,omitempty"` // Optional // WARNING!! + // Updating allow_traversal allows TDF creation to be front-loaded, meaning a customer + // can create encrypted content with an attribute definitions key mapping before + // creating the attribute values needed to decrypt. + AllowTraversal *wrapperspb.BoolValue `protobuf:"bytes,5,opt,name=allow_traversal,json=allowTraversal,proto3" json:"allow_traversal,omitempty"` + // Optional + // WARNING!! // Unsafe reordering requires the full list of values in the new order they should be stored. Updating the order of values in a HIERARCHY-rule Attribute Definition // will retroactively alter access to existing TDFs containing those values. Replacing values on an attribute in place is not supported; values can be unsafely deleted // deleted, created, and unsafely re-ordered as necessary. @@ -414,6 +421,13 @@ func (x *UnsafeUpdateAttributeRequest) GetRule() policy.AttributeRuleTypeEnum { return policy.AttributeRuleTypeEnum(0) } +func (x *UnsafeUpdateAttributeRequest) GetAllowTraversal() *wrapperspb.BoolValue { + if x != nil { + return x.AllowTraversal + } + return nil +} + func (x *UnsafeUpdateAttributeRequest) GetValuesOrder() []string { if x != nil { return x.ValuesOrder @@ -1113,7 +1127,9 @@ var file_policy_unsafe_unsafe_proto_rawDesc = []byte{ 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, + 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xeb, 0x04, 0x0a, 0x1c, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, @@ -1178,7 +1194,7 @@ var file_policy_unsafe_unsafe_proto_rawDesc = []byte{ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xe2, 0x03, 0x0a, 0x1c, 0x55, 0x6e, 0x73, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xa7, 0x04, 0x0a, 0x1c, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, @@ -1206,183 +1222,187 @@ var file_policy_unsafe_unsafe_proto_rawDesc = []byte{ 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, - 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0b, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x50, 0x0a, - 0x1d, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, - 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, - 0x3c, 0x0a, 0x20, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x54, 0x0a, - 0x21, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x22, 0x52, 0x0a, 0x1c, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, - 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, - 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0x50, 0x0a, 0x1d, 0x55, 0x6e, 0x73, 0x61, 0x66, - 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xe7, 0x02, 0x0a, 0x21, 0x55, 0x6e, - 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xa7, 0x02, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x90, 0x02, 0xba, 0x48, 0x8c, 0x02, - 0xba, 0x01, 0x83, 0x02, 0x0a, 0x0c, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x12, 0xb5, 0x01, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x20, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x6e, 0x20, - 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, 0x6e, 0x67, 0x20, 0x68, 0x79, - 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x73, - 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, - 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x2e, 0x20, 0x54, 0x68, - 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, - 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, - 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, 0x3b, 0x74, 0x68, 0x69, 0x73, - 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, - 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, - 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, - 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x72, 0x03, 0x18, 0xfd, 0x01, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x22, 0x49, 0x0a, 0x22, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x41, - 0x0a, 0x25, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, - 0x64, 0x22, 0x4d, 0x0a, 0x26, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x22, 0x57, 0x0a, 0x21, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x18, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, - 0x03, 0xc8, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0x49, 0x0a, 0x22, 0x55, 0x6e, 0x73, - 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x22, 0x70, 0x0a, 0x19, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, - 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x03, 0x6b, - 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, - 0x52, 0x03, 0x6b, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, - 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x22, 0x3e, 0x0a, 0x1a, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, - 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x32, 0xf8, 0x09, 0x0a, 0x0d, 0x55, 0x6e, 0x73, 0x61, 0x66, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x74, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x61, - 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, - 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, - 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x80, - 0x01, 0x0a, 0x19, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2f, 0x2e, 0x70, + 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x43, 0x0a, 0x0f, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x5f, 0x74, 0x72, 0x61, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x54, 0x72, 0x61, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x12, + 0x21, 0x0a, 0x0c, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x22, 0x50, 0x0a, 0x1d, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x22, 0x3c, 0x0a, 0x20, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, + 0x69, 0x64, 0x22, 0x54, 0x0a, 0x21, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0x52, 0x0a, 0x1c, 0x55, 0x6e, 0x73, 0x61, + 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x18, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0x50, 0x0a, 0x1d, + 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, + 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xe7, + 0x02, 0x0a, 0x21, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0xa7, + 0x02, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x90, + 0x02, 0xba, 0x48, 0x8c, 0x02, 0xba, 0x01, 0x83, 0x02, 0x0a, 0x0c, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0xb5, 0x01, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x20, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, + 0x63, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2c, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x69, + 0x6e, 0x67, 0x20, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x75, + 0x6e, 0x64, 0x65, 0x72, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, + 0x6f, 0x74, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, + 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, + 0x72, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x20, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x77, 0x69, + 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, + 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x1a, + 0x3b, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, + 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x61, + 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, + 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x72, 0x03, 0x18, 0xfd, + 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x49, 0x0a, 0x22, 0x55, 0x6e, 0x73, 0x61, + 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x22, 0x41, 0x0a, 0x25, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, + 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4d, 0x0a, 0x26, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, + 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a, 0x21, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0x49, + 0x0a, 0x22, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x70, 0x0a, 0x19, 0x55, 0x6e, 0x73, + 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x18, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, + 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x07, 0x6b, 0x61, + 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, + 0xc8, 0x01, 0x01, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x22, 0x3e, 0x0a, 0x1a, 0x55, + 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x32, 0xf8, 0x09, 0x0a, 0x0d, + 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x74, 0x0a, + 0x15, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, + 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x80, 0x01, 0x0a, 0x19, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, + 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, + 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x74, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, + 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, + 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, - 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, - 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x74, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, + 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x74, 0x0a, 0x15, + 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, + 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, + 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x80, 0x01, 0x0a, 0x19, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, + 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, + 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x74, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2b, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, + 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, + 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x83, 0x01, 0x0a, 0x1a, + 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, - 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x74, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, - 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, - 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x80, 0x01, - 0x0a, 0x19, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2f, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, - 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, - 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x74, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x83, 0x01, 0x0a, 0x1a, 0x55, 0x6e, 0x73, 0x61, 0x66, - 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, - 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8f, 0x01, 0x0a, - 0x1e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, - 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, - 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, + 0x61, 0x66, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x8f, 0x01, 0x0a, 0x1e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x83, - 0x01, 0x0a, 0x1a, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, - 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, - 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x6b, 0x0a, 0x12, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x2e, 0x70, 0x6f, 0x6c, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, + 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, - 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, + 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x83, 0x01, 0x0a, 0x1a, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, + 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x42, 0xac, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x42, 0x0b, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, - 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0xa2, 0x02, 0x03, - 0x50, 0x55, 0x58, 0xaa, 0x02, 0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x55, 0x6e, 0x73, - 0x61, 0x66, 0x65, 0xca, 0x02, 0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x55, 0x6e, 0x73, - 0x61, 0x66, 0x65, 0xe2, 0x02, 0x19, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x55, 0x6e, 0x73, - 0x61, 0x66, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, - 0x02, 0x0e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6b, 0x0a, 0x12, 0x55, 0x6e, 0x73, + 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x12, + 0x28, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, + 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, + 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xac, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x42, 0x0b, 0x55, 0x6e, + 0x73, 0x61, 0x66, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, + 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x75, 0x6e, 0x73, 0x61, + 0x66, 0x65, 0xa2, 0x02, 0x03, 0x50, 0x55, 0x58, 0xaa, 0x02, 0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0xca, 0x02, 0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x5c, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0xe2, 0x02, 0x19, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x5c, 0x55, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x55, + 0x6e, 0x73, 0x61, 0x66, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1421,47 +1441,49 @@ var file_policy_unsafe_unsafe_proto_goTypes = []interface{}{ (*UnsafeDeleteKasKeyResponse)(nil), // 19: policy.unsafe.UnsafeDeleteKasKeyResponse (*policy.Namespace)(nil), // 20: policy.Namespace (policy.AttributeRuleTypeEnum)(0), // 21: policy.AttributeRuleTypeEnum - (*policy.Attribute)(nil), // 22: policy.Attribute - (*policy.Value)(nil), // 23: policy.Value - (*policy.KasKey)(nil), // 24: policy.KasKey + (*wrapperspb.BoolValue)(nil), // 22: google.protobuf.BoolValue + (*policy.Attribute)(nil), // 23: policy.Attribute + (*policy.Value)(nil), // 24: policy.Value + (*policy.KasKey)(nil), // 25: policy.KasKey } var file_policy_unsafe_unsafe_proto_depIdxs = []int32{ 20, // 0: policy.unsafe.UnsafeUpdateNamespaceResponse.namespace:type_name -> policy.Namespace 20, // 1: policy.unsafe.UnsafeReactivateNamespaceResponse.namespace:type_name -> policy.Namespace 20, // 2: policy.unsafe.UnsafeDeleteNamespaceResponse.namespace:type_name -> policy.Namespace 21, // 3: policy.unsafe.UnsafeUpdateAttributeRequest.rule:type_name -> policy.AttributeRuleTypeEnum - 22, // 4: policy.unsafe.UnsafeUpdateAttributeResponse.attribute:type_name -> policy.Attribute - 22, // 5: policy.unsafe.UnsafeReactivateAttributeResponse.attribute:type_name -> policy.Attribute - 22, // 6: policy.unsafe.UnsafeDeleteAttributeResponse.attribute:type_name -> policy.Attribute - 23, // 7: policy.unsafe.UnsafeUpdateAttributeValueResponse.value:type_name -> policy.Value - 23, // 8: policy.unsafe.UnsafeReactivateAttributeValueResponse.value:type_name -> policy.Value - 23, // 9: policy.unsafe.UnsafeDeleteAttributeValueResponse.value:type_name -> policy.Value - 24, // 10: policy.unsafe.UnsafeDeleteKasKeyResponse.key:type_name -> policy.KasKey - 0, // 11: policy.unsafe.UnsafeService.UnsafeUpdateNamespace:input_type -> policy.unsafe.UnsafeUpdateNamespaceRequest - 2, // 12: policy.unsafe.UnsafeService.UnsafeReactivateNamespace:input_type -> policy.unsafe.UnsafeReactivateNamespaceRequest - 4, // 13: policy.unsafe.UnsafeService.UnsafeDeleteNamespace:input_type -> policy.unsafe.UnsafeDeleteNamespaceRequest - 6, // 14: policy.unsafe.UnsafeService.UnsafeUpdateAttribute:input_type -> policy.unsafe.UnsafeUpdateAttributeRequest - 8, // 15: policy.unsafe.UnsafeService.UnsafeReactivateAttribute:input_type -> policy.unsafe.UnsafeReactivateAttributeRequest - 10, // 16: policy.unsafe.UnsafeService.UnsafeDeleteAttribute:input_type -> policy.unsafe.UnsafeDeleteAttributeRequest - 12, // 17: policy.unsafe.UnsafeService.UnsafeUpdateAttributeValue:input_type -> policy.unsafe.UnsafeUpdateAttributeValueRequest - 14, // 18: policy.unsafe.UnsafeService.UnsafeReactivateAttributeValue:input_type -> policy.unsafe.UnsafeReactivateAttributeValueRequest - 16, // 19: policy.unsafe.UnsafeService.UnsafeDeleteAttributeValue:input_type -> policy.unsafe.UnsafeDeleteAttributeValueRequest - 18, // 20: policy.unsafe.UnsafeService.UnsafeDeleteKasKey:input_type -> policy.unsafe.UnsafeDeleteKasKeyRequest - 1, // 21: policy.unsafe.UnsafeService.UnsafeUpdateNamespace:output_type -> policy.unsafe.UnsafeUpdateNamespaceResponse - 3, // 22: policy.unsafe.UnsafeService.UnsafeReactivateNamespace:output_type -> policy.unsafe.UnsafeReactivateNamespaceResponse - 5, // 23: policy.unsafe.UnsafeService.UnsafeDeleteNamespace:output_type -> policy.unsafe.UnsafeDeleteNamespaceResponse - 7, // 24: policy.unsafe.UnsafeService.UnsafeUpdateAttribute:output_type -> policy.unsafe.UnsafeUpdateAttributeResponse - 9, // 25: policy.unsafe.UnsafeService.UnsafeReactivateAttribute:output_type -> policy.unsafe.UnsafeReactivateAttributeResponse - 11, // 26: policy.unsafe.UnsafeService.UnsafeDeleteAttribute:output_type -> policy.unsafe.UnsafeDeleteAttributeResponse - 13, // 27: policy.unsafe.UnsafeService.UnsafeUpdateAttributeValue:output_type -> policy.unsafe.UnsafeUpdateAttributeValueResponse - 15, // 28: policy.unsafe.UnsafeService.UnsafeReactivateAttributeValue:output_type -> policy.unsafe.UnsafeReactivateAttributeValueResponse - 17, // 29: policy.unsafe.UnsafeService.UnsafeDeleteAttributeValue:output_type -> policy.unsafe.UnsafeDeleteAttributeValueResponse - 19, // 30: policy.unsafe.UnsafeService.UnsafeDeleteKasKey:output_type -> policy.unsafe.UnsafeDeleteKasKeyResponse - 21, // [21:31] is the sub-list for method output_type - 11, // [11:21] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 22, // 4: policy.unsafe.UnsafeUpdateAttributeRequest.allow_traversal:type_name -> google.protobuf.BoolValue + 23, // 5: policy.unsafe.UnsafeUpdateAttributeResponse.attribute:type_name -> policy.Attribute + 23, // 6: policy.unsafe.UnsafeReactivateAttributeResponse.attribute:type_name -> policy.Attribute + 23, // 7: policy.unsafe.UnsafeDeleteAttributeResponse.attribute:type_name -> policy.Attribute + 24, // 8: policy.unsafe.UnsafeUpdateAttributeValueResponse.value:type_name -> policy.Value + 24, // 9: policy.unsafe.UnsafeReactivateAttributeValueResponse.value:type_name -> policy.Value + 24, // 10: policy.unsafe.UnsafeDeleteAttributeValueResponse.value:type_name -> policy.Value + 25, // 11: policy.unsafe.UnsafeDeleteKasKeyResponse.key:type_name -> policy.KasKey + 0, // 12: policy.unsafe.UnsafeService.UnsafeUpdateNamespace:input_type -> policy.unsafe.UnsafeUpdateNamespaceRequest + 2, // 13: policy.unsafe.UnsafeService.UnsafeReactivateNamespace:input_type -> policy.unsafe.UnsafeReactivateNamespaceRequest + 4, // 14: policy.unsafe.UnsafeService.UnsafeDeleteNamespace:input_type -> policy.unsafe.UnsafeDeleteNamespaceRequest + 6, // 15: policy.unsafe.UnsafeService.UnsafeUpdateAttribute:input_type -> policy.unsafe.UnsafeUpdateAttributeRequest + 8, // 16: policy.unsafe.UnsafeService.UnsafeReactivateAttribute:input_type -> policy.unsafe.UnsafeReactivateAttributeRequest + 10, // 17: policy.unsafe.UnsafeService.UnsafeDeleteAttribute:input_type -> policy.unsafe.UnsafeDeleteAttributeRequest + 12, // 18: policy.unsafe.UnsafeService.UnsafeUpdateAttributeValue:input_type -> policy.unsafe.UnsafeUpdateAttributeValueRequest + 14, // 19: policy.unsafe.UnsafeService.UnsafeReactivateAttributeValue:input_type -> policy.unsafe.UnsafeReactivateAttributeValueRequest + 16, // 20: policy.unsafe.UnsafeService.UnsafeDeleteAttributeValue:input_type -> policy.unsafe.UnsafeDeleteAttributeValueRequest + 18, // 21: policy.unsafe.UnsafeService.UnsafeDeleteKasKey:input_type -> policy.unsafe.UnsafeDeleteKasKeyRequest + 1, // 22: policy.unsafe.UnsafeService.UnsafeUpdateNamespace:output_type -> policy.unsafe.UnsafeUpdateNamespaceResponse + 3, // 23: policy.unsafe.UnsafeService.UnsafeReactivateNamespace:output_type -> policy.unsafe.UnsafeReactivateNamespaceResponse + 5, // 24: policy.unsafe.UnsafeService.UnsafeDeleteNamespace:output_type -> policy.unsafe.UnsafeDeleteNamespaceResponse + 7, // 25: policy.unsafe.UnsafeService.UnsafeUpdateAttribute:output_type -> policy.unsafe.UnsafeUpdateAttributeResponse + 9, // 26: policy.unsafe.UnsafeService.UnsafeReactivateAttribute:output_type -> policy.unsafe.UnsafeReactivateAttributeResponse + 11, // 27: policy.unsafe.UnsafeService.UnsafeDeleteAttribute:output_type -> policy.unsafe.UnsafeDeleteAttributeResponse + 13, // 28: policy.unsafe.UnsafeService.UnsafeUpdateAttributeValue:output_type -> policy.unsafe.UnsafeUpdateAttributeValueResponse + 15, // 29: policy.unsafe.UnsafeService.UnsafeReactivateAttributeValue:output_type -> policy.unsafe.UnsafeReactivateAttributeValueResponse + 17, // 30: policy.unsafe.UnsafeService.UnsafeDeleteAttributeValue:output_type -> policy.unsafe.UnsafeDeleteAttributeValueResponse + 19, // 31: policy.unsafe.UnsafeService.UnsafeDeleteKasKey:output_type -> policy.unsafe.UnsafeDeleteKasKeyResponse + 22, // [22:32] is the sub-list for method output_type + 12, // [12:22] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_policy_unsafe_unsafe_proto_init() } diff --git a/protocol/go/policy/unsafe/unsafeconnect/unsafe.connect.go b/protocol/go/policy/unsafe/unsafeconnect/unsafe.connect.go index 60f47e1865..0ba4c322be 100644 --- a/protocol/go/policy/unsafe/unsafeconnect/unsafe.connect.go +++ b/protocol/go/policy/unsafe/unsafeconnect/unsafe.connect.go @@ -65,21 +65,6 @@ const ( UnsafeServiceUnsafeDeleteKasKeyProcedure = "/policy.unsafe.UnsafeService/UnsafeDeleteKasKey" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - unsafeServiceServiceDescriptor = unsafe.File_policy_unsafe_unsafe_proto.Services().ByName("UnsafeService") - unsafeServiceUnsafeUpdateNamespaceMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeUpdateNamespace") - unsafeServiceUnsafeReactivateNamespaceMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeReactivateNamespace") - unsafeServiceUnsafeDeleteNamespaceMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeDeleteNamespace") - unsafeServiceUnsafeUpdateAttributeMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeUpdateAttribute") - unsafeServiceUnsafeReactivateAttributeMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeReactivateAttribute") - unsafeServiceUnsafeDeleteAttributeMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeDeleteAttribute") - unsafeServiceUnsafeUpdateAttributeValueMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeUpdateAttributeValue") - unsafeServiceUnsafeReactivateAttributeValueMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeReactivateAttributeValue") - unsafeServiceUnsafeDeleteAttributeValueMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeDeleteAttributeValue") - unsafeServiceUnsafeDeleteKasKeyMethodDescriptor = unsafeServiceServiceDescriptor.Methods().ByName("UnsafeDeleteKasKey") -) - // UnsafeServiceClient is a client for the policy.unsafe.UnsafeService service. type UnsafeServiceClient interface { // --------------------------------------* @@ -115,65 +100,66 @@ type UnsafeServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewUnsafeServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) UnsafeServiceClient { baseURL = strings.TrimRight(baseURL, "/") + unsafeServiceMethods := unsafe.File_policy_unsafe_unsafe_proto.Services().ByName("UnsafeService").Methods() return &unsafeServiceClient{ unsafeUpdateNamespace: connect.NewClient[unsafe.UnsafeUpdateNamespaceRequest, unsafe.UnsafeUpdateNamespaceResponse]( httpClient, baseURL+UnsafeServiceUnsafeUpdateNamespaceProcedure, - connect.WithSchema(unsafeServiceUnsafeUpdateNamespaceMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeUpdateNamespace")), connect.WithClientOptions(opts...), ), unsafeReactivateNamespace: connect.NewClient[unsafe.UnsafeReactivateNamespaceRequest, unsafe.UnsafeReactivateNamespaceResponse]( httpClient, baseURL+UnsafeServiceUnsafeReactivateNamespaceProcedure, - connect.WithSchema(unsafeServiceUnsafeReactivateNamespaceMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeReactivateNamespace")), connect.WithClientOptions(opts...), ), unsafeDeleteNamespace: connect.NewClient[unsafe.UnsafeDeleteNamespaceRequest, unsafe.UnsafeDeleteNamespaceResponse]( httpClient, baseURL+UnsafeServiceUnsafeDeleteNamespaceProcedure, - connect.WithSchema(unsafeServiceUnsafeDeleteNamespaceMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeDeleteNamespace")), connect.WithClientOptions(opts...), ), unsafeUpdateAttribute: connect.NewClient[unsafe.UnsafeUpdateAttributeRequest, unsafe.UnsafeUpdateAttributeResponse]( httpClient, baseURL+UnsafeServiceUnsafeUpdateAttributeProcedure, - connect.WithSchema(unsafeServiceUnsafeUpdateAttributeMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeUpdateAttribute")), connect.WithClientOptions(opts...), ), unsafeReactivateAttribute: connect.NewClient[unsafe.UnsafeReactivateAttributeRequest, unsafe.UnsafeReactivateAttributeResponse]( httpClient, baseURL+UnsafeServiceUnsafeReactivateAttributeProcedure, - connect.WithSchema(unsafeServiceUnsafeReactivateAttributeMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeReactivateAttribute")), connect.WithClientOptions(opts...), ), unsafeDeleteAttribute: connect.NewClient[unsafe.UnsafeDeleteAttributeRequest, unsafe.UnsafeDeleteAttributeResponse]( httpClient, baseURL+UnsafeServiceUnsafeDeleteAttributeProcedure, - connect.WithSchema(unsafeServiceUnsafeDeleteAttributeMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeDeleteAttribute")), connect.WithClientOptions(opts...), ), unsafeUpdateAttributeValue: connect.NewClient[unsafe.UnsafeUpdateAttributeValueRequest, unsafe.UnsafeUpdateAttributeValueResponse]( httpClient, baseURL+UnsafeServiceUnsafeUpdateAttributeValueProcedure, - connect.WithSchema(unsafeServiceUnsafeUpdateAttributeValueMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeUpdateAttributeValue")), connect.WithClientOptions(opts...), ), unsafeReactivateAttributeValue: connect.NewClient[unsafe.UnsafeReactivateAttributeValueRequest, unsafe.UnsafeReactivateAttributeValueResponse]( httpClient, baseURL+UnsafeServiceUnsafeReactivateAttributeValueProcedure, - connect.WithSchema(unsafeServiceUnsafeReactivateAttributeValueMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeReactivateAttributeValue")), connect.WithClientOptions(opts...), ), unsafeDeleteAttributeValue: connect.NewClient[unsafe.UnsafeDeleteAttributeValueRequest, unsafe.UnsafeDeleteAttributeValueResponse]( httpClient, baseURL+UnsafeServiceUnsafeDeleteAttributeValueProcedure, - connect.WithSchema(unsafeServiceUnsafeDeleteAttributeValueMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeDeleteAttributeValue")), connect.WithClientOptions(opts...), ), unsafeDeleteKasKey: connect.NewClient[unsafe.UnsafeDeleteKasKeyRequest, unsafe.UnsafeDeleteKasKeyResponse]( httpClient, baseURL+UnsafeServiceUnsafeDeleteKasKeyProcedure, - connect.WithSchema(unsafeServiceUnsafeDeleteKasKeyMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeDeleteKasKey")), connect.WithClientOptions(opts...), ), } @@ -275,64 +261,65 @@ type UnsafeServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewUnsafeServiceHandler(svc UnsafeServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + unsafeServiceMethods := unsafe.File_policy_unsafe_unsafe_proto.Services().ByName("UnsafeService").Methods() unsafeServiceUnsafeUpdateNamespaceHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeUpdateNamespaceProcedure, svc.UnsafeUpdateNamespace, - connect.WithSchema(unsafeServiceUnsafeUpdateNamespaceMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeUpdateNamespace")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeReactivateNamespaceHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeReactivateNamespaceProcedure, svc.UnsafeReactivateNamespace, - connect.WithSchema(unsafeServiceUnsafeReactivateNamespaceMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeReactivateNamespace")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeDeleteNamespaceHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeDeleteNamespaceProcedure, svc.UnsafeDeleteNamespace, - connect.WithSchema(unsafeServiceUnsafeDeleteNamespaceMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeDeleteNamespace")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeUpdateAttributeHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeUpdateAttributeProcedure, svc.UnsafeUpdateAttribute, - connect.WithSchema(unsafeServiceUnsafeUpdateAttributeMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeUpdateAttribute")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeReactivateAttributeHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeReactivateAttributeProcedure, svc.UnsafeReactivateAttribute, - connect.WithSchema(unsafeServiceUnsafeReactivateAttributeMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeReactivateAttribute")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeDeleteAttributeHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeDeleteAttributeProcedure, svc.UnsafeDeleteAttribute, - connect.WithSchema(unsafeServiceUnsafeDeleteAttributeMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeDeleteAttribute")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeUpdateAttributeValueHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeUpdateAttributeValueProcedure, svc.UnsafeUpdateAttributeValue, - connect.WithSchema(unsafeServiceUnsafeUpdateAttributeValueMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeUpdateAttributeValue")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeReactivateAttributeValueHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeReactivateAttributeValueProcedure, svc.UnsafeReactivateAttributeValue, - connect.WithSchema(unsafeServiceUnsafeReactivateAttributeValueMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeReactivateAttributeValue")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeDeleteAttributeValueHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeDeleteAttributeValueProcedure, svc.UnsafeDeleteAttributeValue, - connect.WithSchema(unsafeServiceUnsafeDeleteAttributeValueMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeDeleteAttributeValue")), connect.WithHandlerOptions(opts...), ) unsafeServiceUnsafeDeleteKasKeyHandler := connect.NewUnaryHandler( UnsafeServiceUnsafeDeleteKasKeyProcedure, svc.UnsafeDeleteKasKey, - connect.WithSchema(unsafeServiceUnsafeDeleteKasKeyMethodDescriptor), + connect.WithSchema(unsafeServiceMethods.ByName("UnsafeDeleteKasKey")), connect.WithHandlerOptions(opts...), ) return "/policy.unsafe.UnsafeService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/protocol/go/wellknownconfiguration/wellknown_configuration.pb.go b/protocol/go/wellknownconfiguration/wellknown_configuration.pb.go index 8933ce80a9..928469bb02 100644 --- a/protocol/go/wellknownconfiguration/wellknown_configuration.pb.go +++ b/protocol/go/wellknownconfiguration/wellknown_configuration.pb.go @@ -7,7 +7,6 @@ package wellknownconfiguration import ( - _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" @@ -162,61 +161,56 @@ var file_wellknownconfiguration_wellknown_configuration_proto_rawDesc = []byte{ 0x77, 0x6e, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1c, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, - 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xce, 0x01, 0x0a, 0x0f, 0x57, - 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x60, - 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, - 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x57, - 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x1a, 0x59, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x22, 0x0a, 0x20, 0x47, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, + 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xce, 0x01, 0x0a, + 0x0f, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x60, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, + 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x1a, 0x59, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, + 0x63, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x22, 0x0a, + 0x20, 0x47, 0x65, 0x74, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x62, 0x0a, 0x21, 0x47, 0x65, 0x74, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, + 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xaa, 0x01, 0x0a, 0x10, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, + 0x6f, 0x77, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x95, 0x01, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x62, 0x0a, 0x21, 0x47, 0x65, 0x74, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, - 0x72, 0x75, 0x63, 0x74, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x32, 0xd4, 0x01, 0x0a, 0x10, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, - 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xbf, 0x01, 0x0a, 0x19, 0x47, 0x65, 0x74, - 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, - 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x47, 0x65, 0x74, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x39, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x57, 0x65, 0x6c, - 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2d, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x24, 0x12, 0x22, 0x2f, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x2d, 0x6b, 0x6e, 0x6f, 0x77, - 0x6e, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x90, 0x02, 0x01, 0x42, 0xf1, 0x01, 0x0a, 0x1a, 0x63, - 0x6f, 0x6d, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x1b, 0x57, 0x65, 0x6c, 0x6c, 0x6b, - 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, - 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, - 0x6f, 0x2f, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xa2, 0x02, 0x03, 0x57, 0x58, 0x58, 0xaa, 0x02, - 0x16, 0x57, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xca, 0x02, 0x16, 0x57, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x6b, + 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x57, 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x57, + 0x65, 0x6c, 0x6c, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, + 0x02, 0x01, 0x42, 0xf1, 0x01, 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x6b, + 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x42, 0x1b, 0x57, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, + 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0xe2, 0x02, 0x22, 0x57, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x16, 0x57, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, - 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0xa2, 0x02, 0x03, 0x57, 0x58, 0x58, 0xaa, 0x02, 0x16, 0x57, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, + 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xca, + 0x02, 0x16, 0x57, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xe2, 0x02, 0x22, 0x57, 0x65, 0x6c, 0x6c, 0x6b, + 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x16, + 0x57, 0x65, 0x6c, 0x6c, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protocol/go/wellknownconfiguration/wellknown_configuration.pb.gw.go b/protocol/go/wellknownconfiguration/wellknown_configuration.pb.gw.go deleted file mode 100644 index 23e484ffbe..0000000000 --- a/protocol/go/wellknownconfiguration/wellknown_configuration.pb.gw.go +++ /dev/null @@ -1,155 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: wellknownconfiguration/wellknown_configuration.proto - -/* -Package wellknownconfiguration is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package wellknownconfiguration - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -func request_WellKnownService_GetWellKnownConfiguration_0(ctx context.Context, marshaler runtime.Marshaler, client WellKnownServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetWellKnownConfigurationRequest - var metadata runtime.ServerMetadata - - msg, err := client.GetWellKnownConfiguration(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_WellKnownService_GetWellKnownConfiguration_0(ctx context.Context, marshaler runtime.Marshaler, server WellKnownServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetWellKnownConfigurationRequest - var metadata runtime.ServerMetadata - - msg, err := server.GetWellKnownConfiguration(ctx, &protoReq) - return msg, metadata, err - -} - -// RegisterWellKnownServiceHandlerServer registers the http handlers for service WellKnownService to "mux". -// UnaryRPC :call WellKnownServiceServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWellKnownServiceHandlerFromEndpoint instead. -func RegisterWellKnownServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WellKnownServiceServer) error { - - mux.Handle("GET", pattern_WellKnownService_GetWellKnownConfiguration_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/wellknownconfiguration.WellKnownService/GetWellKnownConfiguration", runtime.WithHTTPPathPattern("/.well-known/opentdf-configuration")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_WellKnownService_GetWellKnownConfiguration_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WellKnownService_GetWellKnownConfiguration_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterWellKnownServiceHandlerFromEndpoint is same as RegisterWellKnownServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterWellKnownServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterWellKnownServiceHandler(ctx, mux, conn) -} - -// RegisterWellKnownServiceHandler registers the http handlers for service WellKnownService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterWellKnownServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterWellKnownServiceHandlerClient(ctx, mux, NewWellKnownServiceClient(conn)) -} - -// RegisterWellKnownServiceHandlerClient registers the http handlers for service WellKnownService -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WellKnownServiceClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WellKnownServiceClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "WellKnownServiceClient" to call the correct interceptors. -func RegisterWellKnownServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WellKnownServiceClient) error { - - mux.Handle("GET", pattern_WellKnownService_GetWellKnownConfiguration_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/wellknownconfiguration.WellKnownService/GetWellKnownConfiguration", runtime.WithHTTPPathPattern("/.well-known/opentdf-configuration")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_WellKnownService_GetWellKnownConfiguration_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WellKnownService_GetWellKnownConfiguration_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_WellKnownService_GetWellKnownConfiguration_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{".well-known", "opentdf-configuration"}, "")) -) - -var ( - forward_WellKnownService_GetWellKnownConfiguration_0 = runtime.ForwardResponseMessage -) diff --git a/protocol/go/wellknownconfiguration/wellknownconfigurationconnect/wellknown_configuration.connect.go b/protocol/go/wellknownconfiguration/wellknownconfigurationconnect/wellknown_configuration.connect.go index 35ce658693..14f5c6b533 100644 --- a/protocol/go/wellknownconfiguration/wellknownconfigurationconnect/wellknown_configuration.connect.go +++ b/protocol/go/wellknownconfiguration/wellknownconfigurationconnect/wellknown_configuration.connect.go @@ -38,12 +38,6 @@ const ( WellKnownServiceGetWellKnownConfigurationProcedure = "/wellknownconfiguration.WellKnownService/GetWellKnownConfiguration" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - wellKnownServiceServiceDescriptor = wellknownconfiguration.File_wellknownconfiguration_wellknown_configuration_proto.Services().ByName("WellKnownService") - wellKnownServiceGetWellKnownConfigurationMethodDescriptor = wellKnownServiceServiceDescriptor.Methods().ByName("GetWellKnownConfiguration") -) - // WellKnownServiceClient is a client for the wellknownconfiguration.WellKnownService service. type WellKnownServiceClient interface { GetWellKnownConfiguration(context.Context, *connect.Request[wellknownconfiguration.GetWellKnownConfigurationRequest]) (*connect.Response[wellknownconfiguration.GetWellKnownConfigurationResponse], error) @@ -58,11 +52,12 @@ type WellKnownServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewWellKnownServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) WellKnownServiceClient { baseURL = strings.TrimRight(baseURL, "/") + wellKnownServiceMethods := wellknownconfiguration.File_wellknownconfiguration_wellknown_configuration_proto.Services().ByName("WellKnownService").Methods() return &wellKnownServiceClient{ getWellKnownConfiguration: connect.NewClient[wellknownconfiguration.GetWellKnownConfigurationRequest, wellknownconfiguration.GetWellKnownConfigurationResponse]( httpClient, baseURL+WellKnownServiceGetWellKnownConfigurationProcedure, - connect.WithSchema(wellKnownServiceGetWellKnownConfigurationMethodDescriptor), + connect.WithSchema(wellKnownServiceMethods.ByName("GetWellKnownConfiguration")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), @@ -92,10 +87,11 @@ type WellKnownServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewWellKnownServiceHandler(svc WellKnownServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + wellKnownServiceMethods := wellknownconfiguration.File_wellknownconfiguration_wellknown_configuration_proto.Services().ByName("WellKnownService").Methods() wellKnownServiceGetWellKnownConfigurationHandler := connect.NewUnaryHandler( WellKnownServiceGetWellKnownConfigurationProcedure, svc.GetWellKnownConfiguration, - connect.WithSchema(wellKnownServiceGetWellKnownConfigurationMethodDescriptor), + connect.WithSchema(wellKnownServiceMethods.ByName("GetWellKnownConfiguration")), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 3564bddec6..68b0f9902d 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -1,5 +1,140 @@ # Changelog +## [0.21.0](https://github.com/opentdf/platform/compare/sdk/v0.20.0...sdk/v0.21.0) (2026-05-28) + + +### Features + +* **core:** add hybrid NIST EC + ML-KEM key wrapping support ([#3276](https://github.com/opentdf/platform/issues/3276)) ([1209acc](https://github.com/opentdf/platform/commit/1209acc2f8ae24af121f6a2892817c20ebb14d25)) +* **sdk:** add WithPolicyFrom re-wrap helper ([#3476](https://github.com/opentdf/platform/issues/3476)) ([baa1403](https://github.com/opentdf/platform/commit/baa1403cf5fb445623e84b00859c2c0cf8c0a20a)) + + +### Bug Fixes + +* **deps:** bump github.com/opentdf/platform/lib/ocrypto from 0.10.0 to 0.11.0 in /sdk ([#3522](https://github.com/opentdf/platform/issues/3522)) ([e147d12](https://github.com/opentdf/platform/commit/e147d12152076e348f5813148dd8093844c51c8a)) +* **deps:** bump github.com/opentdf/platform/lib/ocrypto from 0.11.0 to 0.12.0 in /sdk ([#3534](https://github.com/opentdf/platform/issues/3534)) ([e95fb70](https://github.com/opentdf/platform/commit/e95fb70342be3aeb87eca102479c962aa2d664e8)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.30.0 to 0.31.0 in /sdk ([#3496](https://github.com/opentdf/platform/issues/3496)) ([1415e8e](https://github.com/opentdf/platform/commit/1415e8e7e9e7f8d76cef0ab65d0045822524b6a5)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.31.0 to 0.32.0 in /sdk ([#3520](https://github.com/opentdf/platform/issues/3520)) ([0385ab4](https://github.com/opentdf/platform/commit/0385ab44ede2b20e9ca557c7033c62e23349944a)) +* **sdk:** DSPX-3464 Adds subject_token_type to RFC 8693 token exchanges ([#3465](https://github.com/opentdf/platform/issues/3465)) ([ed9b0fc](https://github.com/opentdf/platform/commit/ed9b0fca6ca9a733e2904c1905f7c31a9ebdb64d)) + +## [0.20.0](https://github.com/opentdf/platform/compare/sdk/v0.19.0...sdk/v0.20.0) (2026-05-11) + + +### Bug Fixes + +* **deps:** bump module protocol/go to v0.30.0 throughout ([#3459](https://github.com/opentdf/platform/issues/3459)) ([8eaa502](https://github.com/opentdf/platform/commit/8eaa502b0f949ddbe18a5a1dac0931b92eec2351)) + +## [0.19.0](https://github.com/opentdf/platform/compare/sdk/v0.18.0...sdk/v0.19.0) (2026-05-06) + + +### Bug Fixes + +* **deps:** bump the external group across 1 directory with 7 updates ([#3422](https://github.com/opentdf/platform/issues/3422)) ([be0da08](https://github.com/opentdf/platform/commit/be0da0833863d432cf844858f20a0912c2802e51)) + +## [0.18.0](https://github.com/opentdf/platform/compare/sdk/v0.17.0...sdk/v0.18.0) (2026-04-29) + + +### Features + +* **sdk:** IsHealthy(ctx) public reachability probe ([#3412](https://github.com/opentdf/platform/issues/3412)) ([3e2cf98](https://github.com/opentdf/platform/commit/3e2cf981eded81dafaaf30af642592401caa16f3)) + + +### Bug Fixes + +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.27.0 to 0.28.0 in /sdk ([#3415](https://github.com/opentdf/platform/issues/3415)) ([701bd9f](https://github.com/opentdf/platform/commit/701bd9f32fca5d9508331ee19966180e4c54d0e7)) +* **deps:** bump go.opentelemetry.io/otel from 1.40.0 to 1.41.0 in /sdk ([#3399](https://github.com/opentdf/platform/issues/3399)) ([d98418b](https://github.com/opentdf/platform/commit/d98418beb9e42819ba0e8376f43771f2ca7855af)) + +## [0.17.0](https://github.com/opentdf/platform/compare/sdk/v0.16.0...sdk/v0.17.0) (2026-04-24) + + +### Bug Fixes + +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.25.0 to 0.26.0 in /sdk ([#3380](https://github.com/opentdf/platform/issues/3380)) ([5e36f94](https://github.com/opentdf/platform/commit/5e36f943280fec86e2d9a4917c576b6731ed8419)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.26.0 to 0.27.0 in /sdk ([#3393](https://github.com/opentdf/platform/issues/3393)) ([7659957](https://github.com/opentdf/platform/commit/7659957ed9612397d7e72c6b309006224f3cf214)) + +## [0.16.0](https://github.com/opentdf/platform/compare/sdk/v0.15.0...sdk/v0.16.0) (2026-04-21) + + +### ⚠ BREAKING CHANGES + +* **sdk:** reclassify KAS 400 errors — distinguish tamper from misconfiguration ([#3166](https://github.com/opentdf/platform/issues/3166)) + +### Features + +* **policy:** add GetObligationTrigger RPC ([#3318](https://github.com/opentdf/platform/issues/3318)) ([d68e39d](https://github.com/opentdf/platform/commit/d68e39d950d94dcbb98a2f16982ea57f28d9c550)) + + +### Bug Fixes + +* **core:** do not concat slashes directly in url/file paths ([#3290](https://github.com/opentdf/platform/issues/3290)) ([114c2a7](https://github.com/opentdf/platform/commit/114c2a7523235d68ee1afeb8883d478541e11834)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.20.0 to 0.21.0 in /sdk ([#3219](https://github.com/opentdf/platform/issues/3219)) ([c7fde71](https://github.com/opentdf/platform/commit/c7fde7115ab43b1dbab1930c385c31faf2d2b758)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.21.0 to 0.22.0 in /sdk ([#3246](https://github.com/opentdf/platform/issues/3246)) ([67c152c](https://github.com/opentdf/platform/commit/67c152c5805e6c87b3c8751a503267247d4c22e6)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.22.0 to 0.23.0 in /sdk ([#3270](https://github.com/opentdf/platform/issues/3270)) ([68ee42a](https://github.com/opentdf/platform/commit/68ee42ad7646b6ed44f0fc7d93ec3f733eb570b8)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.23.0 to 0.24.0 in /sdk ([#3319](https://github.com/opentdf/platform/issues/3319)) ([0f8db5e](https://github.com/opentdf/platform/commit/0f8db5e47b8bbe276e0de14a46f9fa234213e332)) +* **deps:** bump google.golang.org/grpc from 1.77.0 to 1.79.3 in /sdk ([#3174](https://github.com/opentdf/platform/issues/3174)) ([be8b154](https://github.com/opentdf/platform/commit/be8b15493d42eeaa3d8d8e9a0c4ec7065a0b36f7)) +* **sdk:** normalize issuer URL before OIDC discovery ([#3261](https://github.com/opentdf/platform/issues/3261)) ([61f98c9](https://github.com/opentdf/platform/commit/61f98c94deb9a1b88e62436b6598735479db6e63)) +* **sdk:** reclassify KAS 400 errors — distinguish tamper from misconfiguration ([#3166](https://github.com/opentdf/platform/issues/3166)) ([f04a385](https://github.com/opentdf/platform/commit/f04a3856f004f68df0bcf7e355867971c8df7fdc)) + +## [0.15.0](https://github.com/opentdf/platform/compare/sdk/v0.14.0...sdk/v0.15.0) (2026-03-23) + + +### Bug Fixes + +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.16.0 to 0.20.0 in /sdk ([#3179](https://github.com/opentdf/platform/issues/3179)) ([30bb0a8](https://github.com/opentdf/platform/commit/30bb0a816e9201c1b6f809e927e4260ba077d1d5)) +* **sdk:** AttributeValueExists returns false instead of error for non-existent values ([#3195](https://github.com/opentdf/platform/issues/3195)) ([4e46091](https://github.com/opentdf/platform/commit/4e46091d59cecb2d557a51a370e85813db9ff78f)) + +## [0.14.0](https://github.com/opentdf/platform/compare/sdk/v0.13.0...sdk/v0.14.0) (2026-03-11) + + +### Features + +* **sdk:** DSPX-2418 add attribute discovery methods ([#3082](https://github.com/opentdf/platform/issues/3082)) ([aeeaadd](https://github.com/opentdf/platform/commit/aeeaaddc804ede0a19780b1a9c7a5261076faee7)) + + +### Bug Fixes + +* **ci:** Upgrade toolchain version to 1.25.8 ([#3116](https://github.com/opentdf/platform/issues/3116)) ([e1b7882](https://github.com/opentdf/platform/commit/e1b78822c0380a106e6eec05af78dc1fc9e5701f)) +* **policy:** order List* results by created_at ([#3088](https://github.com/opentdf/platform/issues/3088)) ([ea90ac2](https://github.com/opentdf/platform/commit/ea90ac279abbdf796d1cbe8efd8bac9c8c62de85)) +* **sdk:** remove testcontainers from consumer dependency graph ([#3129](https://github.com/opentdf/platform/issues/3129)) ([f17dcdd](https://github.com/opentdf/platform/commit/f17dcdd77a0096eb3cfd9f7d15033e4f2074cc16)) + +## [0.13.0](https://github.com/opentdf/platform/compare/sdk/v0.12.0...sdk/v0.13.0) (2026-02-17) + + +### ⚠ BREAKING CHANGES + +* **policy:** remove namespace certificate feature ([#3051](https://github.com/opentdf/platform/issues/3051)) + +### Bug Fixes + +* **deps:** bump github.com/opentdf/platform/lib/ocrypto from 0.9.0 to 0.10.0 in /sdk ([#3078](https://github.com/opentdf/platform/issues/3078)) ([527c34d](https://github.com/opentdf/platform/commit/527c34d1f0ce19a1a4d177994c46ce55aa5d9e2e)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.15.0 to 0.16.0 in /sdk ([#3081](https://github.com/opentdf/platform/issues/3081)) ([3d4ce33](https://github.com/opentdf/platform/commit/3d4ce330a08f14dc26e925c6be74f99aaffae834)) +* **docs:** DSPX-2409 replace SDK README code example with working code ([#3055](https://github.com/opentdf/platform/issues/3055)) ([566cb6f](https://github.com/opentdf/platform/commit/566cb6fcc7e906f34a59326708c30f9f2059b21a)) +* Go 1.25 ([#3053](https://github.com/opentdf/platform/issues/3053)) ([65eb7c3](https://github.com/opentdf/platform/commit/65eb7c3d5fe1892de1e4fabb9b3b7894742c3f02)) +* **kas:** Fix EC P-521 typo ([#3075](https://github.com/opentdf/platform/issues/3075)) ([abc088d](https://github.com/opentdf/platform/commit/abc088d6f5f55eab240813faad2e575d87df51c1)) +* **sdk:** conditionally set client_id based on auth method ([#2968](https://github.com/opentdf/platform/issues/2968)) ([abdeb69](https://github.com/opentdf/platform/commit/abdeb693b7a836460d03b78c1233790b5203a076)) + + +### Code Refactoring + +* **policy:** remove namespace certificate feature ([#3051](https://github.com/opentdf/platform/issues/3051)) ([48abb81](https://github.com/opentdf/platform/commit/48abb813ae7accbfcaa6e6ad4bb7071e3476716d)) + +## [0.12.0](https://github.com/opentdf/platform/compare/sdk/v0.11.0...sdk/v0.12.0) (2026-01-27) + + +### ⚠ BREAKING CHANGES + +* remove nanotdf support ([#3013](https://github.com/opentdf/platform/issues/3013)) + +### Features + +* **deps:** Bump ocrypto to v0.9.0 ([#3024](https://github.com/opentdf/platform/issues/3024)) ([cd79950](https://github.com/opentdf/platform/commit/cd799509b15516f840436e6af20a14eebaa0556d)) +* **sdk:** expose base key API ([#3000](https://github.com/opentdf/platform/issues/3000)) ([67de794](https://github.com/opentdf/platform/commit/67de794721ccb7e5f93454043409dff1619fe42c)) + + +### Bug Fixes + +* remove nanotdf support ([#3013](https://github.com/opentdf/platform/issues/3013)) ([90ff7ce](https://github.com/opentdf/platform/commit/90ff7ce50754a1f37ba1cc530507c1f6e15930a0)) + ## [0.11.0](https://github.com/opentdf/platform/compare/sdk/v0.10.0...sdk/v0.11.0) (2026-01-06) diff --git a/sdk/README.md b/sdk/README.md index c30fd569de..849135c071 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -3,6 +3,8 @@ A Go implementation of the OpenTDF protocol, and access library for services included in the Data Security Platform. +**New to the OpenTDF SDK?** See the [OpenTDF SDK Quickstart Guide](https://opentdf.io/category/sdk) for a comprehensive introduction. + Note: if you are consuming the SDK as a submodule you may need to add replace directives as follows: ```go @@ -18,47 +20,163 @@ replace ( ## Quick Start of the Go SDK +This example demonstrates how to create and read TDF (Trusted Data Format) files using the OpenTDF SDK. + +**Prerequisites:** Follow the [OpenTDF Quickstart](https://opentdf.io/quickstart) to get a local platform running, or if you already have a hosted version, replace the values with your OpenTDF platform details. + +For more code examples, see: +- [Creating TDFs](https://opentdf.io/sdks/tdf) +- [Managing policy](https://opentdf.io/sdks/policy) + ```go package main -import "fmt" -import "bytes" -import "io" -import "os" -import "strings" -import "github.com/opentdf/platform/sdk" +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "strings" + "github.com/opentdf/platform/sdk" +) func main() { - s, _ := sdk.New( - sdk.WithAuth(mtls.NewGRPCAuthorizer(creds) /* or OIDC or whatever */), - sdk.WithDataSecurityConfig(/* attribute schemas, kas multi-attribute mapping */), - ) - - plaintext := strings.NewReader("Hello, world!") - var ciphertext bytes.Buffer - _, err := s.CreateTDF( - ciphertext, - plaintext, - sdk.WithDataAttributes("https://example.com/attr/Classification/value/Open"), - ) - if err != nil { - panic(err) - } - - fmt.Printf("Ciphertext is %s bytes long", ciphertext.Len()) - - ct2 := make([]byte, ciphertext.Len()) - copy(ct2, ciphertext.Bytes()) - r, err := s.NewTDFReader(bytes.NewReader(ct2)) - f, err := os.Create("output.txt") - if err != nil { - panic(err) - } - io.Copy(f, r) + // Initialize SDK with platform endpoint and authentication + // Replace these values with your actual configuration: + platformEndpoint := "http://localhost:8080" // Your platform URL + clientID := "opentdf" // Your OAuth client ID + clientSecret := "secret" // Your OAuth client secret + keycloakURL := "http://localhost:8888/auth/realms/opentdf" // Your Keycloak realm URL + + s, err := sdk.New( + platformEndpoint, + sdk.WithClientCredentials(clientID, clientSecret, []string{"email", "profile"}), + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "platform_issuer": keycloakURL, + }), + sdk.WithInsecurePlaintextConn(), // Only for local development with HTTP + ) + if err != nil { + log.Fatalf("Failed to create SDK: %v", err) + } + defer s.Close() + + // Create a TDF + // This attribute is created in the quickstart guide + dataAttribute := "https://opentdf.io/attr/department/value/finance" + + plaintext := strings.NewReader("Hello, world!") + var ciphertext bytes.Buffer + _, err = s.CreateTDF( + &ciphertext, + plaintext, + sdk.WithDataAttributes(dataAttribute), + ) + if err != nil { + log.Fatalf("Failed to create TDF: %v", err) + } + + fmt.Printf("Ciphertext is %d bytes long\n", ciphertext.Len()) + + // Decrypt the TDF + // LoadTDF contacts the Key Access Service (KAS) to verify that this client + // has been granted access to the data attributes, then decrypts the TDF. + // Note: The client must have entitlements configured on the platform first. + r, err := s.LoadTDF(bytes.NewReader(ciphertext.Bytes())) + if err != nil { + log.Fatalf("Failed to load TDF: %v", err) + } + + // Write the decrypted plaintext to a file + f, err := os.Create("output.txt") + if err != nil { + log.Fatalf("Failed to create output file: %v", err) + } + defer f.Close() + + _, err = io.Copy(f, r) + if err != nil { + log.Fatalf("Failed to write decrypted content: %v", err) + } + + fmt.Println("Successfully created and decrypted TDF") } ``` +### Configuration Values + +Replace these placeholder values with your actual configuration: + +| Variable | Default (Quickstart) | Description | +|----------|---------------------|-------------| +| `platformEndpoint` | `http://localhost:8080` | Your OpenTDF platform URL | +| `clientID` | `opentdf` | OAuth client ID (from quickstart) | +| `clientSecret` | `secret` | OAuth client secret (from quickstart) | +| `keycloakURL` | `http://localhost:8888/auth/realms/opentdf` | Your Keycloak realm URL | +| `dataAttribute` | `https://opentdf.io/attr/department/value/finance` | Data attribute FQN (created in quickstart) | + +**Before running:** +1. Follow the [OpenTDF Quickstart](https://opentdf.io/quickstart) to start the platform +2. Create an OAuth client in Keycloak and note the credentials +3. Grant your client entitlements to the `department` attribute (see [Managing policy](https://opentdf.io/sdks/policy)) + +**Expected Output:** +``` +Ciphertext is 1234 bytes long +Successfully created and decrypted TDF +``` + +The `output.txt` file will contain the decrypted plaintext: `Hello, world!` + +### Authentication Options + +The SDK supports multiple authentication methods: + +**Client Credentials (OAuth 2.0):** +```go +sdk.WithClientCredentials("client-id", "client-secret", []string{"scope1", "scope2"}) +``` + +**TLS/mTLS Authentication:** +```go +import "crypto/tls" + +// Load your client certificate and key +cert, err := tls.LoadX509KeyPair("client.crt", "client.key") +if err != nil { + log.Fatal(err) +} + +tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, +} +sdk.WithTLSCredentials(tlsConfig, []string{"audience1", "audience2"}) +``` + +**Custom OAuth2 Token Source:** +```go +import "golang.org/x/oauth2" + +tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "your-token"}) +sdk.WithOAuthAccessTokenSource(tokenSource) +``` + +**Token Exchange:** +```go +sdk.WithTokenExchange("subject-token", []string{"audience1", "audience2"}) +``` + +## Base key + +The platform may publish a base KAS public key in its well-known configuration. Retrieve it via: + +```go +baseKey, err := s.GetBaseKey(ctx) +``` + ## Development To test, run diff --git a/sdk/auth/oauth/oauth.go b/sdk/auth/oauth/oauth.go index 80545f2fe4..9f3bd3a46d 100644 --- a/sdk/auth/oauth/oauth.go +++ b/sdk/auth/oauth/oauth.go @@ -3,6 +3,7 @@ package oauth import ( "context" "encoding/json" + "errors" "fmt" "io" "log/slog" @@ -23,6 +24,8 @@ const ( tokenExpirationBuffer = 10 * time.Second ) +const defaultSubjectTokenType = "urn:ietf:params:oauth:token-type:access_token" //nolint:gosec // OAuth token type URN, not a credential + type CertExchangeInfo struct { HTTPClient *http.Client Audience []string @@ -35,7 +38,10 @@ type ClientCredentials struct { type TokenExchangeInfo struct { SubjectToken string - Audience []string + // SubjectTokenType declares the type of SubjectToken per RFC 8693 §2.1. + // Defaults to urn:ietf:params:oauth:token-type:access_token when empty. + SubjectTokenType string + Audience []string } type Token struct { @@ -71,7 +77,6 @@ func getAccessTokenRequest(tokenEndpoint, dpopNonce string, scopes []string, cli formData := url.Values{} formData.Set("grant_type", "client_credentials") - formData.Set("client_id", clientCredentials.ClientID) if len(scopes) > 0 { formData.Set("scope", strings.Join(scopes, " ")) } @@ -95,6 +100,7 @@ func setClientAuth(cc ClientCredentials, formData *url.Values, req *http.Request if err != nil { return fmt.Errorf("error building signed auth token to authenticate with IDP: %w", err) } + formData.Set("client_id", cc.ClientID) formData.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") formData.Set("client_assertion", string(signedToken)) default: @@ -279,10 +285,18 @@ func DoTokenExchange(ctx context.Context, client *http.Client, tokenEndpoint str } func getTokenExchangeRequest(ctx context.Context, tokenEndpoint, dpopNonce string, scopes []string, clientCredentials ClientCredentials, tokenExchange TokenExchangeInfo, privateJWK *jwk.Key) (*http.Request, error) { + if tokenExchange.SubjectToken == "" { + return nil, errors.New("subject_token is required for token exchange") + } + subjectTokenType := tokenExchange.SubjectTokenType + if subjectTokenType == "" { + subjectTokenType = defaultSubjectTokenType + } data := url.Values{ "grant_type": {"urn:ietf:params:oauth:grant-type:token-exchange"}, "subject_token": {tokenExchange.SubjectToken}, - "requested_token_type": {"urn:ietf:params:oauth:token-type:access_token"}, + "subject_token_type": {subjectTokenType}, + "requested_token_type": {defaultSubjectTokenType}, } for _, a := range tokenExchange.Audience { @@ -290,11 +304,10 @@ func getTokenExchangeRequest(ctx context.Context, tokenEndpoint, dpopNonce strin } if len(scopes) > 0 { - data.Set("scopes", strings.Join(scopes, " ")) + data.Set("scope", strings.Join(scopes, " ")) } - body := strings.NewReader(data.Encode()) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint, body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint, nil) if err != nil { return nil, fmt.Errorf("error getting HTTP request: %w", err) } @@ -310,6 +323,8 @@ func getTokenExchangeRequest(ctx context.Context, tokenEndpoint, dpopNonce strin return nil, err } + req.Body = io.NopCloser(strings.NewReader(data.Encode())) + return req, nil } @@ -333,14 +348,13 @@ func DoCertExchange(ctx context.Context, tokenEndpoint string, exchangeInfo Cert } func getCertExchangeRequest(ctx context.Context, tokenEndpoint string, clientCredentials ClientCredentials, exchangeInfo CertExchangeInfo, key jwk.Key) (*http.Request, error) { - data := url.Values{"grant_type": {"password"}, "client_id": {clientCredentials.ClientID}, "username": {""}, "password": {""}} + data := url.Values{"grant_type": {"password"}, "username": {""}, "password": {""}} for _, a := range exchangeInfo.Audience { data.Add("audience", a) } - body := strings.NewReader(data.Encode()) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint, body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint, nil) if err != nil { return nil, err } @@ -356,5 +370,7 @@ func getCertExchangeRequest(ctx context.Context, tokenEndpoint string, clientCre return nil, err } + req.Body = io.NopCloser(strings.NewReader(data.Encode())) + return req, nil } diff --git a/sdk/auth/oauth/oauth_test.go b/sdk/auth/oauth/oauth_test.go index 91bd644b3e..2ac2a2393f 100644 --- a/sdk/auth/oauth/oauth_test.go +++ b/sdk/auth/oauth/oauth_test.go @@ -2,345 +2,24 @@ package oauth import ( "context" - "crypto" "crypto/rand" "crypto/rsa" - "crypto/tls" - "crypto/x509" - _ "embed" - "encoding/base64" - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "slices" + "io" + "net/url" "testing" "time" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/opentdf/platform/lib/fixtures" - "github.com/opentdf/platform/sdk/httputil" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - tc "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" ) -type OAuthSuite struct { - suite.Suite - dpopJWK jwk.Key - keycloakContainer tc.Container - keycloakEndpoint string - keycloakHTTPSEndpoint string -} - -func TestOAuthTestSuite(t *testing.T) { - suite.Run(t, new(OAuthSuite)) -} - -func (s *OAuthSuite) SetupSuite() { - // Generate RSA Key to use for DPoP - dpopKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - panic(err) - } - - dpopJWK, err := jwk.FromRaw(dpopKey) - s.Require().NoError(err) - s.Require().NoError(dpopJWK.Set("use", "sig")) - s.Require().NoError(dpopJWK.Set("alg", jwa.RS256.String())) - - s.dpopJWK = dpopJWK - ctx := context.Background() - - keycloak, idpEndpoint, idpHTTPSEndpoint := setupKeycloak(ctx, s.T()) - s.keycloakContainer = keycloak - s.keycloakEndpoint = idpEndpoint - s.keycloakHTTPSEndpoint = idpHTTPSEndpoint -} - -func (s *OAuthSuite) TearDownSuite() { - _ = s.keycloakContainer.Terminate(context.Background()) -} - -//go:embed testdata/new-ca.crt -var ca []byte - -func (s *OAuthSuite) TestCertExchangeFromKeycloak() { - clientCredentials := ClientCredentials{ - ClientID: "opentdf-sdk", - ClientAuth: "secret", - } - cert, err := tls.LoadX509KeyPair("testdata/sampleuser.crt", "testdata/sampleuser.key") - rootCAs, _ := x509.SystemCertPool() - rootCAs.AppendCertsFromPEM(ca) - s.Require().NoError(err) - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: []tls.Certificate{cert}, - RootCAs: rootCAs, - } - exhcangeInfo := CertExchangeInfo{ - HTTPClient: httputil.SafeHTTPClientWithTLSConfig(tlsConfig), - Audience: []string{"opentdf-sdk"}, - } - - tok, err := DoCertExchange( - context.Background(), - s.keycloakHTTPSEndpoint, - exhcangeInfo, - clientCredentials, - s.dpopJWK) - s.Require().NoError(err) - - tokenDetails, err := jwt.ParseString(tok.AccessToken, jwt.WithVerify(false)) - s.Require().NoError(err) - - cnfClaim, ok := tokenDetails.Get("cnf") - s.Require().True(ok) - cnfClaimsMap, ok := cnfClaim.(map[string]interface{}) - s.Require().True(ok) - idpKeyFingerprint, ok := cnfClaimsMap["jkt"].(string) - s.Require().True(ok) - s.Require().NotEmpty(idpKeyFingerprint) - pk, err := s.dpopJWK.PublicKey() - s.Require().NoError(err) - hash, err := pk.Thumbprint(crypto.SHA256) - s.Require().NoError(err) - - expectedThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) - s.Equal(expectedThumbprint, idpKeyFingerprint, "didn't get expected fingerprint") - s.Positivef(tok.ExpiresIn, "invalid expiration is before current time: %v", tok) - s.Falsef(tok.Expired(), "got a token that is currently expired: %v", tok) - - name, ok := tokenDetails.Get("name") - s.Require().True(ok) - s.Equal("sample user", name, "got unexpected name") -} - -func (s *OAuthSuite) TestGettingAccessTokenFromKeycloak() { - clientCredentials := ClientCredentials{ - ClientID: "opentdf-sdk", - ClientAuth: "secret", - } - - tok, err := GetAccessToken( - http.DefaultClient, - s.keycloakEndpoint, - []string{"testscope"}, - clientCredentials, - s.dpopJWK) - - s.Require().NoError(err) - - tokenDetails, err := jwt.ParseString(tok.AccessToken, jwt.WithVerify(false)) - s.Require().NoError(err) - - cnfClaim, ok := tokenDetails.Get("cnf") - s.Require().True(ok) - cnfClaimsMap, ok := cnfClaim.(map[string]interface{}) - s.Require().True(ok) - idpKeyFingerprint, ok := cnfClaimsMap["jkt"].(string) - s.Require().True(ok) - s.Require().NotEmpty(idpKeyFingerprint) - pk, err := s.dpopJWK.PublicKey() - s.Require().NoError(err) - hash, err := pk.Thumbprint(crypto.SHA256) - s.Require().NoError(err) - - scope, ok := tokenDetails.Get("scope") - s.Require().True(ok) - scopeString, ok := scope.(string) - s.Require().True(ok) - s.Require().Contains(scopeString, "testscope") - - expectedThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) - s.Equal(expectedThumbprint, idpKeyFingerprint, "didn't get expected fingerprint") - s.Positivef(tok.ExpiresIn, "invalid expiration is before current time: %v", tok) - s.Falsef(tok.Expired(), "got a token that is currently expired: %v", tok) - - // verify that we got a token that has the opentdf-standard role, which only the sdk client has - ra, ok := tokenDetails.Get("realm_access") - s.Require().True(ok) - raMap, ok := ra.(map[string]interface{}) - s.Require().True(ok) - roles, ok := raMap["roles"] - s.Require().True(ok) - rolesList, ok := roles.([]interface{}) - s.Require().True(ok) - s.Require().True(slices.Contains(rolesList, "opentdf-standard"), "missing the `opentdf-standard` role") -} - -func (s *OAuthSuite) TestDoingTokenExchangeWithKeycloak() { - ctx := context.Background() - - clientCredentials := ClientCredentials{ - ClientID: "opentdf-sdk", - ClientAuth: "secret", - } - - subjectToken, err := GetAccessToken( - http.DefaultClient, - s.keycloakEndpoint, - []string{"testscope"}, - clientCredentials, - s.dpopJWK) - s.Require().NoError(err) - - exchangeCredentials := ClientCredentials{ - ClientID: "opentdf", - ClientAuth: "secret", - } - - tokenExchange := TokenExchangeInfo{ - SubjectToken: subjectToken.AccessToken, - Audience: []string{"opentdf-sdk"}, - } - - exchangedTok, err := DoTokenExchange(ctx, http.DefaultClient, s.keycloakEndpoint, []string{}, exchangeCredentials, tokenExchange, s.dpopJWK) - s.Require().NoError(err) - - tokenDetails, err := jwt.ParseString(exchangedTok.AccessToken, jwt.WithVerify(false)) - s.Require().NoError(err) - - cnfClaim, ok := tokenDetails.Get("cnf") - s.Require().True(ok) - cnfClaimsMap, ok := cnfClaim.(map[string]interface{}) - s.Require().True(ok) - idpKeyFingerprint, ok := cnfClaimsMap["jkt"].(string) - s.Require().True(ok) - s.Require().NotEmpty(idpKeyFingerprint) - pk, err := s.dpopJWK.PublicKey() - s.Require().NoError(err) - hash, err := pk.Thumbprint(crypto.SHA256) - s.Require().NoError(err) - - expectedThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) - s.Equal(expectedThumbprint, idpKeyFingerprint, "didn't get expected fingerprint") - s.Positivef(subjectToken.ExpiresIn, "invalid expiration is before current time: %v", subjectToken) - s.Falsef(subjectToken.Expired(), "got a token that is currently expired: %v", subjectToken) - - // verify that we got a token that has the opentdf-standard role, which only the sdk client has - ra, ok := tokenDetails.Get("realm_access") - s.Require().True(ok) - raMap, ok := ra.(map[string]interface{}) - s.Require().True(ok) - roles, ok := raMap["roles"] - s.Require().True(ok) - rolesList, ok := roles.([]interface{}) - s.Require().True(ok) - s.Require().True(slices.Contains(rolesList, "opentdf-standard"), "missing the `opentdf-standard` role") - - // verify that the calling client is the authorized party - azpClaim, ok := tokenDetails.Get("azp") - s.Require().True(ok) - s.Require().Equal(exchangeCredentials.ClientID, azpClaim) - - // verify that the exchanged token has a scope that is only allowed for the client that got the original token - scope, ok := tokenDetails.Get("scope") - s.Require().True(ok) - scopeString, ok := scope.(string) - s.Require().True(ok) - s.Require().Contains(scopeString, "testscope") -} - -func (s *OAuthSuite) TestClientSecretNoNonce() { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - s.Equal("/token", r.URL.Path) - s.NoError(r.ParseForm()) - - validateBasicAuth(r, s.T()) - extractDPoPToken(r, s.T()) - - tok, err := jwt.NewBuilder(). - Issuer("example.org/fake"). - IssuedAt(time.Now()). - Build() - s.NoError(err) - - responseBytes, err := json.Marshal(tok) - s.NoError(err, "error writing response") - - w.Header().Add("Content-Type", "application/json") - _, err = w.Write(responseBytes) - s.NoError(err) - })) - defer server.Close() - - clientCredentials := ClientCredentials{ - ClientID: "theclient", - ClientAuth: "thesecret", - } - _, err := GetAccessToken(http.DefaultClient, server.URL+"/token", []string{"scope1", "scope2"}, clientCredentials, s.dpopJWK) - s.Require().NoError(err, "didn't get a token back from the IdP") -} - -func (s *OAuthSuite) TestClientSecretWithNonce() { - timesCalled := 0 - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - timesCalled++ - s.Equal("/token", r.URL.Path, "surprise http request to mock oauth service") - err := r.ParseForm() - s.NoError(err, "error parsing oauth request") - - validateBasicAuth(r, s.T()) - - if timesCalled == 1 { - w.Header().Add("DPoP-Nonce", "dfdffdfddf") - w.WriteHeader(http.StatusBadRequest) - _, err := w.Write([]byte{}) - s.NoError(err, "error writing response") - return - } else if timesCalled > 2 { - s.T().Logf("made more than two calls to the server: %d", timesCalled) - return - } - - // get the key we used to sign the DPoP token from the header - clientTok := extractDPoPToken(r, s.T()) - - nonce, exists := clientTok.Get("nonce") - if !exists { - s.T().Logf("didn't get nonce assertion") - } - - if nonceStr, ok := nonce.(string); ok { - if nonceStr != "dfdffdfddf" { - s.T().Errorf("Got incorrect nonce: %v", nonce) - } - } else { - s.T().Errorf("Nonce is not a string") - } - - tok, _ := jwt.NewBuilder(). - Issuer("example.org/fake"). - IssuedAt(time.Now()). - Build() - - responseBytes, err := json.Marshal(tok) - if err != nil { - s.T().Errorf("error writing response: %v", err) - } - - w.Header().Add("Content-Type", "application/json") - l, err := w.Write(responseBytes) - s.Len(responseBytes, l) - s.NoError(err) - })) - defer server.Close() - - clientCredentials := ClientCredentials{ - ClientID: "theclient", - ClientAuth: "thesecret", - } - _, err := GetAccessToken(http.DefaultClient, server.URL+"/token", []string{"scope1", "scope2"}, clientCredentials, s.dpopJWK) +func mustGenerateRSAKey(t *testing.T) *rsa.PrivateKey { + t.Helper() + k, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - s.T().Errorf("didn't get a token back from the IdP: %v", err) + t.Fatalf("rsa.GenerateKey: %v", err) } + return k } func TestTokenExpiration_RespectsLeeway(t *testing.T) { @@ -371,238 +50,74 @@ func TestTokenExpiration_RespectsLeeway(t *testing.T) { } } -func (s *OAuthSuite) TestSignedJWTWithNonce() { - // Generate RSA Key to use for DPoP - dpopKey, err := rsa.GenerateKey(rand.Reader, 4096) - s.Require().NoError(err, "error generating dpop key") - dpopJWK, err := jwk.FromRaw(dpopKey) - s.Require().NoError(err) - s.Require().NoError(dpopJWK.Set("use", "sig")) - s.Require().NoError(dpopJWK.Set("alg", jwa.RS256.String())) - - clientAuthKey, err := rsa.GenerateKey(rand.Reader, 4096) - s.Require().NoError(err, "error generating clientAuth key") - clientAuthJWK, err := jwk.FromRaw(clientAuthKey) - s.Require().NoError(err, "error constructing raw JWK") - s.Require().NoError(clientAuthJWK.Set("use", "sig")) - s.Require().NoError(clientAuthJWK.Set("alg", jwa.RS256.String())) - clientPublicKey, err := clientAuthJWK.PublicKey() - s.Require().NoError(err, "error getting public JWK from client auth JWK [%v]", clientAuthJWK) - - timesCalled := 0 - - var url string - getURL := func() string { - return url - } - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - timesCalled++ - - if r.URL.Path != "/token" { - s.T().Errorf("Expected to request '/token', got: %s", r.URL.Path) +// TestGetTokenExchangeRequest_SubjectTokenType verifies that the RFC 8693 required +// field subject_token_type is present in the token exchange POST body and defaults +// to access_token when not explicitly set. +func TestGetTokenExchangeRequest_SubjectTokenType(t *testing.T) { + makeKey := func(t *testing.T) jwk.Key { + t.Helper() + raw, err := jwk.FromRaw(mustGenerateRSAKey(t)) + if err != nil { + t.Fatalf("jwk.FromRaw: %v", err) } - s.NoError(r.ParseForm()) - - validateClientAssertionAuth(r, s.T(), getURL, "theclient", clientPublicKey) - - if timesCalled == 1 { - w.Header().Add("DPoP-Nonce", "dfdffdfddf") - w.WriteHeader(http.StatusBadRequest) - if _, err := w.Write([]byte{}); err != nil { - s.T().Errorf("error writing response: %v", err) - } - return - } else if timesCalled > 2 { - s.T().Logf("made more than two calls to the server: %d", timesCalled) - return + if err := raw.Set(jwk.AlgorithmKey, jwa.RS256); err != nil { + t.Fatalf("set algorithm: %v", err) } - - // get the key we used to sign the DPoP token from the header - clientTok := extractDPoPToken(r, s.T()) - - nonce, exists := clientTok.Get("nonce") - if exists { - value, ok := nonce.(string) - if !ok { - s.T().Errorf("Nonce is not a string") - } else if value != "dfdffdfddf" { - s.T().Errorf("Got incorrect nonce: %v", value) - } - } else { - s.T().Logf("didn't get nonce assertion") + return raw + } + + parseForm := func(t *testing.T, info TokenExchangeInfo) url.Values { + t.Helper() + key := makeKey(t) + req, err := getTokenExchangeRequest( + context.Background(), + "https://idp.example.com/token", + "", + []string{"openid"}, + ClientCredentials{ClientID: "test-client", ClientAuth: "test-secret"}, + info, + &key, + ) + if err != nil { + t.Fatalf("getTokenExchangeRequest: %v", err) } - - tok, _ := jwt.NewBuilder(). - Issuer("example.org/fake"). - IssuedAt(time.Now()). - Build() - - responseBytes, err := json.Marshal(tok) + body, err := io.ReadAll(req.Body) if err != nil { - s.T().Errorf("error writing response: %v", err) + t.Fatalf("read body: %v", err) } - - w.Header().Add("Content-Type", "application/json") - l, err := w.Write(responseBytes) - s.Len(responseBytes, l) - s.NoError(err) - })) - defer server.Close() - - clientCredentials := ClientCredentials{ - ClientID: "theclient", - ClientAuth: clientAuthJWK, - } - - url = server.URL + "/token" - - _, err = GetAccessToken(http.DefaultClient, url, []string{"scope1", "scope2"}, clientCredentials, dpopJWK) - if err != nil { - s.T().Errorf("didn't get a token back from the IdP: %v", err) - } -} - -/* -* -the token endpoint is a string _but_ we only have the value after we create the server -so we need a way get the value of the url after the server has started -* -*/ -func validateClientAssertionAuth(r *http.Request, t *testing.T, tokenEndpoint func() string, clientID string, key jwk.Key) { - if grant := r.Form.Get("grant_type"); grant != "client_credentials" { - t.Logf("got the wrong grant type: %s, expected client_credentials", grant) - } - if assertionType := r.Form.Get("client_assertion_type"); assertionType != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" { - t.Errorf("incorrect client assertion type: %s", assertionType) - } - - clientAssertion := r.Form.Get("client_assertion") - if clientAssertion == "" { - t.Errorf("missing client assertion") - } - - alg := key.Algorithm() - if alg == nil { - t.Logf("no key algorithm specified, using RS256 to verify client signature") - alg = jwa.RS256 - } - - tok, err := jwt.ParseString(clientAssertion, jwt.WithVerify(true), jwt.WithKey(alg, key)) - if err != nil { - t.Fatalf("error verifying client signature on token [%s]: %v", clientAssertion, err) - } - - if tok.Subject() != clientID { - t.Fatalf("incorrect subject: %s", tok.Subject()) - } - - if tok.Issuer() != clientID { - t.Fatalf("incorrect issuer: %s", tok.Issuer()) - } - - expectedAudience := tokenEndpoint() - if len(tok.Audience()) != 1 || tok.Audience()[0] != expectedAudience { - t.Fatalf("incorrect audience: %v", tok.Audience()) - } -} - -func validateBasicAuth(r *http.Request, t *testing.T) { - if grant := r.Form.Get("grant_type"); grant != "client_credentials" { - t.Logf("got the wrong grant type: %s, expected client_credentials", grant) - } - - username, password, ok := r.BasicAuth() - if !ok { - t.Errorf("missing basic auth") - } - if username != "theclient" || password != "thesecret" { - t.Errorf("failed to pass correct username and password. got %s:%s", username, password) - } -} - -func extractDPoPToken(r *http.Request, t *testing.T) jwt.Token { - dpop := r.Header.Get("dpop") - jwsMessage, err := jws.ParseString(dpop) - if err != nil { - t.Errorf("error parsing dpop payload as JWS: %v", err) - } - - sig := jwsMessage.Signatures()[0] - signingKey := sig.ProtectedHeaders().JWK() - - clientTok, err := jwt.ParseString(dpop, jwt.WithVerify(true), jwt.WithKey(signingKey.Algorithm(), signingKey)) - if err != nil { - t.Errorf("failed to parse/verify the dpop token: %v", err) - } - - return clientTok -} - -func setupKeycloak(ctx context.Context, t *testing.T) (tc.Container, string, string) { - containerReq := tc.ContainerRequest{ - Image: "ghcr.io/opentdf/keycloak:sha-8a6d35a", - ExposedPorts: []string{"8082/tcp", "8083/tcp"}, - Cmd: []string{ - "start-dev", "--http-port=8082", "--https-port=8083", "--features=preview", "--verbose", - "-Djavax.net.ssl.trustStorePassword=password", "-Djavax.net.ssl.HostnameVerifier=AllowAll", - "-Djavax.net.debug=ssl", - "-Djavax.net.ssl.trustStore=/truststore/truststore.jks", - "--spi-truststore-file-hostname-verification-policy=ANY", - }, - Files: []tc.ContainerFile{ - {HostFilePath: "testdata/new-ca.jks", ContainerFilePath: "/truststore/truststore.jks", FileMode: int64(0o777)}, - {HostFilePath: "testdata/localhost.crt", ContainerFilePath: "/etc/x509/tls/localhost.crt", FileMode: int64(0o777)}, - {HostFilePath: "testdata/localhost.key", ContainerFilePath: "/etc/x509/tls/localhost.key", FileMode: int64(0o777)}, - }, - Env: map[string]string{ - "KEYCLOAK_ADMIN": "admin", - "KEYCLOAK_ADMIN_PASSWORD": "admin", - "KC_HTTPS_KEY_STORE_PASSWORD": "password", - "KC_HTTPS_KEY_STORE_FILE": "/truststore/truststore.jks", - "KC_HTTPS_CERTIFICATE_FILE": "/etc/x509/tls/localhost.crt", - "KC_HTTPS_CERTIFICATE_KEY_FILE": "/etc/x509/tls/localhost.key", - "KC_HTTPS_CLIENT_AUTH": "request", - }, - - WaitingFor: wait.ForLog("Running the server"), + form, err := url.ParseQuery(string(body)) + if err != nil { + t.Fatalf("parse form: %v", err) + } + return form } - var providerType tc.ProviderType - - if os.Getenv("TESTCONTAINERS_PODMAN") == "true" { - providerType = tc.ProviderPodman - } else { - providerType = tc.ProviderDocker - } + const idTokenURN = "urn:ietf:params:oauth:token-type:id_token" - keycloak, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{ - ProviderType: providerType, - ContainerRequest: containerReq, - Started: true, + t.Run("defaults to access_token when SubjectTokenType is empty", func(t *testing.T) { + form := parseForm(t, TokenExchangeInfo{SubjectToken: "tok"}) + if got := form.Get("subject_token_type"); got != defaultSubjectTokenType { + t.Errorf("subject_token_type = %q, want %q", got, defaultSubjectTokenType) + } }) - if err != nil { - t.Fatalf("error starting keycloak container: %v", err) - } - port, _ := keycloak.MappedPort(ctx, "8082") - keycloakBase := "http://localhost:" + port.Port() - - httpPort, _ := keycloak.MappedPort(ctx, "8083") - keycloakHTTPSBase := "https://localhost:" + httpPort.Port() - realm := "test" - - connectParams := fixtures.KeycloakConnectParams{ - BasePath: keycloakBase, - Username: "admin", - Password: "admin", - Realm: realm, - Audience: "https://test.example.org", - AllowInsecureTLS: true, - } - - err = fixtures.SetupKeycloak(ctx, connectParams) - require.NoError(t, err) + t.Run("uses caller-supplied SubjectTokenType", func(t *testing.T) { + form := parseForm(t, TokenExchangeInfo{SubjectToken: "tok", SubjectTokenType: idTokenURN}) + if got := form.Get("subject_token_type"); got != idTokenURN { + t.Errorf("subject_token_type = %q, want %q", got, idTokenURN) + } + }) - return keycloak, keycloakBase + "/realms/" + realm + "/protocol/openid-connect/token", keycloakHTTPSBase + "/realms/" + realm + "/protocol/openid-connect/token" + t.Run("required RFC 8693 fields are present", func(t *testing.T) { + form := parseForm(t, TokenExchangeInfo{SubjectToken: "my-subject-token"}) + if got := form.Get("grant_type"); got != "urn:ietf:params:oauth:grant-type:token-exchange" { + t.Errorf("grant_type = %q, want token-exchange URN", got) + } + if got := form.Get("subject_token"); got != "my-subject-token" { + t.Errorf("subject_token = %q, want %q", got, "my-subject-token") + } + if got := form.Get("subject_token_type"); got == "" { + t.Error("subject_token_type must not be empty") + } + }) } diff --git a/sdk/basekey.go b/sdk/basekey.go index dde7e179d5..96f43f927c 100644 --- a/sdk/basekey.go +++ b/sdk/basekey.go @@ -20,37 +20,55 @@ const ( baseKeyPublicKey = "public_key" ) -// TODO: Move this function to ocrypto? -func getKasKeyAlg(alg string) policy.Algorithm { - switch alg { - case string(ocrypto.RSA2048Key): - return policy.Algorithm_ALGORITHM_RSA_2048 - case string(ocrypto.RSA4096Key): - return policy.Algorithm_ALGORITHM_RSA_4096 - case string(ocrypto.EC256Key): - return policy.Algorithm_ALGORITHM_EC_P256 - case string(ocrypto.EC384Key): - return policy.Algorithm_ALGORITHM_EC_P384 - case string(ocrypto.EC521Key): - return policy.Algorithm_ALGORITHM_EC_P521 +func getKasKeyAlg(alg string) (policy.Algorithm, error) { + kt, err := ocrypto.ParseKeyType(alg) + if err != nil { + return policy.Algorithm_ALGORITHM_UNSPECIFIED, fmt.Errorf("invalid alg [%s]: %w", alg, err) + } + return KeyTypeToPolicyAlgorithm(kt) +} + +func KeyTypeToPolicyAlgorithm(kt ocrypto.KeyType) (policy.Algorithm, error) { + switch kt { + case ocrypto.RSA2048Key: + return policy.Algorithm_ALGORITHM_RSA_2048, nil + case ocrypto.RSA4096Key: + return policy.Algorithm_ALGORITHM_RSA_4096, nil + case ocrypto.EC256Key: + return policy.Algorithm_ALGORITHM_EC_P256, nil + case ocrypto.EC384Key: + return policy.Algorithm_ALGORITHM_EC_P384, nil + case ocrypto.EC521Key: + return policy.Algorithm_ALGORITHM_EC_P521, nil + case ocrypto.HybridXWingKey: + return policy.Algorithm_ALGORITHM_HPQT_XWING, nil + case ocrypto.HybridSecp256r1MLKEM768Key: + return policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, nil + case ocrypto.HybridSecp384r1MLKEM1024Key: + return policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, nil default: - return policy.Algorithm_ALGORITHM_UNSPECIFIED + return policy.Algorithm_ALGORITHM_UNSPECIFIED, fmt.Errorf("unknown key type: %s", kt) } } -// TODO: Move this function to ocrypto? -func formatAlg(alg policy.Algorithm) (string, error) { +func PolicyAlgorithmToKeyType(alg policy.Algorithm) (ocrypto.KeyType, error) { switch alg { case policy.Algorithm_ALGORITHM_RSA_2048: - return string(ocrypto.RSA2048Key), nil + return ocrypto.RSA2048Key, nil case policy.Algorithm_ALGORITHM_RSA_4096: - return string(ocrypto.RSA4096Key), nil + return ocrypto.RSA4096Key, nil case policy.Algorithm_ALGORITHM_EC_P256: - return string(ocrypto.EC256Key), nil + return ocrypto.EC256Key, nil case policy.Algorithm_ALGORITHM_EC_P384: - return string(ocrypto.EC384Key), nil + return ocrypto.EC384Key, nil case policy.Algorithm_ALGORITHM_EC_P521: - return string(ocrypto.EC521Key), nil + return ocrypto.EC521Key, nil + case policy.Algorithm_ALGORITHM_HPQT_XWING: + return ocrypto.HybridXWingKey, nil + case policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768: + return ocrypto.HybridSecp256r1MLKEM768Key, nil + case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: + return ocrypto.HybridSecp384r1MLKEM1024Key, nil case policy.Algorithm_ALGORITHM_UNSPECIFIED: fallthrough default: @@ -58,6 +76,17 @@ func formatAlg(alg policy.Algorithm) (string, error) { } } +func formatAlg(alg policy.Algorithm) (string, error) { + kt, err := PolicyAlgorithmToKeyType(alg) + return string(kt), err +} + +// GetBaseKey retrieves the platform base KAS key from the well-known configuration. +// The returned key material is expected to be public (algorithm, KID, PEM). +func (s SDK) GetBaseKey(ctx context.Context) (*policy.SimpleKasKey, error) { + return getBaseKey(ctx, s) +} + func getBaseKey(ctx context.Context, s SDK) (*policy.SimpleKasKey, error) { req := &wellknownconfiguration.GetWellKnownConfigurationRequest{} response, err := s.wellknownConfiguration.GetWellKnownConfiguration(ctx, req) @@ -108,7 +137,11 @@ func parseSimpleKasKey(baseKeyMap map[string]interface{}) (*policy.SimpleKasKey, if !ok { return nil, ErrBaseKeyInvalidFormat } - publicKey[baseKeyAlg] = getKasKeyAlg(alg) + a, err := getKasKeyAlg(alg) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrMarshalBaseKeyFailed, err) + } + publicKey[baseKeyAlg] = a baseKeyMap[baseKeyPublicKey] = publicKey configJSON, err := json.Marshal(baseKeyMap) if err != nil { diff --git a/sdk/basekey_test.go b/sdk/basekey_test.go index 4a6faccddf..74ad8f5d95 100644 --- a/sdk/basekey_test.go +++ b/sdk/basekey_test.go @@ -63,7 +63,7 @@ func (m *mockWellKnownService) GetWellKnownConfiguration( } func TestGetKasKeyAlg(t *testing.T) { - tests := []struct { + for _, test := range []struct { name string algStr string expected policy.Algorithm @@ -94,21 +94,45 @@ func TestGetKasKeyAlg(t *testing.T) { expected: policy.Algorithm_ALGORITHM_EC_P521, }, { - name: "unsupported algorithm", - algStr: "unsupported", - expected: policy.Algorithm_ALGORITHM_UNSPECIFIED, + name: "hybrid xwing", + algStr: string(ocrypto.HybridXWingKey), + expected: policy.Algorithm_ALGORITHM_HPQT_XWING, + }, + { + name: "hybrid p256 mlkem768", + algStr: string(ocrypto.HybridSecp256r1MLKEM768Key), + expected: policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, }, { - name: "empty string", - algStr: "", - expected: policy.Algorithm_ALGORITHM_UNSPECIFIED, + name: "hybrid p384 mlkem1024", + algStr: string(ocrypto.HybridSecp384r1MLKEM1024Key), + expected: policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, }, + } { + t.Run(test.name, func(t *testing.T) { + result, err := getKasKeyAlg(test.algStr) + require.NoError(t, err) + assert.Equal(t, test.expected, result, "Algorithm enum mismatch") + }) } - for _, test := range tests { + for _, test := range []struct { + name string + algStr string + }{ + { + name: "unsupported algorithm", + algStr: "unsupported", + }, + { + name: "empty string", + algStr: "", + }, + } { t.Run(test.name, func(t *testing.T) { - result := getKasKeyAlg(test.algStr) - assert.Equal(t, test.expected, result, "Algorithm enum mismatch") + result, err := getKasKeyAlg(test.algStr) + require.Error(t, err) + assert.Equal(t, policy.Algorithm_ALGORITHM_UNSPECIFIED, result, "Algorithm enum mismatch") }) } } @@ -150,6 +174,24 @@ func TestFormatAlg(t *testing.T) { expected: string(ocrypto.EC521Key), // Note: This matches the implementation expectError: false, }, + { + name: "Hybrid X-Wing", + alg: policy.Algorithm_ALGORITHM_HPQT_XWING, + expected: string(ocrypto.HybridXWingKey), + expectError: false, + }, + { + name: "Hybrid P256+ML-KEM-768", + alg: policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, + expected: string(ocrypto.HybridSecp256r1MLKEM768Key), + expectError: false, + }, + { + name: "Hybrid P384+ML-KEM-1024", + alg: policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, + expected: string(ocrypto.HybridSecp384r1MLKEM1024Key), + expectError: false, + }, { name: "Unspecified algorithm", alg: policy.Algorithm_ALGORITHM_UNSPECIFIED, @@ -202,8 +244,8 @@ func (s *BaseKeyTestSuite) TestGetBaseKeySuccess() { mockService := newMockWellKnownService(wellknownConfig, nil) s.sdk.wellknownConfiguration = mockService - // Call getBaseKey - baseKey, err := getBaseKey(s.T().Context(), s.sdk) + // Call exported API + baseKey, err := s.sdk.GetBaseKey(s.T().Context()) // Validate result s.Require().NoError(err) @@ -221,8 +263,8 @@ func (s *BaseKeyTestSuite) TestGetBaseKeyServiceError() { mockService := newMockWellKnownService(nil, errors.New("service unavailable")) s.sdk.wellknownConfiguration = mockService - // Call getBaseKey - baseKey, err := getBaseKey(s.T().Context(), s.sdk) + // Call exported API + baseKey, err := s.sdk.GetBaseKey(s.T().Context()) // Validate result s.Require().True(mockService.called) @@ -241,8 +283,8 @@ func (s *BaseKeyTestSuite) TestGetBaseKeyMissingBaseKey() { mockService := newMockWellKnownService(wellknownConfig, nil) s.sdk.wellknownConfiguration = mockService - // Call getBaseKey - baseKey, err := getBaseKey(s.T().Context(), s.sdk) + // Call exported API + baseKey, err := s.sdk.GetBaseKey(s.T().Context()) // Validate result s.Require().True(mockService.called) @@ -259,8 +301,8 @@ func (s *BaseKeyTestSuite) TestGetBaseKeyInvalidBaseKeyFormat() { mockService := newMockWellKnownService(wellknownConfig, nil) s.sdk.wellknownConfiguration = mockService - // Call getBaseKey - baseKey, err := getBaseKey(s.T().Context(), s.sdk) + // Call exported API + baseKey, err := s.sdk.GetBaseKey(s.T().Context()) // Validate result s.Require().True(mockService.called) @@ -278,8 +320,8 @@ func (s *BaseKeyTestSuite) TestGetBaseKeyEmptyBaseKey() { mockService := newMockWellKnownService(wellknownConfig, nil) s.sdk.wellknownConfiguration = mockService - // Call getBaseKey - baseKey, err := getBaseKey(s.T().Context(), s.sdk) + // Call exported API + baseKey, err := s.sdk.GetBaseKey(s.T().Context()) // Validate result s.Require().True(mockService.called) @@ -300,8 +342,8 @@ func (s *BaseKeyTestSuite) TestGetBaseKeyMissingPublicKey() { mockService := newMockWellKnownService(wellknownConfig, nil) s.sdk.wellknownConfiguration = mockService - // Call getBaseKey - baseKey, err := getBaseKey(s.T().Context(), s.sdk) + // Call exported API + baseKey, err := s.sdk.GetBaseKey(s.T().Context()) // Validate result s.Require().True(mockService.called) @@ -310,6 +352,39 @@ func (s *BaseKeyTestSuite) TestGetBaseKeyMissingPublicKey() { s.Require().ErrorIs(err, ErrBaseKeyInvalidFormat) } +// TestFormatAlg_GetKasKeyAlg_RoundTrip verifies that every supported algorithm +// survives a round-trip through the SDK's own formatAlg → getKasKeyAlg path. +// This locks in the SDK-side contract: formatAlg must produce strings that +// getKasKeyAlg maps back to the original enum. +func TestFormatAlg_GetKasKeyAlg_RoundTrip(t *testing.T) { + supportedAlgs := []struct { + name string + alg policy.Algorithm + }{ + {"RSA-2048", policy.Algorithm_ALGORITHM_RSA_2048}, + {"RSA-4096", policy.Algorithm_ALGORITHM_RSA_4096}, + {"EC-P256", policy.Algorithm_ALGORITHM_EC_P256}, + {"EC-P384", policy.Algorithm_ALGORITHM_EC_P384}, + {"EC-P521", policy.Algorithm_ALGORITHM_EC_P521}, + {"HPQT-XWing", policy.Algorithm_ALGORITHM_HPQT_XWING}, + {"HPQT-P256-MLKEM768", policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768}, + {"HPQT-P384-MLKEM1024", policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024}, + } + + for _, tc := range supportedAlgs { + t.Run(tc.name, func(t *testing.T) { + formatted, err := formatAlg(tc.alg) + require.NoError(t, err, "formatAlg should not error for %s", tc.name) + + roundTripped, err := getKasKeyAlg(formatted) + require.NoError(t, err) + assert.Equal(t, tc.alg, roundTripped, + "round-trip mismatch: formatAlg(%s) = %q → getKasKeyAlg returned %s, want %s", + tc.name, formatted, roundTripped, tc.alg) + }) + } +} + func (s *BaseKeyTestSuite) TestGetBaseKeyInvalidPublicKey() { // Create base key with invalid public_key (string instead of map) wellknownConfig := map[string]interface{}{ @@ -322,8 +397,8 @@ func (s *BaseKeyTestSuite) TestGetBaseKeyInvalidPublicKey() { mockService := newMockWellKnownService(wellknownConfig, nil) s.sdk.wellknownConfiguration = mockService - // Call getBaseKey - baseKey, err := getBaseKey(s.T().Context(), s.sdk) + // Call exported API + baseKey, err := s.sdk.GetBaseKey(s.T().Context()) // Validate result s.Require().True(mockService.called) diff --git a/sdk/bulk.go b/sdk/bulk.go index 9cd2623803..8582c24d2d 100644 --- a/sdk/bulk.go +++ b/sdk/bulk.go @@ -19,12 +19,11 @@ type BulkTDF struct { } type BulkDecryptRequest struct { - TDFs []*BulkTDF - TDF3DecryptOptions []TDFReaderOption // Options for TDF3 Decryptor - NanoTDFDecryptOptions []NanoTDFReaderOption // Options for Nano TDF Decryptor - TDFType TdfType - kasAllowlist AllowList - ignoreAllowList bool + TDFs []*BulkTDF + TDF3DecryptOptions []TDFReaderOption // Options for TDF3 Decryptor + TDFType TdfType + kasAllowlist AllowList + ignoreAllowList bool } // BulkDecryptPrepared holds the prepared state for bulk decryption @@ -97,13 +96,6 @@ func WithTDF3DecryptOptions(options ...TDFReaderOption) BulkDecryptOption { } } -func WithNanoTDFDecryptOptions(options ...NanoTDFReaderOption) BulkDecryptOption { - return func(request *BulkDecryptRequest) error { - request.NanoTDFDecryptOptions = append(request.NanoTDFDecryptOptions, options...) - return nil - } -} - func createBulkRewrapRequest(options ...BulkDecryptOption) (*BulkDecryptRequest, error) { req := &BulkDecryptRequest{} for _, opt := range options { @@ -117,8 +109,6 @@ func createBulkRewrapRequest(options ...BulkDecryptOption) (*BulkDecryptRequest, func (s SDK) createDecryptor(tdf *BulkTDF, req *BulkDecryptRequest) (decryptor, error) { switch req.TDFType { - case Nano: - return createNanoTDFDecryptHandler(tdf.Reader, tdf.Writer, req.NanoTDFDecryptOptions...) case Standard: return s.createTDF3DecryptHandler(tdf.Writer, tdf.Reader, req.TDF3DecryptOptions...) case Invalid: @@ -140,7 +130,6 @@ func (s SDK) setupKasAllowlist(ctx context.Context, bulkReq *BulkDecryptRequest) return fmt.Errorf("failed to get allowlist from registry: %w", err) } bulkReq.kasAllowlist = allowlist - bulkReq.NanoTDFDecryptOptions = append(bulkReq.NanoTDFDecryptOptions, withNanoKasAllowlist(bulkReq.kasAllowlist)) bulkReq.TDF3DecryptOptions = append(bulkReq.TDF3DecryptOptions, withKasAllowlist(bulkReq.kasAllowlist)) } else { s.Logger().Error("no KAS allowlist provided and no KeyAccessServerRegistry available") @@ -204,12 +193,7 @@ func (s SDK) performRewraps(ctx context.Context, bulkReq *BulkDecryptRequest, ka } var rewrapResp map[string][]kaoResult - switch bulkReq.TDFType { - case Nano: - rewrapResp, err = kasClient.nanoUnwrap(ctx, rewrapRequests...) - case Standard, Invalid: - rewrapResp, err = kasClient.unwrap(ctx, rewrapRequests...) - } + rewrapResp, err = kasClient.unwrap(ctx, rewrapRequests...) for id, res := range rewrapResp { allRewrapResp[id] = append(allRewrapResp[id], res...) @@ -322,8 +306,6 @@ func getFulfillableObligations(decryptor decryptor, logger *slog.Logger) []strin switch d := decryptor.(type) { case *tdf3DecryptHandler: return d.reader.config.fulfillableObligationFQNs - case *NanoTDFDecryptHandler: - return d.config.fulfillableObligationFQNs default: logger.Warn("unknown decryptor type, cannot populate obligations", slog.String("type", fmt.Sprintf("%T", d))) return make([]string, 0) diff --git a/sdk/codegen/runner/generate.go b/sdk/codegen/runner/generate.go index 5d663cb051..756b1e5c56 100644 --- a/sdk/codegen/runner/generate.go +++ b/sdk/codegen/runner/generate.go @@ -7,6 +7,7 @@ import ( "os" "path" "path/filepath" + "strings" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -141,7 +142,7 @@ func New%s%s%sConnectWrapper(httpClient connect.HTTPClient, baseURL string, opts prefix, interfaceName, packagePath, - packagePath+"/"+connectPackageName, + path.Join(packagePath, connectPackageName), prefix, interfaceName, suffix, @@ -161,27 +162,30 @@ func New%s%s%sConnectWrapper(httpClient connect.HTTPClient, baseURL string, opts interfaceName) // Generate the interface type definition - wrapperCode += generateInterfaceType(interfaceName, methods, packageName, prefix, suffix) + var builder strings.Builder + builder.WriteString(wrapperCode) + builder.WriteString(generateInterfaceType(interfaceName, methods, packageName, prefix, suffix)) // Now generate a wrapper function for each method in the interface for _, method := range methods { - wrapperCode += generateWrapperMethod(interfaceName, method, packageName, prefix, suffix) + builder.WriteString(generateWrapperMethod(interfaceName, method, packageName, prefix, suffix)) } // Output the generated wrapper code - return wrapperCode + return builder.String() } func generateInterfaceType(interfaceName string, methods []string, packageName, prefix, suffix string) string { // Generate the interface type definition - interfaceType := fmt.Sprintf(` + var builder strings.Builder + builder.WriteString(fmt.Sprintf(` type %s%s%s interface { -`, prefix, interfaceName, suffix) +`, prefix, interfaceName, suffix)) for _, method := range methods { - interfaceType += fmt.Sprintf(` %s(ctx context.Context, req *%s.%sRequest) (*%s.%sResponse, error) -`, method, packageName, method, packageName, method) + builder.WriteString(fmt.Sprintf(` %s(ctx context.Context, req *%s.%sRequest) (*%s.%sResponse, error) +`, method, packageName, method, packageName, method)) } - interfaceType += "}\n" - return interfaceType + builder.WriteString("}\n") + return builder.String() } // Generate the wrapper method for a specific method in the interface diff --git a/sdk/discovery.go b/sdk/discovery.go new file mode 100644 index 0000000000..31f1f26c91 --- /dev/null +++ b/sdk/discovery.go @@ -0,0 +1,217 @@ +package sdk + +import ( + "context" + "errors" + "fmt" + "strings" + + "connectrpc.com/connect" + "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" +) + +const ( + // maxListAttributesPages caps the pagination loop in ListAttributes to prevent + // unbounded memory growth if a server repeatedly returns a non-zero next_offset. + maxListAttributesPages = 1000 + + // maxValidateFQNs matches the server-side limit on GetAttributeValuesByFqns + // so callers get a clear local error instead of a cryptic server rejection. + maxValidateFQNs = 250 +) + +// ListAttributes returns all active attributes available on the platform, auto-paginating +// through all results. An optional namespace name or ID may be provided to filter results. +// +// Use this before calling CreateTDF() to see what attributes are available for data tagging. +// +// Example: +// +// attrs, err := sdk.ListAttributes(ctx) +// for _, a := range attrs { +// fmt.Println(a.GetFqn()) +// } +func (s SDK) ListAttributes(ctx context.Context, namespace ...string) ([]*policy.Attribute, error) { + if len(namespace) > 1 { + return nil, fmt.Errorf("ListAttributes accepts at most one namespace filter, got %d", len(namespace)) + } + req := &attributes.ListAttributesRequest{} + if len(namespace) == 1 { + req.Namespace = namespace[0] + } + + var result []*policy.Attribute + for pages := 0; pages < maxListAttributesPages; pages++ { + resp, err := s.Attributes.ListAttributes(ctx, req) + if err != nil { + return nil, fmt.Errorf("listing attributes: %w", err) + } + if pages == 0 { + if total := resp.GetPagination().GetTotal(); total > 0 { + result = make([]*policy.Attribute, 0, total) + } + } + result = append(result, resp.GetAttributes()...) + + nextOffset := resp.GetPagination().GetNextOffset() + if nextOffset == 0 { + return result, nil + } + req.Pagination = &policy.PageRequest{Offset: nextOffset} + } + return nil, fmt.Errorf("listing attributes: exceeded maximum page limit (%d)", maxListAttributesPages) +} + +// AttributeExists reports whether the attribute definition identified by attributeFqn +// exists on the platform. +// +// attributeFqn should be an attribute-level FQN (no /value/ segment): +// +// https:///attr/ +// +// Returns (true, nil) if the attribute exists, (false, nil) if it does not, +// and (false, err) if a service error occurs. +func (s SDK) AttributeExists(ctx context.Context, attributeFqn string) (bool, error) { + if _, err := NewAttributeNameFQN(attributeFqn); err != nil { + return false, fmt.Errorf("invalid attribute FQN %q: %w", attributeFqn, err) + } + + _, err := s.Attributes.GetAttribute(ctx, &attributes.GetAttributeRequest{ + Identifier: &attributes.GetAttributeRequest_Fqn{Fqn: attributeFqn}, + }) + if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + return false, nil + } + return false, fmt.Errorf("checking attribute existence: %w", err) + } + return true, nil +} + +// AttributeValueExists reports whether the attribute value FQN exists on the platform. +// +// fqn should be a full attribute value FQN (with /value/ segment): +// +// https:///attr//value/ +// +// Returns (true, nil) if the value exists, (false, nil) if it does not, +// and (false, err) if a service error occurs. +func (s SDK) AttributeValueExists(ctx context.Context, fqn string) (bool, error) { + if _, err := NewAttributeValueFQN(fqn); err != nil { + return false, fmt.Errorf("invalid attribute value FQN %q: %w", fqn, err) + } + + resp, err := s.Attributes.GetAttributeValuesByFqns(ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: []string{fqn}, + }) + if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + return false, nil + } + return false, fmt.Errorf("checking attribute value existence: %w", err) + } + + _, found := resp.GetFqnAttributeValues()[fqn] + return found, nil +} + +// ValidateAttributes checks that all provided attribute value FQNs exist on the platform. +// This provides fail-fast behavior: validate attributes before calling CreateTDF() to avoid +// late-stage decryption failures caused by missing or misspelled attributes. +// +// fqns should be full attribute value FQNs in the form: +// +// https:///attr//value/ +// +// Returns ErrAttributeNotFound if any FQNs are missing, with the missing FQNs listed in +// the error message. +// +// Example: +// +// err := sdk.ValidateAttributes(ctx, +// "https://example.com/attr/classification/value/secret", +// "https://example.com/attr/clearance/value/top-secret", +// ) +// if err != nil { +// log.Fatalf("attributes not found: %v", err) +// } +func (s SDK) ValidateAttributes(ctx context.Context, fqns ...string) error { + if len(fqns) == 0 { + return nil + } + + if len(fqns) > maxValidateFQNs { + return fmt.Errorf("too many attribute FQNs: %d exceeds maximum of %d", len(fqns), maxValidateFQNs) + } + + for _, fqn := range fqns { + if _, err := NewAttributeValueFQN(fqn); err != nil { + return fmt.Errorf("invalid attribute value FQN %q: %w", fqn, err) + } + } + + resp, err := s.Attributes.GetAttributeValuesByFqns(ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: fqns, + }) + if err != nil { + return fmt.Errorf("validating attributes: %w", err) + } + + found := resp.GetFqnAttributeValues() + var missing []string + for _, fqn := range fqns { + if _, ok := found[fqn]; !ok { + missing = append(missing, fqn) + } + } + if len(missing) > 0 { + return fmt.Errorf("%w: %s", ErrAttributeNotFound, strings.Join(missing, ", ")) + } + return nil +} + +// GetEntityAttributes returns the attribute value FQNs assigned to an entity (PE or NPE). +// Use this to inspect what attributes a user, service account, or other entity has been +// granted before making authorization decisions or constructing access policies. +// +// The entity parameter identifies the subject. Use the appropriate field for the entity type: +// +// // By email address +// entity := &authorization.Entity{Id: "e1", EntityType: &authorization.Entity_EmailAddress{EmailAddress: "user@example.com"}} +// +// // By username +// entity := &authorization.Entity{Id: "e1", EntityType: &authorization.Entity_UserName{UserName: "alice"}} +// +// // By client ID (NPE / service account) +// entity := &authorization.Entity{Id: "e1", EntityType: &authorization.Entity_ClientId{ClientId: "my-service"}} +// +// // By UUID +// entity := &authorization.Entity{Id: "e1", EntityType: &authorization.Entity_Uuid{Uuid: "550e8400-e29b-41d4-a716-446655440000"}} +// +// Returns a slice of attribute value FQNs (e.g., "https://example.com/attr/clearance/value/secret"). +func (s SDK) GetEntityAttributes(ctx context.Context, entity *authorization.Entity) ([]string, error) { + if entity == nil { + return nil, errors.New("entity must not be nil") + } + + resp, err := s.Authorization.GetEntitlements(ctx, &authorization.GetEntitlementsRequest{ + Entities: []*authorization.Entity{entity}, + }) + if err != nil { + return nil, fmt.Errorf("getting entity attributes: %w", err) + } + + // GetEntitlements returns a slice of EntityEntitlements keyed by entity ID. + // Even though we only request one entity, we must match by ID to locate the + // correct entry — the response slice position is not guaranteed to correspond + // to the request slice position. + entityID := entity.GetId() + for _, e := range resp.GetEntitlements() { + if e.GetEntityId() == entityID { + return e.GetAttributeValueFqns(), nil + } + } + return nil, nil +} diff --git a/sdk/discovery_test.go b/sdk/discovery_test.go new file mode 100644 index 0000000000..d5cc8ff4e8 --- /dev/null +++ b/sdk/discovery_test.go @@ -0,0 +1,524 @@ +package sdk + +import ( + "context" + "errors" + "fmt" + "testing" + + "connectrpc.com/connect" + "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/sdk/sdkconnect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockDiscoveryAttributesClient is a test double for AttributesServiceClient. +type mockDiscoveryAttributesClient struct { + sdkconnect.AttributesServiceClient + + listAttributesFunc func(ctx context.Context, req *attributes.ListAttributesRequest) (*attributes.ListAttributesResponse, error) + getAttributeValuesByFqnsFunc func(ctx context.Context, req *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) + getAttributeFunc func(ctx context.Context, req *attributes.GetAttributeRequest) (*attributes.GetAttributeResponse, error) +} + +func (m *mockDiscoveryAttributesClient) ListAttributes(ctx context.Context, req *attributes.ListAttributesRequest) (*attributes.ListAttributesResponse, error) { + return m.listAttributesFunc(ctx, req) +} + +func (m *mockDiscoveryAttributesClient) GetAttributeValuesByFqns(ctx context.Context, req *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return m.getAttributeValuesByFqnsFunc(ctx, req) +} + +func (m *mockDiscoveryAttributesClient) GetAttribute(ctx context.Context, req *attributes.GetAttributeRequest) (*attributes.GetAttributeResponse, error) { + return m.getAttributeFunc(ctx, req) +} + +// mockDiscoveryAuthzClient is a test double for AuthorizationServiceClient. +type mockDiscoveryAuthzClient struct { + sdkconnect.AuthorizationServiceClient + + getEntitlementsFunc func(ctx context.Context, req *authorization.GetEntitlementsRequest) (*authorization.GetEntitlementsResponse, error) +} + +func (m *mockDiscoveryAuthzClient) GetEntitlements(ctx context.Context, req *authorization.GetEntitlementsRequest) (*authorization.GetEntitlementsResponse, error) { + return m.getEntitlementsFunc(ctx, req) +} + +// newDiscoverySDK creates a minimal SDK with mock service clients for discovery tests. +func newDiscoverySDK(attrClient sdkconnect.AttributesServiceClient, authzClient sdkconnect.AuthorizationServiceClient) SDK { + s := SDK{} + s.Attributes = attrClient + s.Authorization = authzClient + return s +} + +// makeAttr is a test helper to build a policy.Attribute. +func makeAttr(fqn string) *policy.Attribute { + return &policy.Attribute{Fqn: fqn} +} + +// fqnMap is a test helper to build a GetAttributeValuesByFqns response map. +func fqnMap(fqns ...string) map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue { + m := make(map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue, len(fqns)) + for _, f := range fqns { + m[f] = &attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue{} + } + return m +} + +// --- ListAttributes tests --- + +func TestListAttributes_Empty(t *testing.T) { + attrClient := &mockDiscoveryAttributesClient{ + listAttributesFunc: func(_ context.Context, _ *attributes.ListAttributesRequest) (*attributes.ListAttributesResponse, error) { + return &attributes.ListAttributesResponse{}, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + result, err := s.ListAttributes(t.Context()) + require.NoError(t, err) + assert.Empty(t, result) +} + +func TestListAttributes_SinglePage(t *testing.T) { + expected := []*policy.Attribute{ + makeAttr("https://example.com/attr/level/value/high"), + makeAttr("https://example.com/attr/level/value/low"), + } + attrClient := &mockDiscoveryAttributesClient{ + listAttributesFunc: func(_ context.Context, _ *attributes.ListAttributesRequest) (*attributes.ListAttributesResponse, error) { + return &attributes.ListAttributesResponse{Attributes: expected}, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + result, err := s.ListAttributes(t.Context()) + require.NoError(t, err) + assert.Equal(t, expected, result) +} + +func TestListAttributes_MultiPage(t *testing.T) { + page1 := []*policy.Attribute{makeAttr("https://example.com/attr/a/value/1")} + page2 := []*policy.Attribute{makeAttr("https://example.com/attr/b/value/2")} + + calls := 0 + attrClient := &mockDiscoveryAttributesClient{ + listAttributesFunc: func(_ context.Context, req *attributes.ListAttributesRequest) (*attributes.ListAttributesResponse, error) { + calls++ + if req.GetPagination().GetOffset() == 0 { + return &attributes.ListAttributesResponse{ + Attributes: page1, + Pagination: &policy.PageResponse{NextOffset: 1}, + }, nil + } + return &attributes.ListAttributesResponse{ + Attributes: page2, + Pagination: &policy.PageResponse{NextOffset: 0}, + }, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + result, err := s.ListAttributes(t.Context()) + require.NoError(t, err) + assert.Equal(t, 2, calls, "should have paginated twice") + assert.Equal(t, append(page1, page2...), result) +} + +func TestListAttributes_NamespaceFilter(t *testing.T) { + var capturedReq *attributes.ListAttributesRequest + attrClient := &mockDiscoveryAttributesClient{ + listAttributesFunc: func(_ context.Context, req *attributes.ListAttributesRequest) (*attributes.ListAttributesResponse, error) { + capturedReq = req + return &attributes.ListAttributesResponse{}, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + _, err := s.ListAttributes(t.Context(), "my-namespace") + require.NoError(t, err) + assert.Equal(t, "my-namespace", capturedReq.GetNamespace()) +} + +func TestListAttributes_PageLimitExceeded(t *testing.T) { + attrClient := &mockDiscoveryAttributesClient{ + listAttributesFunc: func(_ context.Context, _ *attributes.ListAttributesRequest) (*attributes.ListAttributesResponse, error) { + // Always return a non-zero next_offset to simulate a runaway server. + return &attributes.ListAttributesResponse{ + Attributes: []*policy.Attribute{makeAttr("https://example.com/attr/a/value/1")}, + Pagination: &policy.PageResponse{NextOffset: 1}, + }, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + _, err := s.ListAttributes(t.Context()) + require.Error(t, err) + assert.Contains(t, err.Error(), "exceeded maximum page limit") +} + +func TestListAttributes_ServiceError(t *testing.T) { + attrClient := &mockDiscoveryAttributesClient{ + listAttributesFunc: func(_ context.Context, _ *attributes.ListAttributesRequest) (*attributes.ListAttributesResponse, error) { + return nil, errors.New("service unavailable") + }, + } + s := newDiscoverySDK(attrClient, nil) + + _, err := s.ListAttributes(t.Context()) + require.Error(t, err) + assert.Contains(t, err.Error(), "listing attributes") + assert.Contains(t, err.Error(), "service unavailable") +} + +// --- AttributeExists tests --- + +func TestAttributeExists_Found(t *testing.T) { + attrFQN := "https://example.com/attr/department" + attrClient := &mockDiscoveryAttributesClient{ + getAttributeFunc: func(_ context.Context, _ *attributes.GetAttributeRequest) (*attributes.GetAttributeResponse, error) { + return &attributes.GetAttributeResponse{Attribute: &policy.Attribute{Fqn: attrFQN}}, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + exists, err := s.AttributeExists(t.Context(), attrFQN) + require.NoError(t, err) + assert.True(t, exists) +} + +func TestAttributeExists_NotFound(t *testing.T) { + attrFQN := "https://example.com/attr/nonexistent" + attrClient := &mockDiscoveryAttributesClient{ + getAttributeFunc: func(_ context.Context, _ *attributes.GetAttributeRequest) (*attributes.GetAttributeResponse, error) { + return nil, connect.NewError(connect.CodeNotFound, errors.New("attribute not found")) + }, + } + s := newDiscoverySDK(attrClient, nil) + + exists, err := s.AttributeExists(t.Context(), attrFQN) + require.NoError(t, err) + assert.False(t, exists) +} + +func TestAttributeExists_ServiceError(t *testing.T) { + attrFQN := "https://example.com/attr/department" + attrClient := &mockDiscoveryAttributesClient{ + getAttributeFunc: func(_ context.Context, _ *attributes.GetAttributeRequest) (*attributes.GetAttributeResponse, error) { + return nil, connect.NewError(connect.CodeUnavailable, errors.New("service unavailable")) + }, + } + s := newDiscoverySDK(attrClient, nil) + + exists, err := s.AttributeExists(t.Context(), attrFQN) + require.Error(t, err) + assert.False(t, exists) + assert.Contains(t, err.Error(), "checking attribute existence") +} + +func TestAttributeExists_InvalidFQN(t *testing.T) { + s := newDiscoverySDK(nil, nil) + + exists, err := s.AttributeExists(t.Context(), "not-a-fqn") + require.Error(t, err) + assert.False(t, exists) + assert.Contains(t, err.Error(), "invalid attribute FQN") +} + +func TestAttributeExists_RejectsValueFQN(t *testing.T) { + // A value FQN (with /value/) is not a valid attribute-level FQN. + s := newDiscoverySDK(nil, nil) + + exists, err := s.AttributeExists(t.Context(), "https://example.com/attr/clearance/value/secret") + require.Error(t, err) + assert.False(t, exists) + assert.Contains(t, err.Error(), "invalid attribute FQN") +} + +// --- AttributeValueExists tests --- + +func TestAttributeValueExists_Found(t *testing.T) { + fqn := "https://example.com/attr/department/value/finance" + attrClient := &mockDiscoveryAttributesClient{ + getAttributeValuesByFqnsFunc: func(_ context.Context, _ *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return &attributes.GetAttributeValuesByFqnsResponse{FqnAttributeValues: fqnMap(fqn)}, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + exists, err := s.AttributeValueExists(t.Context(), fqn) + require.NoError(t, err) + assert.True(t, exists) +} + +func TestAttributeValueExists_NotFound(t *testing.T) { + fqn := "https://example.com/attr/department/value/finance" + attrClient := &mockDiscoveryAttributesClient{ + getAttributeValuesByFqnsFunc: func(_ context.Context, _ *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return &attributes.GetAttributeValuesByFqnsResponse{FqnAttributeValues: fqnMap()}, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + exists, err := s.AttributeValueExists(t.Context(), fqn) + require.NoError(t, err) + assert.False(t, exists) +} + +func TestAttributeValueExists_NotFoundError(t *testing.T) { + fqn := "https://example.com/attr/department/value/finance" + attrClient := &mockDiscoveryAttributesClient{ + getAttributeValuesByFqnsFunc: func(_ context.Context, _ *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return nil, connect.NewError(connect.CodeNotFound, errors.New("resource not found")) + }, + } + s := newDiscoverySDK(attrClient, nil) + + exists, err := s.AttributeValueExists(t.Context(), fqn) + require.NoError(t, err) + assert.False(t, exists) +} + +func TestAttributeValueExists_ServiceError(t *testing.T) { + fqn := "https://example.com/attr/department/value/finance" + attrClient := &mockDiscoveryAttributesClient{ + getAttributeValuesByFqnsFunc: func(_ context.Context, _ *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return nil, errors.New("network error") + }, + } + s := newDiscoverySDK(attrClient, nil) + + exists, err := s.AttributeValueExists(t.Context(), fqn) + require.Error(t, err) + assert.False(t, exists) + assert.Contains(t, err.Error(), "checking attribute value existence") +} + +func TestAttributeValueExists_InvalidFQN(t *testing.T) { + s := newDiscoverySDK(nil, nil) + + exists, err := s.AttributeValueExists(t.Context(), "not-a-fqn") + require.Error(t, err) + assert.False(t, exists) + assert.Contains(t, err.Error(), "invalid attribute value FQN") +} + +func TestAttributeValueExists_RejectsAttributeFQN(t *testing.T) { + // An attribute-level FQN (without /value/) is not a valid value FQN. + s := newDiscoverySDK(nil, nil) + + exists, err := s.AttributeValueExists(t.Context(), "https://example.com/attr/department") + require.Error(t, err) + assert.False(t, exists) + assert.Contains(t, err.Error(), "invalid attribute value FQN") +} + +// --- ValidateAttributes tests --- + +func TestValidateAttributes_Empty(t *testing.T) { + s := newDiscoverySDK(nil, nil) + err := s.ValidateAttributes(t.Context()) + require.NoError(t, err) +} + +func TestValidateAttributes_AllFound(t *testing.T) { + fqns := []string{ + "https://example.com/attr/level/value/high", + "https://example.com/attr/type/value/secret", + } + attrClient := &mockDiscoveryAttributesClient{ + getAttributeValuesByFqnsFunc: func(_ context.Context, _ *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return &attributes.GetAttributeValuesByFqnsResponse{FqnAttributeValues: fqnMap(fqns...)}, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + err := s.ValidateAttributes(t.Context(), fqns...) + require.NoError(t, err) +} + +func TestValidateAttributes_SomeMissing(t *testing.T) { + fqns := []string{ + "https://example.com/attr/level/value/high", + "https://example.com/attr/type/value/missing", + } + attrClient := &mockDiscoveryAttributesClient{ + getAttributeValuesByFqnsFunc: func(_ context.Context, _ *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return &attributes.GetAttributeValuesByFqnsResponse{ + FqnAttributeValues: fqnMap(fqns[0]), + }, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + err := s.ValidateAttributes(t.Context(), fqns...) + require.Error(t, err) + require.ErrorIs(t, err, ErrAttributeNotFound) + assert.Contains(t, err.Error(), "https://example.com/attr/type/value/missing") +} + +func TestValidateAttributes_AllMissing(t *testing.T) { + fqns := []string{ + "https://example.com/attr/a/value/x", + "https://example.com/attr/b/value/y", + } + attrClient := &mockDiscoveryAttributesClient{ + getAttributeValuesByFqnsFunc: func(_ context.Context, _ *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return &attributes.GetAttributeValuesByFqnsResponse{FqnAttributeValues: fqnMap()}, nil + }, + } + s := newDiscoverySDK(attrClient, nil) + + err := s.ValidateAttributes(t.Context(), fqns...) + require.Error(t, err) + require.ErrorIs(t, err, ErrAttributeNotFound) +} + +func TestValidateAttributes_TooManyFQNs(t *testing.T) { + fqns := make([]string, maxValidateFQNs+1) + for i := range fqns { + fqns[i] = fmt.Sprintf("https://example.com/attr/level/value/v%d", i) + } + s := newDiscoverySDK(nil, nil) + + err := s.ValidateAttributes(t.Context(), fqns...) + require.Error(t, err) + assert.Contains(t, err.Error(), "too many attribute FQNs") +} + +func TestValidateAttributes_InvalidFQNFormat(t *testing.T) { + s := newDiscoverySDK(nil, nil) + + err := s.ValidateAttributes(t.Context(), "not-a-valid-fqn") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid attribute value FQN") + assert.Contains(t, err.Error(), "not-a-valid-fqn") +} + +func TestValidateAttributes_ServiceError(t *testing.T) { + fqns := []string{"https://example.com/attr/level/value/high"} + attrClient := &mockDiscoveryAttributesClient{ + getAttributeValuesByFqnsFunc: func(_ context.Context, _ *attributes.GetAttributeValuesByFqnsRequest) (*attributes.GetAttributeValuesByFqnsResponse, error) { + return nil, errors.New("network error") + }, + } + s := newDiscoverySDK(attrClient, nil) + + err := s.ValidateAttributes(t.Context(), fqns...) + require.Error(t, err) + assert.Contains(t, err.Error(), "validating attributes") + assert.Contains(t, err.Error(), "network error") +} + +// --- GetEntityAttributes tests --- + +func TestGetEntityAttributes_NilEntity(t *testing.T) { + s := newDiscoverySDK(nil, nil) + _, err := s.GetEntityAttributes(t.Context(), nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "entity must not be nil") +} + +func TestGetEntityAttributes_Found(t *testing.T) { + expectedFQNs := []string{ + "https://example.com/attr/clearance/value/secret", + "https://example.com/attr/country/value/us", + } + authzClient := &mockDiscoveryAuthzClient{ + getEntitlementsFunc: func(_ context.Context, req *authorization.GetEntitlementsRequest) (*authorization.GetEntitlementsResponse, error) { + assert.Len(t, req.GetEntities(), 1) + return &authorization.GetEntitlementsResponse{ + Entitlements: []*authorization.EntityEntitlements{ + {EntityId: "e1", AttributeValueFqns: expectedFQNs}, + }, + }, nil + }, + } + s := newDiscoverySDK(nil, authzClient) + + entity := &authorization.Entity{ + Id: "e1", + EntityType: &authorization.Entity_EmailAddress{EmailAddress: "alice@example.com"}, + } + result, err := s.GetEntityAttributes(t.Context(), entity) + require.NoError(t, err) + assert.Equal(t, expectedFQNs, result) +} + +func TestGetEntityAttributes_NoEntitlements(t *testing.T) { + authzClient := &mockDiscoveryAuthzClient{ + getEntitlementsFunc: func(_ context.Context, _ *authorization.GetEntitlementsRequest) (*authorization.GetEntitlementsResponse, error) { + return &authorization.GetEntitlementsResponse{}, nil + }, + } + s := newDiscoverySDK(nil, authzClient) + + entity := &authorization.Entity{ + Id: "e1", + EntityType: &authorization.Entity_ClientId{ClientId: "my-service"}, + } + result, err := s.GetEntityAttributes(t.Context(), entity) + require.NoError(t, err) + assert.Empty(t, result) +} + +func TestGetEntityAttributes_IDMismatch(t *testing.T) { + authzClient := &mockDiscoveryAuthzClient{ + getEntitlementsFunc: func(_ context.Context, _ *authorization.GetEntitlementsRequest) (*authorization.GetEntitlementsResponse, error) { + return &authorization.GetEntitlementsResponse{ + Entitlements: []*authorization.EntityEntitlements{ + {EntityId: "other-entity", AttributeValueFqns: []string{"https://example.com/attr/a/value/x"}}, + }, + }, nil + }, + } + s := newDiscoverySDK(nil, authzClient) + + entity := &authorization.Entity{ + Id: "e1", + EntityType: &authorization.Entity_EmailAddress{EmailAddress: "alice@example.com"}, + } + result, err := s.GetEntityAttributes(t.Context(), entity) + require.NoError(t, err) + assert.Empty(t, result, "should return empty when no entitlement matches the requested entity ID") +} + +func TestGetEntityAttributes_EmptyEntityID(t *testing.T) { + authzClient := &mockDiscoveryAuthzClient{ + getEntitlementsFunc: func(_ context.Context, _ *authorization.GetEntitlementsRequest) (*authorization.GetEntitlementsResponse, error) { + return &authorization.GetEntitlementsResponse{ + Entitlements: []*authorization.EntityEntitlements{ + {EntityId: "some-entity", AttributeValueFqns: []string{"https://example.com/attr/a/value/x"}}, + }, + }, nil + }, + } + s := newDiscoverySDK(nil, authzClient) + + entity := &authorization.Entity{} // no ID set + result, err := s.GetEntityAttributes(t.Context(), entity) + require.NoError(t, err) + assert.Empty(t, result, "entity with empty ID should not receive entitlements belonging to another entity") +} + +func TestGetEntityAttributes_ServiceError(t *testing.T) { + authzClient := &mockDiscoveryAuthzClient{ + getEntitlementsFunc: func(_ context.Context, _ *authorization.GetEntitlementsRequest) (*authorization.GetEntitlementsResponse, error) { + return nil, errors.New("auth service unavailable") + }, + } + s := newDiscoverySDK(nil, authzClient) + + entity := &authorization.Entity{ + Id: "e1", + EntityType: &authorization.Entity_Uuid{Uuid: "550e8400-e29b-41d4-a716-446655440000"}, + } + _, err := s.GetEntityAttributes(t.Context(), entity) + require.Error(t, err) + assert.Contains(t, err.Error(), "getting entity attributes") + assert.Contains(t, err.Error(), "auth service unavailable") +} diff --git a/sdk/experimental/tdf/doc.go b/sdk/experimental/tdf/doc.go index 73c7e65ca1..8e80e36173 100644 --- a/sdk/experimental/tdf/doc.go +++ b/sdk/experimental/tdf/doc.go @@ -106,7 +106,7 @@ // The TDF writer uses a two-layer architecture: // // 1. TDF Layer (tdf.Writer): Handles encryption, assertions, and TDF protocol logic -// 2. Archive Layer (internal/archive2): Manages ZIP file structure and segment assembly +// 2. Archive Layer (internal/zipstream): Manages ZIP file structure and segment assembly // // This separation enables independent optimization of cryptographic operations // and file format handling. diff --git a/sdk/experimental/tdf/key_access.go b/sdk/experimental/tdf/key_access.go index 6e97701ad5..3974b06cf4 100644 --- a/sdk/experimental/tdf/key_access.go +++ b/sdk/experimental/tdf/key_access.go @@ -165,6 +165,9 @@ func wrapKeyWithPublicKey(symKey []byte, pubKeyInfo keysplit.KASPublicKey) (stri // Determine key type based on algorithm ktype := ocrypto.KeyType(pubKeyInfo.Algorithm) + if ocrypto.IsHybridKeyType(ktype) { + return wrapKeyWithHybrid(ktype, pubKeyInfo.PEM, symKey) + } if ocrypto.IsECKeyType(ktype) { // Handle EC key wrapping return wrapKeyWithEC(ktype, pubKeyInfo.PEM, symKey) @@ -245,3 +248,11 @@ func wrapKeyWithRSA(kasPublicKeyPEM string, symKey []byte) (string, error) { return string(ocrypto.Base64Encode(encryptedKey)), nil } + +func wrapKeyWithHybrid(ktype ocrypto.KeyType, kasPublicKeyPEM string, symKey []byte) (string, string, string, error) { + wrappedDER, err := ocrypto.HybridWrapDEK(ktype, kasPublicKeyPEM, symKey) + if err != nil { + return "", "", "", fmt.Errorf("hybrid wrap failed: %w", err) + } + return string(ocrypto.Base64Encode(wrappedDER)), "hybrid-wrapped", "", nil +} diff --git a/sdk/experimental/tdf/key_access_test.go b/sdk/experimental/tdf/key_access_test.go index 9dac3ca3b6..daea79b69c 100644 --- a/sdk/experimental/tdf/key_access_test.go +++ b/sdk/experimental/tdf/key_access_test.go @@ -35,7 +35,7 @@ SQIDAQAB ) // createTestSplitResult creates a mock SplitResult for testing key access operations -func createTestSplitResult(kasURL, pubKey string, algorithm string) *keysplit.SplitResult { +func createTestSplitResult(pubKey string, algorithm string) *keysplit.SplitResult { // Generate random split data splitData := make([]byte, 32) _, err := rand.Read(splitData) @@ -46,11 +46,11 @@ func createTestSplitResult(kasURL, pubKey string, algorithm string) *keysplit.Sp split := keysplit.Split{ ID: "test-split-1", Data: splitData, - KASURLs: []string{kasURL}, + KASURLs: []string{testKAS1URL}, } pubKeyInfo := keysplit.KASPublicKey{ - URL: kasURL, + URL: testKAS1URL, Algorithm: algorithm, KID: "test-kid-1", PEM: pubKey, @@ -58,14 +58,14 @@ func createTestSplitResult(kasURL, pubKey string, algorithm string) *keysplit.Sp return &keysplit.SplitResult{ Splits: []keysplit.Split{split}, - KASPublicKeys: map[string]keysplit.KASPublicKey{kasURL: pubKeyInfo}, + KASPublicKeys: map[string]keysplit.KASPublicKey{testKAS1URL: pubKeyInfo}, } } func TestBuildKeyAccessObjects(t *testing.T) { t.Run("successfully creates key access objects with RSA public key", func(t *testing.T) { // Test that buildKeyAccessObjects correctly processes RSA keys and creates valid KeyAccess objects - splitResult := createTestSplitResult(testKAS1URL, testRSAPublicKey, "rsa:2048") + splitResult := createTestSplitResult(testRSAPublicKey, "rsa:2048") policyBytes := []byte(testPolicyJSON) metadata := testMetadata @@ -96,7 +96,7 @@ func TestBuildKeyAccessObjects(t *testing.T) { ecPublicKeyPEM, err := ecKeyPair.PublicKeyInPemFormat() require.NoError(t, err, "Should get public key in PEM format") - splitResult := createTestSplitResult(testKAS1URL, ecPublicKeyPEM, "ec:secp256r1") + splitResult := createTestSplitResult(ecPublicKeyPEM, "ec:secp256r1") policyBytes := []byte(testPolicyJSON) metadata := testMetadata @@ -112,6 +112,69 @@ func TestBuildKeyAccessObjects(t *testing.T) { assert.NotEmpty(t, keyAccess.WrappedKey, "Should contain wrapped key data") }) + t.Run("successfully creates key access objects with X-Wing public key", func(t *testing.T) { + xwingKeyPair, err := ocrypto.NewXWingKeyPair() + require.NoError(t, err) + + xwingPublicKeyPEM, err := xwingKeyPair.PublicKeyInPemFormat() + require.NoError(t, err) + + splitResult := createTestSplitResult(xwingPublicKeyPEM, string(ocrypto.HybridXWingKey)) + policyBytes := []byte(testPolicyJSON) + + keyAccessList, err := buildKeyAccessObjects(splitResult, policyBytes, testMetadata) + + require.NoError(t, err, "Should successfully create key access objects with valid X-Wing key") + require.Len(t, keyAccessList, 1) + + keyAccess := keyAccessList[0] + assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType) + assert.NotEmpty(t, keyAccess.WrappedKey) + assert.Empty(t, keyAccess.EphemeralPublicKey) + }) + + t.Run("successfully creates key access objects with P256+ML-KEM-768 public key", func(t *testing.T) { + keyPair, err := ocrypto.NewP256MLKEM768KeyPair() + require.NoError(t, err) + + publicKeyPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + + splitResult := createTestSplitResult(publicKeyPEM, string(ocrypto.HybridSecp256r1MLKEM768Key)) + policyBytes := []byte(testPolicyJSON) + + keyAccessList, err := buildKeyAccessObjects(splitResult, policyBytes, testMetadata) + + require.NoError(t, err, "Should successfully create key access objects with valid P256+ML-KEM-768 key") + require.Len(t, keyAccessList, 1) + + keyAccess := keyAccessList[0] + assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType) + assert.NotEmpty(t, keyAccess.WrappedKey) + assert.Empty(t, keyAccess.EphemeralPublicKey) + }) + + t.Run("successfully creates key access objects with P384+ML-KEM-1024 public key", func(t *testing.T) { + keyPair, err := ocrypto.NewP384MLKEM1024KeyPair() + require.NoError(t, err) + + publicKeyPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + + splitResult := createTestSplitResult(publicKeyPEM, string(ocrypto.HybridSecp384r1MLKEM1024Key)) + policyBytes := []byte(testPolicyJSON) + + keyAccessList, err := buildKeyAccessObjects(splitResult, policyBytes, testMetadata) + + require.NoError(t, err, "Should successfully create key access objects with valid P384+ML-KEM-1024 key") + require.Len(t, keyAccessList, 1) + + keyAccess := keyAccessList[0] + assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType) + assert.NotEmpty(t, keyAccess.WrappedKey) + assert.Empty(t, keyAccess.EphemeralPublicKey) + }) + t.Run("handles multiple KAS URLs in single split", func(t *testing.T) { // Test that multiple KAS URLs in one split create separate KeyAccess objects splitData := make([]byte, 32) @@ -172,7 +235,7 @@ func TestBuildKeyAccessObjects(t *testing.T) { t.Run("handles empty metadata correctly", func(t *testing.T) { // Test that empty metadata is handled without creating encrypted metadata - splitResult := createTestSplitResult(testKAS1URL, testRSAPublicKey, "rsa:2048") + splitResult := createTestSplitResult(testRSAPublicKey, "rsa:2048") keyAccessList, err := buildKeyAccessObjects(splitResult, []byte(testPolicyJSON), "") @@ -424,6 +487,120 @@ func TestWrapKeyWithPublicKey(t *testing.T) { "Ephemeral key should end with PEM footer") }) + t.Run("wraps key with X-Wing public key", func(t *testing.T) { + symKey := make([]byte, 32) + _, err := rand.Read(symKey) + require.NoError(t, err) + + xwingKeyPair, err := ocrypto.NewXWingKeyPair() + require.NoError(t, err) + + xwingPublicKeyPEM, err := xwingKeyPair.PublicKeyInPemFormat() + require.NoError(t, err) + + pubKeyInfo := keysplit.KASPublicKey{ + URL: testKAS1URL, + Algorithm: string(ocrypto.HybridXWingKey), + KID: "test-kid", + PEM: xwingPublicKeyPEM, + } + + wrappedKey, keyType, ephemeralPubKey, err := wrapKeyWithPublicKey(symKey, pubKeyInfo) + + require.NoError(t, err, "Should wrap key with X-Wing public key") + assert.NotEmpty(t, wrappedKey) + assert.Equal(t, "hybrid-wrapped", keyType) + assert.Empty(t, ephemeralPubKey) + + decodedWrappedKey, err := ocrypto.Base64Decode([]byte(wrappedKey)) + require.NoError(t, err) + + privateKeyPEM, err := xwingKeyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(privateKeyPEM)) + require.NoError(t, err) + + plaintext, err := ocrypto.XWingUnwrapDEK(privateKey, decodedWrappedKey) + require.NoError(t, err) + assert.Equal(t, symKey, plaintext) + }) + + t.Run("wraps key with P256+ML-KEM-768 public key", func(t *testing.T) { + symKey := make([]byte, 32) + _, err := rand.Read(symKey) + require.NoError(t, err) + + keyPair, err := ocrypto.NewP256MLKEM768KeyPair() + require.NoError(t, err) + + publicKeyPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + + pubKeyInfo := keysplit.KASPublicKey{ + URL: testKAS1URL, + Algorithm: string(ocrypto.HybridSecp256r1MLKEM768Key), + KID: "test-kid", + PEM: publicKeyPEM, + } + + wrappedKey, keyType, ephemeralPubKey, err := wrapKeyWithPublicKey(symKey, pubKeyInfo) + + require.NoError(t, err, "Should wrap key with P256+ML-KEM-768 public key") + assert.NotEmpty(t, wrappedKey) + assert.Equal(t, "hybrid-wrapped", keyType) + assert.Empty(t, ephemeralPubKey) + + decodedWrappedKey, err := ocrypto.Base64Decode([]byte(wrappedKey)) + require.NoError(t, err) + + privateKeyPEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(privateKeyPEM)) + require.NoError(t, err) + + plaintext, err := ocrypto.P256MLKEM768UnwrapDEK(privateKey, decodedWrappedKey) + require.NoError(t, err) + assert.Equal(t, symKey, plaintext) + }) + + t.Run("wraps key with P384+ML-KEM-1024 public key", func(t *testing.T) { + symKey := make([]byte, 32) + _, err := rand.Read(symKey) + require.NoError(t, err) + + keyPair, err := ocrypto.NewP384MLKEM1024KeyPair() + require.NoError(t, err) + + publicKeyPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + + pubKeyInfo := keysplit.KASPublicKey{ + URL: testKAS1URL, + Algorithm: string(ocrypto.HybridSecp384r1MLKEM1024Key), + KID: "test-kid", + PEM: publicKeyPEM, + } + + wrappedKey, keyType, ephemeralPubKey, err := wrapKeyWithPublicKey(symKey, pubKeyInfo) + + require.NoError(t, err, "Should wrap key with P384+ML-KEM-1024 public key") + assert.NotEmpty(t, wrappedKey) + assert.Equal(t, "hybrid-wrapped", keyType) + assert.Empty(t, ephemeralPubKey) + + decodedWrappedKey, err := ocrypto.Base64Decode([]byte(wrappedKey)) + require.NoError(t, err) + + privateKeyPEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(privateKeyPEM)) + require.NoError(t, err) + + plaintext, err := ocrypto.P384MLKEM1024UnwrapDEK(privateKey, decodedWrappedKey) + require.NoError(t, err) + assert.Equal(t, symKey, plaintext) + }) + t.Run("returns error for empty PEM", func(t *testing.T) { // Test error handling for missing public key PEM symKey := make([]byte, 32) diff --git a/sdk/experimental/tdf/keysplit/attributes.go b/sdk/experimental/tdf/keysplit/attributes.go index e12d838e97..27cc1e386f 100644 --- a/sdk/experimental/tdf/keysplit/attributes.go +++ b/sdk/experimental/tdf/keysplit/attributes.go @@ -7,6 +7,7 @@ import ( "log/slog" "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/sdk" ) const unknownAlgorithm = "unknown" @@ -186,22 +187,11 @@ func extractKASGrants(grants []*policy.KeyAccessServer, kasKeys []*policy.Simple // formatAlgorithm converts policy algorithm enum to string func formatAlgorithm(alg policy.Algorithm) string { - switch alg { - case policy.Algorithm_ALGORITHM_UNSPECIFIED: - return unknownAlgorithm - case policy.Algorithm_ALGORITHM_EC_P256: - return "ec:secp256r1" - case policy.Algorithm_ALGORITHM_EC_P384: - return "ec:secp384r1" - case policy.Algorithm_ALGORITHM_EC_P521: - return "ec:secp521r1" - case policy.Algorithm_ALGORITHM_RSA_2048: - return "rsa:2048" - case policy.Algorithm_ALGORITHM_RSA_4096: - return "rsa:4096" - default: + kt, err := sdk.PolicyAlgorithmToKeyType(alg) + if err != nil { return unknownAlgorithm } + return string(kt) } // convertAlgEnum2Simple converts KAS key algorithm enum to policy algorithm enum @@ -217,6 +207,12 @@ func convertAlgEnum2Simple(a policy.KasPublicKeyAlgEnum) policy.Algorithm { return policy.Algorithm_ALGORITHM_RSA_2048 case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096: return policy.Algorithm_ALGORITHM_RSA_4096 + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING: + return policy.Algorithm_ALGORITHM_HPQT_XWING + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768: + return policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024: + return policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED: return policy.Algorithm_ALGORITHM_UNSPECIFIED default: diff --git a/sdk/experimental/tdf/keysplit/attributes_test.go b/sdk/experimental/tdf/keysplit/attributes_test.go index 4ed96d38a2..b7095e801d 100644 --- a/sdk/experimental/tdf/keysplit/attributes_test.go +++ b/sdk/experimental/tdf/keysplit/attributes_test.go @@ -46,6 +46,21 @@ func TestFormatAlgorithm(t *testing.T) { alg: policy.Algorithm_ALGORITHM_RSA_4096, expected: "rsa:4096", }, + { + name: "HPQT X-Wing", + alg: policy.Algorithm_ALGORITHM_HPQT_XWING, + expected: "hpqt:xwing", + }, + { + name: "HPQT P256+ML-KEM-768", + alg: policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, + expected: "hpqt:secp256r1-mlkem768", + }, + { + name: "HPQT P384+ML-KEM-1024", + alg: policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, + expected: "hpqt:secp384r1-mlkem1024", + }, { name: "unknown algorithm value", alg: policy.Algorithm(999), @@ -97,6 +112,21 @@ func TestConvertAlgEnum2Simple(t *testing.T) { algEnum: policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096, expected: policy.Algorithm_ALGORITHM_RSA_4096, }, + { + name: "HPQT X-Wing", + algEnum: policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING, + expected: policy.Algorithm_ALGORITHM_HPQT_XWING, + }, + { + name: "HPQT P256+ML-KEM-768", + algEnum: policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768, + expected: policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, + }, + { + name: "HPQT P384+ML-KEM-1024", + algEnum: policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024, + expected: policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, + }, { name: "unknown enum value", algEnum: policy.KasPublicKeyAlgEnum(999), diff --git a/sdk/experimental/tdf/writer_test.go b/sdk/experimental/tdf/writer_test.go index 296c582041..08f35cfcc8 100644 --- a/sdk/experimental/tdf/writer_test.go +++ b/sdk/experimental/tdf/writer_test.go @@ -58,6 +58,9 @@ func TestWriterEndToEnd(t *testing.T) { {"GetManifestIncludesInitialPolicy", testGetManifestIncludesInitialPolicy}, {"SparseIndicesInOrder", testSparseIndicesInOrder}, {"SparseIndicesOutOfOrder", testSparseIndicesOutOfOrder}, + {"HybridXWingFlow", testHybridXWingFlow}, + {"HybridP256MLKEM768Flow", testHybridP256MLKEM768Flow}, + {"HybridP384MLKEM1024Flow", testHybridP384MLKEM1024Flow}, } for _, tc := range testCases { @@ -877,6 +880,157 @@ func createTestAttributeWithRule(fqn, kasURL, kid string, rule policy.AttributeR return value } +func createTestAttributeWithAlgorithm(t *testing.T, fqn, kasURL, kid string, alg policy.Algorithm, pem string) *policy.Value { + t.Helper() + value := createTestAttribute(fqn, kasURL, kid) + require.NotEmpty(t, value.GetGrants(), "createTestAttribute returned no grants") + require.NotEmpty(t, value.GetGrants()[0].GetKasKeys(), "createTestAttribute returned no kas keys") + value.GetGrants()[0].GetKasKeys()[0].PublicKey.Algorithm = alg + value.GetGrants()[0].GetKasKeys()[0].PublicKey.Pem = pem + return value +} + +// hybridUnwrapForTest base64-decodes the wrappedKey from a manifest KAO and +// unwraps it with the matching hybrid private key, asserting the recovered DEK +// is non-empty. This proves the writer's `hybrid-wrapped` output is consumable +// by the corresponding ocrypto unwrap path. +func hybridUnwrapForTest(t *testing.T, ktype ocrypto.KeyType, privatePEM, wrappedKeyB64 string) { + t.Helper() + wrappedDER, err := ocrypto.Base64Decode([]byte(wrappedKeyB64)) + require.NoError(t, err, "Base64Decode wrapped key") + + switch ktype { //nolint:exhaustive // only handle hybrid types + case ocrypto.HybridXWingKey: + raw, err := ocrypto.XWingPrivateKeyFromPem([]byte(privatePEM)) + require.NoError(t, err) + dek, err := ocrypto.XWingUnwrapDEK(raw, wrappedDER) + require.NoError(t, err, "XWingUnwrapDEK") + assert.NotEmpty(t, dek, "X-Wing recovered DEK") + case ocrypto.HybridSecp256r1MLKEM768Key: + raw, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(privatePEM)) + require.NoError(t, err) + dek, err := ocrypto.P256MLKEM768UnwrapDEK(raw, wrappedDER) + require.NoError(t, err, "P256MLKEM768UnwrapDEK") + assert.NotEmpty(t, dek, "P-256+ML-KEM-768 recovered DEK") + case ocrypto.HybridSecp384r1MLKEM1024Key: + raw, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(privatePEM)) + require.NoError(t, err) + dek, err := ocrypto.P384MLKEM1024UnwrapDEK(raw, wrappedDER) + require.NoError(t, err, "P384MLKEM1024UnwrapDEK") + assert.NotEmpty(t, dek, "P-384+ML-KEM-1024 recovered DEK") + default: + t.Fatalf("unsupported hybrid key type for round-trip: %s", ktype) + } +} + +func testHybridXWingFlow(t *testing.T) { + ctx := t.Context() + + keyPair, err := ocrypto.NewXWingKeyPair() + require.NoError(t, err) + pubPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privPEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + writer, err := NewWriter(ctx) + require.NoError(t, err) + + _, err = writer.WriteSegment(ctx, 0, []byte("hybrid xwing test data")) + require.NoError(t, err) + + attributes := []*policy.Value{ + createTestAttributeWithAlgorithm( + t, + "https://example.com/attr/Classification/value/Secret", + testKAS1, "xwing-kid", + policy.Algorithm_ALGORITHM_HPQT_XWING, pubPEM, + ), + } + result, err := writer.Finalize(ctx, WithAttributeValues(attributes)) + require.NoError(t, err) + assert.NotEmpty(t, result.Data) + + keyAccess := result.Manifest.KeyAccessObjs[0] + assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType) + assert.NotEmpty(t, keyAccess.WrappedKey) + + validateManifestSchema(t, result.Manifest) + hybridUnwrapForTest(t, ocrypto.HybridXWingKey, privPEM, keyAccess.WrappedKey) +} + +func testHybridP256MLKEM768Flow(t *testing.T) { + ctx := t.Context() + + keyPair, err := ocrypto.NewP256MLKEM768KeyPair() + require.NoError(t, err) + pubPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privPEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + writer, err := NewWriter(ctx) + require.NoError(t, err) + + _, err = writer.WriteSegment(ctx, 0, []byte("hybrid p256 mlkem768 test data")) + require.NoError(t, err) + + attributes := []*policy.Value{ + createTestAttributeWithAlgorithm( + t, + "https://example.com/attr/Classification/value/Secret", + testKAS1, "p256mlkem768-kid", + policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, pubPEM, + ), + } + result, err := writer.Finalize(ctx, WithAttributeValues(attributes)) + require.NoError(t, err) + assert.NotEmpty(t, result.Data) + + keyAccess := result.Manifest.KeyAccessObjs[0] + assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType) + assert.NotEmpty(t, keyAccess.WrappedKey) + + validateManifestSchema(t, result.Manifest) + hybridUnwrapForTest(t, ocrypto.HybridSecp256r1MLKEM768Key, privPEM, keyAccess.WrappedKey) +} + +func testHybridP384MLKEM1024Flow(t *testing.T) { + ctx := t.Context() + + keyPair, err := ocrypto.NewP384MLKEM1024KeyPair() + require.NoError(t, err) + pubPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + privPEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + + writer, err := NewWriter(ctx) + require.NoError(t, err) + + _, err = writer.WriteSegment(ctx, 0, []byte("hybrid p384 mlkem1024 test data")) + require.NoError(t, err) + + attributes := []*policy.Value{ + createTestAttributeWithAlgorithm( + t, + "https://example.com/attr/Classification/value/Secret", + testKAS1, "p384mlkem1024-kid", + policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, pubPEM, + ), + } + result, err := writer.Finalize(ctx, WithAttributeValues(attributes)) + require.NoError(t, err) + assert.NotEmpty(t, result.Data) + + keyAccess := result.Manifest.KeyAccessObjs[0] + assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType) + assert.NotEmpty(t, keyAccess.WrappedKey) + + validateManifestSchema(t, result.Manifest) + hybridUnwrapForTest(t, ocrypto.HybridSecp384r1MLKEM1024Key, privPEM, keyAccess.WrappedKey) +} + // validateManifestSchema validates a TDF manifest against the JSON schema func validateManifestSchema(t *testing.T, manifest *Manifest) { t.Helper() diff --git a/sdk/fuzz_test.go b/sdk/fuzz_test.go index 8c4d0e4070..ef072209ca 100644 --- a/sdk/fuzz_test.go +++ b/sdk/fuzz_test.go @@ -213,78 +213,6 @@ func FuzzLoadTDF(f *testing.F) { }) } -func FuzzReadNanoTDF(f *testing.F) { - sdk := newSDK() - f.Add([]byte{ // seed from xtest - // header - 0x4c, 0x31, 0x4c, // version - 0x00, 0x12, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x3a, 0x38, 0x30, 0x38, 0x30, 0x2f, 0x6b, 0x61, 0x73, // kas - 0x00, // binding_mode - 0x01, // symmetric_and_payload_config - // policy - 0x02, 0x00, 0x68, 0xef, 0x70, 0x7b, 0x5f, 0x20, 0xb9, 0x0b, 0xf5, 0x96, 0xc3, 0xd7, 0x42, 0x85, 0x17, 0x6c, 0xd8, 0x98, - 0xad, 0x47, 0xc4, 0x9a, 0x81, 0x5f, 0x67, 0xc4, 0x0f, 0xff, 0x16, 0xbb, 0xf0, 0xf4, 0xcd, 0x31, 0xa5, 0xf6, 0x86, 0x59, - 0x3d, 0xf1, 0x53, 0x39, 0x3c, 0x3e, 0x16, 0xd8, 0xd2, 0x3b, 0x37, 0x50, 0x86, 0x6c, 0xfd, 0x2b, 0xce, 0xc7, 0x10, 0x89, - 0x66, 0x74, 0x22, 0xf0, 0x3f, 0x16, 0x7a, 0xed, 0x37, 0x93, 0x03, 0x30, 0xcc, 0x05, 0x21, 0xd2, 0x9e, 0x5d, 0xc3, 0x34, - 0xc5, 0x51, 0x60, 0xe6, 0xbf, 0x16, 0xdf, 0x92, 0xd0, 0x8d, 0xb0, 0xf0, 0x57, 0x6f, 0x7c, 0x37, 0xb9, 0x84, 0x44, 0xc7, - 0x64, 0x99, 0x6a, 0xd3, 0x6e, 0xaa, 0x04, - 0xf3, 0x18, 0xe9, 0x0b, 0xd0, 0xdc, 0x05, 0x38, // gmac_binding - // ephemeral_key - 0x03, 0x66, 0x95, 0xd9, 0x3b, 0x84, 0xee, 0xc5, 0x65, 0xc1, 0x13, 0x1c, 0x94, 0xc6, 0x00, 0x8b, 0xcb, 0x6a, 0xf5, 0x90, - 0xd5, 0x0d, 0x90, 0xc5, 0xf4, 0xe5, 0x96, 0x56, 0xb2, 0xd9, 0x4a, 0x9b, 0x51, - // payload - 0x00, 0x00, 0x8f, // length - 0x54, 0x2b, 0x53, // iv - // ciphertext - 0xce, 0x35, 0x1d, 0x0a, 0xd9, 0x7a, 0x81, 0xb5, 0xda, 0x93, 0x39, 0xd5, 0xa2, 0x42, 0x22, 0xa3, 0x64, 0x97, 0x2e, 0x33, - 0x41, 0x84, 0x12, 0x26, 0x81, 0xf5, 0x10, 0xc9, 0xf4, 0x94, 0xb8, 0x55, 0x52, 0x24, 0xeb, 0xaf, 0x89, 0xc3, 0x24, 0x7e, - 0x32, 0xcf, 0xd5, 0xda, 0xa2, 0xcb, 0x98, 0x67, 0x71, 0xc3, 0xa5, 0xf6, 0xa8, 0xe3, 0x4e, 0x64, 0x23, 0x2e, 0x40, 0xee, - 0x2e, 0xd9, 0xa4, 0x97, 0x87, 0x83, 0xd4, 0xe7, 0x11, 0xfe, 0xdb, 0xf4, 0x42, 0xc1, 0x71, 0x3b, 0x5a, 0x07, 0x01, 0x76, - 0xb2, 0xf8, 0x48, 0x23, 0x2d, 0xb3, 0x53, 0x61, 0x98, 0x39, 0x13, 0x7b, 0x45, 0xcd, 0x55, 0x76, 0xbe, 0x71, 0x3a, 0x88, - 0xf3, 0xce, 0xec, 0xc2, 0x68, 0x7d, 0xfd, 0x38, 0x4d, 0x49, 0xef, 0x57, 0x9a, 0xc7, 0x45, 0x81, 0xe4, 0x6f, 0xab, 0x4b, - 0x50, 0xa2, 0x43, 0x08, 0x71, 0x78, 0x43, 0xa2, - 0x66, 0x8e, 0x2b, 0xfd, 0x64, 0xc3, 0xed, 0x09, 0x1f, 0xa6, 0xe8, 0xa2, // mac - }) - - f.Fuzz(func(t *testing.T, data []byte) { - writer := bytes.NewBuffer(nil) - _, err := sdk.ReadNanoTDF(writer, bytes.NewReader(data)) - - require.Error(t, err) // will always err due to no server running - require.Equal(t, 0, writer.Len()) - }) -} - -func FuzzReadPolicyBody(f *testing.F) { - pb := &PolicyBody{ - mode: 0, - rp: remotePolicy{ - url: ResourceLocator{ - protocol: 0, - body: "example.com", - }, - }, - } - f.Add(writeBytes(pb.writePolicyBody)) - pb = &PolicyBody{ - mode: 1, - ep: embeddedPolicy{ - lengthBody: 3, - body: []byte("foo"), - }, - } - f.Add(writeBytes(pb.writePolicyBody)) - - f.Fuzz(func(t *testing.T, data []byte) { - pb = &PolicyBody{} - err := pb.readPolicyBody(bytes.NewReader(data)) - if err != nil { - assert.Zerof(t, *pb, "unexpected %v", *pb) - return - } - }) -} - func FuzzNewResourceLocatorFromReader(f *testing.F) { f.Add([]byte{0x00, 0x00, 0x00}) // zero size f.Add([]byte{0x00, 0xFF, 0x00}) // max size diff --git a/sdk/go.mod b/sdk/go.mod index 0d9b9f9e8e..20bed4e8f5 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -1,99 +1,48 @@ module github.com/opentdf/platform/sdk -go 1.24.0 - -toolchain go1.24.11 +go 1.25.0 require ( - connectrpc.com/connect v1.19.1 - github.com/Masterminds/semver/v3 v3.4.0 + connectrpc.com/connect v1.19.2 + connectrpc.com/grpchealth v1.4.0 + github.com/Masterminds/semver/v3 v3.5.0 github.com/google/uuid v1.6.0 github.com/gowebpki/jcs v1.0.1 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/lestrrat-go/jwx/v2 v2.1.6 - github.com/opentdf/platform/lib/fixtures v0.4.0 - github.com/opentdf/platform/lib/ocrypto v0.8.0 - github.com/opentdf/platform/protocol/go v0.14.0 + github.com/opentdf/platform/lib/ocrypto v0.12.0 + github.com/opentdf/platform/protocol/go v0.32.0 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.40.0 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/oauth2 v0.34.0 - golang.org/x/text v0.32.0 - golang.org/x/tools v0.39.0 - google.golang.org/grpc v1.77.0 - google.golang.org/protobuf v1.36.10 + golang.org/x/oauth2 v0.36.0 + golang.org/x/text v0.37.0 + golang.org/x/tools v0.44.0 + google.golang.org/grpc v1.81.0 + google.golang.org/protobuf v1.36.11 ) require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1 // indirect - dario.cat/mergo v1.0.2 // indirect - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Nerzal/gocloak/v13 v13.9.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.5.1+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.8.4 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.1.0 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect - github.com/moby/sys/sequential v0.6.0 // indirect - github.com/moby/sys/user v0.4.0 // indirect - github.com/moby/sys/userns v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/segmentio/asm v1.2.0 // indirect - github.com/segmentio/ksuid v1.0.4 // indirect - github.com/shirou/gopsutil/v4 v4.25.6 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.38.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/sdk/go.sum b/sdk/go.sum index 605ff7c3a0..0dd3ca89c0 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -1,67 +1,27 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1 h1:uwSqFkn8DDTzNlaV9TxgSXY5OCaNdb4rH+Axd2FujkE= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= -connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= -connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Nerzal/gocloak/v13 v13.9.0 h1:YWsJsdM5b0yhM2Ba3MLydiOlujkBry4TtdzfIzSVZhw= -github.com/Nerzal/gocloak/v13 v13.9.0/go.mod h1:YYuDcXZ7K2zKECyVP7pPqjKxx2AzYSpKDj8d6GuyM10= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= +connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/grpchealth v1.4.0 h1:MJC96JLelARPgZTiRF9KRfY/2N9OcoQvF2EWX07v2IE= +connectrpc.com/grpchealth v1.4.0/go.mod h1:WhW6m1EzTmq3Ky1FE8EfkIpSDc6TfUx2M2KqZO3ts/Q= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= -github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= -github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -72,10 +32,6 @@ github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -92,72 +48,24 @@ github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVf github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opentdf/platform/lib/fixtures v0.4.0 h1:p3Y5MLJEBaWiSmo+QyRNTirvI8LqYDj+HtaE9vYrEJ8= -github.com/opentdf/platform/lib/fixtures v0.4.0/go.mod h1:ctyrVn+eTObHAPy3vrdPO0O1mc3vgQ6lc9pBTdhBAfo= -github.com/opentdf/platform/lib/ocrypto v0.8.0 h1:rit/59go69mRHS3kAJQfX4zSbEgK7j5RMRCrx8UX6JA= -github.com/opentdf/platform/lib/ocrypto v0.8.0/go.mod h1:/TtiJldbP/LO1cvX8bwhnd7SVHSUImBt1EfjG9qEo78= -github.com/opentdf/platform/protocol/go v0.14.0 h1:gOG+VXCD5yb8p/uFDpEfsJndcV9wyzuRewioB8xCWJk= -github.com/opentdf/platform/protocol/go v0.14.0/go.mod h1:/lCXN+m/XCd63fSsSYWA4hGaZXCHPk5drdD0oPx+woc= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/opentdf/platform/lib/ocrypto v0.12.0 h1:N449KWy7VdMO0JwfsrG0kM6Uy8VrEnVvBciwzRHwnlg= +github.com/opentdf/platform/lib/ocrypto v0.12.0/go.mod h1:51UTmAWO6C8ghuMXiktpn63N+fLUQxY6zo8D65Ly0wQ= +github.com/opentdf/platform/protocol/go v0.32.0 h1:XdH/MscjqpESzmfNHSlC3/b84KDRJWrKSoRjbTTfKh4= +github.com/opentdf/platform/protocol/go v0.32.0/go.mod h1:GCiAAv0I8tkQDA2j9FuWzmK78OtIZSl+eAxAf2WHG+4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= -github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= -github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -165,68 +73,45 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/sdk/granter.go b/sdk/granter.go index 8275a04f42..ede578cc17 100644 --- a/sdk/granter.go +++ b/sdk/granter.go @@ -286,6 +286,12 @@ func convertAlgEnum2Simple(a policy.KasPublicKeyAlgEnum) policy.Algorithm { return policy.Algorithm_ALGORITHM_RSA_2048 case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096: return policy.Algorithm_ALGORITHM_RSA_4096 + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING: + return policy.Algorithm_ALGORITHM_HPQT_XWING + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768: + return policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024: + return policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED: return policy.Algorithm_ALGORITHM_UNSPECIFIED default: @@ -293,24 +299,6 @@ func convertAlgEnum2Simple(a policy.KasPublicKeyAlgEnum) policy.Algorithm { } } -// convertStringToAlgorithm converts a string algorithm representation to policy.Algorithm -func convertStringToAlgorithm(alg string) policy.Algorithm { - switch ocrypto.KeyType(strings.ToLower(alg)) { - case ocrypto.EC256Key: - return policy.Algorithm_ALGORITHM_EC_P256 - case ocrypto.EC384Key: - return policy.Algorithm_ALGORITHM_EC_P384 - case ocrypto.EC521Key: - return policy.Algorithm_ALGORITHM_EC_P521 - case ocrypto.RSA2048Key: - return policy.Algorithm_ALGORITHM_RSA_2048 - case ocrypto.RSA4096Key: - return policy.Algorithm_ALGORITHM_RSA_4096 - default: - return policy.Algorithm_ALGORITHM_UNSPECIFIED - } -} - type grantType int const ( @@ -490,31 +478,18 @@ func algProto2String(e policy.KasPublicKeyAlgEnum) string { return string(ocrypto.RSA2048Key) case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096: return string(ocrypto.RSA4096Key) + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING: + return string(ocrypto.HybridXWingKey) + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768: + return string(ocrypto.HybridSecp256r1MLKEM768Key) + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024: + return string(ocrypto.HybridSecp384r1MLKEM1024Key) case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED: return "" } return "" } -func algProto2OcryptoKeyType(e policy.Algorithm) ocrypto.KeyType { - switch e { - case policy.Algorithm_ALGORITHM_EC_P256: - return ocrypto.EC256Key - case policy.Algorithm_ALGORITHM_EC_P384: - return ocrypto.EC384Key - case policy.Algorithm_ALGORITHM_EC_P521: - return ocrypto.EC521Key - case policy.Algorithm_ALGORITHM_RSA_2048: - return ocrypto.RSA2048Key - case policy.Algorithm_ALGORITHM_RSA_4096: - return ocrypto.RSA4096Key - case policy.Algorithm_ALGORITHM_UNSPECIFIED: - return ocrypto.KeyType("") - default: - return ocrypto.KeyType("") - } -} - func storeKeysToCache(logger *slog.Logger, kases []*policy.KeyAccessServer, keys []*policy.SimpleKasKey, c *kasKeyCache, kc *rlKeyCache) { for _, kas := range kases { keys := kas.GetPublicKey().GetCached().GetKeys() @@ -726,7 +701,10 @@ func (r granter) resolveTemplate(ctx context.Context, kaoKeyAlg string, genSplit } o.identifier = kpub.KID // Convert the string algorithm to the appropriate enum - algEnum := convertStringToAlgorithm(kpub.Algorithm) + algEnum, err := getKasKeyAlg(strings.ToLower(kpub.Algorithm)) + if err != nil { + return nil, fmt.Errorf("unsupported algorithm for kas [%s#%s]: %w", o.KASURI(), kpub.KID, err) + } r.keyCache.c[*o] = &policy.SimpleKasKey{ KasUri: o.KASURI(), PublicKey: &policy.SimpleKasPublicKey{ @@ -740,7 +718,10 @@ func (r granter) resolveTemplate(ctx context.Context, kaoKeyAlg string, genSplit if !ok || kpub.GetPublicKey() == nil || kpub.GetPublicKey().GetPem() == "" { return nil, fmt.Errorf("no key found for resource locator [%s]", o) } - algorithm := algProto2OcryptoKeyType(kpub.GetPublicKey().GetAlgorithm()) + algorithm, err := PolicyAlgorithmToKeyType(kpub.GetPublicKey().GetAlgorithm()) + if err != nil { + return nil, fmt.Errorf("invalid algorithm [%v] for kas %s with kid [%s]: %w", kpub.GetPublicKey().GetAlgorithm(), kpub.GetKasUri(), kpub.GetPublicKey().GetKid(), err) + } p = append(p, kaoTpl{o.KASURI(), splitID, o.ID(), kpub.GetPublicKey().GetPem(), algorithm}) } } diff --git a/sdk/granter_test.go b/sdk/granter_test.go index a47c7d6a57..bf29139347 100644 --- a/sdk/granter_test.go +++ b/sdk/granter_test.go @@ -163,7 +163,6 @@ func mockAttributeFor(fqn AttributeNameFQN) *policy.Attribute { case MP.key: g := make([]*policy.KeyAccessServer, 1) g[0] = mockGrant(specifiedKas, "r1") - g[0].PublicKey = createPublicKey("r1", mockRSAPublicKey1, policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048) return &policy.Attribute{ Id: "MP", Namespace: &nsOne, @@ -370,22 +369,6 @@ func mockSimpleKasKey(kas, kid string) *policy.SimpleKasKey { } } -func createPublicKey(kid, pem string, algorithm policy.KasPublicKeyAlgEnum) *policy.PublicKey { - return &policy.PublicKey{ - PublicKey: &policy.PublicKey_Cached{ - Cached: &policy.KasPublicKeySet{ - Keys: []*policy.KasPublicKey{ - { - Kid: kid, - Alg: algorithm, - Pem: pem, - }, - }, - }, - }, - } -} - func mockValueFor(fqn AttributeValueFQN) *policy.Value { an := fqn.Prefix() a := mockAttributeFor(an) diff --git a/sdk/internal/archive/fuzz_test.go b/sdk/internal/archive/fuzz_test.go deleted file mode 100644 index c66893dc43..0000000000 --- a/sdk/internal/archive/fuzz_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package archive - -import ( - "bytes" - "encoding/base64" - "testing" - - "github.com/stretchr/testify/assert" -) - -func unverifiedBase64Bytes(str string) []byte { - b, _ := base64.StdEncoding.DecodeString(str) - return b -} - -func FuzzReader(f *testing.F) { - // seeds derived from existing unit tests - f.Add(unverifiedBase64Bytes("UEsDBC0ACAAAAD2WLTEAAAAAAAAAAAAAAAAJAAAAM" + - "C5wYXlsb2Fk08izTVcCMQg+XVhewRxbr57U17gYv3bdYO41/zR3XrezUEsHCApBjlYhAAAA" + - "IQAAAFBLAwQtAAgAAAA9li0xAAAAAAAAAAAAAAAADwAAADAubWFuaWZlc3QuanNvbnsiZW5" + - "jcnlwdGlvbkluZm9ybWF0aW9uIjp7InR5cGUiOiJzcGxpdCIsInBvbGljeSI6ImV5SjFkV2" + - "xrSWpvaVkyRmhPVEJpWVdFdE5UbGhOQzB4TVdWbUxUbGhNVFl0WVdFMVlqWmtaVGMxWVRCa" + - "klpd2lZbTlrZVNJNmV5SmtZWFJoUVhSMGNtbGlkWFJsY3lJNmJuVnNiQ3dpWkdsemMyVnRJ" + - "anB1ZFd4c2ZYMD0iLCJrZXlBY2Nlc3MiOlt7InR5cGUiOiJ3cmFwcGVkIiwidXJsIjoiaHR" + - "0cDovL2xvY2FsaG9zdDo2NTQzMi8iLCJwcm90b2NvbCI6ImthcyIsIndyYXBwZWRLZXkiOi" + - "JkK3dobEZJdEF2Y3lYYU5ZcWpmRmpiWXVDZVBGcTRyOS9ZSFJLeTJwWmwwRkxqa29oK3FUV" + - "XRJVkZOMFlkYjA5S0M3ZytkUllBdTFTSzYxYjE1MUJYRFJhZG9zQ1crTUlDWUFid1RLWENY" + - "RG15TW1HaVhKU2RHcWxza2NlakVJWXVUbDBXaGwxVisyUlhEZkl1WXZKN1N2YmZ2OExVVmN" + - "tNHFXR1R1RDBjcmVQNnhWaHVQdVE2V1FIOWlZNlA4K3kwUG92MEd3VzNTOWhZdlBjY3pNcG" + - "F0UTZPMytsbGZsYkxGRjZVcVdQMGVZcGxWU21nZXg1V3BjWFlreFJHdGZJTkRhYzBqS1NnM" + - "FpTUDdxbThQNXdPd2F3NlgzbUNQL3ZpYkxXQy9UYUczVEg0bmY2dXgvbWc3NEFvUWxockFs" + - "TUdpMTJwNUxGL0VabVZYeXlrSnhpYkE9PSIsInBvbGljeUJpbmRpbmciOnsiYWxnIjoiSFM" + - "yNTYiLCJoYXNoIjoiWTJRME1qWmhOVE15WWpoa09EQmtZamN5WWpGaE5XWTFZakkwTXpFek" + - "0yRmxaV1pqTTJWa1lqTXhOMlk1TnpNMk5EWmtNV0kxT0RFMU1tRTRNekJrT1E9PSJ9LCJra" + - "WQiOiJyMSJ9XSwibWV0aG9kIjp7ImFsZ29yaXRobSI6IkFFUy0yNTYtR0NNIiwiaXYiOiIi" + - "LCJpc1N0cmVhbWFibGUiOnRydWV9LCJpbnRlZ3JpdHlJbmZvcm1hdGlvbiI6eyJyb290U2l" + - "nbmF0dXJlIjp7ImFsZyI6IkhTMjU2Iiwic2lnIjoiTlRZMk1USTJaVFUxTWpRd09HVTVaR1" + - "kxT0dZM01qSmtObVEwTTJVd05XWTNNRGMwTm1RME1qZG1OVEEwTURKaFpUZzVNREExWVRRM" + - "FlqTTFOekJqTWc9PSJ9LCJzZWdtZW50SGFzaEFsZyI6IkdNQUMiLCJzZWdtZW50U2l6ZURl" + - "ZmF1bHQiOjIwOTcxNTIsImVuY3J5cHRlZFNlZ21lbnRTaXplRGVmYXVsdCI6MjA5NzE4MCw" + - "ic2VnbWVudHMiOlt7Imhhc2giOiJaRFJrTjJJNE1UaGlaamMyWkdRMk1HVmxNelZtWmpNME" + - "56YzFaV0kzWWpNPSIsInNlZ21lbnRTaXplIjo1LCJlbmNyeXB0ZWRTZWdtZW50U2l6ZSI6M" + - "zN9XX19LCJwYXlsb2FkIjp7InR5cGUiOiJyZWZlcmVuY2UiLCJ1cmwiOiIwLnBheWxvYWQi" + - "LCJwcm90b2NvbCI6InppcCIsIm1pbWVUeXBlIjoidGV4dC9wbGFpbiIsImlzRW5jcnlwdGV" + - "kIjp0cnVlfX1QSwcICGOQ8AsFAAALBQAAUEsBAi0ALQAIAAAAPZYtMQpBjlYhAAAAIQAAAA" + - "kAAAAAAAAAAAAAAAAAAAAAADAucGF5bG9hZFBLAQItAC0ACAAAAD2WLTEIY5DwCwUAAAsFA" + - "AAPAAAAAAAAAAAAAAAAAFgAAAAwLm1hbmlmZXN0Lmpzb25QSwUGAAAAAAIAAgB0AAAAoAUA" + - "AAAA")) - f.Add(unverifiedBase64Bytes("UEsDBC0ACAAAAD2WLTEAAAAAAAAAAAAAAAAJAAAAM" + - "C5wYXlsb2FkDSvwsbJutP3SwAxiF0WieCKrIIVAG0Ae4OHfVLFcwnhWAm13w4okVqReL7GB" + - "CmiI3OQIvl2zo7KWZABCfFLDc+9oCaRVnBaOWUy5ruMQlHeXJ3SdSZe0K3F77OHYueUWDh/" + - "WCdb+GG3LVQkOdKPr+GvIcOTktlJJojnFZTZ5fKxKzNwTNrTCAgqdzFU2RH696b3Nl0S3AW" + - "ovOWSM8UQ9mAB+H8x+QlSjHLX5m6OCGRFHLYInvLeHhbbso/8OU11LHjMqHeMOsyJAfpupo" + - "kv59QPa0XfjtXAhHp6M+V1zF3rJl3TTWq3NNnYfm29pYBkV4Cs9nBsZQ+LnhBMqXKLfic8b" + - "vAc+zShk2f6jmaZfiXSLWFDVxZjLGaGCWX8gvkOG8HlajEVI8bSDiC1JO9kIqBJxNFFmyPl" + - "HvMPEkx1sG2ZaYYZERc+JpJQTxnM6jI45JQ4JXCYPUP+m9RVlAkH2Stg719P3USbJFbvxgT" + - "XhsuTH0talbolQdKd3i7Zrl62DLn6GByJ/LqZNiNRy2PgDo2IFpx7J9VUQNfj9RjpoPzRmS" + - "lOIk+MFA4twmhYgWtSU6BsdynSirYZ4zZP0VrJ1TFVPygoGVsNy3CdP39kURmndFq6JPdcF" + - "uZ1Wx3zCur5aqmb6bDz1rIjmBpzkdmqoGWNPpsim6Tzkc6sBe90eASg8ksg40Bu4JVwFUD/" + - "XMH8oGWvP+5xriMckeCOEiGSJ1Ro0JDPv5kWoddLqz4XrPJ5jzy/Y82ZXbIji1PEf04J7nn" + - "NGQVzpYvqZszXNaEkri9VCcC1xgrgMJAYDRuGmGpw28kffaB9hMr2Ee5ubDwysEEAJhSYJb" + - "iityJpbuG8J4JBiKd5kdrr55SOPwG7ycJLdz1e0uhKHFpAyJJgNTRVaALVdm0W0kCmCeZTu" + - "OGL5naIY7iQGVB4iIFOpj2tbb1sm/bhsTz+fzd30Rf/SiNjn1bKXKKFygvBKIZ8rtUZwbp5" + - "FcghXtffgeGOo5omQ0XBUOmKW1V+lRVXUXjL6frYVe1y6ZkZQo+VCE/yKPqOQEZeAJSViWK" + - "7lPpavnSqcsgGZImiF7eeegvTIJks8vJOaqOXfEKpLKlGIpv+/dHrGgmq8OhkPFa/PjHC4Y" + - "EkNjNzL0PwTuX8OPcDAoGZ+DzSVnlS+iISNaN2x28o460YIYrMLeg1G/W8pFAk7zYyWLxLD" + - "T8kLY4FKdidD6OAtgSxJSmvRZnS01x9K1sVFTyy/Ng1SjnuwAM8e9tV3G7ffD1JK8VCglNx" + - "ZfOmKrt28EnKlU7+gAYC6vZQLgYLQzAYe8Dufq4xcUQ8oAmXdpQo+TiFGK7MuWGTZOpEa9w" + - "sQsviEqOqRU6Fsyy0KIYdUWa2NvAww862M9cDhT1UETESHGmOOmuBJunFLzAwKlI1QSwcIc" + - "cpeYxwEAAAcBAAAUEsDBC0ACAAAAD2WLTEAAAAAAAAAAAAAAAAPAAAAMC5tYW5pZmVzdC5q" + - "c29ueyJlbmNyeXB0aW9uSW5mb3JtYXRpb24iOnsidHlwZSI6InNwbGl0IiwicG9saWN5Ijo" + - "iZXlKMWRXbGtJam9pWTJGa09ETmlZalF0TlRsaE5DMHhNV1ZtTFRsaE1UWXRZV0UxWWpaa1" + - "pUYzFZVEJqSWl3aVltOWtlU0k2ZXlKa1lYUmhRWFIwY21saWRYUmxjeUk2Ym5Wc2JDd2laR" + - "2x6YzJWdElqcHVkV3hzZlgwPSIsImtleUFjY2VzcyI6W3sidHlwZSI6IndyYXBwZWQiLCJ1" + - "cmwiOiJodHRwOi8vbG9jYWxob3N0OjY1NDMyLyIsInByb3RvY29sIjoia2FzIiwid3JhcHB" + - "lZEtleSI6ImxDeHJnQ2dRUTlhYUdTRW5mcUpFK1h6a1pBaUVNMW1qRkpHR292MkFGQnJnUl" + - "J2aVU1WjZhNUJnSk15OU9tcWdORG5Db0ozWmQ4a1BzaGdSK25JdmpuUlBDdnRBcUo2NFlMT" + - "XVnaXI2dUxoU1VUb241SE1HRXVZcU1lTVkrNmRnbkdteDN0Ty9uZmJTNDBpQk1sZmxKcG0w" + - "bFNudExjZTFQd1VVbHJ5VkR4cTVUaHVROEFlaS9CUkNPMnpnT3Q2UjQwK3cxcjF3SnEwVXp" + - "MdzAraFY3dlJxdmJxVFluQmF4d3lhdTFhUmxHZ1VQUGFOWmFOcVpiUkdVYko4Z3R1bTRNQ0" + - "5DNmZJajFzR0NyM2FTSjdKTEFFRjlQdm9DL3RQd2diOXpiU0x1M0czb0kzUXY4aVl0Zk5PU" + - "3ZxaEZoajlTdVFTMWlFNGlxYmZ4Skp6Um0yRm9QZz09IiwicG9saWN5QmluZGluZyI6eyJh" + - "bGciOiJIUzI1NiIsImhhc2giOiJNell6TXpFMVpEWTFNVGt3WlRBeFkySXhNVEF6TURObU5" + - "HSTFPR1JqWXpFMVl6RXpaamswWkRrMVpETTFOMkV4WWpFd09XRmhaamxpWlRjMllUZzBZdz" + - "09In0sImtpZCI6InIxIn1dLCJtZXRob2QiOnsiYWxnb3JpdGhtIjoiQUVTLTI1Ni1HQ00iL" + - "CJpdiI6IiIsImlzU3RyZWFtYWJsZSI6dHJ1ZX0sImludGVncml0eUluZm9ybWF0aW9uIjp7" + - "InJvb3RTaWduYXR1cmUiOnsiYWxnIjoiSFMyNTYiLCJzaWciOiJNR014WmpZeFlqazVZbVp" + - "qTkdVNFlqSTVPREEzTWpJeFlURTJOREUzTXpRd01XTmpZVFJsWmpBd05tSmlOVFkwTVdFel" + - "l6WmlNekl6T1dRNE9XRTVNUT09In0sInNlZ21lbnRIYXNoQWxnIjoiR01BQyIsInNlZ21lb" + - "nRTaXplRGVmYXVsdCI6MjA5NzE1MiwiZW5jcnlwdGVkU2VnbWVudFNpemVEZWZhdWx0Ijoy" + - "MDk3MTgwLCJzZWdtZW50cyI6W3siaGFzaCI6Ik5EUTROekZoTmpNNFpUbGhaVEEwT1dKaE5" + - "6RTBZbU5qTUdNd1lUazBPR1E9Iiwic2VnbWVudFNpemUiOjEwMjQsImVuY3J5cHRlZFNlZ2" + - "1lbnRTaXplIjoxMDUyfV19fSwicGF5bG9hZCI6eyJ0eXBlIjoicmVmZXJlbmNlIiwidXJsI" + - "joiMC5wYXlsb2FkIiwicHJvdG9jb2wiOiJ6aXAiLCJtaW1lVHlwZSI6ImFwcGxpY2F0aW9u" + - "L29jdGV0LXN0cmVhbSIsImlzRW5jcnlwdGVkIjp0cnVlfX1QSwcI9qRQPB4FAAAeBQAAUEs" + - "BAi0ALQAIAAAAPZYtMXHKXmMcBAAAHAQAAAkAAAAAAAAAAAAAAAAAAAAAADAucGF5bG9hZF" + - "BLAQItAC0ACAAAAD2WLTH2pFA8HgUAAB4FAAAPAAAAAAAAAAAAAAAAAFMEAAAwLm1hbmlmZ" + - "XN0Lmpzb25QSwUGAAAAAAIAAgB0AAAArgkAAAAA")) - // large defined filename - f.Add(unverifiedBase64Bytes("UEsDBC0ACAAAAH11LzEAAAAAAAAAAAAAAAAJAAAAM" + - "C5wYXlsb2Fk5LJYrTiapi/CUQ0dlqMU0/VmunX+qRIyQghasf6aEVBLBwgke7o5HwAAAB8A" + - "AABQSwMELQAIAAAAfXUvMQAAAAAAAAAAAAAAAA8AAAAwLm1hbmlmZXN0Lmpzb257ImVOY3J" + - "5cHRpb25JbmZvcm1hdGlvbiI6eyJ0eXBlIjoic3BsaXQiLCJwb2xpY3kiOiJleUoxZFdsa0" + - "lqb2lZakF3TW1WaU9USXROV0l4TkMweE1XVm1MVGt4TW1NdFlXRTFZalprWlRjMVlUQmpJa" + - "XdpWW05a2VTSTZleUprWVhSaFFYUjBjbWx5ZFhSbGN5STZiblZzYkN3aVpHbHpjMlZ0SWpw" + - "dWRXeHNmWDA9Iiwia2V5QWNjZXNzIjpbeyJ0eXBlIjoid3JhcHBlZCIsInVybCI6ImV4YW1" + - "wbGUuY29tIiwicHJvdG9jb2wiOiJrYXMiLCJ3cmFwcGVkS2V5IjoiV1dZait3anNMQmtrU2" + - "FjTzZ2dEpJaTBLMUJQMVhtT2lzcFNrdm8wRm5QV0ZLM050UTVzN3YwOVpqQ05NV0JRK1VPa" + - "VhUTVNWa1JkNUdsTHlMblg3bjY4dDBmSDk0RnMyTnRjcFJwMSt6YStjdzVGRldFQy9uQUJp" + - "TmtPdldLeHdqeG5YQ1pEazZ4U3o1ZHdCT1MraUVCYXJ6WGMzR3oxR2JYcm5Ka0YvaitUUDR" + - "rbTJUYUpXN0cybFJaQ0J6T1M5RkpoSEFIcFBIcFF4V2tNK2FuZjJ1WExRV1UxT00vaHFVRz" + - "VFUG9nR0pYM3MxaVRmek4xNFhiczU5TmYyOU1rc284VjhJSnNOWVRPblBIejY4Q3VvOGdjc" + - "XZHd3J0a3FKQmlmYVM3N1FRQWxwUTcrSU9GME9ZSjh1WTZLZG1najltSU1aRUVaYkI3V2hO" + - "blNBbG9paWZBPT0iLCJwb2xpY3lCaW5kaW5nIjp7ImFsZyI6IkhTMjU2IiwiaGFzaCI6Ilp" + - "UY3pZMkV5WkdReVkySTJNRGN4WmpnellXVTVNRGsxWXpnNU5XWXhOalUwWVRjNE5tTXpPV1" + - "EwTW1JM05qQmxOemxsTmpWaVltWTRZalUyWkdNd013PT0ifX1dLCJtZXRob2QiOnsiYWxnb" + - "3JpdGhtIjoiQUVTLTI1Ni1HQ00iLCJpdiI6IiIsImlzU3RyZWFtYWJsZSI6dHJ1ZX0sImlu" + - "dGVncml0eUluZm9ybWF0aW9uIjp7InJvb3RTaWduYXR1cmUiOnsiYWxnIjoiSFMyNTYiLCJ" + - "zaWciOiJNRFZqTURReE1EWmtNR00wWlRRMllUZG1PRFJrWVRJM09UZGlPREk1WVRWak5EVX" + - "hPRGs0TkRreE1HWTFaV1kxTXpKbVpHWmtZMlkwWWprek0yVmhOZz09In0sInNlZ21lbnRIY" + - "XNoQWxnIjoiR01BQyIsInNlZ21lbnRTaXplRGVmYXVsdCI6MjA5NzE1MiwiZW5jcnlwdGVk" + - "U2VnbWVudFNpemVEZWZhdWx0IjoyMDk3MTgwLCJzZWdtZW50cyI6W3siaGFzaCI6IlpETm1" + - "OVFkyWW1FM05XWmxZVGt4TWpNeU5ESXdPRFZoWWpGbVpUbGhNVEU9Iiwic2VnbWVudFNpem" + - "UiOjMsImVuY3J5cHRlZFNlZ21lbnRTaXplIjozMX1dfX0sInBheWxvYWQiOnsidHlwZSI6I" + - "nJlZmVyZW5jZSIsInVybCI6IjAucGF5bG9hZCIsInByb3RvY29sIjoiemlwIiwibWltZVR5" + - "cGUiOiJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iLCJpc0VuY3J5cHRlZCI6dHJ1ZX19UEs" + - "HCALoriwCBQAAAgUAAFBLAQItAC0ACAAAAH11LzEke7o5HwAAAB8AAAAJAAAAAAAAAAAAAA" + - "AAAAAAAAAwLnBheWxvYWRQSwECLQAtAAgAAAB9dS8xAuiuLAIE///tBQAADwAAAAAAAAAAA" + - "AAAAABWAAAAMC5tYW5pZmVzdC5qc29uUEsFBgAAAAACAAIAdAAAAJUFAAAAAA==")) - - f.Fuzz(func(t *testing.T, data []byte) { - reader, err := NewReader(bytes.NewReader(data)) - if err != nil { - return - } - for k := range reader.fileEntries { - b, err := reader.ReadAllFileData(k, 1024*1024*20 /* 20MB Limit */) - if err != nil { - assert.Empty(t, b) - } - } - }) -} diff --git a/sdk/internal/archive/reader.go b/sdk/internal/archive/reader.go deleted file mode 100644 index 90fcac85d4..0000000000 --- a/sdk/internal/archive/reader.go +++ /dev/null @@ -1,278 +0,0 @@ -package archive - -import ( - "encoding/binary" - "errors" - "fmt" - "io" -) - -// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT -// https://rzymek.github.io/post/excel-zip64/ -// Overall .ZIP file format: -// [local file header 1] -// [file data 1] -// [ext 1] -// [data descriptor 1] -// . -// . -// . -// [local file header n] -// [file data n] -// [ext n] -// [data descriptor n] -// [central directory header 1] -// . -// . -// . -// [central directory header n] -// [zip64 end of central directory record] -// [zip64 end of central directory locator] -// [end of central directory record] - -var ( - errZipFormat = errors.New("zip: not a valid zip file") - errZipFileNotFound = errors.New("zip: file not found") - errZipFileSizeError = errors.New("zip: not a valid file size") - errZipFormatFileHeader = errors.New("zip: unable to read local file header") -) - -type ZipFileEntry struct { - index int64 - length int64 -} - -type Reader struct { - readSeeker io.ReadSeeker - fileEntries map[string]ZipFileEntry -} - -// NewReader Create archive reader instance. -func NewReader(readSeeker io.ReadSeeker) (Reader, error) { - reader := Reader{} - reader.fileEntries = make(map[string]ZipFileEntry) - - // read end of central directory record - _, err := readSeeker.Seek(-endOfCDRecordSize, io.SeekEnd) - if err != nil { - return reader, fmt.Errorf("readSeeker.Seek failed: %w", err) - } - - endOfCDRecord := EndOfCDRecord{} - err = binary.Read(readSeeker, binary.LittleEndian, &endOfCDRecord) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - - // check if it's valid zip format - if endOfCDRecord.Signature != endOfCentralDirectorySignature { - return reader, errZipFormat - } - - // check if zip is zip64 or zip32 format - var entryCount uint64 - var centralDirectoryStart uint64 - isZip64 := false - if endOfCDRecord.CentralDirectoryOffset != zip64MagicVal { //nolint:nestif // pkzip is complicated - entryCount = uint64(endOfCDRecord.NumberOfCDRecordEntries) - centralDirectoryStart = uint64(endOfCDRecord.CentralDirectoryOffset) - } else { - isZip64 = true - - // read zip64 end of central directory locator - _, err := readSeeker.Seek(-(endOfCDRecordSize + zip64EndOfCDRecordLocatorSize), io.SeekEnd) - if err != nil { - return reader, fmt.Errorf("readSeeker.Seek failed: %w", err) - } - - zip64EndOfCDRecordLocator := Zip64EndOfCDRecordLocator{} - err = binary.Read(readSeeker, binary.LittleEndian, &zip64EndOfCDRecordLocator) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - - if zip64EndOfCDRecordLocator.Signature != zip64EndOfCDLocatorSignature { - return reader, errZipFormat - } - - // read zip64 end of central directory record - _, err = readSeeker.Seek(int64(zip64EndOfCDRecordLocator.CDOffset), io.SeekStart) - if err != nil { - return reader, fmt.Errorf("readSeeker.Seek failed: %w", err) - } - - zip64EndOfCDRecord := Zip64EndOfCDRecord{} - err = binary.Read(readSeeker, binary.LittleEndian, &zip64EndOfCDRecord) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - - if zip64EndOfCDRecord.Signature != zip64EndOfCDSignature { - return reader, errZipFormat - } - - entryCount = zip64EndOfCDRecord.NumberOfCDRecordEntries - centralDirectoryStart = zip64EndOfCDRecord.StartingDiskCentralDirectoryOffset - } - - nextCD := uint64(0) - cdFileHeader := CDFileHeader{} - - reader.readSeeker = readSeeker - for i := uint64(0); i < entryCount; i++ { - // read central directory header of index(i) - _, err = readSeeker.Seek(int64(nextCD+centralDirectoryStart), io.SeekStart) - if err != nil { - return reader, fmt.Errorf("readSeeker.Seek failed: %w", err) - } - - err = binary.Read(readSeeker, binary.LittleEndian, &cdFileHeader) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - - if cdFileHeader.Signature != centralDirectoryHeaderSignature { - return reader, errZipFormat - } - - // read the filename - fileNameByteArray := make([]byte, cdFileHeader.FilenameLength) - err = binary.Read(readSeeker, binary.LittleEndian, fileNameByteArray) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - - offset := uint64(cdFileHeader.LocalHeaderOffset) - bytesToRead := uint64(cdFileHeader.CompressedSize) - - if isZip64 { //nolint:nestif // pkzip is complicated - // read Zip64 Extended Information extra field id - headerTag := uint16(0) - err = binary.Read(readSeeker, binary.LittleEndian, &headerTag) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - - // read Zip64 Extended Information Extra Field Block Size - blockSize := uint16(0) - err = binary.Read(readSeeker, binary.LittleEndian, &blockSize) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - - if headerTag == zip64ExternalID { - if cdFileHeader.CompressedSize == zip64MagicVal { - compressedSize := uint64(0) - err = binary.Read(readSeeker, binary.LittleEndian, &compressedSize) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - - bytesToRead = compressedSize - } - - if cdFileHeader.UncompressedSize == zip64MagicVal { - uncompressedSize := uint64(0) - err = binary.Read(readSeeker, binary.LittleEndian, &uncompressedSize) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - } - - if cdFileHeader.LocalHeaderOffset == zip64MagicVal { - localHeaderOffset := uint64(0) - err = binary.Read(readSeeker, binary.LittleEndian, &localHeaderOffset) - if err != nil { - return reader, fmt.Errorf("binary.Read failed: %w", err) - } - offset = localHeaderOffset - } - } - } - - // Read each file - localFileHeader := LocalFileHeader{} - _, err = readSeeker.Seek(int64(offset), io.SeekStart) - if err != nil { - return reader, fmt.Errorf("readSeeker.Seek failed: %w", err) - } - err = binary.Read(readSeeker, binary.LittleEndian, &localFileHeader) - if err != nil { - return reader, fmt.Errorf("readSeeker.Seek failed: %w", err) - } - - if localFileHeader.Signature != fileHeaderSignature { - return reader, errZipFormatFileHeader - } - - zipFileEntry := ZipFileEntry{} - zipFileEntry.length = int64(bytesToRead) - zipFileEntry.index = int64(offset) + localFileHeaderSize + - int64(localFileHeader.FilenameLength) + int64(localFileHeader.ExtraFieldLength) - - reader.fileEntries[string(fileNameByteArray)] = zipFileEntry - - nextCD += uint64(cdFileHeader.ExtraFieldLength + cdFileHeader.FilenameLength + cdFileHeaderSize) - } - - return reader, nil -} - -// ReadFileData Read data from file of given length of size. -func (reader Reader) ReadFileData(filename string, index int64, length int64) ([]byte, error) { - fileNameEntry, ok := reader.fileEntries[filename] - if !ok { - return nil, errZipFileNotFound - } - - if length < 0 || length > fileNameEntry.length { - return nil, errZipFileSizeError - } - - return readBytes(reader.readSeeker, fileNameEntry.index+index, length) -} - -// ReadAllFileData Return all the data of the file if the file is available and below the specified size. -// NOTE: Use this method for small file sizes. -func (reader Reader) ReadAllFileData(filename string, maxSize int64) ([]byte, error) { - fileNameEntry, ok := reader.fileEntries[filename] - if !ok { - return nil, errZipFileNotFound - } - if fileNameEntry.length > maxSize { - return nil, fmt.Errorf("%s size too large: %d KiB", filename, fileNameEntry.length/1024) //nolint:mnd // convert byte->kb - } - - return readBytes(reader.readSeeker, fileNameEntry.index, fileNameEntry.length) -} - -// ReadFileSize Return the file size of the filename. -func (reader Reader) ReadFileSize(filename string) (int64, error) { - fileNameEntry, ok := reader.fileEntries[filename] - if !ok { - return -1, errZipFileNotFound - } - - return fileNameEntry.length, nil -} - -// Read bytes reads up to size from input providers -// and return the buffer with the read bytes. -func readBytes(readerSeeker io.ReadSeeker, index, size int64) ([]byte, error) { - _, err := readerSeeker.Seek(index, 0) - if err != nil { - return nil, fmt.Errorf("readerSeeker.Seek failed: %w", err) - } - - buf := make([]byte, size) - n, err := readerSeeker.Read(buf) - if errors.Is(err, io.EOF) { - return buf[:n], io.EOF - } - - if err != nil { - return buf[:n], fmt.Errorf("readerSeeker.Read failed: %w", err) - } - - return buf, nil -} diff --git a/sdk/internal/archive/reader_test.go b/sdk/internal/archive/reader_test.go deleted file mode 100644 index 2f605d0ad7..0000000000 --- a/sdk/internal/archive/reader_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package archive - -import ( - "archive/zip" - "bytes" - "io" - "os" - "strconv" - "testing" -) - -func TestCreateArchiveReader(t *testing.T) { // use native library("archive/zip") to create zip files - nativeZipFiles(t) - - // use custom implementation to unzip - customUnzip(t) -} - -func nativeZipFiles(t *testing.T) { - for index := 0; index < len(writeBuffer); index++ { - writeBuffer[index] = 0xFF - } - - // create the zip files using naive library - for index, zipEntries := range ArchiveTests { // zip file name as index - zipFileName := strconv.Itoa(index) + ".zip" - - // Open the zip file - archive, err := os.Create(zipFileName) - if err != nil { - t.Fatalf("Fail to create to archive: %v", err) - } - defer func(archive *os.File) { - err := archive.Close() - if err != nil { - t.Fatalf("Fail to close the closer: %v", err) - } - }(archive) - - // Create a new zip writer. - writer := zip.NewWriter(archive) - defer func(writer *zip.Writer) { - err := writer.Close() - if err != nil { - t.Fatalf("Fail to close the writer: %v", err) - } - }(writer) - - // Iterate over the entries to create files - for _, entry := range zipEntries.files { - input, err := writer.CreateHeader(&zip.FileHeader{ - Name: entry.filename, - Method: zip.Store, - }) - if err != nil { - t.Fatalf("Fail to create to archive entry:%s %v", entry.filename, err) - } - - totalBytes := entry.size - var bytesToWrite int64 - for totalBytes > 0 { - if totalBytes >= stepSize { - totalBytes -= stepSize - bytesToWrite = stepSize - } else { - bytesToWrite = totalBytes - totalBytes = 0 - } - - reader := bytes.NewReader(writeBuffer[:bytesToWrite]) - _, err = io.Copy(input, reader) - if err != nil { - t.Fatalf("Fail to write to archive file:%s : %v", entry.filename, err) - } - } - } - } -} - -func customUnzip(t *testing.T) { - // unzip the zip files using the custom implementation - // test the zip file you created - for index, fileEntries := range ArchiveTests { - // zip file name as index - zipFileName := strconv.Itoa(index) + ".zip" - - readSeeker, err := os.Open(zipFileName) - if err != nil { - t.Fatalf("Fail to open archive file:%s %v", zipFileName, err) - } - - defer func(readSeeker *os.File) { - err := readSeeker.Close() - if err != nil { - t.Fatalf("Fail to close archive file:%v", err) - } - }(readSeeker) - - reader, err := NewReader(readSeeker) - if err != nil { - t.Fatalf("Fail to create archive %v", err) - } - - // Iterate over the files in the zip file - for _, zipEntry := range fileEntries.files { - totalBytes, err := reader.ReadFileSize(zipEntry.filename) - if err != nil { - t.Fatalf("Fail to read the file:%s size archive", zipEntry.filename) - } - - fileIndex := int64(0) - var bytesToRead int64 - for totalBytes > 0 { - if totalBytes >= stepSize { - totalBytes -= stepSize - bytesToRead = stepSize - } else { - bytesToRead = totalBytes - totalBytes = 0 - } - - buf, err := reader.ReadFileData(zipEntry.filename, fileIndex, bytesToRead) - if err != nil { - t.Fatalf("Fail to read from archive file:%s : %v", zipEntry.filename, err) - } - - fileIndex += bytesToRead - - if !bytes.Equal(buf, writeBuffer[:bytesToRead]) { - t.Fatalf("Fail to compare zip contents") - } - } - } - - err = os.Remove(zipFileName) - if err != nil { - t.Fatalf("Fail to remove zip file :%s archive %v", zipFileName, err) - } - } -} diff --git a/sdk/internal/archive/tdf3_reader.go b/sdk/internal/archive/tdf3_reader.go deleted file mode 100644 index 77daf1ab20..0000000000 --- a/sdk/internal/archive/tdf3_reader.go +++ /dev/null @@ -1,63 +0,0 @@ -package archive - -import ( - "io" -) - -type TDFReader struct { - archiveReader Reader - manifestMaxSize int64 -} - -const ( - TDFManifestFileName = "0.manifest.json" - TDFPayloadFileName = "0.payload" - manifestMaxSize = 1024 * 1024 * 10 // 10 MB -) - -type TDFReaderOptions func(*TDFReader) - -func WithTDFManifestMaxSize(size int64) TDFReaderOptions { - return func(tdfReader *TDFReader) { - tdfReader.manifestMaxSize = size - } -} - -// NewTDFReader Create tdf reader instance. -func NewTDFReader(readSeeker io.ReadSeeker, opt ...TDFReaderOptions) (TDFReader, error) { - archiveReader, err := NewReader(readSeeker) - if err != nil { - return TDFReader{}, err - } - - tdfArchiveReader := TDFReader{manifestMaxSize: manifestMaxSize} - tdfArchiveReader.archiveReader = archiveReader - for _, o := range opt { - o(&tdfArchiveReader) - } - - return tdfArchiveReader, nil -} - -// Manifest Return the manifest of the tdf. -func (tdfReader TDFReader) Manifest() (string, error) { - fileContent, err := tdfReader.archiveReader.ReadAllFileData(TDFManifestFileName, tdfReader.manifestMaxSize) - if err != nil { - return "", err - } - return string(fileContent), nil -} - -// ReadPayload Return the payload of given length from index. -func (tdfReader TDFReader) ReadPayload(index, length int64) ([]byte, error) { - return tdfReader.archiveReader.ReadFileData(TDFPayloadFileName, index, length) -} - -// PayloadSize Return the size of the payload. -func (tdfReader TDFReader) PayloadSize() (int64, error) { - size, err := tdfReader.archiveReader.ReadFileSize(TDFPayloadFileName) - if err != nil { - return -1, err - } - return size, nil -} diff --git a/sdk/internal/archive/tdf3_writer.go b/sdk/internal/archive/tdf3_writer.go deleted file mode 100644 index ce7a5777bb..0000000000 --- a/sdk/internal/archive/tdf3_writer.go +++ /dev/null @@ -1,44 +0,0 @@ -package archive - -import "io" - -type TDFWriter struct { - archiveWriter *Writer -} - -// NewTDFWriter Create tdf writer instance. -func NewTDFWriter(writer io.Writer) *TDFWriter { - tdfWriter := TDFWriter{} - tdfWriter.archiveWriter = NewWriter(writer) - - return &tdfWriter -} - -// SetPayloadSize Set 0.payload file size. -func (tdfWriter *TDFWriter) SetPayloadSize(payloadSize int64) error { - if payloadSize >= zip64MagicVal { - tdfWriter.archiveWriter.EnableZip64() - } - - return tdfWriter.archiveWriter.AddHeader(TDFPayloadFileName, payloadSize) -} - -// AppendManifest Add the manifest to tdf archive. -func (tdfWriter *TDFWriter) AppendManifest(manifest string) error { - err := tdfWriter.archiveWriter.AddHeader(TDFManifestFileName, int64(len(manifest))) - if err != nil { - return err - } - - return tdfWriter.archiveWriter.AddData([]byte(manifest)) -} - -// AppendPayload Add payload to sdk archive. -func (tdfWriter *TDFWriter) AppendPayload(data []byte) error { - return tdfWriter.archiveWriter.AddData(data) -} - -// Finish Finished adding all the files in zip archive. -func (tdfWriter *TDFWriter) Finish() (int64, error) { - return tdfWriter.archiveWriter.Finish() -} diff --git a/sdk/internal/archive/tdf3_writer_reader_test.go b/sdk/internal/archive/tdf3_writer_reader_test.go deleted file mode 100644 index 81b676af29..0000000000 --- a/sdk/internal/archive/tdf3_writer_reader_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package archive - -import ( - "bytes" - "os" - "strconv" - "testing" -) - -type TDF3Entry struct { - manifest string - payloadSize int64 - tdfSize int64 -} - -var TDF3Tests = []TDF3Entry{ //nolint:gochecknoglobals // This global is used as test harness for other tests - { - manifest: "some manifest", - payloadSize: oneKB, - tdfSize: 1291, - }, - { - manifest: `{ - "encryptionInformation": { - "integrityInformation": { - "encryptedSegmentSizeDefault": 1048604, - "rootSignature": { - "alg": "HS256", - "sig": "ZjEwYWRjMzJkNzVhMmNkMzljYTU3ZDg3YTJjNjMyMGYwOTZkYjZhZDY4ZTE1Y2Y1MzRlNTdjNjBhNjdlNWUwMQ==" - }, - "segmentHashAlg": "GMAC", - "segmentSizeDefault": 1048576, - "segments": [ - { - "encryptedSegmentSize": 228, - "hash": "YWRkNDhhZWM0Y2VhNmQwZjU5Y2ViOTc5MmFhYzdlOTI=", - "segmentSize": 200 - } - ] - }, - "keyAccess": [ - { - "encryptedMetadata": "eyJjaXBoZXJ0ZXh0IjoidkwyOUVVb1IyOFpVNStiMzFDdE1iNFFVODF5dVhPTnM3SUtDYlZNcDloZkg3dCs2UFRPaG00VFAwbVRDc3R3UEFkeU1ucHltbk4rWWNON0hmbytDIiwiaXYiOiJ2TDI5RVVvUjI4WlU1K2IzIn0=", - "policyBinding": "Zjk1Mjg2ZDljMzYwNGE5ZmU3YWE2M2UzOWRmMjA5MGU2OTJkYTZiYjExNjFkZmZjNTI2N2JkMWY5M2Y3MzIzZQ==", - "protocol": "kas", - "type": "wrapped", - "url": "http://localhost:65432/api/kas", - "wrappedKey": "ARu5wnJPNDaivQymXKOogyC2n11QP4Jf8ZYtrAcYQnUmE9hsjQD2R+48js5T1LkNLp5TzaRREF5sSk5/dhlBge/YXVcT42d5lNp0SecAF68dsso/aXq+G2sRJFVWdYKAtc32mr8KJiPisHtPlPFPM7u37lU0YX93lsqIxiUPn6qkxkD4cEozvA9UgB8YZ8alJtNACnpbOUebJeRLkHbxXM7DzW4gur/lu88lRUtCdaHNBeSOTCgWi2oqTU70asyoFQVVD7R80xKblam5k/B3PKhCkerZkDwyy5D4eODbbqKpGfbluW6NWEM+HtYnJFa+2kJB51yqylsbUnfpWEBQDA==" - } - ], - "method": { - "algorithm": "AES-256-GCM", - "isStreamable": true, - "iv": "vL29EUoR28ZU5+b3" - }, - "policy": "eyJib2R5Ijp7ImRhdGFBdHRyaWJ1dGVzIjpbXSwiZGlzc2VtIjpbXX0sInV1aWQiOiJlMDk0NmVhNC1mZDMzLTQ3ODktODM3Ny1hMzhiMjNhOTc1MmIifQ==", - "type": "split" - }, - "payload": { - "isEncrypted": true, - "mimeType": "application/octet-stream", - "protocol": "zip", - "type": "reference", - "url": "0.payload" - } -}`, - payloadSize: 10 * oneMB, - tdfSize: 10487693, - }, - { - manifest: `{ - "encryptionInformation": { - "integrityInformation": { - "encryptedSegmentSizeDefault": 1048604, - "rootSignature": { - "alg": "HS256", - "sig": "ZjEwYWRjMzJkNzVhMmNkMzljYTU3ZDg3YTJjNjMyMGYwOTZkYjZhZDY4ZTE1Y2Y1MzRlNTdjNjBhNjdlNWUwMQ==" - }, - "segmentHashAlg": "GMAC", - "segmentSizeDefault": 1048576, - "segments": [ - { - "encryptedSegmentSize": 228, - "hash": "YWRkNDhhZWM0Y2VhNmQwZjU5Y2ViOTc5MmFhYzdlOTI=", - "segmentSize": 200 - } - ] - }, - "keyAccess": [ - { - "encryptedMetadata": "eyJjaXBoZXJ0ZXh0IjoidkwyOUVVb1IyOFpVNStiMzFDdE1iNFFVODF5dVhPTnM3SUtDYlZNcDloZkg3dCs2UFRPaG00VFAwbVRDc3R3UEFkeU1ucHltbk4rWWNON0hmbytDIiwiaXYiOiJ2TDI5RVVvUjI4WlU1K2IzIn0=", - "policyBinding": "Zjk1Mjg2ZDljMzYwNGE5ZmU3YWE2M2UzOWRmMjA5MGU2OTJkYTZiYjExNjFkZmZjNTI2N2JkMWY5M2Y3MzIzZQ==", - "protocol": "kas", - "type": "wrapped", - "url": "http://localhost:65432/api/kas", - "wrappedKey": "ARu5wnJPNDaivQymXKOogyC2n11QP4Jf8ZYtrAcYQnUmE9hsjQD2R+48js5T1LkNLp5TzaRREF5sSk5/dhlBge/YXVcT42d5lNp0SecAF68dsso/aXq+G2sRJFVWdYKAtc32mr8KJiPisHtPlPFPM7u37lU0YX93lsqIxiUPn6qkxkD4cEozvA9UgB8YZ8alJtNACnpbOUebJeRLkHbxXM7DzW4gur/lu88lRUtCdaHNBeSOTCgWi2oqTU70asyoFQVVD7R80xKblam5k/B3PKhCkerZkDwyy5D4eODbbqKpGfbluW6NWEM+HtYnJFa+2kJB51yqylsbUnfpWEBQDA==" - } - ], - "method": { - "algorithm": "AES-256-GCM", - "isStreamable": true, - "iv": "vL29EUoR28ZU5+b3" - }, - "policy": "eyJib2R5Ijp7ImRhdGFBdHRyaWJ1dGVzIjpbXSwiZGlzc2VtIjpbXX0sInV1aWQiOiJlMDk0NmVhNC1mZDMzLTQ3ODktODM3Ny1hMzhiMjNhOTc1MmIifQ==", - "type": "split" - }, - "payload": { - "isEncrypted": true, - "mimeType": "application/octet-stream", - "protocol": "zip", - "type": "reference", - "url": "0.payload" - } -}`, - payloadSize: 3 * oneGB, - tdfSize: 3145729933, - }, -} - -func TestTDF3Writer_and_Reader(t *testing.T) { // Create tdf files - writeTDFs(t) - - // Read the tdf files - // NOTE: It will also deletes after reading them - readTDFs(t) -} - -func writeTDFs(t *testing.T) { - for index := 0; index < len(writeBuffer); index++ { - writeBuffer[index] = 0xFF - } - - for index, tdf3Entry := range TDF3Tests { // tdf3 file name as index - tdf3Name := strconv.Itoa(index) + ".zip" - - writer, err := os.Create(tdf3Name) - if err != nil { - t.Fatalf("Fail to open archive file: %v", err) - } - - defer func(outputProvider *os.File) { - err := outputProvider.Close() - if err != nil { - t.Fatalf("Fail to close archive file: %v", err) - } - }(writer) - - tdf3Writer := NewTDFWriter(writer) - - // write payload - totalBytes := tdf3Entry.payloadSize - err = tdf3Writer.SetPayloadSize(totalBytes) - if err != nil { - t.Fatalf("Fail to set payload size: %v", err) - } - - var bytesToWrite int64 - for totalBytes > 0 { - if totalBytes >= stepSize { - totalBytes -= stepSize - bytesToWrite = stepSize - } else { - bytesToWrite = totalBytes - totalBytes = 0 - } - - err = tdf3Writer.AppendPayload(writeBuffer[:bytesToWrite]) - if err != nil { - t.Fatalf("Fail to add payload to tdf3 writer: %v", err) - } - } - - // write manifest - err = tdf3Writer.AppendManifest(tdf3Entry.manifest) - if err != nil { - t.Fatalf("Fail to add payload to tdf3 writer: %v", err) - } - - tdfSize, err := tdf3Writer.Finish() - if err != nil { - t.Fatalf("Fail to close tdf3 writer: %v", err) - } - - if tdfSize != tdf3Entry.tdfSize { - t.Errorf("tdf size test failed expected %v, got %v", tdfSize, tdf3Entry.tdfSize) - } - } -} - -func readTDFs(t *testing.T) { - for index, tdf3Entry := range TDF3Tests { - // tdf3 file name as index - tdf3Name := strconv.Itoa(index) + ".zip" - - inputProvider, err := os.Open(tdf3Name) - if err != nil { - t.Fatalf("Fail to open archive file:%s %v", tdf3Name, err) - } - - defer func(inputProvider *os.File) { - err := inputProvider.Close() - if err != nil { - t.Fatalf("Fail to close archive file:%s %v", tdf3Name, err) - } - }(inputProvider) - - tdf3Reader, err := NewTDFReader(inputProvider) - if err != nil { - t.Fatalf("Fail to create archive %v", err) - } - - // read manifest - manifest, err := tdf3Reader.Manifest() - if err != nil { - t.Fatalf("Fail to read manifest from tdf3 reader %v", err) - } - - if manifest != tdf3Entry.manifest { - t.Fatalf("Fail to compate manifest contents") - } - - // read the payload - readIndex := int64(0) - var bytesToRead int64 - totalBytes := tdf3Entry.payloadSize - for totalBytes > 0 { - if totalBytes >= stepSize { - totalBytes -= stepSize - bytesToRead = stepSize - } else { - bytesToRead = totalBytes - totalBytes = 0 - } - - buf, err := tdf3Reader.ReadPayload(readIndex, bytesToRead) - if err != nil { - t.Fatalf("Fail to read from tdf3 reader: %v", err) - } - - readIndex += bytesToRead - - if !bytes.Equal(buf, writeBuffer[:bytesToRead]) { - t.Fatalf("Fail to compare zip contents") - } - } - - err = os.Remove(tdf3Name) - if err != nil { - t.Fatalf("Fail to remove zip file :%s archive %v", tdf3Name, err) - } - } -} diff --git a/sdk/internal/archive/writer.go b/sdk/internal/archive/writer.go deleted file mode 100644 index aaed7d8d76..0000000000 --- a/sdk/internal/archive/writer.go +++ /dev/null @@ -1,478 +0,0 @@ -//nolint:mnd // pkzip magics and lengths are inlined for clarity -package archive - -import ( - "bytes" - "encoding/binary" - "fmt" - "hash/crc32" - "io" - "math" - "time" -) - -// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT -// https://rzymek.github.io/post/excel-zip64/ -// Overall .ZIP file format: -// [local file header 1] -// [file data 1] -// [ext 1] -// [data descriptor 1] -// . -// . -// . -// [local file header n] -// [file data n] -// [ext n] -// [data descriptor n] -// [central directory header 1] -// . -// . -// . -// [central directory header n] -// [zip64 end of central directory record] -// [zip64 end of central directory locator] -// [end of central directory record] - -// Usage of IArchiveWriter interface: -// -// NOTE: Make sure write the largest file first so the implementation can decide zip32 vs zip64 - -type WriteState int - -const ( - Initial WriteState = iota - Appending - Finished -) - -type FileInfo struct { - crc uint32 - size int64 - offset int64 - filename string - fileTime uint16 - fileDate uint16 - flag uint16 -} - -type Writer struct { - writer io.Writer - currentOffset, lastOffsetCDFileHeader uint64 - FileInfo - fileInfoEntries []FileInfo - writeState WriteState - isZip64 bool - totalBytes int64 -} - -// NewWriter Create tdf3 writer instance. -func NewWriter(writer io.Writer) *Writer { - archiveWriter := Writer{} - - archiveWriter.writer = writer - archiveWriter.writeState = Initial - archiveWriter.currentOffset = 0 - archiveWriter.lastOffsetCDFileHeader = 0 - archiveWriter.fileInfoEntries = make([]FileInfo, 0) - - return &archiveWriter -} - -// EnableZip64 Enable zip 64. -func (writer *Writer) EnableZip64() { - writer.isZip64 = true -} - -// AddHeader set size of the file. calling this method means finished writing -// the previous file and starting a new file. -func (writer *Writer) AddHeader(filename string, size int64) error { - if len(writer.filename) != 0 { - err := fmt.Errorf("writer: cannot add a new file until the current "+ - "file write is not completed:%s", writer.filename) - return err - } - - if !writer.isZip64 { - writer.isZip64 = size > zip64MagicVal - } - - writer.writeState = Initial - writer.size = size - writer.filename = filename - - return nil -} - -// AddData Add data to the zip archive. -func (writer *Writer) AddData(data []byte) error { - localFileHeader := LocalFileHeader{} - fileTime, fileDate := writer.getTimeDateUnMSDosFormat() - - if writer.writeState == Initial { //nolint:nestif // pkzip is complicated - localFileHeader.Signature = fileHeaderSignature - localFileHeader.Version = zipVersion - // since payload is added by chunks we set General purpose bit flag to 0x08 - localFileHeader.GeneralPurposeBitFlag = 0x08 - localFileHeader.CompressionMethod = 0 // no compression - localFileHeader.LastModifiedTime = fileTime - localFileHeader.LastModifiedDate = fileDate - localFileHeader.Crc32 = 0 - - localFileHeader.CompressedSize = 0 - localFileHeader.UncompressedSize = 0 - localFileHeader.ExtraFieldLength = 0 - - if writer.isZip64 { - localFileHeader.CompressedSize = zip64MagicVal - localFileHeader.UncompressedSize = zip64MagicVal - localFileHeader.ExtraFieldLength = zip64ExtendedLocalInfoExtraFieldSize - } - - localFileHeader.FilenameLength = uint16(len(writer.filename)) - - // write localFileHeader - buf := new(bytes.Buffer) - err := binary.Write(buf, binary.LittleEndian, localFileHeader) - if err != nil { - return fmt.Errorf("binary.Write failed: %w", err) - } - - err = writer.writeData(buf.Bytes()) - if err != nil { - return fmt.Errorf("io.Writer.Write failed: %w", err) - } - - // write the file name - err = writer.writeData([]byte(writer.filename)) - if err != nil { - return fmt.Errorf("io.Writer.Write failed: %w", err) - } - - if writer.isZip64 { - zip64ExtendedLocalInfoExtraField := Zip64ExtendedLocalInfoExtraField{} - zip64ExtendedLocalInfoExtraField.Signature = zip64ExternalID - zip64ExtendedLocalInfoExtraField.Size = zip64ExtendedLocalInfoExtraFieldSize - 4 - zip64ExtendedLocalInfoExtraField.OriginalSize = uint64(writer.size) - zip64ExtendedLocalInfoExtraField.CompressedSize = uint64(writer.size) - - buf = new(bytes.Buffer) - err := binary.Write(buf, binary.LittleEndian, zip64ExtendedLocalInfoExtraField) - if err != nil { - return fmt.Errorf("binary.Write failed: %w", err) - } - - err = writer.writeData(buf.Bytes()) - if err != nil { - return fmt.Errorf("io.Writer.Write failed: %w", err) - } - } - - writer.writeState = Appending - - // calculate the initial crc - writer.crc = crc32.Checksum([]byte(""), crc32.MakeTable(crc32.IEEE)) - writer.fileTime = fileTime - writer.fileDate = fileDate - } - - // now write the contents - err := writer.writeData(data) - if err != nil { - return fmt.Errorf("io.Writer.Write failed: %w", err) - } - - // calculate the crc32 - writer.crc = crc32.Update(writer.crc, - crc32.MakeTable(crc32.IEEE), data) - - // update the file size - writer.offset += int64(len(data)) - - // check if we reached end - if writer.offset >= writer.size { - writer.writeState = Finished - - writer.offset = int64(writer.currentOffset) - writer.flag = 0x08 - - writer.fileInfoEntries = append(writer.fileInfoEntries, writer.FileInfo) - } - - if writer.writeState == Finished { //nolint:nestif // pkzip is complicated - if writer.isZip64 { - zip64DataDescriptor := Zip64DataDescriptor{} - zip64DataDescriptor.Signature = dataDescriptorSignature - zip64DataDescriptor.Crc32 = writer.crc - zip64DataDescriptor.CompressedSize = uint64(writer.size) - zip64DataDescriptor.UncompressedSize = uint64(writer.size) - - // write the data descriptor - buf := new(bytes.Buffer) - err := binary.Write(buf, binary.LittleEndian, zip64DataDescriptor) - if err != nil { - return fmt.Errorf("binary.Write failed: %w", err) - } - - err = writer.writeData(buf.Bytes()) - if err != nil { - return fmt.Errorf("io.Writer.Write failed: %w", err) - } - - writer.currentOffset += localFileHeaderSize - writer.currentOffset += uint64(len(writer.filename)) - writer.currentOffset += uint64(writer.size) - writer.currentOffset += zip64DataDescriptorSize - writer.currentOffset += zip64ExtendedLocalInfoExtraFieldSize - } else { - zip32DataDescriptor := Zip32DataDescriptor{} - zip32DataDescriptor.Signature = dataDescriptorSignature - zip32DataDescriptor.Crc32 = writer.crc - zip32DataDescriptor.CompressedSize = uint32(writer.size) - zip32DataDescriptor.UncompressedSize = uint32(writer.size) - - // write the data descriptor - buf := new(bytes.Buffer) - err := binary.Write(buf, binary.LittleEndian, zip32DataDescriptor) - if err != nil { - return fmt.Errorf("binary.Write failed: %w", err) - } - - err = writer.writeData(buf.Bytes()) - if err != nil { - return fmt.Errorf("io.Writer.Write failed: %w", err) - } - - writer.currentOffset += localFileHeaderSize - writer.currentOffset += uint64(len(writer.filename)) - writer.currentOffset += uint64(writer.size) - writer.currentOffset += zip32DataDescriptorSize - } - - // reset the current file info since we reached the total size of the file - writer.FileInfo = FileInfo{} - } - - return nil -} - -// Finish Finished adding all the files in zip archive. -func (writer *Writer) Finish() (int64, error) { - err := writer.writeCentralDirectory() - if err != nil { - return writer.totalBytes, err - } - - err = writer.writeEndOfCentralDirectory() - if err != nil { - return writer.totalBytes, fmt.Errorf("io.Writer.Write failed: %w", err) - } - - return writer.totalBytes, nil -} - -// WriteZip64EndOfCentralDirectory write the zip64 end of central directory record struct to the archive. -func (writer *Writer) WriteZip64EndOfCentralDirectory() error { - zip64EndOfCDRecord := Zip64EndOfCDRecord{} - zip64EndOfCDRecord.Signature = zip64EndOfCDSignature - zip64EndOfCDRecord.RecordSize = zip64EndOfCDRecordSize - 12 - zip64EndOfCDRecord.VersionMadeBy = zipVersion - zip64EndOfCDRecord.VersionToExtract = zipVersion - zip64EndOfCDRecord.DiskNumber = 0 - zip64EndOfCDRecord.StartDiskNumber = 0 - zip64EndOfCDRecord.NumberOfCDRecordEntries = uint64(len(writer.fileInfoEntries)) - zip64EndOfCDRecord.TotalCDRecordEntries = uint64(len(writer.fileInfoEntries)) - zip64EndOfCDRecord.CentralDirectorySize = writer.lastOffsetCDFileHeader - writer.currentOffset - zip64EndOfCDRecord.StartingDiskCentralDirectoryOffset = writer.currentOffset - - // write the zip64 end of central directory record struct - buf := new(bytes.Buffer) - err := binary.Write(buf, binary.LittleEndian, zip64EndOfCDRecord) - if err != nil { - return fmt.Errorf("binary.Write failed: %w", err) - } - - err = writer.writeData(buf.Bytes()) - if err != nil { - return fmt.Errorf("io.Writer.Write failed: %w", err) - } - - return nil -} - -// WriteZip64EndOfCentralDirectoryLocator write the zip64 end of central directory locator struct -// to the archive. -func (writer *Writer) WriteZip64EndOfCentralDirectoryLocator() error { - zip64EndOfCDRecordLocator := Zip64EndOfCDRecordLocator{} - zip64EndOfCDRecordLocator.Signature = zip64EndOfCDLocatorSignature - zip64EndOfCDRecordLocator.CDStartDiskNumber = 0 - zip64EndOfCDRecordLocator.CDOffset = writer.lastOffsetCDFileHeader - zip64EndOfCDRecordLocator.NumberOfDisks = 1 - - // write the zip64 end of central directory locator struct - buf := new(bytes.Buffer) - err := binary.Write(buf, binary.LittleEndian, zip64EndOfCDRecordLocator) - if err != nil { - return fmt.Errorf("binary.Write failed: %w", err) - } - - err = writer.writeData(buf.Bytes()) - if err != nil { - return fmt.Errorf("io.Writer.Write failed: %w", err) - } - - return nil -} - -// GetTimeDateUnMSDosFormat Get the time and date in MSDOS format. -const defaultSecondValue = 29 - -const monthShift = 5 - -const baseYear = 80 - -const halfSecond = 2 - -func (writer *Writer) getTimeDateUnMSDosFormat() (uint16, uint16) { - t := time.Now().UTC() - timeInDos := t.Hour()<<11 | t.Minute()<<5 | int(math.Max(float64(t.Second()/halfSecond), float64(defaultSecondValue))) - dateInDos := (t.Year()-baseYear)<<9 | int((t.Month()+1)<= zip64MagicVal { - archiveWriter.EnableZip64() - } - - // add data - for i := 0; i < len(test.files); i++ { - fileInfo := test.files[i] - - err = archiveWriter.AddHeader(fileInfo.filename, fileInfo.size) - if err != nil { - t.Fatalf("Fail to set the size of file in archive: %v", err) - } - - totalBytes := fileInfo.size - var bytesToWrite int64 - for totalBytes > 0 { - if totalBytes >= stepSize { - totalBytes -= stepSize - bytesToWrite = stepSize - } else { - bytesToWrite = totalBytes - totalBytes = 0 - } - - err = archiveWriter.AddData(writeBuffer[:bytesToWrite]) - if err != nil { - t.Fatalf("Fail to write to archive: %v", err) - } - } - } - - archiveSize, err := archiveWriter.Finish() - if err != nil { - t.Fatalf("Fail to close to archive: %v", err) - } - - if archiveSize != test.archiveSize { - t.Errorf("archive size test failed expected %v, got %v", archiveSize, test.archiveSize) - } - } -} - -func nativeUnzips(t *testing.T) { - // Read buffer - readSize := int64(2 * oneMB) - readBuffer := make([]byte, readSize) - - // test the zip file you created - for index := range ArchiveTests { - // zip file name as index - zipFileName := strconv.Itoa(index) + ".zip" - - // Open the zip file - r, err := zip.OpenReader(zipFileName) - if err != nil { - t.Fatalf("Fail to open to archive: %v", err) - } - defer func(r *zip.ReadCloser) { - err := r.Close() - if err != nil { - t.Fatalf("Fail to close to archive: %v", err) - } - }(r) - - // Iterate over the files in the zip file - for _, f := range r.File { - // open the file - fc, err := f.Open() - if err != nil { - t.Fatalf("Fail to open zip:%s archive %v", zipFileName, err) - } - defer func(fc io.ReadCloser) { - err := fc.Close() - if err != nil { - t.Fatalf("Fail to close file %v", err) - } - }(fc) - - fileInfo := f.FileInfo() - totalBytes := fileInfo.Size() - for totalBytes > 0 { - var bytesToRead int64 - if totalBytes >= stepSize { - totalBytes -= stepSize - bytesToRead = stepSize - } else { - bytesToRead = totalBytes - totalBytes = 0 - } - - if _, err = fc.Read(readBuffer[:bytesToRead]); err != nil { - t.Fatalf("Fail to read from archive file:%s : %v", fileInfo.Name(), err) - } - - if !bytes.Equal(readBuffer[:bytesToRead], writeBuffer[:bytesToRead]) { - t.Fatalf("Fail to compare zip contents") - } - } - } - - err = os.Remove(zipFileName) - if err != nil { - t.Fatalf("Fail to remove zip file :%s archive %v", zipFileName, err) - } - } -} diff --git a/sdk/internal/archive/zip_headers.go b/sdk/internal/archive/zip_headers.go deleted file mode 100644 index 36d86f31ee..0000000000 --- a/sdk/internal/archive/zip_headers.go +++ /dev/null @@ -1,119 +0,0 @@ -package archive - -const ( - fileHeaderSignature = 0x04034b50 // (PK♥♦ or "PK\3\4") - dataDescriptorSignature = 0x08074b50 - centralDirectoryHeaderSignature = 0x02014b50 - endOfCentralDirectorySignature = 0x06054b50 - zip64EndOfCDLocatorSignature = 0x07064b50 - zip64MagicVal = 0xFFFFFFFF - zip64EndOfCDSignature = 0x06064b50 - zip64ExternalID = 0x0001 - zipVersion = 0x2D // version 4.5 of the PKZIP specification -) - -const ( - endOfCDRecordSize = 22 - zip64EndOfCDRecordLocatorSize = 20 - zip64EndOfCDRecordSize = 56 - cdFileHeaderSize = 46 - localFileHeaderSize = 30 - zip64ExtendedLocalInfoExtraFieldSize = 20 - zip64DataDescriptorSize = 24 - zip32DataDescriptorSize = 16 - zip64ExtendedInfoExtraFieldSize = 28 -) - -type LocalFileHeader struct { - Signature uint32 - Version uint16 - GeneralPurposeBitFlag uint16 - CompressionMethod uint16 - LastModifiedTime uint16 - LastModifiedDate uint16 - Crc32 uint32 - CompressedSize uint32 - UncompressedSize uint32 - FilenameLength uint16 - ExtraFieldLength uint16 -} - -type Zip32DataDescriptor struct { - Signature uint32 - Crc32 uint32 - CompressedSize uint32 - UncompressedSize uint32 -} - -type Zip64DataDescriptor struct { - Signature uint32 - Crc32 uint32 - CompressedSize uint64 - UncompressedSize uint64 -} - -type CDFileHeader struct { - Signature uint32 - VersionCreated uint16 - VersionNeeded uint16 - GeneralPurposeBitFlag uint16 - CompressionMethod uint16 - LastModifiedTime uint16 - LastModifiedDate uint16 - Crc32 uint32 - CompressedSize uint32 - UncompressedSize uint32 - FilenameLength uint16 - ExtraFieldLength uint16 - FileCommentLength uint16 - DiskNumberStart uint16 - InternalFileAttributes uint16 - ExternalFileAttributes uint32 - LocalHeaderOffset uint32 -} - -type EndOfCDRecord struct { - Signature uint32 - DiskNumber uint16 - StartDiskNumber uint16 - NumberOfCDRecordEntries uint16 - TotalCDRecordEntries uint16 - SizeOfCentralDirectory uint32 - CentralDirectoryOffset uint32 - CommentLength uint16 -} - -type Zip64EndOfCDRecord struct { - Signature uint32 - RecordSize uint64 - VersionMadeBy uint16 - VersionToExtract uint16 - DiskNumber uint32 - StartDiskNumber uint32 - NumberOfCDRecordEntries uint64 - TotalCDRecordEntries uint64 - CentralDirectorySize uint64 - StartingDiskCentralDirectoryOffset uint64 -} - -type Zip64EndOfCDRecordLocator struct { - Signature uint32 - CDStartDiskNumber uint32 - CDOffset uint64 - NumberOfDisks uint32 -} - -type Zip64ExtendedLocalInfoExtraField struct { - Signature uint16 - Size uint16 - OriginalSize uint64 - CompressedSize uint64 -} - -type Zip64ExtendedInfoExtraField struct { - Signature uint16 - Size uint16 - OriginalSize uint64 - CompressedSize uint64 - LocalFileHeaderOffset uint64 -} diff --git a/sdk/internal/zipstream/crc32combine_test.go b/sdk/internal/zipstream/crc32combine_test.go index fe7eca1ad2..25076b727a 100644 --- a/sdk/internal/zipstream/crc32combine_test.go +++ b/sdk/internal/zipstream/crc32combine_test.go @@ -9,11 +9,11 @@ import ( ) func TestCRC32CombineIEEE_Basic(t *testing.T) { - rand.Seed(42) + rng := rand.New(rand.NewSource(42)) a := make([]byte, 1024) b := make([]byte, 2048) - rand.Read(a) - rand.Read(b) + rng.Read(a) + rng.Read(b) crcA := crc32.ChecksumIEEE(a) crcB := crc32.ChecksumIEEE(b) @@ -28,12 +28,12 @@ func TestCRC32CombineIEEE_Basic(t *testing.T) { } func TestCRC32CombineIEEE_MultiChunks(t *testing.T) { - rand.Seed(42) + rng := rand.New(rand.NewSource(42)) chunks := make([][]byte, 10) for i := range chunks { - n := 1 + rand.Intn(8192) + n := 1 + rng.Intn(8192) chunks[i] = make([]byte, n) - rand.Read(chunks[i]) + rng.Read(chunks[i]) } // Combine sequentially diff --git a/sdk/internal/zipstream/segment_writer_test.go b/sdk/internal/zipstream/segment_writer_test.go index 8636778fc5..77f3ee3eae 100644 --- a/sdk/internal/zipstream/segment_writer_test.go +++ b/sdk/internal/zipstream/segment_writer_test.go @@ -417,23 +417,10 @@ func TestSegmentWriter_LargeNumberOfSegments(t *testing.T) { testSegments[i] = []byte(fmt.Sprintf("Segment %d data", i)) } - var allBytes []byte - // Write all segments in reverse order for i := segmentCount - 1; i >= 0; i-- { - bytes, err := writer.WriteSegment(ctx, i, uint64(len(testSegments[i])), crc32.ChecksumIEEE(testSegments[i])) + _, err := writer.WriteSegment(ctx, i, uint64(len(testSegments[i])), crc32.ChecksumIEEE(testSegments[i])) require.NoError(t, err, "Failed to write segment %d", i) - - // Store in logical order for final assembly - if i == 0 { - allBytes = append([]byte{}, bytes...) // Segment 0 goes first - for j := 1; j < segmentCount; j++ { - allBytes = append(allBytes, make([]byte, 0)...) // Placeholder - } - } else { - // This is simplified - in practice you'd need proper ordering - allBytes = append(allBytes, bytes...) - } } // Finalize diff --git a/sdk/internal/zipstream/tdf3_reader.go b/sdk/internal/zipstream/tdf3_reader.go index 47e2313ee7..e1b9d67657 100644 --- a/sdk/internal/zipstream/tdf3_reader.go +++ b/sdk/internal/zipstream/tdf3_reader.go @@ -7,29 +7,41 @@ import ( ) type TDFReader struct { - archiveReader Reader + archiveReader Reader + manifestMaxSize int64 } const ( manifestMaxSize = 1024 * 1024 * 10 // 10 MB ) +type TDFReaderOptions func(*TDFReader) + +func WithTDFManifestMaxSize(size int64) TDFReaderOptions { + return func(tdfReader *TDFReader) { + tdfReader.manifestMaxSize = size + } +} + // NewTDFReader Create tdf reader instance. -func NewTDFReader(readSeeker io.ReadSeeker) (TDFReader, error) { +func NewTDFReader(readSeeker io.ReadSeeker, opt ...TDFReaderOptions) (TDFReader, error) { archiveReader, err := NewReader(readSeeker) if err != nil { return TDFReader{}, err } - tdfArchiveReader := TDFReader{} + tdfArchiveReader := TDFReader{manifestMaxSize: manifestMaxSize} tdfArchiveReader.archiveReader = archiveReader + for _, o := range opt { + o(&tdfArchiveReader) + } return tdfArchiveReader, nil } // Manifest Return the manifest of the tdf. func (tdfReader TDFReader) Manifest() (string, error) { - fileContent, err := tdfReader.archiveReader.ReadAllFileData(TDFManifestFileName, manifestMaxSize) + fileContent, err := tdfReader.archiveReader.ReadAllFileData(TDFManifestFileName, tdfReader.manifestMaxSize) if err != nil { return "", err } diff --git a/sdk/kas_client.go b/sdk/kas_client.go index 772198f35e..bb2b1f7f3a 100644 --- a/sdk/kas_client.go +++ b/sdk/kas_client.go @@ -170,85 +170,6 @@ func upgradeRewrapErrorV1(err error, requests []*kas.UnsignedRewrapRequest_WithP }, nil } -func (k *KASClient) nanoUnwrap(ctx context.Context, requests ...*kas.UnsignedRewrapRequest_WithPolicyRequest) (map[string][]kaoResult, error) { - keypair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) - if err != nil { - return nil, fmt.Errorf("ocrypto.NewECKeyPair failed :%w", err) - } - - publicKeyAsPem, err := keypair.PublicKeyInPemFormat() - if err != nil { - return nil, fmt.Errorf("ocrypto.NewECKeyPair.PublicKeyInPemFormat failed :%w", err) - } - - privateKeyAsPem, err := keypair.PrivateKeyInPemFormat() - if err != nil { - return nil, fmt.Errorf("ocrypto.NewECKeyPair.PrivateKeyInPemFormat failed :%w", err) - } - response, err := k.makeRewrapRequest(ctx, requests, publicKeyAsPem) - if err != nil { - return nil, err - } - - // If the session key is empty, all responses are errors - spk := response.GetSessionPublicKey() - if spk == "" { - policyResults := make(map[string][]kaoResult) - err = errors.New("nanoUnwrap: session public key is empty") - for _, results := range response.GetResponses() { - var kaoKeys []kaoResult - for _, kao := range results.GetResults() { - requiredObligationsForKAO := k.retrieveObligationsFromMetadata(kao.GetMetadata()) - if kao.GetStatus() == statusPermit { - kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), Error: err, RequiredObligations: requiredObligationsForKAO}) - } else { - kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), Error: errors.New(kao.GetError()), RequiredObligations: requiredObligationsForKAO}) - } - } - policyResults[results.GetPolicyId()] = kaoKeys - } - - return policyResults, nil - } - - sessionKey, err := ocrypto.ComputeECDHKey([]byte(privateKeyAsPem), []byte(spk)) - if err != nil { - return nil, fmt.Errorf("nanoUnwrap: ocrypto.ComputeECDHKey failed :%w", err) - } - - sessionKey, err = ocrypto.CalculateHKDF(versionSalt(), sessionKey) - if err != nil { - return nil, fmt.Errorf("nanoUnwrap: ocrypto.CalculateHKDF failed:%w", err) - } - - aesGcm, err := ocrypto.NewAESGcm(sessionKey) - if err != nil { - return nil, fmt.Errorf("nanoUnwrap: ocrypto.NewAESGcm failed:%w", err) - } - - policyResults := make(map[string][]kaoResult) - for _, results := range response.GetResponses() { - var kaoKeys []kaoResult - for _, kao := range results.GetResults() { - requiredObligationsForKAO := k.retrieveObligationsFromMetadata(kao.GetMetadata()) - if kao.GetStatus() == statusPermit { - wrappedKey := kao.GetKasWrappedKey() - key, err := aesGcm.Decrypt(wrappedKey) - if err != nil { - kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), Error: err, RequiredObligations: requiredObligationsForKAO}) - } else { - kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), SymmetricKey: key, RequiredObligations: requiredObligationsForKAO}) - } - } else { - kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), Error: errors.New(kao.GetError()), RequiredObligations: requiredObligationsForKAO}) - } - } - policyResults[results.GetPolicyId()] = kaoKeys - } - - return policyResults, nil -} - func (k *KASClient) unwrap(ctx context.Context, requests ...*kas.UnsignedRewrapRequest_WithPolicyRequest) (map[string][]kaoResult, error) { if k.sessionKey == nil { return nil, errors.New("session key is nil") @@ -504,7 +425,7 @@ type KASKeyFetcher interface { func (s SDK) getPublicKey(ctx context.Context, kasurl, algorithm, kidToFind string) (*KASInfo, error) { if s.kasKeyCache != nil { - if cachedValue := s.kasKeyCache.get(kasurl, algorithm, kidToFind); nil != cachedValue { + if cachedValue := s.get(kasurl, algorithm, kidToFind); nil != cachedValue { return cachedValue, nil } } @@ -538,7 +459,7 @@ func (s SDK) getPublicKey(ctx context.Context, kasurl, algorithm, kidToFind stri PublicKey: resp.Msg.GetPublicKey(), } if s.kasKeyCache != nil { - s.kasKeyCache.store(ki) + s.store(ki) } return &ki, nil } diff --git a/sdk/kas_client_test.go b/sdk/kas_client_test.go index e1f0ecd8c8..7fd4b6b60c 100644 --- a/sdk/kas_client_test.go +++ b/sdk/kas_client_test.go @@ -1,12 +1,10 @@ package sdk import ( - "bytes" "context" "crypto/sha256" "encoding/base64" "encoding/json" - "io" "net/http" "testing" "time" @@ -29,7 +27,7 @@ import ( type FakeAccessTokenSource struct { dpopKey jwk.Key asymDecryption ocrypto.AsymDecryption - asymEncryption ocrypto.AsymEncryption + asymEncryption ocrypto.PublicKeyEncryptor accessToken string } @@ -46,7 +44,7 @@ func getTokenSource(t *testing.T) FakeAccessTokenSource { dpopPEM, _ := dpopKey.PrivateKeyInPemFormat() decryption, _ := ocrypto.NewAsymDecryption(dpopPEM) dpopPEMPublic, _ := dpopKey.PublicKeyInPemFormat() - encryption, _ := ocrypto.NewAsymEncryption(dpopPEMPublic) + encryption, _ := ocrypto.FromPublicPEM(dpopPEMPublic) dpopJWK, err := jwk.ParseKey([]byte(dpopPEM), jwk.WithPEM(true)) if err != nil { t.Fatalf("error creating JWK: %v", err) @@ -115,8 +113,8 @@ func TestCreatingRequest(t *testing.T) { require.NoError(t, protojson.Unmarshal([]byte(requestBodyJSON), &requestBody), "error unmarshaling request body") - _, err = ocrypto.NewAsymEncryption(requestBody.GetClientPublicKey()) - require.NoError(t, err, "NewAsymEncryption failed, incorrect public key include") + _, err = ocrypto.FromPublicPEM(requestBody.GetClientPublicKey()) + require.NoError(t, err, "FromPublicPEM failed, incorrect public key include") require.Len(t, requestBody.GetRequests(), 1) require.Len(t, requestBody.GetRequests()[0].GetKeyAccessObjects(), 1) @@ -143,8 +141,8 @@ func Test_StoreKASKeys(t *testing.T) { ) require.NoError(t, err) - assert.Nil(t, s.kasKeyCache.get("https://localhost:8080", "ec:secp256r1", "e1")) - assert.Nil(t, s.kasKeyCache.get("https://localhost:8080", "rsa:2048", "r1")) + assert.Nil(t, s.get("https://localhost:8080", "ec:secp256r1", "e1")) + assert.Nil(t, s.get("https://localhost:8080", "rsa:2048", "r1")) require.NoError(t, s.StoreKASKeys("https://localhost:8080", &policy.KasPublicKeySet{ Keys: []*policy.KasPublicKey{ @@ -152,10 +150,10 @@ func Test_StoreKASKeys(t *testing.T) { {Pem: "sample", Kid: "r1", Alg: policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048}, }, })) - assert.Nil(t, s.kasKeyCache.get("https://nowhere", "alg:unknown", "")) - assert.Nil(t, s.kasKeyCache.get("https://localhost:8080", "alg:unknown", "")) - ecKey := s.kasKeyCache.get("https://localhost:8080", "ec:secp256r1", "e1") - rsaKey := s.kasKeyCache.get("https://localhost:8080", "rsa:2048", "r1") + assert.Nil(t, s.get("https://nowhere", "alg:unknown", "")) + assert.Nil(t, s.get("https://localhost:8080", "alg:unknown", "")) + ecKey := s.get("https://localhost:8080", "ec:secp256r1", "e1") + rsaKey := s.get("https://localhost:8080", "rsa:2048", "r1") require.NotNil(t, ecKey) require.Equal(t, "e1", ecKey.KID) require.NotNil(t, rsaKey) @@ -465,7 +463,7 @@ func Test_processRSAResponse(t *testing.T) { // Create a mock AsymEncryption to create the wrapped key publicKeyPEM, err := mockPrivateKey.PublicKeyInPemFormat() require.NoError(t, err) - mockEncryptor, err := ocrypto.NewAsymEncryption(publicKeyPEM) + mockEncryptor, err := ocrypto.FromPublicPEM(publicKeyPEM) require.NoError(t, err) symmetricKey := []byte("supersecretkey") @@ -646,282 +644,6 @@ func Test_processECResponse(t *testing.T) { require.Equal(t, "https://example.com/attr/attr2/value/val2", result2[0].RequiredObligations[0]) } -type mockService interface { - Process(req *http.Request) (*http.Response, error) -} - -type MockKas struct { - t *testing.T - obligations map[string][]string // policyID -> obligations - policyDecisions map[string]string // policyID -> "permit" or "fail" -} - -func (f *MockKas) Process(req *http.Request) (*http.Response, error) { - // 1. KAS generates its own ephemeral keypair for the ECDH exchange. - kasKeypair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) - require.NoError(f.t, err) - kasPublicKeyPEM, err := kasKeypair.PublicKeyInPemFormat() - require.NoError(f.t, err) - kasPrivateKeyPEM, err := kasKeypair.PrivateKeyInPemFormat() - require.NoError(f.t, err) - - // 2. Extract the client's public key from the incoming request. - bodyBytes, err := io.ReadAll(req.Body) - require.NoError(f.t, err) - req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) // Restore body - - var bodyJSON map[string]interface{} - err = json.Unmarshal(bodyBytes, &bodyJSON) - require.NoError(f.t, err) - signedRequestToken, ok := bodyJSON["signedRequestToken"].(string) - require.True(f.t, ok) - - // We need a public key to verify the token, but for this mock we can parse without verification. - token, err := jwt.ParseString(signedRequestToken, jwt.WithVerify(false)) - require.NoError(f.t, err) - - requestBodyClaim, _ := token.Get("requestBody") - requestBodyJSON, _ := requestBodyClaim.(string) - var unsignedReq kaspb.UnsignedRewrapRequest - err = protojson.Unmarshal([]byte(requestBodyJSON), &unsignedReq) - require.NoError(f.t, err) - clientPublicKeyPEM := unsignedReq.GetClientPublicKey() - - // 3. Compute the shared secret (ECDH) and derive the session key (HKDF). - ecdhKey, err := ocrypto.ComputeECDHKey([]byte(kasPrivateKeyPEM), []byte(clientPublicKeyPEM)) - require.NoError(f.t, err) - sessionKey, err := ocrypto.CalculateHKDF(versionSalt(), ecdhKey) - require.NoError(f.t, err) - - // 4. Encrypt the symmetric key using the derived session key. - encryptor, err := ocrypto.NewAESGcm(sessionKey) - require.NoError(f.t, err) - symmetricKey := []byte("supersecretkey1") - wrappedKey, err := encryptor.Encrypt(symmetricKey) - require.NoError(f.t, err) - - // 5. Construct the KAS rewrap response. - rewrapResponse := &kaspb.RewrapResponse{ - SessionPublicKey: kasPublicKeyPEM, - } - for _, req := range unsignedReq.GetRequests() { - policyID := req.GetPolicy().GetId() - - // Determine if this policy should be permitted or failed - decision := "permit" // default to permit - if f.policyDecisions != nil { - if d, exists := f.policyDecisions[policyID]; exists { - decision = d - } - } - - var kaoResult *kaspb.KeyAccessRewrapResult - var metadata map[string]*structpb.Value - if fqns, exists := f.obligations[policyID]; exists { - metadata = createMetadataWithObligations(fqns) - } - if decision == "permit" { - // For permitted policies: no metadata/obligations - kaoResult = &kaspb.KeyAccessRewrapResult{ - KeyAccessObjectId: req.GetKeyAccessObjects()[0].GetKeyAccessObjectId(), - Status: "permit", - Result: &kaspb.KeyAccessRewrapResult_KasWrappedKey{ - KasWrappedKey: wrappedKey, - }, - Metadata: metadata, - } - } else { - kaoResult = &kaspb.KeyAccessRewrapResult{ - KeyAccessObjectId: req.GetKeyAccessObjects()[0].GetKeyAccessObjectId(), - Status: "fail", - Result: &kaspb.KeyAccessRewrapResult_Error{ - Error: "denied by policy", - }, - Metadata: metadata, - } - } - - rewrapResponse.Responses = append(rewrapResponse.Responses, &kaspb.PolicyRewrapResult{ - PolicyId: policyID, - Results: []*kaspb.KeyAccessRewrapResult{kaoResult}, - }) - } - - responseBody, err := protojson.Marshal(rewrapResponse) - require.NoError(f.t, err) - - mockHTTPResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(responseBody)), - Header: make(http.Header), - } - mockHTTPResponse.Header.Set("Content-Type", "application/json") - - return mockHTTPResponse, nil -} - -// mockRoundTripper is a mock implementation of http.RoundTripper for testing. -type mockRoundTripper struct { - Response *http.Response - mockService mockService - Err error -} - -// RoundTrip implements the http.RoundTripper interface. -func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if m.Err != nil || m.Response != nil { - return m.Response, m.Err - } - return m.mockService.Process(req) -} - -func Test_nanoUnwrap(t *testing.T) { - // 1. Set up the mock HTTP client - mockClient := &http.Client{ - Transport: &mockRoundTripper{mockService: &MockKas{ - t: t, - obligations: map[string][]string{ - "policy1": {"https://example.com/attr/attr1/value/val1"}, - "policy2": {"https://example.com/attr/attr2/value/val2"}, - }, - policyDecisions: map[string]string{ - "policy1": "permit", // policy1 should be permitted - "policy2": "fail", // policy2 should be failed - }, - }}, - } - - // 2. Create the KAS client with the mocked HTTP client - tokenSource := getTokenSource(t) - c := newKASClient(mockClient, []connect.ClientOption{connect.WithProtoJSON()}, tokenSource, nil, nil) - - // 3. Define a dummy request. - dummyKeyAccess := []*kaspb.UnsignedRewrapRequest_WithPolicyRequest{ - { - Policy: &kaspb.UnsignedRewrapRequest_WithPolicy{ - Id: "policy1", - }, - KeyAccessObjects: []*kaspb.UnsignedRewrapRequest_WithKeyAccessObject{ - { - KeyAccessObject: &kaspb.KeyAccess{KasUrl: "https://kas.example.com"}, - }, - }, - }, - { - Policy: &kaspb.UnsignedRewrapRequest_WithPolicy{ - Id: "policy2", - }, - KeyAccessObjects: []*kaspb.UnsignedRewrapRequest_WithKeyAccessObject{ - { - KeyAccessObject: &kaspb.KeyAccess{KasUrl: "https://kas.example.com"}, - }, - }, - }, - } - - // 4. Call nanoUnwrap - policyResults, err := c.nanoUnwrap(t.Context(), dummyKeyAccess...) - require.NoError(t, err) - require.Len(t, policyResults, 2) - - // 5. Assertions - // Policy1 should be permitted - has symmetric key, no error, no obligations - result1, ok := policyResults["policy1"] - require.True(t, ok) - require.Len(t, result1, 1) - require.Equal(t, []byte("supersecretkey1"), result1[0].SymmetricKey) - require.NoError(t, result1[0].Error) - require.Len(t, result1[0].RequiredObligations, 1) - require.Equal(t, "https://example.com/attr/attr1/value/val1", result1[0].RequiredObligations[0]) - - // Policy2 should be failed - has error, no symmetric key, has obligations - result2, ok := policyResults["policy2"] - require.True(t, ok) - require.Len(t, result2, 1) - require.Nil(t, result2[0].SymmetricKey, "Failed policies should not have symmetric key") - require.Error(t, result2[0].Error) - require.Contains(t, result2[0].Error.Error(), "denied by policy") - require.Len(t, result2[0].RequiredObligations, 1) - require.Equal(t, "https://example.com/attr/attr2/value/val2", result2[0].RequiredObligations[0]) -} - -func Test_nanoUnwrap_EmptySPK_WithObligations(t *testing.T) { - // 1. Construct the KAS rewrap response with empty SPK and obligations - rewrapResponse := &kaspb.RewrapResponse{ - SessionPublicKey: "", // Empty Session Public Key - Responses: []*kaspb.PolicyRewrapResult{ - { - PolicyId: "policy1", - Results: []*kaspb.KeyAccessRewrapResult{ - { - KeyAccessObjectId: "kao1", - Status: "fail", - Result: &kaspb.KeyAccessRewrapResult_Error{ - Error: "denied by policy", - }, - Metadata: createMetadataWithObligations([]string{ - "https://example.com/attr/attr1/value/val1", - }), - }, - }, - }, - }, - } - - responseBody, err := protojson.Marshal(rewrapResponse) - require.NoError(t, err) - - mockHTTPResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(responseBody)), - Header: make(http.Header), - } - mockHTTPResponse.Header.Set("Content-Type", "application/json") - - // 2. Set up the mock HTTP client to return the crafted response - mockClient := &http.Client{ - Transport: &mockRoundTripper{Response: mockHTTPResponse}, - } - - // 3. Create the KAS client with the mocked HTTP client - tokenSource := getTokenSource(t) - c := newKASClient(mockClient, []connect.ClientOption{connect.WithProtoJSON()}, tokenSource, nil, nil) - - // 4. Define a dummy request that matches the response - dummyKeyAccess := []*kaspb.UnsignedRewrapRequest_WithPolicyRequest{ - { - Policy: &kaspb.UnsignedRewrapRequest_WithPolicy{ - Id: "policy1", - }, - KeyAccessObjects: []*kaspb.UnsignedRewrapRequest_WithKeyAccessObject{ - { - KeyAccessObjectId: "kao1", - KeyAccessObject: &kaspb.KeyAccess{KasUrl: "https://kas.example.com"}, - }, - }, - }, - } - - // 5. Call nanoUnwrap - policyResults, err := c.nanoUnwrap(t.Context(), dummyKeyAccess...) - require.NoError(t, err, "nanoUnwrap should not return a top-level error in this case") - require.Len(t, policyResults, 1) - - // 6. Assertions - result, ok := policyResults["policy1"] - require.True(t, ok) - require.Len(t, result, 1) - - // Assert that the KAO result contains an error - require.Error(t, result[0].Error) - require.Contains(t, result[0].Error.Error(), "denied by policy") - require.Nil(t, result[0].SymmetricKey) - - // Assert that obligations are still present despite the KAO error - require.Len(t, result[0].RequiredObligations, 1) - require.Equal(t, "https://example.com/attr/attr1/value/val1", result[0].RequiredObligations[0]) -} - func createMetadataWithObligations(obligations []string) map[string]*structpb.Value { metadata := make(map[string]*structpb.Value) if len(obligations) == 0 { diff --git a/sdk/nanotdf.go b/sdk/nanotdf.go deleted file mode 100644 index 543987742b..0000000000 --- a/sdk/nanotdf.go +++ /dev/null @@ -1,1324 +0,0 @@ -package sdk - -import ( - "bytes" - "context" - "crypto/elliptic" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "log/slog" - "net/http" - "sync" - "time" - - "connectrpc.com/connect" - "github.com/opentdf/platform/protocol/go/kas" - "github.com/opentdf/platform/protocol/go/policy" - "github.com/opentdf/platform/sdk/auth" - - "github.com/opentdf/platform/lib/ocrypto" -) - -// ============================================================================================================ -// Support for nanoTDF operations -// -// See also the nanotdf_config.go interface -// -// ============================================================================================================ - -// / Constants -const ( - kMaxTDFSize = ((16 * 1024 * 1024) - 3 - 32) //nolint:mnd // 16 mb - 3(iv) - 32(max auth tag) - // kDatasetMaxMBBytes = 2097152 // 2mb - - // Max size of the encrypted tdfs - // 16mb payload - // ~67kb of policy - // 133 of signature - // kMaxEncryptedNTDFSize = (16 * 1024 * 1024) + (68 * 1024) + 133 //nolint:mnd // See comment block above - - kIvPadding = 9 - kNanoTDFIvSize = 3 - kNanoTDFGMACLength = 8 - kNanoTDFMagicStringAndVersion = "L1L" - kMaxIters = 1<<24 - 1 -) - -/******************************** Header************************** - | Section | Minimum Length (B) | Maximum Length (B) | - |--------------------|---------------------|---------------------| - | Magic Number | 2 | 2 | - | Version | 1 | 1 | - | KAS | 3 | 257 | - | ECC Mode | 1 | 1 | - | Payload + Sig Mode | 1 | 1 | - | Policy | 3 | 257 | - | Ephemeral Key | 33 | 67 | - ********************************* Header*************************/ - -type NanoTDFHeader struct { - kasURL ResourceLocator - bindCfg bindingConfig - sigCfg signatureConfig - EphemeralKey []byte - PolicyMode PolicyType - PolicyBody []byte - gmacPolicyBinding []byte - ecdsaPolicyBindingR []byte - ecdsaPolicyBindingS []byte -} - -type ecdsaPolicyBinding struct { - r []byte - s []byte - ephemeralPubKey []byte - digest []byte - curve elliptic.Curve -} - -type gmacPolicyBinding struct { - binding []byte - digest []byte -} - -type PolicyBind interface { - Verify() (bool, error) - fmt.Stringer -} - -func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error) { - header := NanoTDFHeader{} - var size uint32 - - // Read and validate magic number - magicNumber := make([]byte, len(kNanoTDFMagicStringAndVersion)) - l, err := reader.Read(magicNumber) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - if magicNumber[0] != kNanoTDFMagicStringAndVersion[0] || magicNumber[1] != kNanoTDFMagicStringAndVersion[1] || magicNumber[2] != kNanoTDFMagicStringAndVersion[2] { - return header, 0, fmt.Errorf(" io.Reader.Read magic number failed : %w", err) - } - size += uint32(l) - - if string(magicNumber) != kNanoTDFMagicStringAndVersion { - return header, 0, errors.New("not a valid nano tdf") - } - - // Read resource locator - resource, err := NewResourceLocatorFromReader(reader) - if err != nil { - return header, 0, fmt.Errorf("call to NewResourceLocatorFromReader failed :%w", err) - } - size += uint32(resource.getLength()) - header.kasURL = *resource - - getLogger().Debug("checkpoint NewNanoTDFHeaderFromReader", slog.Uint64("resource_locator", uint64(resource.getLength()))) - - // Read ECC and Binding Mode - oneBytes := make([]byte, 1) - l, err = reader.Read(oneBytes) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - header.bindCfg = deserializeBindingCfg(oneBytes[0]) - - // Check ephemeral ECC Params Enum - if header.bindCfg.eccMode != ocrypto.ECCModeSecp256r1 { - return header, 0, errors.New("current implementation of nano tdf only support secp256r1 curve") - } - - // Read Payload and Sig Mode - l, err = reader.Read(oneBytes) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - header.sigCfg = deserializeSignatureCfg(oneBytes[0]) - - // Read policy type - l, err = reader.Read(oneBytes) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - - policyMode := PolicyType(oneBytes[0]) - if err := validNanoTDFPolicyMode(policyMode); err != nil { - return header, 0, errors.Join(fmt.Errorf("unsupported policy mode: %v", policyMode), err) - } - - // Read policy length - const kSizeOfUint16 = 2 - twoBytes := make([]byte, kSizeOfUint16) - l, err = reader.Read(twoBytes) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - policyLength := binary.BigEndian.Uint16(twoBytes) - getLogger().Debug("checkpoint NewNanoTDFHeaderFromReader", slog.Uint64("policy_length", uint64(policyLength))) - - // Read policy body - header.PolicyMode = policyMode - header.PolicyBody = make([]byte, policyLength) - l, err = reader.Read(header.PolicyBody) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - - // Read policy binding - if header.bindCfg.useEcdsaBinding { //nolint:nestif // TODO: refactor - // Read rBytes len and its contents - l, err = reader.Read(oneBytes) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - - header.ecdsaPolicyBindingR = make([]byte, oneBytes[0]) - l, err = reader.Read(header.ecdsaPolicyBindingR) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - - // Read sBytes len and its contents - l, err = reader.Read(oneBytes) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - - header.ecdsaPolicyBindingS = make([]byte, oneBytes[0]) - l, err = reader.Read(header.ecdsaPolicyBindingS) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - } else { - header.gmacPolicyBinding = make([]byte, kNanoTDFGMACLength) - l, err = reader.Read(header.gmacPolicyBinding) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - } - - ephemeralKeySize, err := getECCKeyLength(header.bindCfg.eccMode) - if err != nil { - return header, 0, fmt.Errorf("getECCKeyLength :%w", err) - } - - // Read ephemeral Key - ephemeralKey := make([]byte, ephemeralKeySize) - l, err = reader.Read(ephemeralKey) - if err != nil { - return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - size += uint32(l) - header.EphemeralKey = ephemeralKey - - getLogger().Debug("checkpoint NewNanoTDFHeaderFromReader", slog.Uint64("header_size", uint64(size))) - - return header, size, nil -} - -func (header *NanoTDFHeader) GetKasURL() ResourceLocator { - return header.kasURL -} - -// GetCipher -- get the cipher from the nano tdf header -func (header *NanoTDFHeader) GetCipher() CipherMode { - return header.sigCfg.cipher -} - -func (header *NanoTDFHeader) IsEcdsaBindingEnabled() bool { - return header.bindCfg.useEcdsaBinding -} - -func (header *NanoTDFHeader) ECCurve() (elliptic.Curve, error) { - return ocrypto.GetECCurveFromECCMode(header.bindCfg.eccMode) -} - -func (header *NanoTDFHeader) VerifyPolicyBinding() (bool, error) { - policyBind, err := header.PolicyBinding() - if err != nil { - return false, err - } - return policyBind.Verify() -} - -func (b *ecdsaPolicyBinding) Verify() (bool, error) { - ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(b.curve, b.ephemeralPubKey) - if err != nil { - return false, err - } - - return ocrypto.VerifyECDSASig(b.digest, - b.r, - b.s, - ephemeralECDSAPublicKey), nil -} - -func (b *ecdsaPolicyBinding) String() string { - return string(ocrypto.SHA256AsHex(append(b.r, b.s...))) -} - -func (b *gmacPolicyBinding) Verify() (bool, error) { - bindingToCheck := b.digest[len(b.digest)-kNanoTDFGMACLength:] - return bytes.Equal(bindingToCheck, b.binding), nil -} - -func (b *gmacPolicyBinding) String() string { - return hex.EncodeToString(b.binding) -} - -func (header *NanoTDFHeader) PolicyBinding() (PolicyBind, error) { - digest := ocrypto.CalculateSHA256(header.PolicyBody) - - if header.IsEcdsaBindingEnabled() { - curve, err := ocrypto.GetECCurveFromECCMode(header.bindCfg.eccMode) - if err != nil { - return nil, err - } - return &ecdsaPolicyBinding{ - r: header.ecdsaPolicyBindingR, - s: header.ecdsaPolicyBindingS, - ephemeralPubKey: header.EphemeralKey, - curve: curve, - digest: digest, - }, nil - } - - return &gmacPolicyBinding{ - binding: header.gmacPolicyBinding, - digest: digest, - }, nil -} - -// ============================================================================================================ - -// embeddedPolicy - policy for data that is stored locally within the nanoTDF -type embeddedPolicy struct { - lengthBody uint16 - body []byte -} - -// getLength - size in bytes of the serialized content of this object -// func (ep *embeddedPolicy) getLength() uint16 { -// const ( -// kUint16Len = 2 -// ) -// return uint16(kUint16Len /* length word length */ + len(ep.body) /* body data length */) -// } - -// writeEmbeddedPolicy - writes the content of the to the supplied writer -func (ep embeddedPolicy) writeEmbeddedPolicy(writer io.Writer) error { - // store uint16 in big endian format - const ( - kUint16Len = 2 - ) - buf := make([]byte, kUint16Len) - binary.BigEndian.PutUint16(buf, ep.lengthBody) - if _, err := writer.Write(buf); err != nil { - return err - } - getLogger().Debug("writeEmbeddedPolicy", slog.Uint64("policy_length", uint64(ep.lengthBody))) - - if _, err := writer.Write(ep.body); err != nil { - return err - } - getLogger().Debug("writeEmbeddedPolicy", slog.Uint64("policy_body", uint64(len(ep.body)))) - - return nil -} - -// readEmbeddedPolicy - reads an embeddedPolicy from the supplied reader -func (ep *embeddedPolicy) readEmbeddedPolicy(reader io.Reader) error { - if err := binary.Read(reader, binary.BigEndian, &ep.lengthBody); err != nil { - return errors.Join(ErrNanoTDFHeaderRead, err) - } - body := make([]byte, ep.lengthBody) - if err := binary.Read(reader, binary.BigEndian, &body); err != nil { - return errors.Join(ErrNanoTDFHeaderRead, err) - } - ep.body = body - return nil -} - -// ============================================================================================================ - -// remotePolicy - locator value for policy content that is stored externally to the nanoTDF -type remotePolicy struct { - url ResourceLocator -} - -// getLength - size in bytes of the serialized content of this object -// func (rp *remotePolicy) getLength() uint16 { -// return rp.url.getLength() -// } - -// ============================================================================================================ - -type bindingConfig struct { - useEcdsaBinding bool - eccMode ocrypto.ECCMode -} - -type signatureConfig struct { - hasSignature bool - signatureMode ocrypto.ECCMode - cipher CipherMode -} - -type collectionConfig struct { - iterations uint32 - header []byte - useCollection bool - symKey []byte - mux sync.Mutex -} - -type policyInfo struct { - body PolicyBody - // binding *eccSignature -} - -// type eccSignature struct { -// value []byte -// } - -// type eccKey struct { -// Key []byte -// } - -type CipherMode int - -const ( - cipherModeAes256gcm64Bit CipherMode = 0 - cipherModeAes256gcm96Bit CipherMode = 1 - cipherModeAes256gcm104Bit CipherMode = 2 - cipherModeAes256gcm112Bit CipherMode = 3 - cipherModeAes256gcm120Bit CipherMode = 4 - cipherModeAes256gcm128Bit CipherMode = 5 -) - -const ( - ErrNanoTDFHeaderRead = Error("nanoTDF read error") -) - -// Binding config byte format -// --------------------------------- -// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | -// --------------------------------- -// | E | x | x | x | x | M | M | M | -// --------------------------------- -// bit 7 - use ECDSA -// bit 6-3 - reserved -// bit 2-0 - ECC Curve enum - -// deserializeBindingCfg - read byte of binding config into bindingConfig struct -func deserializeBindingCfg(b byte) bindingConfig { - cfg := bindingConfig{} - // Shift to low nybble test low bit - cfg.useEcdsaBinding = (b >> 7 & 0b00000001) == 1 //nolint:mnd // better readability as literal - // shift to low nybble and use low 3 bits - cfg.eccMode = ocrypto.ECCMode(b & 0b00000111) //nolint:mnd // better readability as literal - - return cfg -} - -// serializeBindingCfg - take info from bindingConfig struct and encode as single byte -func serializeBindingCfg(bindCfg bindingConfig) byte { - var bindSerial byte = 0x00 - - // Set high bit if ecdsa binding is enabled - if bindCfg.useEcdsaBinding { - bindSerial |= 0b10000000 - } - // Mask value to low 3 bytes and shift to high nybble - bindSerial |= (byte(bindCfg.eccMode) & 0b00000111) //nolint:mnd // better readability as literal - - return bindSerial -} - -// Signature config byte format -// --------------------------------- -// | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | -// --------------------------------- -// | S | M | M | M | C | C | C | C | -// --------------------------------- -// bit 8 - has signature -// bit 5-7 - eccMode -// bit 1-4 - cipher - -// deserializeSignatureCfg - decode byte of signature config into signatureCfg struct -func deserializeSignatureCfg(b byte) signatureConfig { - cfg := signatureConfig{} - // Shift high bit down and mask to test for value - cfg.hasSignature = (b >> 7 & 0b000000001) == 1 //nolint:mnd // better readability as literal - // Shift high nybble down and mask for eccmode value - cfg.signatureMode = ocrypto.ECCMode((b >> 4) & 0b00000111) //nolint:mnd // better readability as literal - // Mask low nybble for cipher value - cfg.cipher = CipherMode(b & 0b00001111) //nolint:mnd // better readability as literal - - return cfg -} - -// serializeSignatureCfg - take info from signatureConfig struct and encode as single byte -func serializeSignatureCfg(sigCfg signatureConfig) byte { - var sigSerial byte = 0x00 - - // Set high bit if signature is enabled - if sigCfg.hasSignature { - sigSerial |= 0b10000000 - } - // Mask low 3 bits of mode and shift to high nybble - sigSerial |= byte((sigCfg.signatureMode)&0b00000111) << 4 //nolint:mnd // better readability as literal - // Mask low nybble of cipher - sigSerial |= byte((sigCfg.cipher) & 0b00001111) //nolint:mnd // better readability as literal - - return sigSerial -} - -// ============================================================================================================ -// ECC info -// ============================================================================================================ - -// Key length sizes for different curves -const ( - kCurveSecp256r1KeySize = 33 - kCurveSecp256k1KeySize = 33 - kCurveSecp384r1KeySize = 49 - kCurveSecp521r1KeySize = 67 -) - -// getECCKeyLength - return the length in bytes of a key related to the specified curve -func getECCKeyLength(curve ocrypto.ECCMode) (uint8, error) { - var numberOfBytes uint8 - switch curve { - case ocrypto.ECCModeSecp256r1: - numberOfBytes = kCurveSecp256r1KeySize - case ocrypto.ECCModeSecp256k1: - numberOfBytes = kCurveSecp256k1KeySize - case ocrypto.ECCModeSecp384r1: - numberOfBytes = kCurveSecp384r1KeySize - case ocrypto.ECCModeSecp521r1: - numberOfBytes = kCurveSecp521r1KeySize - default: - return 0, fmt.Errorf("unknown cipher mode:%d", curve) - } - return numberOfBytes, nil -} - -// ============================================================================================================ -// Auth Tag info -// ============================================================================================================ - -// auth tag size in bytes for different ciphers -const ( - kCipher64AuthTagSize = 8 - kCipher96AuthTagSize = 12 - kCipher104AuthTagSize = 13 - kCipher112AuthTagSize = 14 - kCipher120AuthTagSize = 15 - kCipher128AuthTagSize = 16 -) - -// SizeOfAuthTagForCipher - Return the size in bytes of auth tag to be used for aes gcm encryption -func SizeOfAuthTagForCipher(cipherType CipherMode) (int, error) { - var numberOfBytes int - switch cipherType { - case cipherModeAes256gcm64Bit: - - numberOfBytes = kCipher64AuthTagSize - case cipherModeAes256gcm96Bit: - - numberOfBytes = kCipher96AuthTagSize - case cipherModeAes256gcm104Bit: - numberOfBytes = kCipher104AuthTagSize - case cipherModeAes256gcm112Bit: - - numberOfBytes = kCipher112AuthTagSize - case cipherModeAes256gcm120Bit: - - numberOfBytes = kCipher120AuthTagSize - case cipherModeAes256gcm128Bit: - - numberOfBytes = kCipher128AuthTagSize - default: - - return 0, fmt.Errorf("unknown cipher mode:%d", cipherType) - } - return numberOfBytes, nil -} - -// ============================================================================================================ -// NanoTDF Collection Header Store -// ============================================================================================================ - -const ( - kDefaultExpirationTime = 5 * time.Minute - kDefaultCleaningInterval = 10 * time.Minute -) - -type collectionStore struct { - cache sync.Map - expireDuration time.Duration - closeChan chan struct{} -} - -type collectionStoreEntry struct { - key []byte - encryptedHeader []byte - expire time.Time -} - -func newCollectionStore(expireDuration, cleaningInterval time.Duration) *collectionStore { - store := &collectionStore{expireDuration: expireDuration, cache: sync.Map{}, closeChan: make(chan struct{})} - store.startJanitor(cleaningInterval) - return store -} - -func (c *collectionStore) startJanitor(cleaningInterval time.Duration) { - go func() { - ticker := time.NewTicker(cleaningInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - now := time.Now() - c.cache.Range(func(key, value any) bool { - entry, _ := value.(*collectionStoreEntry) - if now.Compare(entry.expire) >= 0 { - c.cache.Delete(key) - } - return true - }) - case <-c.closeChan: - return - } - } - }() -} - -func (c *collectionStore) store(header, key []byte) { - hash := ocrypto.SHA256AsHex(header) - expire := time.Now().Add(c.expireDuration) - c.cache.Store(string(hash), &collectionStoreEntry{key: key, encryptedHeader: header, expire: expire}) -} - -func (c *collectionStore) get(header []byte) ([]byte, bool) { - hash := ocrypto.SHA256AsHex(header) - itemIntf, ok := c.cache.Load(string(hash)) - if !ok { - return nil, false - } - item, _ := itemIntf.(*collectionStoreEntry) - // check for hash collision - if bytes.Equal(item.encryptedHeader, header) { - return item.key, true - } - return nil, false -} - -func (c *collectionStore) close() { - c.closeChan <- struct{}{} -} - -// ============================================================================================================ -// NanoTDF Header read/write -// ============================================================================================================ - -func writeNanoTDFHeader(writer io.Writer, config NanoTDFConfig) ([]byte, uint32, uint32, error) { - if config.collectionCfg.useCollection { - // If concurrently writing, we must know what iteration we are on in a threadsafe way - // also when we need to safely read the header to ensure not rewritten in next max iteration - config.collectionCfg.mux.Lock() - defer config.collectionCfg.mux.Unlock() - - // Store iteration and header and increment iteration - iteration := config.collectionCfg.iterations - config.collectionCfg.iterations++ - header := config.collectionCfg.header - // Reset iteration if reached max iters - if iteration == kMaxIters { - config.collectionCfg.iterations = 0 - } - // Return saved header - if iteration != 0 { - n, err := writer.Write(header) - return config.collectionCfg.symKey, uint32(n), iteration, err - } - // First Iteration: header has not been calculated, will write to header and save for later use. - buf := &bytes.Buffer{} - writer = io.MultiWriter(writer, buf) - defer func() { config.collectionCfg.header = buf.Bytes() }() - } - - var totalBytes uint32 - - // Write the magic number - l, err := writer.Write([]byte(kNanoTDFMagicStringAndVersion)) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - - getLogger().Debug("writeNanoTDFHeader", slog.Uint64("magic_number", uint64(len(kNanoTDFMagicStringAndVersion)))) - - // Write the kas url - err = config.kasURL.writeResourceLocator(writer) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(config.kasURL.getLength()) - getLogger().Debug("writeNanoTDFHeader", slog.Uint64("resource_locator_number", uint64(config.kasURL.getLength()))) - - // Write ECC And Binding Mode - l, err = writer.Write([]byte{serializeBindingCfg(config.bindCfg)}) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - - // Write Payload and Sig Mode - l, err = writer.Write([]byte{serializeSignatureCfg(config.sigCfg)}) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - - // Write policy mode - config.policy.body.mode = config.policyMode - l, err = writer.Write([]byte{byte(config.policy.body.mode)}) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - - // Create policy object - policyObj, err := createPolicyObject(config.attributes) - if err != nil { - return nil, 0, 0, fmt.Errorf("fail to create policy object:%w", err) - } - - policyObjectAsStr, err := json.Marshal(policyObj) - if err != nil { - return nil, 0, 0, fmt.Errorf("json.Marshal failed:%w", err) - } - - // Create the symmetric key - symmetricKey, err := createNanoTDFSymmetricKey(config) - if err != nil { - return nil, 0, 0, err - } - - // Set the symmetric key in the collection config - if config.collectionCfg.useCollection { - config.collectionCfg.symKey = symmetricKey - } - - embeddedP, err := createNanoTDFEmbeddedPolicy(symmetricKey, policyObjectAsStr, config) - if err != nil { - return nil, 0, 0, fmt.Errorf("failed to create embedded policy:%w", err) - } - - err = embeddedP.writeEmbeddedPolicy(writer) - if err != nil { - return nil, 0, 0, fmt.Errorf("writeEmbeddedPolicy failed:%w", err) - } - - // size of uint16 - const kSizeOfUint16 = 2 - totalBytes += kSizeOfUint16 + uint32(len(embeddedP.body)) - - digest := ocrypto.CalculateSHA256(embeddedP.body) - - if config.bindCfg.useEcdsaBinding { //nolint:nestif // TODO: refactor - rBytes, sBytes, err := ocrypto.ComputeECDSASig(digest, config.keyPair.PrivateKey) - if err != nil { - return nil, 0, 0, fmt.Errorf("ComputeECDSASig failed:%w", err) - } - - // write rBytes len and rBytes contents - l, err = writer.Write([]byte{uint8(len(rBytes))}) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - - l, err = writer.Write(rBytes) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - - // write sBytes len and sBytes contents - l, err = writer.Write([]byte{uint8(len(sBytes))}) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - - l, err = writer.Write(sBytes) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - } else { - binding := digest[len(digest)-kNanoTDFGMACLength:] - l, err = writer.Write(binding) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - } - - ephemeralPublicKeyKey, _ := ocrypto.CompressedECPublicKey(config.bindCfg.eccMode, config.keyPair.PrivateKey.PublicKey) - - l, err = writer.Write(ephemeralPublicKeyKey) - if err != nil { - return nil, 0, 0, err - } - totalBytes += uint32(l) - - return symmetricKey, totalBytes, 0, nil -} - -func nonZeroRandomPaddedIV() ([]byte, error) { - const ( - loopCountLimit = 10 - ) - loopCount := 1 - for { - ivPadded := make([]byte, 0, ocrypto.GcmStandardNonceSize) - noncePadding := make([]byte, kIvPadding) - ivPadded = append(ivPadded, noncePadding...) - iv, err := ocrypto.RandomBytes(kNanoTDFIvSize) - if err != nil { - return nil, fmt.Errorf("ocrypto.RandomBytes failed:%w", err) - } - ivPadded = append(ivPadded, iv...) - for _, b := range ivPadded { - if b != 0 { - return ivPadded, nil - } - } - // all zero IV, this is extremely rare so should be able to be addressed in the next loop - if loopCount >= loopCountLimit { // crazy, there must be an issue with the constants - return nil, errors.New("nonZeroPaddedIV loop exceeded limit") - } - loopCount++ - } -} - -// ============================================================================================================ -// NanoTDF Encrypt -// ============================================================================================================ - -// CreateNanoTDF - reads plain text from the given reader and saves it to the writer, subject to the given options -func (s SDK) CreateNanoTDF(writer io.Writer, reader io.Reader, config NanoTDFConfig) (uint32, error) { - if writer == nil { - return 0, errors.New("writer is nil") - } - if reader == nil { - return 0, errors.New("reader is nil") - } - var totalSize uint32 - buf := bytes.Buffer{} - size, err := buf.ReadFrom(reader) - if err != nil { - return 0, err - } - - if size > kMaxTDFSize { - return 0, errors.New("exceeds max size for nano tdf") - } - - ki, err := getKasInfoForNanoTDF(&s, &config) - if err != nil { - return 0, fmt.Errorf("getKasInfoForNanoTDF failed: %w", err) - } - - config.kasPublicKey, err = ocrypto.ECPubKeyFromPem([]byte(ki.PublicKey)) - if err != nil { - return 0, fmt.Errorf("ocrypto.ECPubKeyFromPem failed: %w", err) - } - - // Create nano tdf header - key, totalSize, iteration, err := writeNanoTDFHeader(writer, config) - if err != nil { - return 0, fmt.Errorf("writeNanoTDFHeader failed:%w", err) - } - - s.Logger().Debug("checkpoint CreateNanoTDF", slog.Uint64("header", uint64(totalSize))) - - aesGcm, err := ocrypto.NewAESGcm(key) - if err != nil { - return 0, fmt.Errorf("ocrypto.NewAESGcm failed:%w", err) - } - var ivPadded []byte - if config.collectionCfg.useCollection { - ivPadded = make([]byte, gcmIvSize) - iv := make([]byte, binary.MaxVarintLen32) - binary.LittleEndian.PutUint32(iv, iteration) - copy(ivPadded[kIvPadding:], iv[:kNanoTDFIvSize]) - } else { - ivPadded, err = nonZeroRandomPaddedIV() - if err != nil { - return 0, err - } - } - - tagSize, err := SizeOfAuthTagForCipher(config.sigCfg.cipher) - if err != nil { - return 0, fmt.Errorf("SizeOfAuthTagForCipher failed:%w", err) - } - - cipherData, err := aesGcm.EncryptWithIVAndTagSize(ivPadded, buf.Bytes(), tagSize) - if err != nil { - return 0, err - } - - // Write the length of the payload as int24 - cipherDataWithoutPadding := cipherData[kIvPadding:] - const ( - kUint32BufLen = 4 - ) - uint32Buf := make([]byte, kUint32BufLen) - binary.BigEndian.PutUint32(uint32Buf, uint32(len(cipherDataWithoutPadding))) - l, err := writer.Write(uint32Buf[1:]) - if err != nil { - return 0, err - } - totalSize += uint32(l) - - s.Logger().Debug("checkpoint CreateNanoTDF", slog.Uint64("payload_length", uint64(len(cipherDataWithoutPadding)))) - - // write cipher data - l, err = writer.Write(cipherDataWithoutPadding) - if err != nil { - return 0, err - } - totalSize += uint32(l) - - return totalSize, nil -} - -// ============================================================================================================ -// NanoTDF Decrypt -// ============================================================================================================ - -type NanoTDFDecryptHandler struct { - reader io.ReadSeeker - writer io.Writer - - header NanoTDFHeader - headerBuf []byte - - config *NanoTDFReaderConfig -} - -type NanoTDFReader struct { - reader io.ReadSeeker - tokenSource auth.AccessTokenSource - httpClient *http.Client - connectOptions []connect.ClientOption - collectionStore *collectionStore - - header NanoTDFHeader - headerBuf []byte - payloadKey []byte - - config *NanoTDFReaderConfig - requiredObligations *RequiredObligations -} - -func createNanoTDFDecryptHandler(reader io.ReadSeeker, writer io.Writer, opts ...NanoTDFReaderOption) (*NanoTDFDecryptHandler, error) { - nanoTdfReaderConfig, err := newNanoTDFReaderConfig(opts...) - if err != nil { - return nil, fmt.Errorf("newNanoTDFReaderConfig failed: %w", err) - } - return &NanoTDFDecryptHandler{ - reader: reader, - writer: writer, - config: nanoTdfReaderConfig, - }, nil -} - -func (n *NanoTDFDecryptHandler) CreateRewrapRequest(ctx context.Context) (map[string]*kas.UnsignedRewrapRequest_WithPolicyRequest, error) { - var err error - n.header, n.headerBuf, err = getNanoTDFHeader(n.reader) - if err != nil { - return nil, err - } - - return createNanoRewrapRequest(ctx, n.config, n.header, n.headerBuf) -} - -func (n *NanoTDFDecryptHandler) Decrypt(ctx context.Context, result []kaoResult) (int, error) { - return decryptNanoTDF(ctx, n.reader, n.writer, result, &n.header) -} - -func (s SDK) LoadNanoTDF(ctx context.Context, reader io.ReadSeeker, opts ...NanoTDFReaderOption) (*NanoTDFReader, error) { - nanoTdfReaderConfig, err := newNanoTDFReaderConfig(opts...) - if err != nil { - return nil, fmt.Errorf("newNanoTDFReaderConfig failed: %w", err) - } - - useGlobalFulfillableObligations := len(nanoTdfReaderConfig.fulfillableObligationFQNs) == 0 && len(s.fulfillableObligationFQNs) > 0 - if useGlobalFulfillableObligations { - nanoTdfReaderConfig.fulfillableObligationFQNs = s.fulfillableObligationFQNs - } - - nanoTdfReaderConfig.kasAllowlist, err = getKasAllowList(ctx, nanoTdfReaderConfig.kasAllowlist, s, nanoTdfReaderConfig.ignoreAllowList) - if err != nil { - return nil, err - } - - header, headerBuf, err := getNanoTDFHeader(reader) - if err != nil { - return nil, fmt.Errorf("getNanoTDFHeader: %w", err) - } - - return &NanoTDFReader{ - reader: reader, - tokenSource: s.tokenSource, - httpClient: s.conn.Client, - connectOptions: s.conn.Options, - config: nanoTdfReaderConfig, - collectionStore: s.collectionStore, - header: header, - headerBuf: headerBuf, - }, nil -} - -// Do all network behavior (Rewrap request) -func (n *NanoTDFReader) Init(ctx context.Context) error { - if n.payloadKey != nil { - return nil - } - - return n.getNanoRewrapKey(ctx) -} - -func (n *NanoTDFReader) DecryptNanoTDF(ctx context.Context, writer io.Writer) (int, error) { - if n.payloadKey == nil { - err := n.getNanoRewrapKey(ctx) - if err != nil { - return 0, err - } - } - - return decryptNanoTDF(ctx, n.reader, writer, []kaoResult{{SymmetricKey: n.payloadKey}}, &n.header) -} - -// ReadNanoTDF - read the nano tdf and return the decrypted data from it -func (s SDK) ReadNanoTDF(writer io.Writer, reader io.ReadSeeker, opts ...NanoTDFReaderOption) (int, error) { - return s.ReadNanoTDFContext(context.Background(), writer, reader, opts...) -} - -// ReadNanoTDFContext - allows cancelling the reader -func (s SDK) ReadNanoTDFContext(ctx context.Context, writer io.Writer, reader io.ReadSeeker, opts ...NanoTDFReaderOption) (int, error) { - r, err := s.LoadNanoTDF(ctx, reader, opts...) - if err != nil { - return 0, fmt.Errorf("LoadNanoTDF: %w", err) - } - - err = r.getNanoRewrapKey(ctx) - if err != nil { - return 0, fmt.Errorf("getNanoRewrapKey: %w", err) - } - - return r.DecryptNanoTDF(ctx, writer) -} - -/* -* Returns the obligations required for access to the NanoTDF payload. -* -* If obligations are not populated we call Init() to populate them, -* which will result in a rewrap call. - */ -func (n *NanoTDFReader) Obligations(ctx context.Context) (RequiredObligations, error) { - if n.requiredObligations != nil { - return *n.requiredObligations, nil - } - - err := n.Init(ctx) - // Do not return error if we required obligations after Init() - // It's possible that an error was returned do to required obligations - if n.requiredObligations != nil && len(n.requiredObligations.FQNs) > 0 { - return *n.requiredObligations, nil - } - - return RequiredObligations{FQNs: []string{}}, err -} - -func (n *NanoTDFReader) getNanoRewrapKey(ctx context.Context) error { - req, err := createNanoRewrapRequest(ctx, n.config, n.header, n.headerBuf) - if err != nil { - return fmt.Errorf("CreateRewrapRequest: %w", err) - } - - if n.collectionStore != nil { - if key, found := n.collectionStore.get(n.headerBuf); found { - n.payloadKey = key - return nil - } - } - - client := newKASClient(n.httpClient, n.connectOptions, n.tokenSource, nil, n.config.fulfillableObligationFQNs) - kasURL, err := n.header.kasURL.GetURL() - if err != nil { - return fmt.Errorf("nano header kasUrl: %w", err) - } - - policyResult, err := client.nanoUnwrap(ctx, req[kasURL]) - if err != nil { - return fmt.Errorf("rewrap failed: %w", err) - } - result, ok := policyResult["policy"] - if !ok || len(result) != 1 { - return errors.New("policy was not found in rewrap response") - } - - // Populate obligations after policy result is found. - n.requiredObligations = &RequiredObligations{FQNs: result[0].RequiredObligations} - - if result[0].Error != nil { - errToReturn := fmt.Errorf("rewrapError: %w", result[0].Error) - return getKasErrorToReturn(result[0].Error, errToReturn) - } - - if n.collectionStore != nil { - n.collectionStore.store(n.headerBuf, result[0].SymmetricKey) - } - - n.payloadKey = result[0].SymmetricKey - - return nil -} - -func versionSalt() []byte { - digest := sha256.New() - digest.Write([]byte(kNanoTDFMagicStringAndVersion)) - return digest.Sum(nil) -} - -// createNanoTDFSymmetricKey creates the symmetric key for nanoTDF header -func createNanoTDFSymmetricKey(config NanoTDFConfig) ([]byte, error) { - if config.kasPublicKey == nil { - return nil, errors.New("KAS public key is required for encrypted policy mode") - } - - ecdhKey, err := ocrypto.ConvertToECDHPrivateKey(config.keyPair.PrivateKey) - if err != nil { - return nil, fmt.Errorf("ocrypto.ConvertToECDHPrivateKey failed:%w", err) - } - - symKey, err := ocrypto.ComputeECDHKeyFromECDHKeys(config.kasPublicKey, ecdhKey) - if err != nil { - return nil, fmt.Errorf("ocrypto.ComputeECDHKeyFromEC failed:%w", err) - } - - salt := versionSalt() - symmetricKey, err := ocrypto.CalculateHKDF(salt, symKey) - if err != nil { - return nil, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err) - } - - return symmetricKey, nil -} - -func getKasInfoForNanoTDF(s *SDK, config *NanoTDFConfig) (*KASInfo, error) { - var err error - // * Attempt to use base key if present and ECC. - ki, err := getNanoKasInfoFromBaseKey(s) - if err == nil { - err = updateConfigWithBaseKey(ki, config) - if err == nil { - return ki, nil - } - } - - s.logger.Debug("getNanoKasInfoFromBaseKey failed, falling back to default kas", slog.String("error", err.Error())) - - kasURL, err := config.kasURL.GetURL() - if err != nil { - return nil, fmt.Errorf("config.kasURL failed:%w", err) - } - if kasURL == "https://" || kasURL == "http://" { - return nil, errors.New("config.kasUrl is empty") - } - ki, err = s.getPublicKey(context.Background(), kasURL, config.bindCfg.eccMode.String(), "") - if err != nil { - return nil, fmt.Errorf("getECPublicKey failed:%w", err) - } - - // update KAS URL with kid if set - if ki.KID != "" && !s.nanoFeatures.noKID { - err = config.kasURL.setURLWithIdentifier(kasURL, ki.KID) - if err != nil { - return nil, fmt.Errorf("getECPublicKey setURLWithIdentifier failed:%w", err) - } - } - - return ki, nil -} - -func updateConfigWithBaseKey(ki *KASInfo, config *NanoTDFConfig) error { - ecMode, err := ocrypto.ECKeyTypeToMode(ocrypto.KeyType(ki.Algorithm)) - if err != nil { - return fmt.Errorf("ocrypto.ECKeyTypeToMode failed: %w", err) - } - err = config.kasURL.setURLWithIdentifier(ki.URL, ki.KID) - if err != nil { - return fmt.Errorf("config.kasURL setURLWithIdentifier failed: %w", err) - } - config.bindCfg.eccMode = ecMode - - return nil -} - -func getNanoKasInfoFromBaseKey(s *SDK) (*KASInfo, error) { - baseKey, err := getBaseKey(context.Background(), *s) - if err != nil { - return nil, err - } - - // Check if algorithm is one of the supported EC algorithms - algorithm := baseKey.GetPublicKey().GetAlgorithm() - if algorithm != policy.Algorithm_ALGORITHM_EC_P256 && - algorithm != policy.Algorithm_ALGORITHM_EC_P384 && - algorithm != policy.Algorithm_ALGORITHM_EC_P521 { - return nil, fmt.Errorf("base key algorithm is not supported for nano: %s", algorithm) - } - - alg, err := formatAlg(baseKey.GetPublicKey().GetAlgorithm()) - if err != nil { - return nil, fmt.Errorf("formatAlg failed: %w", err) - } - - return &KASInfo{ - URL: baseKey.GetKasUri(), - PublicKey: baseKey.GetPublicKey().GetPem(), - KID: baseKey.GetPublicKey().GetKid(), - Algorithm: alg, - }, nil -} - -func getNanoTDFHeader(reader io.ReadSeeker) (NanoTDFHeader, []byte, error) { - var err error - var headerSize uint32 - var header NanoTDFHeader - header, headerSize, err = NewNanoTDFHeaderFromReader(reader) - if err != nil { - return header, []byte{}, err - } - _, err = reader.Seek(0, io.SeekStart) - if err != nil { - return header, []byte{}, fmt.Errorf("readSeeker.Seek failed: %w", err) - } - - headerBuf := make([]byte, headerSize) - _, err = reader.Read(headerBuf) - if err != nil { - return header, []byte{}, fmt.Errorf("readSeeker.Read failed: %w", err) - } - - return header, headerBuf, nil -} - -func createNanoRewrapRequest(ctx context.Context, config *NanoTDFReaderConfig, header NanoTDFHeader, headerBuf []byte) (map[string]*kas.UnsignedRewrapRequest_WithPolicyRequest, error) { - kasURL, err := header.kasURL.GetURL() - if err != nil { - return nil, err - } - - if config.ignoreAllowList { - slog.WarnContext(ctx, "kasAllowlist is ignored, kas url is allowed", slog.String("kas_url", kasURL)) - } else if !config.kasAllowlist.IsAllowed(kasURL) { - return nil, fmt.Errorf("KasAllowlist: kas url %s is not allowed", kasURL) - } - - req := &kas.UnsignedRewrapRequest_WithPolicyRequest{ - KeyAccessObjects: []*kas.UnsignedRewrapRequest_WithKeyAccessObject{ - { - KeyAccessObjectId: "kao-0", - KeyAccessObject: &kas.KeyAccess{KasUrl: kasURL, Header: headerBuf}, - }, - }, - Policy: &kas.UnsignedRewrapRequest_WithPolicy{ - Id: "policy", - }, - Algorithm: "ec:secp256r1", - } - return map[string]*kas.UnsignedRewrapRequest_WithPolicyRequest{kasURL: req}, nil -} - -func decryptNanoTDF(ctx context.Context, reader io.ReadSeeker, writer io.Writer, result []kaoResult, header *NanoTDFHeader) (int, error) { - var err error - if len(result) != 1 { - return 0, errors.New("improper result from kas") - } - - if result[0].Error != nil { - return 0, result[0].Error - } - key := result[0].SymmetricKey - - const ( - kPayloadLoadLengthBufLength = 4 - ) - payloadLengthBuf := make([]byte, kPayloadLoadLengthBufLength) - _, err = reader.Read(payloadLengthBuf[1:]) - if err != nil { - return 0, fmt.Errorf(" io.Reader.Read failed :%w", err) - } - - payloadLength := binary.BigEndian.Uint32(payloadLengthBuf) - slog.DebugContext(ctx, "decrypt", slog.Uint64("payload_length", uint64(payloadLength))) - - cipherData := make([]byte, payloadLength) - _, err = reader.Read(cipherData) - if err != nil { - return 0, fmt.Errorf("readSeeker.Seek failed: %w", err) - } - - aesGcm, err := ocrypto.NewAESGcm(key) - if err != nil { - return 0, fmt.Errorf("ocrypto.NewAESGcm failed:%w", err) - } - - ivPadded := make([]byte, 0, ocrypto.GcmStandardNonceSize) - noncePadding := make([]byte, kIvPadding) - ivPadded = append(ivPadded, noncePadding...) - iv := cipherData[:kNanoTDFIvSize] - ivPadded = append(ivPadded, iv...) - - tagSize, err := SizeOfAuthTagForCipher(header.sigCfg.cipher) - if err != nil { - return 0, fmt.Errorf("SizeOfAuthTagForCipher failed:%w", err) - } - - decryptedData, err := aesGcm.DecryptWithIVAndTagSize(ivPadded, cipherData[kNanoTDFIvSize:], tagSize) - if err != nil { - return 0, err - } - - writeLen, err := writer.Write(decryptedData) - if err != nil { - return 0, err - } - - return writeLen, nil -} diff --git a/sdk/nanotdf_config.go b/sdk/nanotdf_config.go deleted file mode 100644 index 4e4686ea7d..0000000000 --- a/sdk/nanotdf_config.go +++ /dev/null @@ -1,176 +0,0 @@ -package sdk - -import ( - "crypto/ecdh" - "fmt" - - "github.com/opentdf/platform/lib/ocrypto" -) - -// ============================================================================================================ -// Support for specifying configuration information for nanoTDF operations -// -// The config information in this structure is referenced once at the beginning of the nanoTDF -// operation, and is not consulted again. It is safe to create a config, use it in one operation, modify it, -// and use it again in a second operation. The modification will only affect the second operation in that case. -// -// ============================================================================================================ - -type NanoTDFConfig struct { - keyPair ocrypto.ECKeyPair - kasPublicKey *ecdh.PublicKey - attributes []AttributeValueFQN - cipher CipherMode - kasURL ResourceLocator - sigCfg signatureConfig - policy policyInfo - bindCfg bindingConfig - collectionCfg *collectionConfig - policyMode PolicyType // Added field for policy mode -} - -type NanoTDFOption func(*NanoTDFConfig) error - -// NewNanoTDFConfig - Create a new instance of a nanoTDF config -func (s SDK) NewNanoTDFConfig() (*NanoTDFConfig, error) { - // TODO FIXME - how to pass in mode value and use here before 'c' is initialized? - newECKeyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) - if err != nil { - return nil, fmt.Errorf("ocrypto.NewRSAKeyPair failed: %w", err) - } - - c := &NanoTDFConfig{ - keyPair: newECKeyPair, - bindCfg: bindingConfig{ - useEcdsaBinding: false, - eccMode: ocrypto.ECCModeSecp256r1, - }, - cipher: kCipher96AuthTagSize, - sigCfg: signatureConfig{ - hasSignature: false, - signatureMode: ocrypto.ECCModeSecp256r1, - cipher: cipherModeAes256gcm96Bit, - }, - collectionCfg: &collectionConfig{ - iterations: 0, - useCollection: false, - header: []byte{}, - }, - policyMode: NanoTDFPolicyModeDefault, - } - - return c, nil -} - -// SetKasURL - set the URL of the KAS endpoint to be used for this nanoTDF -func (config *NanoTDFConfig) SetKasURL(url string) error { - return config.kasURL.setURL(url) -} - -// SetAttributes - set the attributes to be used for this nanoTDF -func (config *NanoTDFConfig) SetAttributes(attributes []string) error { - config.attributes = make([]AttributeValueFQN, len(attributes)) - for i, a := range attributes { - v, err := NewAttributeValueFQN(a) - if err != nil { - return err - } - config.attributes[i] = v - } - return nil -} - -// EnableECDSAPolicyBinding enable ecdsa policy binding -func (config *NanoTDFConfig) EnableECDSAPolicyBinding() { - config.bindCfg.useEcdsaBinding = true -} - -// EnableCollection Experimental: Enables Collection in NanoTDFConfig. -// Reuse NanoTDFConfig to add nTDFs to a Collection. -func (config *NanoTDFConfig) EnableCollection() { - config.collectionCfg.useCollection = true -} - -// SetPolicyMode sets whether the policy should be encrypted or plaintext -func (config *NanoTDFConfig) SetPolicyMode(mode PolicyType) error { - if err := validNanoTDFPolicyMode(mode); err != nil { - return err - } - config.policyMode = mode - return nil -} - -// WithNanoDataAttributes appends the given data attributes to the bound policy -func WithNanoDataAttributes(attributes ...string) NanoTDFOption { - return func(c *NanoTDFConfig) error { - for _, a := range attributes { - v, err := NewAttributeValueFQN(a) - if err != nil { - return err - } - c.attributes = append(c.attributes, v) - } - return nil - } -} - -// WithECDSAPolicyBinding enable ecdsa policy binding -func WithECDSAPolicyBinding() NanoTDFOption { - return func(c *NanoTDFConfig) error { - c.bindCfg.useEcdsaBinding = true - return nil - } -} - -type NanoTDFReaderConfig struct { - kasAllowlist AllowList - ignoreAllowList bool - fulfillableObligationFQNs []string -} - -func newNanoTDFReaderConfig(opt ...NanoTDFReaderOption) (*NanoTDFReaderConfig, error) { - c := &NanoTDFReaderConfig{} - - for _, o := range opt { - err := o(c) - if err != nil { - return nil, err - } - } - - return c, nil -} - -type NanoTDFReaderOption func(*NanoTDFReaderConfig) error - -func WithNanoKasAllowlist(kasList []string) NanoTDFReaderOption { - return func(c *NanoTDFReaderConfig) error { - allowlist, err := newAllowList(kasList) - if err != nil { - return fmt.Errorf("failed to create kas allowlist: %w", err) - } - c.kasAllowlist = allowlist - return nil - } -} - -func withNanoKasAllowlist(allowlist AllowList) NanoTDFReaderOption { - return func(c *NanoTDFReaderConfig) error { - c.kasAllowlist = allowlist - return nil - } -} - -func WithNanoIgnoreAllowlist(ignore bool) NanoTDFReaderOption { - return func(c *NanoTDFReaderConfig) error { - c.ignoreAllowList = ignore - return nil - } -} - -func WithNanoTDFFulfillableObligationFQNs(fqns []string) NanoTDFReaderOption { - return func(c *NanoTDFReaderConfig) error { - c.fulfillableObligationFQNs = fqns - return nil - } -} diff --git a/sdk/nanotdf_config_test.go b/sdk/nanotdf_config_test.go deleted file mode 100644 index 43625f8bef..0000000000 --- a/sdk/nanotdf_config_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package sdk - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestNanoTDFConfig1 - Create a new config, verify that the config contains valid PEMs for the key pair -func TestNanoTDFConfig1(t *testing.T) { - var s SDK - conf, err := s.NewNanoTDFConfig() - if err != nil { - t.Fatal(err) - } - pemPrvKey, err := conf.keyPair.PrivateKeyInPemFormat() - if err != nil { - t.Fatal(err) - } - - if len(pemPrvKey) == 0 { - t.Fatal("no private key") - } - - pemPubKey, err := conf.keyPair.PublicKeyInPemFormat() - if err != nil { - t.Fatal(err) - } - if len(pemPubKey) == 0 { - t.Fatal("no public key") - } -} - -// TestNanoTDFConfig2 - set kas url, retrieve kas url, verify value is correct -func TestNanoTDFConfig2(t *testing.T) { - const ( - kasURL = "https://test.virtru.com" - ) - - var s SDK - conf, err := s.NewNanoTDFConfig() - if err != nil { - t.Fatal(err) - } - err = conf.SetKasURL(kasURL) - if err != nil { - t.Fatal(err) - } - - readKasURL, err := conf.kasURL.GetURL() - if err != nil { - t.Fatal(err) - } - if readKasURL != kasURL { - t.Fatalf("expect %s, got %s", kasURL, readKasURL) - } -} - -func TestNewNanoTDFReaderConfig(t *testing.T) { - t.Run("Valid options", func(t *testing.T) { - config, err := newNanoTDFReaderConfig( - WithNanoKasAllowlist([]string{"https://example.com:443", "https://another.com"}), - WithNanoIgnoreAllowlist(true), - ) - require.NoError(t, err, "Expected no error when creating NanoTDFReaderConfig with valid options") - assert.NotNil(t, config, "Expected NanoTDFReaderConfig to be created") - assert.True(t, config.ignoreAllowList, "Expected ignoreAllowList to be true") - assert.True(t, config.kasAllowlist.IsAllowed("https://example.com:443"), "Expected KAS URL to be allowed") - assert.True(t, config.kasAllowlist.IsAllowed("https://another.com"), "Expected KAS URL to be allowed") - }) - - t.Run("Invalid KAS URL in allowlist", func(t *testing.T) { - config, err := newNanoTDFReaderConfig( - WithNanoKasAllowlist([]string{""}), - ) - require.Error(t, err, "Expected an error when creating NanoTDFReaderConfig with invalid KAS URL") - assert.Nil(t, config, "Expected NanoTDFReaderConfig to be nil") - }) -} - -func TestWithNanoKasAllowlist(t *testing.T) { - t.Run("Valid KAS URLs", func(t *testing.T) { - config := &NanoTDFReaderConfig{} - err := WithNanoKasAllowlist([]string{"https://example.com:443", "https://another.com"})(config) - require.NoError(t, err, "Expected no error when adding valid KAS URLs to allowlist") - assert.True(t, config.kasAllowlist.IsAllowed("https://example.com"), "Expected KAS URL to be allowed") - assert.True(t, config.kasAllowlist.IsAllowed("https://another.com"), "Expected KAS URL to be allowed") - }) - - t.Run("Invalid KAS URL", func(t *testing.T) { - config := &NanoTDFReaderConfig{} - err := WithNanoKasAllowlist([]string{""})(config) - require.Error(t, err, "Expected an error when adding invalid KAS URL to allowlist") - }) -} - -func TestWithNanoIgnoreAllowlist(t *testing.T) { - t.Run("Set ignoreAllowList to true", func(t *testing.T) { - config := &NanoTDFReaderConfig{} - err := WithNanoIgnoreAllowlist(true)(config) - require.NoError(t, err, "Expected no error when setting ignoreAllowList to true") - assert.True(t, config.ignoreAllowList, "Expected ignoreAllowList to be true") - }) - - t.Run("Set ignoreAllowList to false", func(t *testing.T) { - config := &NanoTDFReaderConfig{} - err := WithNanoIgnoreAllowlist(false)(config) - require.NoError(t, err, "Expected no error when setting ignoreAllowList to false") - assert.False(t, config.ignoreAllowList, "Expected ignoreAllowList to be false") - }) -} - -func TestWithNanoKasAllowlist_with(t *testing.T) { - t.Run("Valid AllowList", func(t *testing.T) { - allowlist := AllowList{"https://example.com:443": true} - config := &NanoTDFReaderConfig{} - err := withNanoKasAllowlist(allowlist)(config) - require.NoError(t, err, "Expected no error when setting valid AllowList") - assert.True(t, config.kasAllowlist.IsAllowed("https://example.com"), "Expected KAS URL to be allowed") - }) - - t.Run("Empty AllowList", func(t *testing.T) { - allowlist := AllowList{} - config := &NanoTDFReaderConfig{} - err := withNanoKasAllowlist(allowlist)(config) - require.NoError(t, err, "Expected no error when setting empty AllowList") - assert.False(t, config.kasAllowlist.IsAllowed("https://example.com:443"), "Expected KAS URL to not be allowed") - }) -} - -func TestSetPolicyMode(t *testing.T) { - t.Run("Set to plaintext", func(t *testing.T) { - var s SDK - conf, err := s.NewNanoTDFConfig() - require.NoError(t, err) - - err = conf.SetPolicyMode(NanoTDFPolicyModePlainText) - require.NoError(t, err) - assert.Equal(t, NanoTDFPolicyModePlainText, conf.policyMode) - }) - - t.Run("Set to encrypted", func(t *testing.T) { - var s SDK - conf, err := s.NewNanoTDFConfig() - require.NoError(t, err) - - err = conf.SetPolicyMode(NanoTDFPolicyModeEncrypted) - require.NoError(t, err) - assert.Equal(t, NanoTDFPolicyModeEncrypted, conf.policyMode) - }) - - t.Run("Set to invalid mode", func(t *testing.T) { - var s SDK - conf, err := s.NewNanoTDFConfig() - require.NoError(t, err) - - err = conf.SetPolicyMode(PolicyType(99)) // Assuming 99 is an invalid policyType - require.Error(t, err) - assert.NotEqual(t, PolicyType(99), conf.policyMode) - }) -} diff --git a/sdk/nanotdf_policy.go b/sdk/nanotdf_policy.go deleted file mode 100644 index a23b268b03..0000000000 --- a/sdk/nanotdf_policy.go +++ /dev/null @@ -1,152 +0,0 @@ -package sdk - -import ( - "encoding/binary" - "errors" - "fmt" - "io" - - "github.com/opentdf/platform/lib/ocrypto" -) - -// ============================================================================================================ -// Support for nanoTDF policy operations -// -// ============================================================================================================ - -type PolicyType uint8 - -const ( - NanoTDFPolicyModeRemote PolicyType = iota - NanoTDFPolicyModePlainText - NanoTDFPolicyModeEncrypted - NanoTDFPolicyModeEncryptedPolicyKeyAccess - - NanoTDFPolicyModeDefault = NanoTDFPolicyModeEncrypted -) - -var ( - ErrNanoTDFUnsupportedPolicyMode = errors.New("unsupported policy mode") - ErrNanoTDFInvalidPolicyMode = errors.New("invalid policy mode") -) - -type PolicyBody struct { - mode PolicyType - rp remotePolicy - ep embeddedPolicy -} - -// getLength - size in bytes of the serialized content of this object -// func (pb *PolicyBody) getLength() uint16 { // nolint:unused future use -// var result uint16 -// -// result = 1 /* policy mode byte */ -// -// if pb.mode == policyTypeRemotePolicy { -// result += pb.rp.getLength() -// } else { -// // If it's not remote, assume embedded policy -// result += pb.ep.getLength() -// } -// -// return result -// } - -// readPolicyBody - helper function to decode input data into a PolicyBody object -func (pb *PolicyBody) readPolicyBody(reader io.Reader) error { - var mode PolicyType - if err := binary.Read(reader, binary.BigEndian, &mode); err != nil { - return err - } - switch mode { - case NanoTDFPolicyModeRemote: - var rl ResourceLocator - if err := rl.readResourceLocator(reader); err != nil { - return errors.Join(ErrNanoTDFHeaderRead, err) - } - pb.rp = remotePolicy{url: rl} - case NanoTDFPolicyModeEncrypted: - case NanoTDFPolicyModeEncryptedPolicyKeyAccess: - case NanoTDFPolicyModePlainText: - var ep embeddedPolicy - if err := ep.readEmbeddedPolicy(reader); err != nil { - return errors.Join(ErrNanoTDFHeaderRead, err) - } - pb.ep = ep - default: - return errors.New("unknown policy type") - } - return nil -} - -// writePolicyBody - helper function to encode and write a PolicyBody object -func (pb *PolicyBody) writePolicyBody(writer io.Writer) error { - var err error - - switch pb.mode { - case NanoTDFPolicyModeRemote: // remote policy - resource locator - if err = binary.Write(writer, binary.BigEndian, pb.mode); err != nil { - return err - } - if err = pb.rp.url.writeResourceLocator(writer); err != nil { - return err - } - return nil - case NanoTDFPolicyModeEncrypted: - case NanoTDFPolicyModeEncryptedPolicyKeyAccess: - case NanoTDFPolicyModePlainText: - // embedded policy - inline - if err = binary.Write(writer, binary.BigEndian, pb.mode); err != nil { - return err - } - if err = pb.ep.writeEmbeddedPolicy(writer); err != nil { - return err - } - default: - return errors.New("unsupported policy mode") - } - return nil -} - -func validNanoTDFPolicyMode(mode PolicyType) error { - switch mode { - case NanoTDFPolicyModePlainText, NanoTDFPolicyModeEncrypted: - return nil - case NanoTDFPolicyModeRemote, NanoTDFPolicyModeEncryptedPolicyKeyAccess: - return ErrNanoTDFUnsupportedPolicyMode - default: - return ErrNanoTDFInvalidPolicyMode - } -} - -// createEmbeddedPolicy creates an embedded policy object, encrypting it if required by the policy mode -func createNanoTDFEmbeddedPolicy(symmetricKey []byte, policyObjectAsStr []byte, config NanoTDFConfig) (embeddedPolicy, error) { - if config.policyMode == NanoTDFPolicyModeEncrypted { - aesGcm, err := ocrypto.NewAESGcm(symmetricKey) - if err != nil { - return embeddedPolicy{}, fmt.Errorf("ocrypto.NewAESGcm failed:%w", err) - } - - tagSize, err := SizeOfAuthTagForCipher(config.sigCfg.cipher) - if err != nil { - return embeddedPolicy{}, fmt.Errorf("SizeOfAuthTagForCipher failed:%w", err) - } - - const kIvLength = 12 - iv := make([]byte, kIvLength) - cipherText, err := aesGcm.EncryptWithIVAndTagSize(iv, policyObjectAsStr, tagSize) - if err != nil { - return embeddedPolicy{}, fmt.Errorf("AesGcm.EncryptWithIVAndTagSize failed:%w", err) - } - - return embeddedPolicy{ - lengthBody: uint16(len(cipherText) - len(iv)), - body: cipherText[len(iv):], - }, nil - } - - return embeddedPolicy{ - lengthBody: uint16(len(policyObjectAsStr)), - body: policyObjectAsStr, - }, nil -} diff --git a/sdk/nanotdf_policy_test.go b/sdk/nanotdf_policy_test.go deleted file mode 100644 index aca63f588d..0000000000 --- a/sdk/nanotdf_policy_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package sdk - -import ( - "bytes" - "crypto/ecdh" - "crypto/rand" - "io" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - kSampleURLBody = "test.virtru.com" - // kSampleUrlProto = policyTypeRemotePolicy - kSampleURLFull = "https://" + kSampleURLBody -) - -// TestNanoTDFPolicyWrite - Create a new policy, write it to a buffer -func TestNanoTDFPolicy(t *testing.T) { - pb := &PolicyBody{ - mode: NanoTDFPolicyModeRemote, - rp: remotePolicy{ - url: ResourceLocator{ - protocol: 1, - body: kSampleURLBody, - }, - }, - } - - buffer := new(bytes.Buffer) - err := pb.writePolicyBody(io.Writer(buffer)) - if err != nil { - t.Fatal(err) - } - - pb2 := &PolicyBody{} - err = pb2.readPolicyBody(bytes.NewReader(buffer.Bytes())) - if err != nil { - t.Fatal(err) - } - - fullURL, err := pb2.rp.url.GetURL() - if err != nil { - t.Fatal(err) - } - if fullURL != kSampleURLFull { - t.Fatal(fullURL) - } -} - -func TestCreateEmbeddedPolicy(t *testing.T) { - // Test data - policyData := []byte(`{"attributes":["https://example.com/attr/Classification/value/S"]}`) - - t.Run("plaintext policy", func(t *testing.T) { - config, err := new(SDK).NewNanoTDFConfig() - require.NoError(t, err) - err = config.SetPolicyMode(NanoTDFPolicyModePlainText) - require.NoError(t, err) - - policy, err := createNanoTDFEmbeddedPolicy(make([]byte, 32), policyData, *config) - require.NoError(t, err) - assert.Equal(t, uint16(len(policyData)), policy.lengthBody) - assert.Equal(t, policyData, policy.body) - }) - - t.Run("encrypted policy", func(t *testing.T) { - config, err := new(SDK).NewNanoTDFConfig() - require.NoError(t, err) - - // Defaults to encrypted policy - - // Setup KAS public key - key, err := ecdh.P256().GenerateKey(rand.Reader) - require.NoError(t, err) - config.kasPublicKey = key.PublicKey() - - policy, err := createNanoTDFEmbeddedPolicy(make([]byte, 32), policyData, *config) - require.NoError(t, err) - - // Verify the encrypted policy is different from input and has expected length - assert.NotEqual(t, policyData, policy.body) - assert.NotEmpty(t, policy.body, "Encrypted policy body should not be empty") - assert.Equal(t, uint16(len(policy.body)), policy.lengthBody) - - assert.NotEqual(t, policyData, policy.body, "Policy body should be encrypted and different from original data") - assert.NotEmpty(t, policy.body, "Policy body should not be empty after encryption") - }) -} diff --git a/sdk/nanotdf_test.go b/sdk/nanotdf_test.go deleted file mode 100644 index ca93db8e63..0000000000 --- a/sdk/nanotdf_test.go +++ /dev/null @@ -1,1423 +0,0 @@ -package sdk - -import ( - "bytes" - "context" - "crypto/ecdh" - "crypto/rand" - "crypto/x509" - "encoding/gob" - "encoding/hex" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "io" - "log/slog" - "net/http" - "os" - "strings" - "testing" - - "connectrpc.com/connect" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/opentdf/platform/lib/ocrypto" - "github.com/opentdf/platform/protocol/go/kas" - "github.com/opentdf/platform/protocol/go/policy" - "github.com/opentdf/platform/protocol/go/wellknownconfiguration" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/types/known/structpb" -) - -const ( - nanoFakePem = "pem" - fakeObligationFQN = "https://fake.example.com/obl/value/obligation1" -) - -// mockTransport is a custom RoundTripper that intercepts HTTP requests -type mockTransport struct { - publicKey string - kid string - kasKeyPair ocrypto.KeyPair // Store the KAS key pair for consistent crypto operations -} - -// nanotdfEqual compares two nanoTdf structures for equality. -func nanoTDFEqual(a, b *NanoTDFHeader) bool { - // Compare kasURL field - if a.kasURL.protocol != b.kasURL.protocol || a.kasURL.getLength() != b.kasURL.getLength() || a.kasURL.body != b.kasURL.body { - return false - } - - // Compare binding field - if a.bindCfg.useEcdsaBinding != b.bindCfg.useEcdsaBinding || a.bindCfg.eccMode != b.bindCfg.eccMode { - return false - } - - // Compare sigCfg field - if a.sigCfg.hasSignature != b.sigCfg.hasSignature || a.sigCfg.signatureMode != b.sigCfg.signatureMode || a.sigCfg.cipher != b.sigCfg.cipher { - return false - } - - // Compare policy field - // if a.PolicyBinding != b.PolicyBinding) { - // return false - // } - - // Compare EphemeralPublicKey field - if !bytes.Equal(a.EphemeralKey, b.EphemeralKey) { - return false - } - - // If all comparisons passed, the structures are equal - return true -} - -//// policyBodyEqual compares two PolicyBody instances for equality. -// func policyBodyEqual(a, b PolicyBody) bool { //nolint:unused future usage -// // Compare based on the concrete type of PolicyBody -// switch a.mode { -// case policyTypeRemotePolicy: -// return remotePolicyEqual(a.rp, b.rp) -// case policyTypeEmbeddedPolicyPlainText: -// case policyTypeEmbeddedPolicyEncrypted: -// case policyTypeEmbeddedPolicyEncryptedPolicyKeyAccess: -// return embeddedPolicyEqual(a.ep, b.ep) -// } -// return false -// } - -//// remotePolicyEqual compares two remotePolicy instances for equality. -// func remotePolicyEqual(a, b remotePolicy) bool { // nolint:unused future usage -// // Compare url field -// if a.url.protocol != b.url.protocol || a.url.getLength() != b.url.getLength() || a.url.body != b.url.body { -// return false -// } -// return true -// } -// -//// embeddedPolicyEqual compares two embeddedPolicy instances for equality. -// func embeddedPolicyEqual(a, b embeddedPolicy) bool { // nolint:unused future usage -// // Compare lengthBody and body fields -// return a.lengthBody == b.lengthBody && bytes.Equal(a.body, b.body) -// } -// -//// eccSignatureEqual compares two eccSignature instances for equality. -// func eccSignatureEqual(a, b *eccSignature) bool { // nolint:unused future usage -// // Compare value field -// return bytes.Equal(a.value, b.value) -// } - -func init() { - // Register the remotePolicy type with gob - gob.Register(&remotePolicy{}) -} - -func NotTestReadNanoTDFHeader(t *testing.T) { - // Prepare a sample nanoTdf structure - goodHeader := NanoTDFHeader{ - kasURL: ResourceLocator{ - protocol: urlProtocolHTTPS, - body: "kas.virtru.com", - }, - bindCfg: bindingConfig{ - useEcdsaBinding: true, - eccMode: ocrypto.ECCModeSecp256r1, - }, - sigCfg: signatureConfig{ - hasSignature: true, - signatureMode: ocrypto.ECCModeSecp256r1, - cipher: cipherModeAes256gcm64Bit, - }, - // PolicyBinding: policyInfo{ - // body: PolicyBody{ - // mode: policyTypeRemotePolicy, - // rp: remotePolicy{ - // url: ResourceLocator{ - // protocol: urlProtocolHTTPS, - // body: "kas.virtru.com/policy", - // }, - // }, - // }, - // binding: &eccSignature{ - // value: []byte{181, 228, 19, 166, 2, 17, 229, 241}, - // }, - // }, - EphemeralKey: []byte{ - 123, 34, 52, 160, 205, 63, 54, 255, 123, 186, 109, - 143, 232, 223, 35, 246, 44, 157, 9, 53, 111, 133, - 130, 248, 169, 207, 21, 18, 108, 138, 157, 164, 108, - }, - } - - const ( - kExpectedHeaderSize = 128 - ) - - // Serialize the sample nanoTdf structure into a byte slice using gob - file, err := os.Open("nanotdfspec.ntdf") - if err != nil { - t.Fatalf("Cannot open nanoTdf file: %v", err) - } - defer file.Close() - - resultHeader, headerSize, err := NewNanoTDFHeaderFromReader(io.ReadSeeker(file)) - if err != nil { - t.Fatalf("Error while reading nanoTdf header: %v", err) - } - - if headerSize != kExpectedHeaderSize { - t.Fatalf("expecting length %d, got %d", kExpectedHeaderSize, headerSize) - } - - // Compare the result with the original nanoTdf structure - if !nanoTDFEqual(&resultHeader, &goodHeader) { - t.Error("Result does not match the expected nanoTdf structure.") - } -} - -const ( -// sdkPrivateKey = `-----BEGIN PRIVATE KEY----- -// MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1HjFYV8D16BQszNW -// 6Hx/JxTE53oqk5/bWaIj4qV5tOyhRANCAAQW1Hsq0tzxN6ObuXqV+JoJN0f78Em/ -// PpJXUV02Y6Ex3WlxK/Oaebj8ATsbfaPaxrhyCWB3nc3w/W6+lySlLPn5 -// -----END PRIVATE KEY-----` - -// sdkPublicKey = `-----BEGIN PUBLIC KEY----- -// MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFtR7KtLc8Tejm7l6lfiaCTdH+/BJ -// vz6SV1FdNmOhMd1pcSvzmnm4/AE7G32j2sa4cglgd53N8P1uvpckpSz5+Q== -// -----END PUBLIC KEY-----` - -// kasPrivateKey = `-----BEGIN PRIVATE KEY----- -// MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgu2Hmm80uUzQB1OfB -// PyMhWIyJhPA61v+j0arvcLjTwtqhRANCAASHCLUHY4szFiVV++C9+AFMkEL2gG+O -// byN4Hi7Ywl8GMPOAPcQdIeUkoTd9vub9PcuSj23I8/pLVzs23qhefoUf -// -----END PRIVATE KEY-----` - -// kasPublicKey = `-----BEGIN PUBLIC KEY----- -// -// MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhwi1B2OLMxYlVfvgvfgBTJBC9oBv -// jm8jeB4u2MJfBjDzgD3EHSHlJKE3fb7m/T3Lko9tyPP6S1c7Nt6oXn6FHw== -// -----END PUBLIC KEY-----` -) - -// disabled for now, no remote policy support yet -func NotTestNanoTDFEncryptFile(t *testing.T) { - const ( - kExpectedOutSize = 128 - ) - - var s SDK - infile, err := os.Open("nanotest1.txt") - if err != nil { - t.Fatal(err) - } - - // try to delete the output file in case it exists already - ignore error if it doesn't exist - _ = os.Remove("nanotest1.ntdf") - - outfile, err := os.Create("nanotest1.ntdf") - if err != nil { - t.Fatal(err) - } - - // TODO - populate config properly - kasURL := "https://kas.virtru.com/kas" - var config NanoTDFConfig - err = config.kasURL.setURL(kasURL) - if err != nil { - t.Fatal(err) - } - - outSize, err := s.CreateNanoTDF(io.Writer(outfile), io.ReadSeeker(infile), config) - if err != nil { - t.Fatal(err) - } - if outSize != kExpectedOutSize { - t.Fatalf("expecting length %d, got %d", kExpectedOutSize, outSize) - } -} - -// disabled for now -func NotTestCreateNanoTDF(t *testing.T) { - var s SDK - - grpc.WithTransportCredentials(insecure.NewCredentials()) - - infile, err := os.Open("nanotest1.txt") - if err != nil { - t.Fatal(err) - } - - // try to delete the output file in case it exists already - ignore error if it doesn't exist - _ = os.Remove("nanotest1.ntdf") - - outfile, err := os.Create("nanotest1.ntdf") - if err != nil { - t.Fatal(err) - } - - // TODO - populate config properly - kasURL := "https://kas.virtru.com/kas" - var config NanoTDFConfig - err = config.kasURL.setURL(kasURL) - if err != nil { - t.Fatal(err) - } - - _, err = s.CreateNanoTDF(io.Writer(outfile), io.ReadSeeker(infile), config) - if err != nil { - t.Fatal(err) - } -} - -func TestNonZeroRandomPaddedIV(t *testing.T) { - iv, err := nonZeroRandomPaddedIV() - require.NoError(t, err) - require.NotNil(t, iv) - assert.Len(t, iv, ocrypto.GcmStandardNonceSize) - - // Ensure that the IV is not all zeros - allZero := true - for _, b := range iv { - if b != 0 { - allZero = false - break - } - } - assert.False(t, allZero, "IV should not be all zeros") -} - -func TestCreateNanoTDF(t *testing.T) { - tests := []struct { - name string - writer io.Writer - reader io.Reader - config NanoTDFConfig - expectedError string - }{ - { - name: "Nil writer", - writer: nil, - reader: bytes.NewReader([]byte("test data")), - config: NanoTDFConfig{}, - expectedError: "writer is nil", - }, - { - name: "Nil reader", - writer: new(bytes.Buffer), - reader: nil, - config: NanoTDFConfig{}, - expectedError: "reader is nil", - }, - { - name: "Empty NanoTDFConfig", - writer: new(bytes.Buffer), - reader: bytes.NewReader([]byte("test data")), - config: NanoTDFConfig{}, - expectedError: "config.kasUrl is empty", - }, - { - name: "KAS Identifier NanoTDFConfig", - writer: new(bytes.Buffer), - reader: bytes.NewReader([]byte("test data")), - config: NanoTDFConfig{ - kasURL: ResourceLocator{ - protocol: 1, - body: "kas.com", - identifier: "e0", - }, - }, - expectedError: "error making request", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s, err := New("http://localhost:8080", WithPlatformConfiguration(PlatformConfiguration{})) - require.NoError(t, err) - _, err = s.CreateNanoTDF(tt.writer, tt.reader, tt.config) - if tt.expectedError != "" { - assert.ErrorContains(t, err, tt.expectedError) - return - } - require.NoError(t, err) - }) - } -} - -func TestDataSet(t *testing.T) { - const ( - kasURL = "https://test.virtru.com" - ) - - var s SDK - conf, err := s.NewNanoTDFConfig() - if err != nil { - t.Fatal(err) - } - err = conf.SetKasURL(kasURL) - if err != nil { - t.Fatal(err) - } - - err = conf.SetAttributes([]string{"https://examples.com/attr/attr1/value/value1"}) - if err != nil { - t.Fatal(err) - } - - key, err := ecdh.P256().GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - conf.kasPublicKey = key.PublicKey() - - getHeaderAndSymKey := func(cfg *NanoTDFConfig) ([]byte, []byte) { - out := &bytes.Buffer{} - symKey, _, _, err := writeNanoTDFHeader(out, *cfg) - if err != nil { - t.Fatal() - } - - return out.Bytes(), symKey - } - - header1, _ := getHeaderAndSymKey(conf) - header2, _ := getHeaderAndSymKey(conf) - - if bytes.Equal(header1, header2) { - t.Fatal("headers should not match") - } - - conf.EnableCollection() - header1, symKey1 := getHeaderAndSymKey(conf) - header2, symKey2 := getHeaderAndSymKey(conf) - - if !bytes.Equal(symKey1, symKey2) { - t.Fatal("keys should match") - } - if !bytes.Equal(header1, header2) { - t.Fatal("headers should match") - } - - for i := 2; i <= kMaxIters; i++ { - header, _ := getHeaderAndSymKey(conf) - if !bytes.Equal(header, header1) { - t.Fatal("max iteration reset occurred too early, headers differ") - } - } - - header, _ := getHeaderAndSymKey(conf) - if bytes.Equal(header, header1) { - t.Fatal("header did not reset") - } -} - -type NanoSuite struct { - suite.Suite - mockTransport *mockTransport -} - -func TestNanoTDF(t *testing.T) { - suite.Run(t, new(NanoSuite)) -} - -func (s *NanoSuite) SetupSuite() { - // Create a single mock transport instance for the entire test suite - s.mockTransport = newMockTransport() -} - -// mockWellKnownServiceClient is a mock implementation of sdkconnect.WellKnownServiceClient -type mockWellKnownServiceClient struct { - mockResponse func() (*wellknownconfiguration.GetWellKnownConfigurationResponse, error) -} - -func (m *mockWellKnownServiceClient) GetWellKnownConfiguration(_ context.Context, _ *wellknownconfiguration.GetWellKnownConfigurationRequest) (*wellknownconfiguration.GetWellKnownConfigurationResponse, error) { - if m.mockResponse != nil { - return m.mockResponse() - } - return nil, errors.New("no mock response configured") -} - -func (s *NanoSuite) Test_CreateNanoTDF_BaseKey() { - // Mock KAS Info - mockKASInfo := &KASInfo{ - URL: "https://kas.example.com", - PublicKey: mockECPublicKey1, - KID: "key-p256", - } - - baseKey := createTestBaseKeyMap(&s.Suite, policy.Algorithm_ALGORITHM_EC_P256, mockKASInfo.KID, mockKASInfo.PublicKey, mockKASInfo.URL) - wellKnown := createWellKnown(baseKey) - mockClient := createMockWellKnownServiceClient(&s.Suite, wellKnown, nil) - - // Create SDK - sdk := &SDK{ - config: config{ - logger: slog.Default(), - }, - wellknownConfiguration: mockClient, - } - - config, err := sdk.NewNanoTDFConfig() - s.Require().NoError(err) - - err = config.SetKasURL("http://should-change.com") - s.Require().NoError(err) - - // Mock writer and reader - writer := new(bytes.Buffer) - reader := bytes.NewReader([]byte("test data")) - - // Call CreateNanoTDF - _, err = sdk.CreateNanoTDF(writer, reader, *config) - s.Require().NoError(err) - - // Check that writer is not empty - s.Require().NotEmpty(writer.Bytes()) -} - -func (s *NanoSuite) Test_GetKasInfoForNanoTDF_BaseKey() { - tests := []struct { - name string - algorithm policy.Algorithm - kasURI string - publicKeyPem string - kid string - wellKnownError error - expectedInfo *KASInfo - expectedError string - }{ - { - name: "Base Key Enabled - EC P256 - Success", - algorithm: policy.Algorithm_ALGORITHM_EC_P256, - kasURI: "https://kas.example.com", - publicKeyPem: nanoFakePem, - kid: "key-p256", - expectedInfo: &KASInfo{ - URL: "https://kas.example.com", - PublicKey: nanoFakePem, - KID: "key-p256", - Algorithm: "ec:secp256r1", - }, - }, - { - name: "Base Key Enabled - EC P384 - Success", - algorithm: policy.Algorithm_ALGORITHM_EC_P384, - kasURI: "https://kas.example.com", - publicKeyPem: nanoFakePem, - kid: "key-p384", - expectedInfo: &KASInfo{ - URL: "https://kas.example.com", - PublicKey: nanoFakePem, - KID: "key-p384", - Algorithm: "ec:secp384r1", - }, - }, - { - name: "Base Key Enabled - EC P521 - Success", - algorithm: policy.Algorithm_ALGORITHM_EC_P521, - kasURI: "https://kas.example.com", - publicKeyPem: nanoFakePem, - kid: "key-p521", - expectedInfo: &KASInfo{ - URL: "https://kas.example.com", - PublicKey: nanoFakePem, - KID: "key-p521", - Algorithm: "ec:secp521r1", - }, - }, - } - - for _, tt := range tests { - s.Run(tt.name, func() { - // Create a mock wellknown configuration response - baseKey := createTestBaseKeyMap(&s.Suite, tt.algorithm, tt.kid, tt.publicKeyPem, tt.kasURI) - wellKnown := createWellKnown(baseKey) - mockClient := createMockWellKnownServiceClient(&s.Suite, wellKnown, tt.wellKnownError) - - // Create SDK with mocked wellknown client - sdk := &SDK{ - wellknownConfiguration: mockClient, - } - - // Create a NanoTDFConfig - config := NanoTDFConfig{ - bindCfg: bindingConfig{ - eccMode: ocrypto.ECCModeSecp384r1, - }, - } - kasURL := "https://should-not-change.com" - err := config.SetKasURL(kasURL) - s.Require().NoError(err) - - // Call the getKasInfoForNanoTDF function - info, err := getKasInfoForNanoTDF(sdk, &config) - - // Check for expected errors - if tt.expectedError != "" { - s.Require().Error(err) - s.Require().Nil(info) - return - } - - // Check success case - s.Require().NoError(err) - s.Require().NotNil(info) - s.Require().Equal(tt.expectedInfo.URL, info.URL) - s.Require().Equal(tt.expectedInfo.PublicKey, info.PublicKey) - s.Require().Equal(tt.expectedInfo.KID, info.KID) - s.Require().Equal(tt.expectedInfo.Algorithm, info.Algorithm) - // Ensure the config was updated. - actualURL, err := config.kasURL.GetURL() - s.Require().NoError(err) - s.Require().Equal(tt.kasURI, actualURL) - expectedEcMode, err := ocrypto.ECKeyTypeToMode(ocrypto.KeyType(tt.expectedInfo.Algorithm)) - s.Require().NoError(err) - s.Require().Equal(expectedEcMode, config.bindCfg.eccMode) - }) - } -} - -func (s *NanoSuite) Test_PopulateNanoBaseKeyWithMockWellKnown() { - // Define test cases - tests := []struct { - name string - algorithm policy.Algorithm - kasURI string - publicKeyPem string - kid string - wellKnownError error - expectedInfo *KASInfo - expectedError string - }{ - { - name: "EC P256 - Success", - algorithm: policy.Algorithm_ALGORITHM_EC_P256, - kasURI: "https://kas.example.com", - publicKeyPem: nanoFakePem, - kid: "key-p256", - expectedInfo: &KASInfo{ - URL: "https://kas.example.com", - PublicKey: nanoFakePem, - KID: "key-p256", - Algorithm: "ec:secp256r1", - }, - }, - { - name: "EC P384 - Success", - algorithm: policy.Algorithm_ALGORITHM_EC_P384, - kasURI: "https://kas.example.com", - publicKeyPem: nanoFakePem, - kid: "key-p384", - expectedInfo: &KASInfo{ - URL: "https://kas.example.com", - PublicKey: nanoFakePem, - KID: "key-p384", - Algorithm: "ec:secp384r1", - }, - }, - { - name: "EC P521 - Success", - algorithm: policy.Algorithm_ALGORITHM_EC_P521, - kasURI: "https://kas.example.com", - publicKeyPem: nanoFakePem, - kid: "key-p521", - expectedInfo: &KASInfo{ - URL: "https://kas.example.com", - PublicKey: nanoFakePem, - KID: "key-p521", - Algorithm: "ec:secp521r1", - }, - }, - { - name: "Error from WellKnown Config", - algorithm: policy.Algorithm_ALGORITHM_EC_P256, - kasURI: "https://kas.example.com", - wellKnownError: errors.New("failed to get configuration"), - expectedError: "unable to retrieve config information", - }, - { - name: "Unsupported algorithm RSA 2048", - algorithm: policy.Algorithm_ALGORITHM_RSA_2048, - kasURI: "https://localhost:8080", - publicKeyPem: nanoFakePem, - kid: "key-rsa", - expectedError: "base key algorithm is not supported for nano", - }, - } - - for _, tt := range tests { - s.Run(tt.name, func() { - // Create a mock wellknown configuration response - baseKey := createTestBaseKeyMap(&s.Suite, tt.algorithm, tt.kid, tt.publicKeyPem, tt.kasURI) - wellKnown := createWellKnown(baseKey) - mockClient := createMockWellKnownServiceClient(&s.Suite, wellKnown, tt.wellKnownError) - - // Create SDK with mocked wellknown client - sdk := &SDK{ - wellknownConfiguration: mockClient, - } - - // Call the real populateNanoBaseKey function - info, err := getNanoKasInfoFromBaseKey(sdk) - - // Check for expected errors - if tt.expectedError != "" { - s.Require().Error(err) - s.Require().Contains(err.Error(), tt.expectedError) - s.Require().Nil(info) - return - } - - // Check success case - s.Require().NoError(err) - s.Require().NotNil(info) - s.Require().Equal(tt.expectedInfo.URL, info.URL) - s.Require().Equal(tt.expectedInfo.PublicKey, info.PublicKey) - s.Require().Equal(tt.expectedInfo.KID, info.KID) - s.Require().Equal(tt.expectedInfo.Algorithm, info.Algorithm) - }) - } -} - -func createMockWellKnownServiceClient(s *suite.Suite, wellKnownConfig map[string]interface{}, wellKnownError error) *mockWellKnownServiceClient { - return &mockWellKnownServiceClient{ - mockResponse: func() (*wellknownconfiguration.GetWellKnownConfigurationResponse, error) { - if wellKnownError != nil { - return nil, wellKnownError - } - - cfg, err := structpb.NewStruct(wellKnownConfig) - s.Require().NoError(err, "Failed to create struct from well-known configuration") - - return &wellknownconfiguration.GetWellKnownConfigurationResponse{ - Configuration: cfg, - }, nil - }, - } -} - -// Test suite for NanoTDF Reader functionality -func (s *NanoSuite) Test_NanoTDFReader_LoadNanoTDF() { - // Create a real NanoTDF for testing - sdk, err := s.createTestSDK() - sdk.fulfillableObligationFQNs = []string{"https://example.com/obl/value/obl1"} - s.Require().NoError(err) - nanoTDFData, err := s.createRealNanoTDF(sdk) - s.Require().NoError(err) - reader := bytes.NewReader(nanoTDFData) - - // Test successful load with ignore allowlist - nanoReader, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoIgnoreAllowlist(true)) - s.Require().NoError(err) - s.Require().NotNil(nanoReader) - s.Require().Equal(reader, nanoReader.reader) - s.Require().NotNil(nanoReader.config) - s.Require().True(nanoReader.config.ignoreAllowList) - s.Require().Len(nanoReader.config.fulfillableObligationFQNs, 1) - s.Require().Equal("https://example.com/obl/value/obl1", nanoReader.config.fulfillableObligationFQNs[0]) - - // Test with KAS allowlist - allowedURLs := []string{"https://kas.example.com"} - reader = bytes.NewReader(nanoTDFData) // Reset reader - nanoReader2, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoKasAllowlist(allowedURLs)) - s.Require().NoError(err) - s.Require().NotNil(nanoReader2.config.kasAllowlist) - s.Require().True(nanoReader2.config.kasAllowlist.IsAllowed("https://kas.example.com")) - - // Test with fulfillable obligations - obligations := []string{"obligation1", "obligation2"} - reader = bytes.NewReader(nanoTDFData) // Reset reader - nanoReader3, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoTDFFulfillableObligationFQNs(obligations), WithNanoIgnoreAllowlist(true)) - s.Require().NoError(err) - s.Require().Equal(obligations, nanoReader3.config.fulfillableObligationFQNs) - - // Test with invalid reader (nil) - _, err = sdk.LoadNanoTDF(s.T().Context(), nil) - s.Require().Error(err) -} - -func (s *NanoSuite) Test_NanoTDFReader_Init_WithPayloadKeySet() { - // Create a real NanoTDF for testing - sdk, err := s.createTestSDK() - s.Require().NoError(err) - nanoTDFData, err := s.createRealNanoTDF(sdk) - s.Require().NoError(err) - reader := bytes.NewReader(nanoTDFData) - nanoReader, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoIgnoreAllowlist(true)) - s.Require().NoError(err) - - // Test that calling Init twice doesn't cause issues when payloadKey is set - nanoReader.payloadKey = []byte("mock-key") - err = nanoReader.Init(s.T().Context()) - s.Require().NoError(err) // Should return early since payloadKey is set -} - -func (s *NanoSuite) Test_NanoTDFReader_Init_WithoutPayloadKeySet() { - // Create a real NanoTDF for testing - sdk, err := s.createTestSDK() - s.Require().NoError(err) - nanoTDFData, err := s.createRealNanoTDF(sdk) - s.Require().NoError(err) - reader := bytes.NewReader(nanoTDFData) - - nanoReader, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoIgnoreAllowlist(true)) - s.Require().NoError(err) - - err = nanoReader.Init(s.T().Context()) - s.Require().NoError(err) - s.Require().NotNil(nanoReader.payloadKey) -} - -func (s *NanoSuite) Test_NanoTDFReader_ObligationsSupport() { - // Create a real NanoTDF for testing - sdk, err := s.createTestSDK() - s.Require().NoError(err) - nanoTDFData, err := s.createRealNanoTDF(sdk) - s.Require().NoError(err) - reader := bytes.NewReader(nanoTDFData) - nanoReader, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoIgnoreAllowlist(true)) - s.Require().NoError(err) - s.Require().Nil(nanoReader.requiredObligations) - - // Mock some triggered obligations as would happen during rewrap - mockObligations := RequiredObligations{ - FQNs: []string{"obligation1", "obligation2"}, - } - nanoReader.requiredObligations = &mockObligations - - // Verify obligations are stored - s.Require().NotNil(nanoReader.requiredObligations) - s.Require().Len(nanoReader.requiredObligations.FQNs, 2) - s.Require().Contains(nanoReader.requiredObligations.FQNs, "obligation1") - s.Require().Contains(nanoReader.requiredObligations.FQNs, "obligation2") -} - -func (s *NanoSuite) Test_NanoTDFReader_DecryptNanoTDF() { - // Create a real NanoTDF for testing - sdk, err := s.createTestSDK() - s.Require().NoError(err) - nanoTDFData, err := s.createRealNanoTDF(sdk) - s.Require().NoError(err) - reader := bytes.NewReader(nanoTDFData) - writer := &bytes.Buffer{} - - nanoReader, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoIgnoreAllowlist(true)) - s.Require().NoError(err) - - _, err = nanoReader.DecryptNanoTDF(s.T().Context(), writer) - s.Require().NoError(err) - s.Require().Equal([]byte("Virtru!!!!"), writer.Bytes()) -} - -func (s *NanoSuite) Test_NanoTDFReader_RealWorkflow() { - // Test the complete workflow: Create -> Load -> Parse Header - originalData := []byte("This is test data for NanoTDF encryption!") - - // Step 1: Create a real NanoTDF - input := bytes.NewReader(originalData) - output := &bytes.Buffer{} - - // Create SDK with consistent mock transport - sdk, err := s.createTestSDK() - s.Require().NoError(err) - - config, err := sdk.NewNanoTDFConfig() - s.Require().NoError(err) - - err = config.SetKasURL("https://kas.example.com") - s.Require().NoError(err) - - err = config.SetAttributes([]string{"https://example.com/attr/classification/value/secret"}) - s.Require().NoError(err) - - // The kasPublicKey will be fetched automatically from the mock HTTP client during CreateNanoTDF - - // Create the NanoTDF - tdfSize, err := sdk.CreateNanoTDF(output, input, *config) - s.Require().NoError(err) - s.Require().Positive(tdfSize) - - // Step 2: Load the created NanoTDF - tdfData := output.Bytes() - reader := bytes.NewReader(tdfData) - - nanoReader, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoIgnoreAllowlist(true)) - s.Require().NoError(err) - s.Require().NotNil(nanoReader) - - // Step 3: Validate the header (it should be loaded automatically) - s.Require().NotNil(nanoReader.headerBuf) - s.Require().NotEmpty(nanoReader.headerBuf) - - // Check KAS URL - kasURL, err := nanoReader.header.kasURL.GetURL() - s.Require().NoError(err) - s.Require().Equal("https://kas.example.com", kasURL) - - // Check policy mode and other header fields - s.Require().Equal(PolicyType(2), nanoReader.header.PolicyMode) // Embedded encrypted policy - s.Require().NotNil(nanoReader.header.PolicyBody) - s.Require().NotEmpty(nanoReader.header.PolicyBody) - s.Require().NotNil(nanoReader.header.EphemeralKey) - s.Require().Len(nanoReader.header.EphemeralKey, 33) // secp256r1 compressed key - - _, err = nanoReader.Obligations(s.T().Context()) - s.Require().NoError(err) -} - -func (s *NanoSuite) Test_NanoTDF_Obligations() { - sdk, err := s.createTestSDK() - s.Require().NoError(err) - encryptedPolicyTDF, err := s.createRealNanoTDF(sdk) - s.Require().NoError(err) - - // Table-driven test for nano TDF obligations support - testCases := []struct { - name string - fulfillableObligations []string - requiredObligations []string - expectError error - populateObligations []string - }{ - { - name: "Rewrap not called prior - Call Rewrap", - expectError: nil, - requiredObligations: []string{fakeObligationFQN}, - }, - { - name: "Rewrap called - Obligations populated", - expectError: nil, - requiredObligations: []string{"https://example.com/attr/attr1/value/value1"}, - fulfillableObligations: []string{"https://example.com/attr/attr1/value/value1"}, - populateObligations: []string{"https://example.com/attr/attr1/value/value1"}, - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - reader := bytes.NewReader(encryptedPolicyTDF) - nanoReader, err := sdk.LoadNanoTDF(s.T().Context(), reader, WithNanoTDFFulfillableObligationFQNs(tc.fulfillableObligations), WithNanoIgnoreAllowlist(true)) - s.Require().NoError(err) - // Check that it has fulfillable obligations set - if len(tc.fulfillableObligations) > 0 { - s.Require().NotNil(nanoReader.config.fulfillableObligationFQNs) - s.Require().Equal(tc.fulfillableObligations, nanoReader.config.fulfillableObligationFQNs) - } else { - s.Require().Empty(nanoReader.config.fulfillableObligationFQNs) - } - - if tc.populateObligations != nil { - nanoReader.requiredObligations = &RequiredObligations{FQNs: tc.populateObligations} - } - - // Initialize the reader (this will parse the header) - obl, err := nanoReader.Obligations(s.T().Context()) - if tc.expectError != nil { - s.Require().Error(err) - s.Require().Empty(obl.FQNs) - s.Require().ErrorIs(err, tc.expectError) - return - } - s.Require().NoError(err) - s.Require().Equal(tc.requiredObligations, obl.FQNs) - - // Call again to verify caching - obl, err = nanoReader.Obligations(s.T().Context()) - s.Require().NoError(err) - s.Require().Equal(tc.requiredObligations, obl.FQNs) - }) - } -} - -func (s *NanoSuite) Test_PolicyBinding_GMAC() { - // Create test policy data - policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`) - - // Create GMAC binding - need to simulate having GMAC at end of the digest - gmacBytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} - // Append GMAC to the digest to simulate real scenario - policyData = append(policyData, gmacBytes...) - - digest := ocrypto.CalculateSHA256(policyData) - - // For testing, we will use the last bytes as the GMAC binding - gmacBytes = digest[len(digest)-len(gmacBytes):] - - binding := &gmacPolicyBinding{ - binding: gmacBytes, - digest: digest, - } - - // Test String function - s.Require().Equal(hex.EncodeToString(gmacBytes), binding.String(), "GMAC hash should return binding data directly") - - // Test Verify function - should pass with correct binding - valid, err := binding.Verify() - s.Require().NoError(err) - s.Require().True(valid, "GMAC binding should be valid when binding matches digest suffix") - - // Test Verify function with wrong binding - should fail - wrongBinding := &gmacPolicyBinding{ - binding: []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01}, - digest: digest, - } - valid, err = wrongBinding.Verify() - s.Require().NoError(err) - s.Require().False(valid, "GMAC binding should be invalid when binding doesn't match digest suffix") -} - -func (s *NanoSuite) Test_PolicyBinding_ECDSA() { - // Create a test ECDSA key pair - keyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) - s.Require().NoError(err) - - // Create test policy data - policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`) - digest := ocrypto.CalculateSHA256(policyData) - - // Sign the digest - r, sBytes, err := ocrypto.ComputeECDSASig(digest, keyPair.PrivateKey) - s.Require().NoError(err) - - // Get the public key in compressed format - compressedPubKey, err := ocrypto.CompressedECPublicKey(ocrypto.ECCModeSecp256r1, keyPair.PrivateKey.PublicKey) - s.Require().NoError(err) - - binding := &ecdsaPolicyBinding{ - r: r, - s: sBytes, - ephemeralPubKey: compressedPubKey, - digest: digest, - curve: keyPair.PrivateKey.Curve, - } - - // Test String function - expectedHash := string(ocrypto.SHA256AsHex(append(r, sBytes...))) - s.Require().NotEmpty(binding.String(), "Hash should not be empty") - s.Require().Equal(expectedHash, binding.String(), "ECDSA hash should be SHA256 of r||s") - - // Test Verify function - should pass with correct signature - valid, err := binding.Verify() - s.Require().NoError(err) - s.Require().True(valid, "ECDSA binding should be valid with correct signature") - - // Test Verify function with wrong signature - should fail - invalidR := make([]byte, 32) - invalidS := make([]byte, 32) - for i := range invalidR { - invalidR[i] = byte(i) - invalidS[i] = byte(i + 10) - } - - wrongBinding := &ecdsaPolicyBinding{ - r: invalidR, - s: invalidS, - ephemeralPubKey: compressedPubKey, - digest: digest, - curve: keyPair.PrivateKey.Curve, - } - - valid, err = wrongBinding.Verify() - s.Require().NoError(err) - s.Require().False(valid, "ECDSA binding should be invalid with wrong signature") -} - -func (s *NanoSuite) Test_NanoTDFHeader_VerifyPolicyBinding() { - s.Run("ECDSA Policy Binding Verification", func() { - // Create a test ECDSA key pair - keyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) - s.Require().NoError(err) - - // Create test policy data - policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`) - digest := ocrypto.CalculateSHA256(policyData) - - // Sign the digest - r, sBytes, err := ocrypto.ComputeECDSASig(digest, keyPair.PrivateKey) - s.Require().NoError(err) - - // Get compressed public key - compressedPubKey, err := ocrypto.CompressedECPublicKey(ocrypto.ECCModeSecp256r1, keyPair.PrivateKey.PublicKey) - s.Require().NoError(err) - - // Create header with ECDSA binding - header := &NanoTDFHeader{ - bindCfg: bindingConfig{ - useEcdsaBinding: true, - eccMode: ocrypto.ECCModeSecp256r1, - }, - PolicyBody: policyData, - EphemeralKey: compressedPubKey, - ecdsaPolicyBindingR: r, - ecdsaPolicyBindingS: sBytes, - } - - // Test VerifyPolicyBinding method - valid, err := header.VerifyPolicyBinding() - s.Require().NoError(err) - s.Require().True(valid, "ECDSA policy binding should be valid") - }) - - s.Run("GMAC Policy Binding Verification", func() { - // Create test policy data - policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`) - - // Create GMAC binding - need to simulate having GMAC at end of the digest - gmacBytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} - // Append GMAC to the digest to simulate real scenario - policyData = append(policyData, gmacBytes...) - - digest := ocrypto.CalculateSHA256(policyData) - - // For testing, we will use the last bytes as the GMAC binding - gmacBytes = digest[len(digest)-len(gmacBytes):] - - // Create header with GMAC binding - header := &NanoTDFHeader{ - bindCfg: bindingConfig{ - useEcdsaBinding: false, - }, - PolicyBody: policyData, - gmacPolicyBinding: gmacBytes, - } - - // Test VerifyPolicyBinding method - valid, err := header.VerifyPolicyBinding() - s.Require().NoError(err) - s.Require().True(valid, "GMAC hash should match") - }) - - s.Run("Policy Binding Creation Error", func() { - // Create header with invalid ECC mode to trigger error in PolicyBinding() - header := &NanoTDFHeader{ - bindCfg: bindingConfig{ - useEcdsaBinding: true, - eccMode: 255, // Invalid ECC mode - }, - PolicyBody: []byte("test"), - } - - // Test VerifyPolicyBinding method with error case - valid, err := header.VerifyPolicyBinding() - s.Require().Error(err) - s.Require().False(valid) - s.Require().Contains(err.Error(), "unsupported nanoTDF ecc mode", "Error should be related to curve/ECC mode") - }) -} - -// Helper function to create real NanoTDF data for testing -func (s *NanoSuite) createRealNanoTDF(sdk *SDK) ([]byte, error) { - // Read the test file content - input := bytes.NewReader([]byte("Virtru!!!!")) - output := &bytes.Buffer{} - - // Create a NanoTDF config - config, err := sdk.NewNanoTDFConfig() - if err != nil { - return nil, err - } - - // Set a test KAS URL - err = config.SetKasURL("https://kas.example.com") - if err != nil { - return nil, err - } - - // Set test attributes - err = config.SetAttributes([]string{"https://example.com/attr/attr1/value/value1"}) - if err != nil { - return nil, err - } - - err = config.SetPolicyMode(NanoTDFPolicyModeDefault) - if err != nil { - return nil, err - } - - // The kasPublicKey will be fetched automatically from the mock HTTP client during CreateNanoTDF - - // Create the NanoTDF - _, err = sdk.CreateNanoTDF(output, input, *config) - if err != nil { - return nil, err - } - - return output.Bytes(), nil -} - -func (s *NanoSuite) createMockHTTPClient() *http.Client { - return &http.Client{ - Transport: s.mockTransport, - } -} - -// Helper function to create a properly configured SDK for testing -func (s *NanoSuite) createTestSDK() (*SDK, error) { - sdk, err := New("http://localhost:8080", WithPlatformConfiguration(PlatformConfiguration{})) - if err != nil { - return nil, err - } - - sdk.conn.Client = s.createMockHTTPClient() - sdk.conn.Options = []connect.ClientOption{connect.WithProtoJSON()} - sdk.tokenSource = getTokenSource(s.T()) - - return sdk, nil -} - -func newMockTransport() *mockTransport { - // Generate a consistent KAS key pair for the mock - kasKeyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) - if err != nil { - panic(fmt.Sprintf("Failed to generate KAS key pair: %v", err)) - } - - publicKeyPEM, err := kasKeyPair.PublicKeyInPemFormat() - if err != nil { - panic(fmt.Sprintf("Failed to get public key PEM: %v", err)) - } - - return &mockTransport{ - publicKey: publicKeyPEM, - kid: "e1", - kasKeyPair: kasKeyPair, - } -} - -func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // Check if this is a PublicKey request to KAS - if strings.Contains(req.URL.Path, "/kas.AccessService/PublicKey") { - // Create a mock PublicKeyResponse in the format expected by Connect RPC - response := &kas.PublicKeyResponse{ - PublicKey: m.publicKey, - Kid: m.kid, - } - - // Marshal the response to JSON using Connect protocol format - responseJSON, err := json.Marshal(response) - if err != nil { - return nil, fmt.Errorf("failed to marshal mock response: %w", err) - } - - // Create a mock HTTP response - resp := &http.Response{ - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - Header: http.Header{ - "Content-Type": []string{"application/json"}, - }, - Body: io.NopCloser(bytes.NewReader(responseJSON)), - ContentLength: int64(len(responseJSON)), - Request: req, - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, - } - - return resp, nil - } - - // Check if this is a Rewrap request to KAS - if strings.Contains(req.URL.Path, "/kas.AccessService/Rewrap") { - return m.handleRewrapRequest(req) - } - - // For any other requests, return an error - return nil, fmt.Errorf("unexpected request to %s", req.URL.String()) -} - -// handleRewrapRequest handles mock rewrap requests for testing -func (m *mockTransport) handleRewrapRequest(req *http.Request) (*http.Response, error) { - // Read the request body - bodyBytes, err := io.ReadAll(req.Body) - if err != nil { - return nil, fmt.Errorf("failed to read request body: %w", err) - } - - // Parse the Connect RPC request - var bodyJSON map[string]interface{} - err = json.Unmarshal(bodyBytes, &bodyJSON) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal request body: %w", err) - } - - // Extract the signed request token - signedRequestToken, ok := bodyJSON["signedRequestToken"].(string) - if !ok { - return nil, errors.New("missing signedRequestToken in request") - } - - // Parse the JWT token without verification (for testing) - token, err := jwt.ParseString(signedRequestToken, jwt.WithVerify(false)) - if err != nil { - return nil, fmt.Errorf("failed to parse JWT token: %w", err) - } - - // Extract the request body from the JWT - requestBodyClaim, ok := token.Get("requestBody") - if !ok { - return nil, errors.New("missing requestBody in JWT") - } - - requestBodyJSON, ok := requestBodyClaim.(string) - if !ok { - return nil, errors.New("requestBody is not a string") - } - - // Parse the unsigned rewrap request - var unsignedReq kas.UnsignedRewrapRequest - err = protojson.Unmarshal([]byte(requestBodyJSON), &unsignedReq) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal unsigned request: %w", err) - } - - // Get the client's public key (for the rewrap session key) - clientPublicKeyPEM := unsignedReq.GetClientPublicKey() - - // Extract the NanoTDF header from the KeyAccessObject to get the ephemeral public key - if len(unsignedReq.GetRequests()) == 0 || len(unsignedReq.GetRequests()[0].GetKeyAccessObjects()) == 0 { - return nil, errors.New("no key access objects in request") - } - - headerBuf := unsignedReq.GetRequests()[0].GetKeyAccessObjects()[0].GetKeyAccessObject().GetHeader() - if len(headerBuf) == 0 { - return nil, errors.New("no header in key access object") - } - - // Parse the NanoTDF header to extract the ephemeral public key - headerReader := bytes.NewReader(headerBuf) - nanoHeader, _, err := NewNanoTDFHeaderFromReader(headerReader) - if err != nil { - return nil, fmt.Errorf("failed to parse NanoTDF header: %w", err) - } - - // Get the KAS private key for ECDH computation - kasPrivateKeyForECDH, err := m.kasKeyPair.PrivateKeyInPemFormat() - if err != nil { - return nil, fmt.Errorf("failed to get KAS private key: %w", err) - } - - // Convert ephemeral public key to PEM format for ECDH computation - curve, err := nanoHeader.ECCurve() - if err != nil { - return nil, fmt.Errorf("failed to get ECC curve: %w", err) - } - - ephemeralPublicKey, err := ocrypto.UncompressECPubKey(curve, nanoHeader.EphemeralKey) - if err != nil { - return nil, fmt.Errorf("failed to uncompress ephemeral public key: %w", err) - } - - // Convert to PEM format using the same method as the real KAS service - derBytes, err := x509.MarshalPKIXPublicKey(ephemeralPublicKey) - if err != nil { - return nil, fmt.Errorf("failed to marshal ECDSA public key: %w", err) - } - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: derBytes, - } - ephemeralPublicKeyPEM := pem.EncodeToMemory(pemBlock) - - // Compute ECDH shared secret between KAS private key and ephemeral public key - // This recreates the symmetric key that was used during NanoTDF creation - ecdhSharedSecret, err := ocrypto.ComputeECDHKey([]byte(kasPrivateKeyForECDH), ephemeralPublicKeyPEM) - if err != nil { - return nil, fmt.Errorf("failed to compute ECDH shared secret: %w", err) - } - - // Derive the symmetric key using the same process as createNanoTDFSymmetricKey - originalSymmetricKey, err := ocrypto.CalculateHKDF(versionSalt(), ecdhSharedSecret) - if err != nil { - return nil, fmt.Errorf("failed to derive symmetric key: %w", err) - } - - // Now generate a new ephemeral key pair for the rewrap session - rewrapKasKeyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) - if err != nil { - return nil, fmt.Errorf("failed to generate rewrap KAS key pair: %w", err) - } - - rewrapKasPublicKeyPEM, err := rewrapKasKeyPair.PublicKeyInPemFormat() - if err != nil { - return nil, fmt.Errorf("failed to get rewrap KAS public key PEM: %w", err) - } - - rewrapKasPrivateKeyPEM, err := rewrapKasKeyPair.PrivateKeyInPemFormat() - if err != nil { - return nil, fmt.Errorf("failed to get rewrap KAS private key PEM: %w", err) - } - - // Compute ECDH shared secret between client's rewrap public key and new KAS ephemeral private key - rewrapEcdhKey, err := ocrypto.ComputeECDHKey([]byte(rewrapKasPrivateKeyPEM), []byte(clientPublicKeyPEM)) - if err != nil { - return nil, fmt.Errorf("failed to compute rewrap ECDH key: %w", err) - } - - // Derive session key using HKDF with version salt - sessionKey, err := ocrypto.CalculateHKDF(versionSalt(), rewrapEcdhKey) - if err != nil { - return nil, fmt.Errorf("failed to calculate rewrap session key: %w", err) - } - - // Create AES-GCM encryptor with session key - encryptor, err := ocrypto.NewAESGcm(sessionKey) - if err != nil { - return nil, fmt.Errorf("failed to create AES-GCM encryptor: %w", err) - } - - // Encrypt the original symmetric key with the rewrap session key - wrappedKey, err := encryptor.Encrypt(originalSymmetricKey) - if err != nil { - return nil, fmt.Errorf("failed to encrypt symmetric key: %w", err) - } - - // Build the rewrap response - rewrapResponse := &kas.RewrapResponse{ - SessionPublicKey: rewrapKasPublicKeyPEM, - Responses: []*kas.PolicyRewrapResult{ - { - PolicyId: "policy", - Results: []*kas.KeyAccessRewrapResult{ - { - KeyAccessObjectId: "kao-0", - Status: "permit", - Result: &kas.KeyAccessRewrapResult_KasWrappedKey{ - KasWrappedKey: wrappedKey, - }, - Metadata: createMetadataWithObligations([]string{fakeObligationFQN}), - }, - }, - }, - }, - } - - // Marshal the response - responseJSON, err := protojson.Marshal(rewrapResponse) - if err != nil { - return nil, fmt.Errorf("failed to marshal rewrap response: %w", err) - } - - // Create HTTP response - resp := &http.Response{ - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - Header: http.Header{ - "Content-Type": []string{"application/json"}, - }, - Body: io.NopCloser(bytes.NewReader(responseJSON)), - ContentLength: int64(len(responseJSON)), - Request: req, - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, - } - - return resp, nil -} diff --git a/sdk/nanotdfspec.ntdf b/sdk/nanotdfspec.ntdf deleted file mode 100644 index b2ab8b8271..0000000000 Binary files a/sdk/nanotdfspec.ntdf and /dev/null differ diff --git a/sdk/options.go b/sdk/options.go index df36a7a9f1..ba63bb092a 100644 --- a/sdk/options.go +++ b/sdk/options.go @@ -37,12 +37,10 @@ type config struct { dpopKey *ocrypto.RsaKeyPair ipc bool tdfFeatures tdfFeatures - nanoFeatures nanoFeatures customAccessTokenSource auth.AccessTokenSource oauthAccessTokenSource oauth2.TokenSource coreConn *ConnectRPCConnection entityResolutionConn *ConnectRPCConnection - collectionStore *collectionStore shouldValidatePlatformConnectivity bool fulfillableObligationFQNs []string logger *slog.Logger @@ -54,12 +52,6 @@ type tdfFeatures struct { noKID bool } -// Options specific to NanoTDF protocol features -type nanoFeatures struct { - // noKID For backward compatibility, don't store the KID in the KAS ResourceLocator. - noKID bool -} - type PlatformConfiguration map[string]interface{} // WithInsecureSkipVerifyConn returns an Option that sets up HTTPS connection without verification. @@ -73,13 +65,6 @@ func WithInsecureSkipVerifyConn() Option { } } -// WithStoreCollectionHeaders Experimental: returns an Option that sets up storing dataset keys for nTDFs -func WithStoreCollectionHeaders() Option { - return func(c *config) { - c.collectionStore = newCollectionStore(kDefaultExpirationTime, kDefaultCleaningInterval) - } -} - // WithInsecurePlaintextConn returns an Option that sets up HTTP connection sent in the clear. func WithInsecurePlaintextConn() Option { return func(c *config) { @@ -104,6 +89,7 @@ func WithTLSCredentials(tls *tls.Config, audience []string) Option { } // WithTokenEndpoint When we implement service discovery using a .well-known endpoint this option may become deprecated + // Deprecated: SDK will discover the token endpoint from the platform configuration func WithTokenEndpoint(tokenEndpoint string) Option { return func(c *config) { @@ -189,7 +175,10 @@ func WithPlatformConfiguration(platformConfiguration PlatformConfiguration) Opti } } -// WithConnectionValidation will validate connection to a healthy, running platform +// WithConnectionValidation will validate connection to a healthy, running platform at +// SDK construction time. For runtime readiness probes (e.g. Kubernetes liveness/readiness +// endpoints), use SDK.IsHealthy(ctx) instead — it honors ctx for deadlines and tracing +// and does not fail-fast at construction. func WithConnectionValidation() Option { return func(c *config) { c.shouldValidatePlatformConnectivity = true @@ -226,14 +215,6 @@ func WithExtraClientOptions(opts ...connect.ClientOption) Option { } } -// WithNoKIDInNano disables storing the KID in the KAS ResourceLocator. -// This allows generating NanoTDF files that are compatible with legacy file formats (no KID). -func WithNoKIDInNano() Option { - return func(c *config) { - c.nanoFeatures.noKID = true - } -} - // WithFulfillableObligationFQNs sets the list of obligation FQNs that can func WithFulfillableObligationFQNs(fqns []string) Option { return func(c *config) { diff --git a/sdk/options_test.go b/sdk/options_test.go deleted file mode 100644 index 44b957e87f..0000000000 --- a/sdk/options_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package sdk - -import ( - "testing" -) - -func TestWithKIDInNano(t *testing.T) { - tests := []struct { - name string - kid bool - want bool - }{ - { - name: "noKID to be true", - kid: false, - want: true, - }, - { - name: "noKID to be false", - kid: true, - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &config{} - - if !tt.kid { - option := WithNoKIDInNano() - option(c) - } - - if c.nanoFeatures.noKID != tt.want { - t.Errorf("WithKIDInNano() = %v, want %v", c.nanoFeatures.noKID, tt.want) - } - }) - } -} diff --git a/sdk/readme_test.go b/sdk/readme_test.go new file mode 100644 index 0000000000..45f649e456 --- /dev/null +++ b/sdk/readme_test.go @@ -0,0 +1,157 @@ +package sdk_test + +import ( + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// TestREADMECodeBlocks verifies that all Go code blocks in the README compile successfully. +// This ensures the documentation stays accurate and up-to-date with the actual API. +func TestREADMECodeBlocks(t *testing.T) { + // Read the README file + readmePath := filepath.Join("..", "sdk", "README.md") + content, err := os.ReadFile(readmePath) + if err != nil { + t.Fatalf("Failed to read README.md: %v", err) + } + + // Extract Go code blocks + codeBlocks := extractGoCodeBlocks(string(content)) + if len(codeBlocks) == 0 { + t.Fatal("No Go code blocks found in README.md") + } + + t.Logf("Found %d Go code block(s) in README.md", len(codeBlocks)) + + // Test each code block that is a complete program + testedCount := 0 + for i, code := range codeBlocks { + // Only test complete programs (those with package main) + if !strings.Contains(code, "package main") { + t.Logf("Skipping code block %d (not a complete program)", i+1) + continue + } + + testedCount++ + t.Run(formatBlockName(i, code), func(t *testing.T) { + if err := testCodeBlock(t, code); err != nil { + t.Errorf("Code block %d failed to compile:\n%v", i+1, err) + } + }) + } + + if testedCount == 0 { + t.Fatal("No complete program code blocks found in README.md") + } + t.Logf("Tested %d complete program(s)", testedCount) +} + +// extractGoCodeBlocks finds all Go code blocks in the markdown content. +func extractGoCodeBlocks(content string) []string { + // Match code blocks that start with ```go and end with ``` + re := regexp.MustCompile("(?s)```go\n(.*?)```") + matches := re.FindAllStringSubmatch(content, -1) + + blocks := make([]string, 0, len(matches)) + for _, match := range matches { + if len(match) > 1 { + blocks = append(blocks, match[1]) + } + } + return blocks +} + +// formatBlockName creates a readable test name from the code block. +func formatBlockName(index int, code string) string { + lines := strings.Split(strings.TrimSpace(code), "\n") + if len(lines) == 0 { + return "empty_block" + } + + // Try to find a meaningful identifier in the first few lines + for _, line := range lines[:minInt(5, len(lines))] { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "package ") { + return strings.TrimPrefix(line, "package ") + } + if strings.HasPrefix(line, "func ") { + return strings.Fields(line)[1] + } + } + + return "code_block_" + string(rune('A'+index)) +} + +// testCodeBlock attempts to compile a code block. +func testCodeBlock(t *testing.T, code string) error { + // Create a temporary directory + tmpDir := t.TempDir() + + // Write the code to main.go + mainPath := filepath.Join(tmpDir, "main.go") + if err := os.WriteFile(mainPath, []byte(code), 0o644); err != nil { + return err + } + + // Initialize go module + cmd := exec.Command("go", "mod", "init", "example") + cmd.Dir = tmpDir + if output, err := cmd.CombinedOutput(); err != nil { + t.Logf("go mod init output: %s", output) + return err + } + + // Get the absolute path to the platform directory + // When running from sdk directory, we need to go up one level + platformDir, err := filepath.Abs("..") + if err != nil { + return err + } + + // Add replace directives for local modules + replacements := []string{ + "github.com/opentdf/platform/sdk=" + filepath.Join(platformDir, "sdk"), + "github.com/opentdf/platform/lib/ocrypto=" + filepath.Join(platformDir, "lib/ocrypto"), + "github.com/opentdf/platform/protocol/go=" + filepath.Join(platformDir, "protocol/go"), + } + + for _, replace := range replacements { + editCmd := exec.Command("go", "mod", "edit", "-replace", replace) + editCmd.Dir = tmpDir + if output, err := editCmd.CombinedOutput(); err != nil { + t.Logf("go mod edit output: %s", output) + return err + } + } + + // Run go mod tidy + cmd = exec.Command("go", "mod", "tidy") + cmd.Dir = tmpDir + if output, err := cmd.CombinedOutput(); err != nil { + t.Logf("go mod tidy output: %s", output) + return err + } + + // Attempt to build + cmd = exec.Command("go", "build", "-o", "/dev/null", "main.go") + cmd.Dir = tmpDir + output, err := cmd.CombinedOutput() + if err != nil { + t.Logf("Build output:\n%s", output) + return err + } + + t.Logf("Code block compiled successfully") + return nil +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/sdk/resource_locator.go b/sdk/resource_locator.go index 83f0d6c077..39c0b99424 100644 --- a/sdk/resource_locator.go +++ b/sdk/resource_locator.go @@ -10,7 +10,7 @@ import ( ) // ============================================================================================================ -// Support for serializing/deserializing URLS for nano usage +// Support for serializing/deserializing URLs in the compact/binary encoding // // If an URL is specified as "https://some.site.com/endpoint" // the storage format for this is to strip off the leading "https://" prefix and encode as 0 (or 1 for http) @@ -266,11 +266,6 @@ func (rl ResourceLocator) writeResourceLocator(writer io.Writer) error { return nil } -// getLength - return the serialized length (in bytes) of this object -func (rl ResourceLocator) getLength() uint16 { - return uint16(1 /* protocol byte */ + 1 /* length byte */ + len(rl.body) + len(rl.identifier)) -} - // setURLWithIdentifier - Store a fully qualified protocol+body string and an identifier into a ResourceLocator. func (rl *ResourceLocator) setURLWithIdentifier(url string, identifier string) error { if identifier == "" { diff --git a/sdk/schema/manifest-lax.schema.json b/sdk/schema/manifest-lax.schema.json index a31abd75fb..2d9c730279 100644 --- a/sdk/schema/manifest-lax.schema.json +++ b/sdk/schema/manifest-lax.schema.json @@ -52,7 +52,7 @@ "type": { "description": "The type of key access object.", "type": "string", - "enum": ["wrapped", "remote"] + "enum": ["wrapped", "ec-wrapped", "hybrid-wrapped", "remote"] }, "url": { "description": "A fully qualified URL pointing to a key access service responsible for managing access to the encryption keys.", diff --git a/sdk/schema/manifest.schema.json b/sdk/schema/manifest.schema.json index 6488623fa2..8a0e47ac94 100644 --- a/sdk/schema/manifest.schema.json +++ b/sdk/schema/manifest.schema.json @@ -52,7 +52,7 @@ "type": { "description": "The type of key access object.", "type": "string", - "enum": ["ec-wrapped", "remote", "wrapped"] + "enum": ["ec-wrapped", "hybrid-wrapped", "remote", "wrapped"] }, "url": { "description": "A fully qualified URL pointing to a key access service responsible for managing access to the encryption keys.", @@ -239,4 +239,4 @@ } }, "required": ["payload", "encryptionInformation"] - } \ No newline at end of file + } diff --git a/sdk/sdk.go b/sdk/sdk.go index 55545408b6..fa809e6e12 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -23,7 +23,7 @@ import ( "github.com/opentdf/platform/sdk/audit" "github.com/opentdf/platform/sdk/auth" "github.com/opentdf/platform/sdk/httputil" - "github.com/opentdf/platform/sdk/internal/archive" + "github.com/opentdf/platform/sdk/internal/zipstream" "github.com/opentdf/platform/sdk/sdkconnect" "github.com/xeipuuv/gojsonschema" healthpb "google.golang.org/grpc/health/grpc_health_v1" @@ -32,9 +32,12 @@ import ( const ( // Failure while connecting to a service. // Check your configuration and/or retry. - ErrGrpcDialFailed = Error("failed to dial grpc endpoint") - ErrShutdownFailed = Error("failed to shutdown sdk") - ErrPlatformUnreachable = Error("platform unreachable or not responding") + ErrGrpcDialFailed = Error("failed to dial grpc endpoint") + ErrShutdownFailed = Error("failed to shutdown sdk") + ErrPlatformUnreachable = Error("platform unreachable or not responding") + // ErrHealthCheckUnsupported is returned by SDK.IsHealthy when the SDK is configured + // in IPC mode, which does not support the gRPC Health protocol. + ErrHealthCheckUnsupported = Error("health check not supported in IPC mode") ErrPlatformConfigFailed = Error("failed to retrieve platform configuration") ErrPlatformEndpointMalformed = Error("platform endpoint is malformed") ErrPlatformIssuerNotFound = Error("issuer not found in well-known idp configuration") @@ -43,6 +46,7 @@ const ( ErrPlatformEndpointNotFound = Error("platform_endpoint not found in well-known configuration") ErrAccessTokenInvalid = Error("access token is invalid") ErrWellKnowConfigEmpty = Error("well-known configuration is empty") + ErrAttributeNotFound = Error("attribute not found") ) var ( @@ -78,7 +82,6 @@ func setPackageLogger(logger *slog.Logger) { type SDK struct { config *kasKeyCache - *collectionStore conn *ConnectRPCConnection tokenSource auth.AccessTokenSource Actions sdkconnect.ActionServiceClient @@ -216,7 +219,6 @@ func New(platformEndpoint string, opts ...Option) (*SDK, error) { return &SDK{ config: *cfg, - collectionStore: cfg.collectionStore, kasKeyCache: newKasKeyCache(), conn: &ConnectRPCConnection{Client: platformConn.Client, Endpoint: platformConn.Endpoint, Options: platformConn.Options}, tokenSource: accessTokenSource, @@ -298,9 +300,6 @@ func buildIDPTokenSource(c *config) (auth.AccessTokenSource, error) { } func (s SDK) Close() error { - if s.collectionStore != nil { - s.close() - } return nil } @@ -314,11 +313,30 @@ func (s SDK) Conn() *ConnectRPCConnection { return s.conn } +// IsHealthy reports whether the platform's gRPC Health v1 endpoint is reachable and SERVING. +// The check honors ctx for deadline and cancellation; OTEL tracing works automatically when +// otelconnect.NewInterceptor is registered via WithExtraClientOptions at SDK construction. +// +// Returns: +// - (true, nil) when the platform reports SERVING. +// - (false, nil) when the platform is reachable but reports NOT_SERVING or UNKNOWN. +// - (false, ErrHealthCheckUnsupported) when the SDK is configured in IPC mode. +// - (false, error) wrapping ErrPlatformUnreachable on transport failure or ctx errors. +func (s SDK) IsHealthy(ctx context.Context) (bool, error) { + if s.ipc || s.conn == nil { + return false, ErrHealthCheckUnsupported + } + healthy, err := checkPlatformHealth(ctx, s.conn.Endpoint, s.conn.Client, s.conn.Options) + if err != nil { + return false, errors.Join(ErrPlatformUnreachable, err) + } + return healthy, nil +} + type TdfType string const ( Invalid TdfType = "Invalid" - Nano TdfType = "Nano" Standard TdfType = "Standard" ) @@ -327,12 +345,8 @@ func (t TdfType) String() string { return string(t) } -var ( - // ZIP file Signature - zipSignature = []byte{0x50, 0x4B, 0x03, 0x04} - // Nano TDF Signature - nanoSignature = []byte{0x4C, 0x31, 0x4C} -) +// ZIP file Signature +var zipSignature = []byte{0x50, 0x4B, 0x03, 0x04} // GetTdfType returns the type of TDF based on the reader. // Reader is reset after the check. @@ -359,11 +373,6 @@ func GetTdfType(reader io.ReadSeeker) TdfType { return Standard } - // Check if the first 3 bytes match the Nano signature - if bytes.Equal(buffer[:3], nanoSignature) { - return Nano - } - return Invalid } @@ -387,9 +396,9 @@ var manifestStrictSchema []byte // to validate against all previously known schema versions. func IsValidTdf(reader io.ReadSeeker) (bool, error) { // create tdf reader - tdfReader, err := archive.NewTDFReader(reader) + tdfReader, err := zipstream.NewTDFReader(reader) if err != nil { - return false, fmt.Errorf("archive.NewTDFReader failed: %w", err) + return false, fmt.Errorf("zipstream.NewTDFReader failed: %w", err) } manifest, err := tdfReader.Manifest() @@ -427,29 +436,42 @@ func isValidManifest(manifest string, intensity SchemaValidationIntensity) (bool return true, nil } -// IsValidNanoTdf detects whether, or not the reader is a valid Nano TDF. -// Reader is reset after the check. -func IsValidNanoTdf(reader io.ReadSeeker) (bool, error) { - _, _, err := NewNanoTDFHeaderFromReader(reader) - _, _ = reader.Seek(0, io.SeekStart) // Ignore the error as we're just checking if it's a valid nano TDF - return err == nil, err -} - -// Test connectability to the platform and validate a healthy status -func validateHealthyPlatformConnection(platformEndpoint string, httpClient *http.Client, options []connect.ClientOption) error { +// checkPlatformHealth issues a single gRPC Health v1 Check against the platform endpoint and +// reports whether the response is SERVING. ctx controls deadline, cancellation, and trace-context. +func checkPlatformHealth( + ctx context.Context, + endpoint string, + httpClient *http.Client, + options []connect.ClientOption, +) (bool, error) { + checkURL, err := url.JoinPath(endpoint, "grpc.health.v1.Health", "Check") + if err != nil { + return false, err + } healthClient := connect.NewClient[healthpb.HealthCheckRequest, healthpb.HealthCheckResponse]( httpClient, - platformEndpoint+"/grpc.health.v1.Health/Check", + checkURL, options..., ) - res, err := healthClient.CallUnary( - context.Background(), - connect.NewRequest(&healthpb.HealthCheckRequest{}), - ) - if err != nil || res.Msg.GetStatus() != healthpb.HealthCheckResponse_SERVING { - return errors.Join(ErrPlatformUnreachable, err) + res, err := healthClient.CallUnary(ctx, connect.NewRequest(&healthpb.HealthCheckRequest{})) + if err != nil { + return false, err } + return res.Msg.GetStatus() == healthpb.HealthCheckResponse_SERVING, nil +} +// validateHealthyPlatformConnection is the construction-time reachability gate used by New when +// WithConnectionValidation is set. Callers pass cfg.extraClientOptions (pre-auth) because the +// auth and audit interceptors are assembled after this gate fires; the runtime SDK.IsHealthy +// method uses the post-interceptor s.conn.Options instead. +func validateHealthyPlatformConnection(platformEndpoint string, httpClient *http.Client, options []connect.ClientOption) error { + healthy, err := checkPlatformHealth(context.Background(), platformEndpoint, httpClient, options) + if err != nil { + return errors.Join(ErrPlatformUnreachable, err) + } + if !healthy { + return ErrPlatformUnreachable + } return nil } @@ -475,7 +497,10 @@ func getTokenEndpoint(c config) (string, error) { return "", errors.New("platform_issuer is not set, or is not a string") } - oidcConfigURL := issuerURL + "/.well-known/openid-configuration" + oidcConfigURL, err := url.JoinPath(issuerURL, ".well-known/openid-configuration") + if err != nil { + return "", fmt.Errorf("invalid issuer URL %q: %w", issuerURL, err) + } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, oidcConfigURL, nil) if err != nil { @@ -517,7 +542,7 @@ func getTokenEndpoint(c config) (string, error) { // so only store the most recent known key per url & algorithm pair. func (s *SDK) StoreKASKeys(url string, keys *policy.KasPublicKeySet) error { for _, key := range keys.GetKeys() { - s.kasKeyCache.store(KASInfo{ + s.store(KASInfo{ URL: url, PublicKey: key.GetPem(), KID: key.GetKid(), diff --git a/sdk/sdk_test.go b/sdk/sdk_test.go index c270fc3098..810dede16c 100644 --- a/sdk/sdk_test.go +++ b/sdk/sdk_test.go @@ -2,10 +2,15 @@ package sdk_test import ( "bytes" + "context" "encoding/base64" + "net/http" + "net/http/httptest" "reflect" "testing" + "time" + "connectrpc.com/grpchealth" "github.com/opentdf/platform/protocol/go/policy/attributes/attributesconnect" "github.com/opentdf/platform/protocol/go/policy/kasregistry/kasregistryconnect" "github.com/opentdf/platform/protocol/go/policy/resourcemapping/resourcemappingconnect" @@ -112,37 +117,6 @@ func Test_ShouldCreateNewSDK_NoCredentials(t *testing.T) { assert.NotNil(t, s) } -func TestNew_ShouldValidateGoodNanoTdf(t *testing.T) { - goodNanoTdfStr := "TDFMABJsb2NhbGhvc3Q6ODA4MC9rYXOAAQIA2qvjMRfg7b27lT2kf9SwHRkDIg8ZXtfRoiIvdMUHq/gL5AUMfmv4Di8sKCyLkmUm/WITVj5hDeV/z4JmQ0JL7ZxqSmgZoK6TAHvkKhUly4zMEWMRXH8IktKhFKy1+fD+3qwDopqWAO5Nm2nYQqi75atEFckstulpNKg3N+Ul22OHr/ZuR127oPObBDYNRfktBdzoZbEQcPlr8q1B57q6y5SPZFjEzL9weK+uS5bUJWkF3nsHASo2bZw7IPhTZxoFVmCDjwvj6MbxNa7zG6aClHJ162zKxLLnD9TtIHuZ59R7LgiSieipXeExj+ky9OgIw5DfwyUuxsQLtKpMIAFPmLY9Hy2naUJxke0MT1EUBgastCq+YtFGslV9LJo/A8FtrRqludwtM0O+Z9FlAkZ1oNL7M7uOkLrh7eRrv+C1AAAX6FaBQoOtqnmyu6Jp+VzkxDddEeLRUyI=" - goodDecodedData, err := base64.StdEncoding.DecodeString(goodNanoTdfStr) - in := bytes.NewReader(goodDecodedData) - - require.NoError(t, err) - // Decode the base64 string - isValid, err := sdk.IsValidNanoTdf(in) - require.NoError(t, err) - - assert.True(t, isValid) - - // Try again to see if the reader has been reset - isValid, err = sdk.IsValidNanoTdf(in) - require.NoError(t, err) - - assert.True(t, isValid) -} - -func TestNew_ShouldNotValidateBadNanoTdf(t *testing.T) { - badNanoTdfStr := "TDFMABfg7b27lT2kf9SwHRkDIg8ZXtfRoiIvdMUHq/gL5AUMfmv4Di8sKCyLkmUm/WITVj5hDeV/z4JmQ0JL7ZxqSmgZoK6TAHvkKhUly4zMEWMRXH8IktKhFKy1+fD+3qwDopqWAO5Nm2nYQqi75atEFckstulpNKg3N+Ul22OHr/ZuR127oPObBDYNRfktBdzoZbEQcPlr8q1B57q6y5SPZFjEzL9weK+uS5bUJWkF3nsHASo2bZw7IPhTZxoFVmCDjwvj6MbxNa7zG6aClHJ162zKxLLnD9TtIHuZ59R7LgiSieipXeExj+ky9OgIw5DfwyUuxsQLtKpMIAFPmLY9Hy2naUJxke0MT1EUBgastCq+YtFGslV9LJo/A8FtrRqludwtM0O+Z9FlAkZ1oNL7M7uOkLrh7eRrv+C1AAAX6FaBQoOtqnmyu6Jp+VzkxDddEeLRUyI=" - badDecodedData, err := base64.StdEncoding.DecodeString(badNanoTdfStr) - in := bytes.NewReader(badDecodedData) - - require.NoError(t, err) - // Decode the base64 string - isValid, _ := sdk.IsValidNanoTdf(in) - // Error is ok here, as it acts as a sort of reason for the nanotdf not being valid - assert.False(t, isValid) -} - func TestNew_ShouldValidateStandardTdf(t *testing.T) { goodStandardTdf := "UEsDBC0ACAAAAJ2TFTEAAAAAAAAAAAAAAAAJAAAAMC5wYXlsb2Fktu4m+vdwl0mtjhY3U5e7TG2o1s8ifK+RAhFNjRjGTLJ7V3w5UEsHCGiY7skkAAAAJAAAAFBLAwQtAAgAAACdkxUxAAAAAAAAAAAAAAAADwAAADAubWFuaWZlc3QuanNvbnsiZW5jcnlwdGlvbkluZm9ybWF0aW9uIjp7InR5cGUiOiJzcGxpdCIsInBvbGljeSI6ImV5SjFkV2xrSWpvaU1HTTFORGsyWlRZdE5EYzRaaTB4TVdWbUxXSXlOakV0WWpJMVl6UmhORE14TjJFM0lpd2lZbTlrZVNJNmV5SmtZWFJoUVhSMGNtbGlkWFJsY3lJNlczc2lZWFIwY21saWRYUmxJam9pYUhSMGNITTZMeTlsZUdGdGNHeGxMbU52YlM5aGRIUnlMMkYwZEhJeEwzWmhiSFZsTDNaaGJIVmxNU0lzSW1ScGMzQnNZWGxPWVcxbElqb2lJaXdpYVhORVpXWmhkV3gwSWpwbVlXeHpaU3dpY0hWaVMyVjVJam9pSWl3aWEyRnpWVkpNSWpvaUluMWRMQ0prYVhOelpXMGlPbHRkZlgwPSIsImtleUFjY2VzcyI6W3sidHlwZSI6IndyYXBwZWQiLCJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJwcm90b2NvbCI6ImthcyIsIndyYXBwZWRLZXkiOiJ0VVMvUE9TaVBtOGV6OGhyL2dMVGN6Y1lOT0trcUNEclZiQTBWdHZna29QbHB0M1BDZVpTdDNndnlQNVZKZXBNMmNqdVBhUWJJUGlyMjlWdVJ2T1RXZmQzRUh1KzgyVCtFNEVZbEpBM25VbDdGQTRMUGZhUEtXWk1zTExHUkJJVUxZT0VhMWJma1MvUm9Xb0EwK283WlFFVkNhYmdJN2JFRDJKV2Q2aG1yam1iUnM2d0lwOVFXNUs4Q3dJWjZVZjlGMXEwRDViTmlrbGxHaCtiaVJsV1NucEwxbHBPaFdva1gxdUJsU0VRSDNvM2JtVXFTNVVaUjRmYUxuTW5xOGR0bS8wYnJjTjUwaFNiK0xTTlZkd2daTEszTTRHTmxEeGdzcDkxY0VuYjZoZktLemdSY0VCS0tMQTF1b3BXNHdCRG9BamFuWWplQlZVT3ZBZEI5ek45T3c9PSIsInBvbGljeUJpbmRpbmciOnsiYWxnIjoiSFMyNTYiLCJoYXNoIjoiWmpBek1HWXlZekl4WlRCbU16Tm1NamhoTWpGalpqSTJaRE5oWlRrMk5ERTNaREJoWlRrM05ESTJNREExTnpVMU1UVTFNV0ZpTTJSak9EUTFabU0yWWc9PSJ9LCJraWQiOiJyMSJ9XSwibWV0aG9kIjp7ImFsZ29yaXRobSI6IkFFUy0yNTYtR0NNIiwiaXYiOiIiLCJpc1N0cmVhbWFibGUiOnRydWV9LCJpbnRlZ3JpdHlJbmZvcm1hdGlvbiI6eyJyb290U2lnbmF0dXJlIjp7ImFsZyI6IkhTMjU2Iiwic2lnIjoiWkdWaFltRmtNRGhsTURCbU1UVm1ZekJtTVdFME0ySmhOamhrTmpBMVpUazFNVGRtWmpoa1pETmtNekk0Tldaa01XUXhOVFZsWXpjME1EVXhPRE13Tmc9PSJ9LCJzZWdtZW50SGFzaEFsZyI6IkdNQUMiLCJzZWdtZW50U2l6ZURlZmF1bHQiOjIwOTcxNTIsImVuY3J5cHRlZFNlZ21lbnRTaXplRGVmYXVsdCI6MjA5NzE4MCwic2VnbWVudHMiOlt7Imhhc2giOiJNakkzWTJGbU9URXdNakV4TkdRNFpERTRZelkwWTJJeU4ySTFOemRqTXprPSIsInNlZ21lbnRTaXplIjo4LCJlbmNyeXB0ZWRTZWdtZW50U2l6ZSI6MzZ9XX19LCJwYXlsb2FkIjp7InR5cGUiOiJyZWZlcmVuY2UiLCJ1cmwiOiIwLnBheWxvYWQiLCJwcm90b2NvbCI6InppcCIsIm1pbWVUeXBlIjoiYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtIiwiaXNFbmNyeXB0ZWQiOnRydWV9fVBLBwgwpFOlrwUAAK8FAABQSwECLQAtAAgAAACdkxUxaJjuySQAAAAkAAAACQAAAAAAAAAAAAAAAAAAAAAAMC5wYXlsb2FkUEsBAi0ALQAIAAAAnZMVMTCkU6WvBQAArwUAAA8AAAAAAAAAAAAAAAAAWwAAADAubWFuaWZlc3QuanNvblBLBQYAAAAAAgACAHQAAABHBgAAAAA=" goodDecodedData, err := base64.StdEncoding.DecodeString(goodStandardTdf) @@ -170,7 +144,7 @@ func TestNew_ShouldNotValidateBadStandardTdf(t *testing.T) { require.NoError(t, err) // Decode the base64 string isValid, err := sdk.IsValidTdf(in) - // Error is ok here, as it acts as a sort of reason for the nanotdf not being valid + // Error is ok here; it documents why the input is invalid. assert.False(t, isValid) require.Error(t, err) } @@ -184,7 +158,7 @@ func TestIsInvalid_MissingRequiredManifestPayloadField(t *testing.T) { require.NoError(t, err) // Decode the base64 string isValid, err := sdk.IsValidTdf(in) - // Error is ok here, as it acts as a sort of reason for the nanotdf not being valid + // Error is ok here; it documents why the input is invalid. assert.False(t, isValid) require.ErrorIs(t, err, sdk.ErrInvalidPerSchema) } @@ -316,17 +290,6 @@ func TestIsPlatformEndpointMalformed(t *testing.T) { } } -func Test_GetType_NanoTDF(t *testing.T) { - nano := "TDFMABJsb2NhbGhvc3Q6ODA4MC9rYXOAAQIA2qvjMRfg7b27lT2kf9SwHRkDIg8ZXtfRoiIvdMUHq/gL5AUMfmv4Di8sKCyLkmUm/WITVj5hDeV/z4JmQ0JL7ZxqSmgZoK6TAHvkKhUly4zMEWMRXH8IktKhFKy1+fD+3qwDopqWAO5Nm2nYQqi75atEFckstulpNKg3N+Ul22OHr/ZuR127oPObBDYNRfktBdzoZbEQcPlr8q1B57q6y5SPZFjEzL9weK+uS5bUJWkF3nsHASo2bZw7IPhTZxoFVmCDjwvj6MbxNa7zG6aClHJ162zKxLLnD9TtIHuZ59R7LgiSieipXeExj+ky9OgIw5DfwyUuxsQLtKpMIAFPmLY9Hy2naUJxke0MT1EUBgastCq+YtFGslV9LJo/A8FtrRqludwtM0O+Z9FlAkZ1oNL7M7uOkLrh7eRrv+C1AAAX6FaBQoOtqnmyu6Jp+VzkxDddEeLRUyI=" - nanoDecoded, err := base64.StdEncoding.DecodeString(nano) - require.NoError(t, err) - - in := bytes.NewReader(nanoDecoded) - tdfType := sdk.GetTdfType(in) - - assert.Equal(t, sdk.Nano, tdfType) -} - func Test_GetType_TDF(t *testing.T) { tdf := "UEsDBC0ACAAAAJ2TFTEAAAAAAAAAAAAAAAAJAAAAMC5wYXlsb2Fktu4m+vdwl0mtjhY3U5e7TG2o1s8ifK+RAhFNjRjGTLJ7V3w5UEsHCGiY7skkAAAAJAAAAFBLAwQtAAgAAACdkxUxAAAAAAAAAAAAAAAADwAAADAubWFuaWZlc3QuanNvbnsiZW5jcnlwdGlvbkluZm9ybWF0aW9uIjp7InR5cGUiOiJzcGxpdCIsInBvbGljeSI6ImV5SjFkV2xrSWpvaU1HTTFORGsyWlRZdE5EYzRaaTB4TVdWbUxXSXlOakV0WWpJMVl6UmhORE14TjJFM0lpd2lZbTlrZVNJNmV5SmtZWFJoUVhSMGNtbGlkWFJsY3lJNlczc2lZWFIwY21saWRYUmxJam9pYUhSMGNITTZMeTlsZUdGdGNHeGxMbU52YlM5aGRIUnlMMkYwZEhJeEwzWmhiSFZsTDNaaGJIVmxNU0lzSW1ScGMzQnNZWGxPWVcxbElqb2lJaXdpYVhORVpXWmhkV3gwSWpwbVlXeHpaU3dpY0hWaVMyVjVJam9pSWl3aWEyRnpWVkpNSWpvaUluMWRMQ0prYVhOelpXMGlPbHRkZlgwPSIsImtleUFjY2VzcyI6W3sidHlwZSI6IndyYXBwZWQiLCJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJwcm90b2NvbCI6ImthcyIsIndyYXBwZWRLZXkiOiJ0VVMvUE9TaVBtOGV6OGhyL2dMVGN6Y1lOT0trcUNEclZiQTBWdHZna29QbHB0M1BDZVpTdDNndnlQNVZKZXBNMmNqdVBhUWJJUGlyMjlWdVJ2T1RXZmQzRUh1KzgyVCtFNEVZbEpBM25VbDdGQTRMUGZhUEtXWk1zTExHUkJJVUxZT0VhMWJma1MvUm9Xb0EwK283WlFFVkNhYmdJN2JFRDJKV2Q2aG1yam1iUnM2d0lwOVFXNUs4Q3dJWjZVZjlGMXEwRDViTmlrbGxHaCtiaVJsV1NucEwxbHBPaFdva1gxdUJsU0VRSDNvM2JtVXFTNVVaUjRmYUxuTW5xOGR0bS8wYnJjTjUwaFNiK0xTTlZkd2daTEszTTRHTmxEeGdzcDkxY0VuYjZoZktLemdSY0VCS0tMQTF1b3BXNHdCRG9BamFuWWplQlZVT3ZBZEI5ek45T3c9PSIsInBvbGljeUJpbmRpbmciOnsiYWxnIjoiSFMyNTYiLCJoYXNoIjoiWmpBek1HWXlZekl4WlRCbU16Tm1NamhoTWpGalpqSTJaRE5oWlRrMk5ERTNaREJoWlRrM05ESTJNREExTnpVMU1UVTFNV0ZpTTJSak9EUTFabU0yWWc9PSJ9LCJraWQiOiJyMSJ9XSwibWV0aG9kIjp7ImFsZ29yaXRobSI6IkFFUy0yNTYtR0NNIiwiaXYiOiIiLCJpc1N0cmVhbWFibGUiOnRydWV9LCJpbnRlZ3JpdHlJbmZvcm1hdGlvbiI6eyJyb290U2lnbmF0dXJlIjp7ImFsZyI6IkhTMjU2Iiwic2lnIjoiWkdWaFltRmtNRGhsTURCbU1UVm1ZekJtTVdFME0ySmhOamhrTmpBMVpUazFNVGRtWmpoa1pETmtNekk0Tldaa01XUXhOVFZsWXpjME1EVXhPRE13Tmc9PSJ9LCJzZWdtZW50SGFzaEFsZyI6IkdNQUMiLCJzZWdtZW50U2l6ZURlZmF1bHQiOjIwOTcxNTIsImVuY3J5cHRlZFNlZ21lbnRTaXplRGVmYXVsdCI6MjA5NzE4MCwic2VnbWVudHMiOlt7Imhhc2giOiJNakkzWTJGbU9URXdNakV4TkdRNFpERTRZelkwWTJJeU4ySTFOemRqTXprPSIsInNlZ21lbnRTaXplIjo4LCJlbmNyeXB0ZWRTZWdtZW50U2l6ZSI6MzZ9XX19LCJwYXlsb2FkIjp7InR5cGUiOiJyZWZlcmVuY2UiLCJ1cmwiOiIwLnBheWxvYWQiLCJwcm90b2NvbCI6InppcCIsIm1pbWVUeXBlIjoiYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtIiwiaXNFbmNyeXB0ZWQiOnRydWV9fVBLBwgwpFOlrwUAAK8FAABQSwECLQAtAAgAAACdkxUxaJjuySQAAAAkAAAACQAAAAAAAAAAAAAAAAAAAAAAMC5wYXlsb2FkUEsBAi0ALQAIAAAAnZMVMTCkU6WvBQAArwUAAA8AAAAAAAAAAAAAAAAAWwAAADAubWFuaWZlc3QuanNvblBLBQYAAAAAAgACAHQAAABHBgAAAAA=" tdfDecoded, err := base64.StdEncoding.DecodeString(tdf) @@ -355,3 +318,198 @@ func Test_GetType_Invalid2Bytes(t *testing.T) { assert.Equal(t, sdk.Invalid, tdfType) } + +func TestErrHealthCheckUnsupported_Distinct(t *testing.T) { + assert.NotEqual(t, sdk.ErrHealthCheckUnsupported, sdk.ErrPlatformUnreachable) + assert.Equal(t, "health check not supported in IPC mode", sdk.ErrHealthCheckUnsupported.Error()) +} + +// newHealthTestServer starts an httptest.Server serving grpc.health.v1.Health with the +// configured status for the empty service name (the SDK's reachability probe). +func newHealthTestServer(t *testing.T, status grpchealth.Status) *httptest.Server { + t.Helper() + checker := grpchealth.NewStaticChecker() + checker.SetStatus("", status) + mux := http.NewServeMux() + path, handler := grpchealth.NewHandler(checker) + mux.Handle(path, handler) + return httptest.NewServer(mux) +} + +func TestSDK_IsHealthy_IPCMode_ReturnsErrHealthCheckUnsupported(t *testing.T) { + // IPC mode requires a coreConn; provide a dummy one to satisfy sdk.New. + dummyConn := &sdk.ConnectRPCConnection{ + Endpoint: "http://localhost:0", + Client: http.DefaultClient, + } + s, err := sdk.New("", + sdk.WithIPC(), + sdk.WithCustomCoreConnection(dummyConn), + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "idp": map[string]interface{}{ + "issuer": "https://example.org", + "authorization_endpoint": "https://example.org/auth", + "token_endpoint": "https://example.org/token", + }, + }), + ) + require.NoError(t, err) + require.NotNil(t, s) + + healthy, err := s.IsHealthy(context.Background()) + assert.False(t, healthy) + require.ErrorIs(t, err, sdk.ErrHealthCheckUnsupported) +} + +func TestSDK_IsHealthy_Unreachable_ReturnsErrPlatformUnreachable(t *testing.T) { + s, err := sdk.New(badPlatformEndpoint, + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "idp": map[string]interface{}{ + "issuer": "https://example.org", + "authorization_endpoint": "https://example.org/auth", + "token_endpoint": "https://example.org/token", + }, + }), + ) + require.NoError(t, err) + require.NotNil(t, s) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + start := time.Now() + healthy, err := s.IsHealthy(ctx) + elapsed := time.Since(start) + + assert.False(t, healthy) + require.ErrorIs(t, err, sdk.ErrPlatformUnreachable) + assert.Less(t, elapsed, 2*time.Second, "health check should return promptly against a closed port, not wait for the ctx deadline") +} + +func TestSDK_IsHealthy_ContextCanceled_ReturnsQuickly(t *testing.T) { + s, err := sdk.New(badPlatformEndpoint, + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "idp": map[string]interface{}{ + "issuer": "https://example.org", + "authorization_endpoint": "https://example.org/auth", + "token_endpoint": "https://example.org/token", + }, + }), + ) + require.NoError(t, err) + require.NotNil(t, s) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // canceled before the call + + start := time.Now() + healthy, err := s.IsHealthy(ctx) + elapsed := time.Since(start) + + assert.False(t, healthy) + require.Error(t, err) + require.ErrorIs(t, err, sdk.ErrPlatformUnreachable) + require.ErrorIs(t, err, context.Canceled) + assert.Less(t, elapsed, 500*time.Millisecond, "pre-canceled ctx should short-circuit") +} + +func TestSDK_IsHealthy_Serving(t *testing.T) { + ts := newHealthTestServer(t, grpchealth.StatusServing) + defer ts.Close() + + s, err := sdk.New(ts.URL, + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "idp": map[string]interface{}{ + "issuer": "https://example.org", + "authorization_endpoint": "https://example.org/auth", + "token_endpoint": "https://example.org/token", + }, + }), + ) + require.NoError(t, err) + require.NotNil(t, s) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + healthy, err := s.IsHealthy(ctx) + require.NoError(t, err) + assert.True(t, healthy) +} + +func TestSDK_IsHealthy_NotServing(t *testing.T) { + ts := newHealthTestServer(t, grpchealth.StatusNotServing) + defer ts.Close() + + s, err := sdk.New(ts.URL, + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "idp": map[string]interface{}{ + "issuer": "https://example.org", + "authorization_endpoint": "https://example.org/auth", + "token_endpoint": "https://example.org/token", + }, + }), + ) + require.NoError(t, err) + require.NotNil(t, s) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + healthy, err := s.IsHealthy(ctx) + require.NoError(t, err) + assert.False(t, healthy) +} + +// TestSDK_IsHealthy_Unknown locks the contract that an UNKNOWN status from a reachable +// platform returns (false, nil) — distinct from transport errors which wrap ErrPlatformUnreachable. +func TestSDK_IsHealthy_Unknown(t *testing.T) { + ts := newHealthTestServer(t, grpchealth.StatusUnknown) + defer ts.Close() + + s, err := sdk.New(ts.URL, + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "idp": map[string]interface{}{ + "issuer": "https://example.org", + "authorization_endpoint": "https://example.org/auth", + "token_endpoint": "https://example.org/token", + }, + }), + ) + require.NoError(t, err) + require.NotNil(t, s) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + healthy, err := s.IsHealthy(ctx) + require.NoError(t, err) + assert.False(t, healthy) +} + +// TestSDK_IsHealthy_TrailingSlashEndpoint verifies that a platform endpoint +// with a trailing slash does not produce a double-slash in the request URL, +// which strict HTTP routers can reject. +func TestSDK_IsHealthy_TrailingSlashEndpoint(t *testing.T) { + ts := newHealthTestServer(t, grpchealth.StatusServing) + defer ts.Close() + + s, err := sdk.New(ts.URL+"/", + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "idp": map[string]interface{}{ + "issuer": "https://example.org", + "authorization_endpoint": "https://example.org/auth", + "token_endpoint": "https://example.org/token", + }, + }), + ) + require.NoError(t, err) + require.NotNil(t, s) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + healthy, err := s.IsHealthy(ctx) + require.NoError(t, err) + assert.True(t, healthy) +} diff --git a/sdk/sdkconnect/namespaces.go b/sdk/sdkconnect/namespaces.go index f7e64e21be..b1d02386b8 100644 --- a/sdk/sdkconnect/namespaces.go +++ b/sdk/sdkconnect/namespaces.go @@ -26,8 +26,6 @@ type NamespaceServiceClient interface { RemoveKeyAccessServerFromNamespace(ctx context.Context, req *namespaces.RemoveKeyAccessServerFromNamespaceRequest) (*namespaces.RemoveKeyAccessServerFromNamespaceResponse, error) AssignPublicKeyToNamespace(ctx context.Context, req *namespaces.AssignPublicKeyToNamespaceRequest) (*namespaces.AssignPublicKeyToNamespaceResponse, error) RemovePublicKeyFromNamespace(ctx context.Context, req *namespaces.RemovePublicKeyFromNamespaceRequest) (*namespaces.RemovePublicKeyFromNamespaceResponse, error) - AssignCertificateToNamespace(ctx context.Context, req *namespaces.AssignCertificateToNamespaceRequest) (*namespaces.AssignCertificateToNamespaceResponse, error) - RemoveCertificateFromNamespace(ctx context.Context, req *namespaces.RemoveCertificateFromNamespaceRequest) (*namespaces.RemoveCertificateFromNamespaceResponse, error) } func (w *NamespaceServiceClientConnectWrapper) GetNamespace(ctx context.Context, req *namespaces.GetNamespaceRequest) (*namespaces.GetNamespaceResponse, error) { @@ -110,21 +108,3 @@ func (w *NamespaceServiceClientConnectWrapper) RemovePublicKeyFromNamespace(ctx } return res.Msg, err } - -func (w *NamespaceServiceClientConnectWrapper) AssignCertificateToNamespace(ctx context.Context, req *namespaces.AssignCertificateToNamespaceRequest) (*namespaces.AssignCertificateToNamespaceResponse, error) { - // Wrap Connect RPC client request - res, err := w.NamespaceServiceClient.AssignCertificateToNamespace(ctx, connect.NewRequest(req)) - if res == nil { - return nil, err - } - return res.Msg, err -} - -func (w *NamespaceServiceClientConnectWrapper) RemoveCertificateFromNamespace(ctx context.Context, req *namespaces.RemoveCertificateFromNamespaceRequest) (*namespaces.RemoveCertificateFromNamespaceResponse, error) { - // Wrap Connect RPC client request - res, err := w.NamespaceServiceClient.RemoveCertificateFromNamespace(ctx, connect.NewRequest(req)) - if res == nil { - return nil, err - } - return res.Msg, err -} diff --git a/sdk/sdkconnect/obligations.go b/sdk/sdkconnect/obligations.go index e51e808128..a1e7427dba 100644 --- a/sdk/sdkconnect/obligations.go +++ b/sdk/sdkconnect/obligations.go @@ -28,6 +28,7 @@ type ObligationsServiceClient interface { CreateObligationValue(ctx context.Context, req *obligations.CreateObligationValueRequest) (*obligations.CreateObligationValueResponse, error) UpdateObligationValue(ctx context.Context, req *obligations.UpdateObligationValueRequest) (*obligations.UpdateObligationValueResponse, error) DeleteObligationValue(ctx context.Context, req *obligations.DeleteObligationValueRequest) (*obligations.DeleteObligationValueResponse, error) + GetObligationTrigger(ctx context.Context, req *obligations.GetObligationTriggerRequest) (*obligations.GetObligationTriggerResponse, error) AddObligationTrigger(ctx context.Context, req *obligations.AddObligationTriggerRequest) (*obligations.AddObligationTriggerResponse, error) RemoveObligationTrigger(ctx context.Context, req *obligations.RemoveObligationTriggerRequest) (*obligations.RemoveObligationTriggerResponse, error) ListObligationTriggers(ctx context.Context, req *obligations.ListObligationTriggersRequest) (*obligations.ListObligationTriggersResponse, error) @@ -132,6 +133,15 @@ func (w *ObligationsServiceClientConnectWrapper) DeleteObligationValue(ctx conte return res.Msg, err } +func (w *ObligationsServiceClientConnectWrapper) GetObligationTrigger(ctx context.Context, req *obligations.GetObligationTriggerRequest) (*obligations.GetObligationTriggerResponse, error) { + // Wrap Connect RPC client request + res, err := w.ServiceClient.GetObligationTrigger(ctx, connect.NewRequest(req)) + if res == nil { + return nil, err + } + return res.Msg, err +} + func (w *ObligationsServiceClientConnectWrapper) AddObligationTrigger(ctx context.Context, req *obligations.AddObligationTriggerRequest) (*obligations.AddObligationTriggerResponse, error) { // Wrap Connect RPC client request res, err := w.ServiceClient.AddObligationTrigger(ctx, connect.NewRequest(req)) diff --git a/sdk/tdf.go b/sdk/tdf.go index aba570aa9b..1d6ef1182c 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "hash/crc32" "io" "log/slog" "net/http" @@ -23,7 +24,7 @@ import ( "github.com/google/uuid" "github.com/opentdf/platform/lib/ocrypto" "github.com/opentdf/platform/sdk/auth" - "github.com/opentdf/platform/sdk/internal/archive" + "github.com/opentdf/platform/sdk/internal/zipstream" "github.com/opentdf/platform/sdk/sdkconnect" "google.golang.org/grpc/codes" ) @@ -32,6 +33,7 @@ const ( keyAccessSchemaVersion = "1.0" maxFileSizeSupported = 68719476736 // 64gb defaultMimeType = "application/octet-stream" + zip64MagicVal = int64(^uint32(0)) tdfAsZip = "zip" gcmIvSize = 12 aesBlockSize = 16 @@ -41,6 +43,7 @@ const ( kKeySize = 32 kWrapped = "wrapped" kECWrapped = "ec-wrapped" + kHybridWrapped = "hybrid-wrapped" kKasProtocol = "kas" kSplitKeyType = "split" kGCMCipherAlgorithm = "AES-256-GCM" @@ -58,7 +61,7 @@ type Reader struct { connectOptions []connect.ClientOption manifest Manifest unencryptedMetadata []byte - tdfReader archive.TDFReader + tdfReader zipstream.TDFReader cursor int64 aesGcm ocrypto.AesGcm payloadSize int64 @@ -79,6 +82,17 @@ type TDFObject struct { payloadKey [kKeySize]byte } +type countingWriter struct { + writer io.Writer + written int64 +} + +func (cw *countingWriter) Write(p []byte) (int, error) { + n, err := cw.writer.Write(p) + cw.written += int64(n) + return n, err +} + type tdf3DecryptHandler struct { writer io.Writer reader *Reader @@ -197,16 +211,29 @@ func (s SDK) CreateTDFContext(ctx context.Context, writer io.Writer, reader io.R encryptedSegmentSize := segmentSize + gcmIvSize + aesBlockSize payloadSize := inputSize + (totalSegments * (gcmIvSize + aesBlockSize)) - tdfWriter := archive.NewTDFWriter(writer) - err = tdfWriter.SetPayloadSize(payloadSize) - if err != nil { - return nil, fmt.Errorf("archive.SetPayloadSize failed: %w", err) + zipMode := zipstream.Zip64Auto + if payloadSize >= zip64MagicVal { + zipMode = zipstream.Zip64Always } + expectedSegments := int(totalSegments) + if expectedSegments < 1 { + expectedSegments = 1 + } + + archiveWriter := zipstream.NewSegmentTDFWriter( + expectedSegments, + zipstream.WithZip64Mode(zipMode), + zipstream.WithMaxSegments(expectedSegments), + ) + + outputWriter := &countingWriter{writer: writer} + var readPos int64 - var aggregateHash string + var aggregateHashBuilder strings.Builder readBuf := bytes.NewBuffer(make([]byte, 0, tdfConfig.defaultSegmentSize)) + segmentIndex := 0 for totalSegments != 0 { // adjust read size readSize := segmentSize if (inputSize - readPos) < segmentSize { @@ -227,7 +254,20 @@ func (s SDK) CreateTDFContext(ctx context.Context, writer io.Writer, reader io.R return nil, fmt.Errorf("io.ReadSeeker.Read failed: %w", err) } - err = tdfWriter.AppendPayload(cipherData) + crc := crc32.ChecksumIEEE(cipherData) + headerBytes, err := archiveWriter.WriteSegment(ctx, segmentIndex, uint64(len(cipherData)), crc) + if err != nil { + return nil, fmt.Errorf("zipstream.WriteSegment failed: %w", err) + } + + if len(headerBytes) > 0 { + _, err = outputWriter.Write(headerBytes) + if err != nil { + return nil, fmt.Errorf("io.writer.Write failed: %w", err) + } + } + + _, err = outputWriter.Write(cipherData) if err != nil { return nil, fmt.Errorf("io.writer.Write failed: %w", err) } @@ -238,7 +278,7 @@ func (s SDK) CreateTDFContext(ctx context.Context, writer io.Writer, reader io.R return nil, fmt.Errorf("splitKey.GetSignaturefailed: %w", err) } - aggregateHash += segmentSig + aggregateHashBuilder.WriteString(segmentSig) segmentInfo := Segment{ Hash: string(ocrypto.Base64Encode([]byte(segmentSig))), Size: readSize, @@ -249,9 +289,10 @@ func (s SDK) CreateTDFContext(ctx context.Context, writer io.Writer, reader io.R totalSegments-- readPos += readSize + segmentIndex++ } - rootSignature, err := calculateSignature([]byte(aggregateHash), tdfObject.payloadKey[:], + rootSignature, err := calculateSignature([]byte(aggregateHashBuilder.String()), tdfObject.payloadKey[:], tdfConfig.integrityAlgorithm, tdfConfig.useHex) if err != nil { return nil, fmt.Errorf("splitKey.GetSignaturefailed: %w", err) @@ -285,7 +326,7 @@ func (s SDK) CreateTDFContext(ctx context.Context, writer io.Writer, reader io.R tdfObject.manifest.MimeType = mimeType tdfObject.manifest.Protocol = tdfAsZip tdfObject.manifest.Type = tdfZipReference - tdfObject.manifest.URL = archive.TDFPayloadFileName + tdfObject.manifest.URL = zipstream.TDFPayloadFileName tdfObject.manifest.IsEncrypted = true var signedAssertion []Assertion @@ -319,7 +360,7 @@ func (s SDK) CreateTDFContext(ctx context.Context, writer io.Writer, reader io.R } var completeHashBuilder strings.Builder - completeHashBuilder.WriteString(aggregateHash) + completeHashBuilder.WriteString(aggregateHashBuilder.String()) if tdfConfig.useHex { completeHashBuilder.Write(hashOfAssertionAsHex) } else { @@ -352,16 +393,22 @@ func (s SDK) CreateTDFContext(ctx context.Context, writer io.Writer, reader io.R return nil, fmt.Errorf("json.Marshal failed:%w", err) } - err = tdfWriter.AppendManifest(string(manifestAsStr)) + finalBytes, err := archiveWriter.Finalize(ctx, manifestAsStr) if err != nil { - return nil, fmt.Errorf("TDFWriter.AppendManifest failed:%w", err) + return nil, fmt.Errorf("zipstream.Finalize failed: %w", err) } - tdfObject.size, err = tdfWriter.Finish() + _, err = outputWriter.Write(finalBytes) if err != nil { - return nil, fmt.Errorf("TDFWriter.Finish failed:%w", err) + return nil, fmt.Errorf("io.writer.Write failed: %w", err) } + if err := archiveWriter.Close(); err != nil { + return nil, fmt.Errorf("zipstream.Close failed: %w", err) + } + + tdfObject.size = outputWriter.written + return tdfObject, nil } @@ -393,7 +440,7 @@ func (tdfConfig *TDFConfig) initKAOTemplate(ctx context.Context, s SDK) error { tdfConfig.splitPlan, err = g.plan(make([]string, 0), uuidSplitIDGenerator) case noKeysFound: var baseKey *policy.SimpleKasKey - baseKey, err = getBaseKeyFromWellKnown(ctx, s) + baseKey, err = s.GetBaseKey(ctx) if err == nil { err = populateKasInfoFromBaseKey(baseKey, tdfConfig) } else { @@ -628,7 +675,15 @@ func createKeyAccess(kasInfo KASInfo, symKey []byte, policyBinding PolicyBinding } ktype := ocrypto.KeyType(kasInfo.Algorithm) - if ocrypto.IsECKeyType(ktype) { + switch { + case ocrypto.IsHybridKeyType(ktype): + wrappedKey, err := generateWrapKeyWithHybrid(kasInfo.Algorithm, kasInfo.PublicKey, symKey) + if err != nil { + return KeyAccess{}, err + } + keyAccess.KeyType = kHybridWrapped + keyAccess.WrappedKey = wrappedKey + case ocrypto.IsECKeyType(ktype): mode, err := ocrypto.ECKeyTypeToMode(ktype) if err != nil { return KeyAccess{}, err @@ -640,7 +695,7 @@ func createKeyAccess(kasInfo KASInfo, symKey []byte, policyBinding PolicyBinding keyAccess.KeyType = kECWrapped keyAccess.WrappedKey = wrappedKeyInfo.wrappedKey keyAccess.EphemeralPublicKey = wrappedKeyInfo.publicKey - } else { + default: wrappedKey, err := generateWrapKeyWithRSA(kasInfo.PublicKey, symKey) if err != nil { return KeyAccess{}, err @@ -702,9 +757,9 @@ func generateWrapKeyWithEC(mode ocrypto.ECCMode, kasPublicKey string, symKey []b } func generateWrapKeyWithRSA(publicKey string, symKey []byte) (string, error) { - asymEncrypt, err := ocrypto.NewAsymEncryption(publicKey) + asymEncrypt, err := ocrypto.FromPublicPEM(publicKey) if err != nil { - return "", fmt.Errorf("generateWrapKeyWithRSA: ocrypto.NewAsymEncryption failed:%w", err) + return "", fmt.Errorf("generateWrapKeyWithRSA: ocrypto.FromPublicPEM failed:%w", err) } wrappedKey, err := asymEncrypt.Encrypt(symKey) @@ -715,6 +770,14 @@ func generateWrapKeyWithRSA(publicKey string, symKey []byte) (string, error) { return string(ocrypto.Base64Encode(wrappedKey)), nil } +func generateWrapKeyWithHybrid(algorithm, publicKeyPEM string, symKey []byte) (string, error) { + wrappedDER, err := ocrypto.HybridWrapDEK(ocrypto.KeyType(algorithm), publicKeyPEM, symKey) + if err != nil { + return "", fmt.Errorf("generateWrapKeyWithHybrid: %w", err) + } + return string(ocrypto.Base64Encode(wrappedDER)), nil +} + // create policy object func createPolicyObject(attributes []AttributeValueFQN) (PolicyObject, error) { uuidObj, err := uuid.NewUUID() @@ -756,6 +819,37 @@ func allowListFromKASRegistry(ctx context.Context, logger *slog.Logger, kasRegis return kasAllowlist, nil } +// WithPolicyFrom returns a [TDFOption] that binds the source TDF's policy +// (its attribute value FQNs) to the new TDF being created. +// +// Use this in re-wrap pipelines to preserve the source policy without +// having to know about the manifest's base64 + JSON encoding: +// +// if ok, _ := sdk.IsValidTdf(file); !ok { +// // pass through unchanged +// } +// reader, err := s.LoadTDF(file) +// if err != nil { +// return err +// } +// _, err = s.CreateTDF(out, transformed, sdk.WithPolicyFrom(reader)) +// +// [Reader.Init] is not required: [Reader.DataAttributes] reads the policy +// from the manifest, which [SDK.LoadTDF] has already populated. Calling +// Init would trigger an unnecessary KAS rewrap. +func WithPolicyFrom(r *Reader) TDFOption { + return func(c *TDFConfig) error { + if r == nil { + return errors.New("WithPolicyFrom: nil Reader") + } + attrs, err := r.DataAttributes() + if err != nil { + return fmt.Errorf("WithPolicyFrom: extracting source attributes: %w", err) + } + return WithDataAttributes(attrs...)(c) + } +} + // LoadTDF loads the tdf and prepare for reading the payload from TDF func (s SDK) LoadTDF(reader io.ReadSeeker, opts ...TDFReaderOption) (*Reader, error) { if s.kasSessionKey != nil { @@ -768,9 +862,9 @@ func (s SDK) LoadTDF(reader io.ReadSeeker, opts ...TDFReaderOption) (*Reader, er } // create tdf reader - tdfReader, err := archive.NewTDFReader(reader, archive.WithTDFManifestMaxSize(config.maxManifestSize)) + tdfReader, err := zipstream.NewTDFReader(reader, zipstream.WithTDFManifestMaxSize(config.maxManifestSize)) if err != nil { - return nil, fmt.Errorf("archive.NewTDFReader failed: %w", err) + return nil, fmt.Errorf("zipstream.NewTDFReader failed: %w", err) } useGlobalFulfillableObligations := len(config.fulfillableObligationFQNs) == 0 && len(s.fulfillableObligationFQNs) > 0 if useGlobalFulfillableObligations { @@ -1473,15 +1567,6 @@ func isLessThanSemver(version, target string) (bool, error) { return v1.LessThan(v2), nil } -func getBaseKeyFromWellKnown(ctx context.Context, s SDK) (*policy.SimpleKasKey, error) { - key, err := getBaseKey(ctx, s) - if err != nil { - return nil, err - } - - return key, nil -} - func populateKasInfoFromBaseKey(key *policy.SimpleKasKey, tdfConfig *TDFConfig) error { if key == nil { return errors.New("populateKasInfoFromBaseKey failed: key is nil") @@ -1531,11 +1616,26 @@ func createKaoTemplateFromKasInfo(kasInfoArr []KASInfo) []kaoTpl { return kaoTemplate } +// getKasErrorToReturn classifies KAS rewrap errors. KAS intentionally uses a +// generic "bad request" for policy binding and DEK failures to avoid leaking +// information about secret key material. Descriptive messages indicate +// client/configuration issues that are safe to surface as non-tamper errors. func getKasErrorToReturn(err error, defaultError error) error { errToReturn := defaultError - if strings.Contains(err.Error(), codes.InvalidArgument.String()) { - errToReturn = errors.Join(ErrRewrapBadRequest, errToReturn) - } else if strings.Contains(err.Error(), codes.PermissionDenied.String()) { + errStr := err.Error() + + switch { + case strings.Contains(errStr, codes.InvalidArgument.String()): + // Per-KAO errors are serialized as plain strings through the proto + // response, so we match on a substring anchored to the gRPC status + // description. Generic "bad request" = potential tamper; anything + // else = client/configuration error. + if strings.Contains(errStr, kasGenericBadRequest) { + errToReturn = errors.Join(ErrRewrapBadRequest, errToReturn) + } else { + errToReturn = errors.Join(ErrKASRequestError, errToReturn) + } + case strings.Contains(errStr, codes.PermissionDenied.String()): errToReturn = errors.Join(ErrRewrapForbidden, errToReturn) } diff --git a/sdk/tdf_hybrid_test.go b/sdk/tdf_hybrid_test.go new file mode 100644 index 0000000000..86eb1e98e5 --- /dev/null +++ b/sdk/tdf_hybrid_test.go @@ -0,0 +1,84 @@ +package sdk + +import ( + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateKeyAccessWithXWingKey(t *testing.T) { + symKey := []byte("0123456789abcdef0123456789abcdef") + keyAccess, err := createKeyAccess(KASInfo{ + URL: "https://kas.example.com", + KID: "xwing-kid", + Algorithm: string(ocrypto.HybridXWingKey), + PublicKey: mockHybridXWingPublicKey, + }, symKey, PolicyBinding{}, "", "") + require.NoError(t, err) + + assert.Equal(t, kHybridWrapped, keyAccess.KeyType) + assert.Empty(t, keyAccess.EphemeralPublicKey) + assert.NotEmpty(t, keyAccess.WrappedKey) + + privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(mockHybridXWingPrivateKey)) + require.NoError(t, err) + + wrappedKey, err := ocrypto.Base64Decode([]byte(keyAccess.WrappedKey)) + require.NoError(t, err) + + plaintext, err := ocrypto.XWingUnwrapDEK(privateKey, wrappedKey) + require.NoError(t, err) + assert.Equal(t, symKey, plaintext) +} + +func TestCreateKeyAccessWithP256MLKEM768Key(t *testing.T) { + symKey := []byte("0123456789abcdef0123456789abcdef") + keyAccess, err := createKeyAccess(KASInfo{ + URL: "https://kas.example.com", + KID: "p256mlkem768-kid", + Algorithm: string(ocrypto.HybridSecp256r1MLKEM768Key), + PublicKey: mockHybridP256MLKEM768PublicKey, + }, symKey, PolicyBinding{}, "", "") + require.NoError(t, err) + + assert.Equal(t, kHybridWrapped, keyAccess.KeyType) + assert.Empty(t, keyAccess.EphemeralPublicKey) + assert.NotEmpty(t, keyAccess.WrappedKey) + + privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(mockHybridP256MLKEM768PrivateKey)) + require.NoError(t, err) + + wrappedKey, err := ocrypto.Base64Decode([]byte(keyAccess.WrappedKey)) + require.NoError(t, err) + + plaintext, err := ocrypto.P256MLKEM768UnwrapDEK(privateKey, wrappedKey) + require.NoError(t, err) + assert.Equal(t, symKey, plaintext) +} + +func TestCreateKeyAccessWithP384MLKEM1024Key(t *testing.T) { + symKey := []byte("0123456789abcdef0123456789abcdef") + keyAccess, err := createKeyAccess(KASInfo{ + URL: "https://kas.example.com", + KID: "p384mlkem1024-kid", + Algorithm: string(ocrypto.HybridSecp384r1MLKEM1024Key), + PublicKey: mockHybridP384MLKEM1024PublicKey, + }, symKey, PolicyBinding{}, "", "") + require.NoError(t, err) + + assert.Equal(t, kHybridWrapped, keyAccess.KeyType) + assert.Empty(t, keyAccess.EphemeralPublicKey) + assert.NotEmpty(t, keyAccess.WrappedKey) + + privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(mockHybridP384MLKEM1024PrivateKey)) + require.NoError(t, err) + + wrappedKey, err := ocrypto.Base64Decode([]byte(keyAccess.WrappedKey)) + require.NoError(t, err) + + plaintext, err := ocrypto.P384MLKEM1024UnwrapDEK(privateKey, wrappedKey) + require.NoError(t, err) + assert.Equal(t, symKey, plaintext) +} diff --git a/sdk/tdf_rewrap_test.go b/sdk/tdf_rewrap_test.go new file mode 100644 index 0000000000..8b5319f520 --- /dev/null +++ b/sdk/tdf_rewrap_test.go @@ -0,0 +1,17 @@ +package sdk + +import ( + "strings" + "testing" +) + +func TestWithPolicyFrom_NilReader(t *testing.T) { + cfg := &TDFConfig{} + err := WithPolicyFrom(nil)(cfg) + if err == nil { + t.Fatal("expected error for nil Reader") + } + if !strings.Contains(err.Error(), "nil Reader") { + t.Errorf("error = %v, want to mention nil Reader", err) + } +} diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index cd0c9178dd..650fce17b1 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -16,6 +16,7 @@ import ( "encoding/pem" "errors" "fmt" + "hash/crc32" "io" "log/slog" "net/http" @@ -42,7 +43,7 @@ import ( "github.com/opentdf/platform/protocol/go/policy/kasregistry/kasregistryconnect" wellknownpb "github.com/opentdf/platform/protocol/go/wellknownconfiguration" wellknownconnect "github.com/opentdf/platform/protocol/go/wellknownconfiguration/wellknownconfigurationconnect" - "github.com/opentdf/platform/sdk/internal/archive" + "github.com/opentdf/platform/sdk/internal/zipstream" "google.golang.org/grpc/codes" "google.golang.org/grpc/resolver" "google.golang.org/grpc/status" @@ -258,6 +259,120 @@ A1UdIwQYMBaAFLg9mMeD25ZGvmjSYaunIPoeekzlMA8GA1UdEwEB/wQFMAMBAf8w CgYIKoZIzj0EAwIDSAAwRQIhALYXC70t37RlmIkRDlUTehiVEHpSQXz04wQ9Ivw+ 4h4hAiBNR3rD3KieiJaiJrCfM6TPJL7TIch7pAhMHdG6IPJMoQ== -----END CERTIFICATE-----` + + mockHybridXWingPublicKey = `-----BEGIN XWING PUBLIC KEY----- +4pKOW2Z+TqohuAO7Z8m1J9Ik5jbLOZgpfKKTdEXD4mqLgXa8D5RNaYhtkfJn4js6 +MWO35ZJqlfVuCoBnruillfoEY2VD6OVB7AQqBJyzhLeAFsOYgwLIF9UBXixSo6ym +eAA6GhYvzViUx/oQSHCI6JWhKhINXqar7usmFlxC/da4rolS9AGAkpx3S8F1VRyN +tBahkbcLDptIqzRgOJWaXuYdtRqiuqahJyEWLjal/FQa0+dTA5oWn4IWUZRGvVut +T/AFXMHMPdY7JmmmhNJAJTqcblHJ+FJ1YTBi0bEQE7F4XlqpdRxwISJSAGCZN6WD +eTVF9CmpM0iJbnlN5cvAcwCFGamL5II0wwEhftMDIoCb+IiR8NQcQfJnFus9DvhR +k5k7HVtjoumWWksL+tOudUtmYaOnV4CwyAunc8W+vsQ6edoCQWy8peRMvVcoaDUE +tVuACiiwIWADLbsmGyiBR6x4/ashJLs46XsB4pmB1BfIf4yis+B5VvLCnZIsMvKH +jLl81PMH4AsIytN0H/GOkJI/y1iGZsJkCZN15WJ2tAuwyMqz0yooh1lZIedgTheE +lBKYx1YlFvmSCZBfLnKOIWFHpqkQ8OTGXWgBfvhwyhEui3ZhJbhmgBQrYAeN8ZcP +s2isMmmzKVRcMzVFXaynv+UIiWVAH5BeXUbL0BVIkJulgXJptEyXs6JPGbEu1/ug +uvaWx6xWQ3NiR+BOA5eIigwjoQEtyYRczlg0kUcivca+/TiE3yh2gkU2xhLNFXdL +mHw1jNIbD5O5UPoGEdJ2XRLLVZJhhdmSWpN51FoW/OA6tJaMedtaNwjKD9wjJYEM +izoy9cSNuWZNKMlNdNc9aPfIcQoD2xKKKZR7iHdVNpRQEYiWFrxqT8aY3JN8UjEy +hdqeS7VAbQc28Ts7kxNa2xRmBQaOn3pb/eO+5LV/Y0Y4E7U7TXt3eBwbo5luf6Sg +dnBjypqJAmUzpEl6MrOs7Hh7RxByyKDIgYiNAaNUnwOI7USjxlN5sKhpj+iKvCYF +oCmRBMQ8w+p2s3GiXtB/whh1gwtIzTGkBpyaJrpjrFm67Bp88aUG3GJ5QKy/YPct +tVKRPcNRhlsrOsEwB0qE/+Qj2gcPDAydGIp3hxiJxFgun1RQp/BkiEJW22xN2Vim +n+tyUcRRvQCEoSU0+NXHddQvkzwHJVpdvsgRfgxHIlpTGXBReaQnRGZl2zEDVZGb +vva6ubOu8XhMJAs1n9M41aVlP4bDmwmNv/oTKwi2kSdObkZv2INLucxtiJZ3BhbC +NMiEXVU9DdS/USqZFpcQHqYB63hAMdmRsaM1wVW7HbRumglJY+A/hqgaV2wGtjNR +5qNHS4FTT7euxsOs0PIxE3sOgDmseTd0UditGXd3O5xx7LkBe4GdNTuXq+rMgiZf +P0i2ugDCV4McS4zKfpamYPQKqBufsiNkbMSVu+Mb7kQcgZZBY8gTnEyEY2k9oiJ5 +v6dH/oNa5ohGvHJjEcm4/3y3C7er0OKKM8JLkuIuUYsP7yqPY0V8LPjKI/wS0wxS +yv8kgAJe+QrgMAOzPsw5fAeK5Lrz+OhSRsF/hRJnlAgbv/UAHZQR7ueSrL6Tj8tY +Ubd0E5EZlOEecFd/41z5QA== +-----END XWING PUBLIC KEY-----` + + // X-Wing private key is a 32-byte seed; the full X25519 + ML-KEM-768 + // components are derived from it at runtime by circl. + mockHybridXWingPrivateKey = `-----BEGIN XWING PRIVATE KEY----- +7uCvk28wGoVrwW5nU2huW28UXWa5tZMom6Zds8uohrA= +-----END XWING PRIVATE KEY-----` + + mockHybridP256MLKEM768PublicKey = `-----BEGIN SECP256R1 MLKEM768 PUBLIC KEY----- +BGtgB2txkSmwez5qXBEmetZijnkCuYWdhPvcPoAHr5Aiv1Zk895ARtcgbO1KIye8 +52Wnc48CY2baUfSHptVKmfsnRbmsDEdZoTWyaaaAmieMws9Z9yZuwVNB2sx7AQES +d7ozp2t+nAftGc0WhUYxXDaup0usxA0QkpR7VqDa/ATqek+XlQm3TJ4pzMMIPK9H +FY6iUJchpAZ7+nQesKpDErrzQxeoGbCOOIqxAyaHNVgbxAxVIj8c23iwdMZKwMPC +LDb22Do61X7vFV0WCTTeVRJJ6lXVA6TkyVpXmb2A4gpUtSTGssINSSxxrA/wklA8 +gpqb4lurdYylQxBwArs0wnzhmThm2T/gOW89Mw6XnLbNqosqlcESwjfKXCKt63ZB +ZDVR2Wlc2RxWu0jysaoqUkYIZREceoFjSUWwagI0o8TVeAiVUBJD0AgtNcouJjad +pJvB6MYQlpe2F2sr1gvOlZph1IBkgpO8dRlKZ1yEgzUchG1tummcs3/lq2ois8fl +ZABkxVs9fMkO5G3vOgzucYjQe698sLTzaRygiKHnVS5GsWhBOq6eWVfOiwt+Klvv +KI6AfBQ2UFXsrBqXW8sjApWQ1xZVICtSBsksBhuf8GDh3AlNYxAJw0AAO6vgc7is +SRVZKCOsCKby+LxlFiEabGYbES07iHBvqx5ZCGHl+RB5SqJzWV/nPIrHrBmNiFvq +EYTnnLJ1hyqa7ABjGCeisoU6Js3yw1L6QMwtZS4sjMQdSWCoVqIeBimFtbFacVKR +OhR2YUVesQ8hgp21CjtJRMF8917OCbc1xrMj4by5tr5Aq0LjRlL+EcIcNgkjJrz9 +sXrXK2GvxZcf/FknshQ4pme+AJIvp771I3SLVEvdhbf35aRJCIQ3xQ5Y6Ijxa5Ag +wls8WMkAwQw3IBjNeYVcYn6GiTOet7yAoU+Z9Jx06SK/kqQ7HMJDChkJtLJLNJH/ +a7HrRZlZ4TA92kLP+R/ym6Kle8rN1iNiUXLxmKUR51vuNKnPVh4AGKQb0MYQurjo +YWcVg6I6MYV2XFIJAiqTBdC8kWUyZFYrw5M4E3RM+0M/slEWyQAf1L26FQodUKce +dqpo94trsCjTpbRQ+aTSNZdAHMwxkrR02TzUmoWTl0TUE8nS6yxTwUcd9wy+5KnX +mzHD42I6eyTgnAhLiZOOEpBXBX6VM4Zq1z6GO25id7kju2kgEREFnFVf7G3JWrZz +yXJ1SBLVlina4kmexJTTAyzi9IQqcAewm6nsc7byBpBenBA6aAjYZMQYtaSK+IRE +6norOKeJ0w+ZpAPdC43Mw7GBhD4eI3FQcJOce2L9yXgPYQCEaEJvDJLmJKo26iq5 +WmYEQwe1abKouruKNyaON2TEWLQBSqujca8MFKBOsXisvApkdm+fK3CLVjgSCAM4 +7LfBDFSHA7wj+KC2oXqUgwinBSgYkkJ6Vahny6GadZdpZAmYQCNLhgDDMAOcVxUx +aSQeIECzocR08QslRXNV542FlXlVx2b/ZC5yaFwAUYOACbyYq1azeTOIWyFoKGKa +uRpK+2jglo7wB7kU5LcIB8ZTFwLjGDZsEKdbmgzV+yVDqxXU1llgvKfhFmOfSALF +S39eOBaIhT1WOWpxinZRaagtmi8UQ0uE0uGhIV7fe+lLGuLxYKa/KYQe7wL6j0sL +wQ== +-----END SECP256R1 MLKEM768 PUBLIC KEY-----` + + mockHybridP256MLKEM768PrivateKey = `-----BEGIN SECP256R1 MLKEM768 PRIVATE KEY----- +j60Fn/LBoCGnWihNB3Q6lXJaO4EkMCrEnOT3/z0zZ6Kdr+6m7ww3N6lLMP+Rb2O1 +3gjrOyiNXWbaTwl1mI2vi03PoW2bJbE1+wJuiT/2njjrBRyIxLqRD/4zbiscmhOp +-----END SECP256R1 MLKEM768 PRIVATE KEY-----` + + mockHybridP384MLKEM1024PublicKey = `-----BEGIN SECP384R1 MLKEM1024 PUBLIC KEY----- +BFFAmpsfvYsT+TgT6noyJJ/0402OEfhJCdG9cXJDQPo5Q7VGRTr14roVHy8VP6Me +p4aiDnR2RaL+K7SmqsGRzP/vOSNz3ApEl24pAI8CnLMHDhkM7fKyAQrDje4aqQQv +2/a7h0LFLy27wBaSzpKmCzVrDtoFrVY8G9smtWoxyGucRpT5qxvhNcoHxpt3fyaV +mn0yXgyHZNiyxxsrPtjWA4x3bATWl5+bMRZ8rlFcmVbYFuGmLcnqXzf3ESyaCxxi +nCRGnIqWSPrlB56kS+6xXIykOsXSLgjaA1QUG/3XSNjXcKlayMa4g6qalMQ0v5jR +M2Z3FZzKnIpzJT1HejYJOC1JavnsOgWGkjGyIRZwfgpExdkCaMOQn9vVBbF7axDI +pY48aFNJEUooKYUpfKPypksbnRIJjNd2Wek2F7H2wyYrfx62MalHmOCheNoGaTGW +diokDS81m8jIUVGGuzGlbaPWLz9wBKgofzGrlPnispojJu2ZRK0aHAM6eOz5N1Zn +zh6SVgTmVD9GGo3FgVMcBxmICxroLEEUuov8vkq1dkfwDHXZDmoTRTHrefzbyjNj +K1xUIdlbWWjmUokmZ4OqAck0nRwImL13qYvWMpcWYWUxpxHzPozXBBKIxTnAI9UM +VU85htoJIh5iMXZoy+/bu3TawdaMHNWbtOlcjyE2m3m2F0Z7v7RVHuP2mYfErW/H +NRCEKhsmUulJRRDnVvdQCwrpE+rDCCf6zcybaDzhSKMkZlP8V0UDGr4Ghgg7KfYD +hg1ppiVlFhljvMn8c2fGrWqFu1KjJ2MHbpIBIPu6pnOjjyuJp7xFtsh4ReqMbNsX +WMcJF83UvJvxd6+AgGiVbckRXUz5PEF6GeLxACKzMnwDqu9VWOSXifYbhuCWbv6M +diYVqg7zjafUU1Rpwolbc2rpGiwciYX7APrnM7jnVQk6kY7GyyxoXFjhk+vGf0+r +OAHKei4ldZnLObukFdOQiIhWIdCSoh47MutmsS7RLCmVOPzozkMzg+GgoeMxgt4U +sgWMhtP2dmi2VVY0PD0HeN1LCGJqqCPyop15QkNxYTiXx5sgwggppfkZrcbJLCuT +l5q5NRzwF3a3wZerjJd1L3QXM+QprnUIlwGCg25ZWnAzZB0FvxExIQ1wsusJZ24x +uPiBACs2sSvlZoPAN0imG6EWFvX8SfNzpTnpQP6jR2iAQea0EJPqSYv0kmtYIux3 +shtacT0DH/5TaiG6DITyzTzUV1OTY/mpBqBkzVfwLN9BCzyINHmSDjsmExCKY3CY +Kd3RLuLDdM0AkjHHcl9zo7RypdAGIKnScqASeoR3yOUaP4LKysCUB6kBDg3ZftQR +IHiLOC6aMMKMdZbpKDuECBYDxblTOrcBhnAlX7eokZk1IEMSbmWngwHTfVRbfx+A +A5pUOCjxkYanl234p4KZYLRFMonalmkLb3msgY81ctzZjOxIXAb4XkirAiGDZe2V +T32HKlVjK2hzZpwRjZZ5rKS5IuRUiF6KZnnqDDzaI4dSfd4Sy1+FtmaAMwvidjf7 +FaPxCaJAccnVObwwjYNFuN1otiK6d860ot9AEUbInOOFEpTqzUUChY7Yr9Y4H/L3 +iYRbiH6LLfzjzIiGUEIkWCcar6jyZoKqwD7sMgMVGBOQjRqGx79KVgbSt1uakCBM +UaS2AEwsmFVVD8HCBGkFW5n1kAtrtmpAVZQiSSALW1BDl8csWz+SwGFpVqmHXHBp +EAYQb4HpGOE5X4sIIj1iY/B0vqpZHCUWbTUzJ7TLfVunawHVbrhKfwcIxuU2BHb3 +fg8giZ6GzNrUwN8YLeejct4iOv7IPF1XUJiqH5Qmb5USx96jtaJJLY+QDwtxfDcc +acsJKxn1MpRgrlS4vx8HOIqUhYlAnc2gmoU5BvZAXhXSUaXTLCpBGJN0uhc0Lyo3 +d2BKQzJEqmEHtB8cU544AKdjwgbWsJhFtukXYg6rSYDZjTlKSztyV1omJ+9EUIN5 +v7CCLVfKunF0Yx1LAL8JjQUhaB3cGHicmq9lC8pSBkAjKn3ZIrO8zuCDt6FTtzYZ +QVLjh0Hwh2hYanBLTDpLCpPxOdjYtWWWTRMnZ1wLwFQ0dPhbY8d6hQMrH5bBgg37 +dG0Gc/66ok9LqouBxAbrPBiooEhENhm4j2uiUSSKxY/bwlNHkgBle/VImlJ8Gt08 +f+Pe1WdvKDHXa4HUtLeScODyYJAVXQF6Ww1pLvCRy/gd +-----END SECP384R1 MLKEM1024 PUBLIC KEY-----` + + mockHybridP384MLKEM1024PrivateKey = `-----BEGIN SECP384R1 MLKEM1024 PRIVATE KEY----- +0Tu+86kOEFj2BP8fRnWqaPq6d+E1Ufhl7/OJqmg1zLoYgPtEW4QBfLgxUM8Q0nUj +bh9BKtvBsaDd3GXyKpjIM0zXv8mzSf+bZQQ9zfaaSMJ3RsPoPhViUXLYnvbDrKfM +cql24SfEHKvj8gifFuV/eg== +-----END SECP384R1 MLKEM1024 PRIVATE KEY-----` ) type TestReadAt struct { @@ -348,6 +463,7 @@ func (s *TDFSuite) Test_SimpleTDF() { tdfOptions []TDFOption tdfReadOptions []TDFReaderOption useHex bool + expectedSize int64 // override default expectedTdfSize if non-zero } metaData := []byte(`{"displayName" : "openTDF go sdk"}`) @@ -427,6 +543,54 @@ func (s *TDFSuite) Test_SimpleTDF() { }, useHex: true, }, + { + name: "metadata-hybrid-p256-mlkem768", + tdfOptions: []TDFOption{ + WithKasInformation(KASInfo{ + URL: s.kasTestURLLookup["https://f.kas/"], + PublicKey: "", + }), + WithMetaData(string(metaData)), + WithDataAttributes(attributes...), + WithWrappingKeyAlg(ocrypto.HybridSecp256r1MLKEM768Key), + }, + tdfReadOptions: []TDFReaderOption{ + WithKasAllowlist([]string{s.kasTestURLLookup["https://f.kas/"]}), + }, + expectedSize: 3364, + }, + { + name: "metadata-hybrid-p384-mlkem1024", + tdfOptions: []TDFOption{ + WithKasInformation(KASInfo{ + URL: s.kasTestURLLookup["https://g.kas/"], + PublicKey: "", + }), + WithMetaData(string(metaData)), + WithDataAttributes(attributes...), + WithWrappingKeyAlg(ocrypto.HybridSecp384r1MLKEM1024Key), + }, + tdfReadOptions: []TDFReaderOption{ + WithKasAllowlist([]string{s.kasTestURLLookup["https://g.kas/"]}), + }, + expectedSize: 4048, + }, + { + name: "metadata-hybrid-xwing", + tdfOptions: []TDFOption{ + WithKasInformation(KASInfo{ + URL: s.kasTestURLLookup["https://h.kas/"], + PublicKey: "", + }), + WithMetaData(string(metaData)), + WithDataAttributes(attributes...), + WithWrappingKeyAlg(ocrypto.HybridXWingKey), + }, + tdfReadOptions: []TDFReaderOption{ + WithKasAllowlist([]string{s.kasTestURLLookup["https://h.kas/"]}), + }, + expectedSize: 3320, + }, } for _, config := range testConfigs { @@ -447,11 +611,14 @@ func (s *TDFSuite) Test_SimpleTDF() { tdfObj, err := s.sdk.CreateTDF(fileWriter, bufReader, config.tdfOptions...) s.Require().NoError(err) + expected := expectedTdfSize if config.useHex { - s.InDelta(float64(expectedTdfSizeWithHex), float64(tdfObj.size), 36.0) - } else { - s.InDelta(float64(expectedTdfSize), float64(tdfObj.size), 36.0) + expected = expectedTdfSizeWithHex } + if config.expectedSize != 0 { + expected = config.expectedSize + } + s.InDelta(float64(expected), float64(tdfObj.size), 64.0) // test meta data and build meta data readSeeker, err := os.Open(tdfFilename) @@ -2376,7 +2543,7 @@ func (s *TDFSuite) Test_Autoconfigure() { _ = os.Remove(plaintTextFileName) _ = os.Remove(tdfFileName) }() - s.sdk.kasKeyCache.store(KASInfo{}) + s.sdk.store(KASInfo{}) // test encrypt tdo := s.testEncrypt(s.sdk, []TDFOption{WithKasInformation(kasInfoList...)}, plaintTextFileName, tdfFileName, test) @@ -2433,6 +2600,59 @@ func rotateKey(k *FakeKas, kid, private, public string) func() { } } +func (s *TDFSuite) Test_LargeManifest_WithMaxManifest() { + const maxManifestSize = 1024 * 1024 // 1MB + + // Helper to create a large manifest JSON string + createLargeManifest := func(size int) []byte { + manifest := map[string]interface{}{ + "payload": map[string]interface{}{ + "data": string(bytes.Repeat([]byte{'a'}, size)), + }, + "tdf_spec_version": TDFSpecVersion, + } + b, err := json.Marshal(manifest) + s.Require().NoError(err) + return b + } + + // Helper to create a TDF file in memory for testing + createTestTDF := func(manifest []byte, payload []byte) *bytes.Reader { + tdfBuffer := new(bytes.Buffer) + writer := zipstream.NewSegmentTDFWriter(1, zipstream.WithZip64Mode(zipstream.Zip64Auto)) + + segmentHeader, err := writer.WriteSegment(context.Background(), 0, uint64(len(payload)), crc32.ChecksumIEEE(payload)) + s.Require().NoError(err) + if len(segmentHeader) > 0 { + _, err = tdfBuffer.Write(segmentHeader) + s.Require().NoError(err) + } + _, err = tdfBuffer.Write(payload) + s.Require().NoError(err) + + finalBytes, err := writer.Finalize(context.Background(), manifest) + s.Require().NoError(err) + _, err = tdfBuffer.Write(finalBytes) + s.Require().NoError(err) + s.Require().NoError(writer.Close()) + + return bytes.NewReader(tdfBuffer.Bytes()) + } + + // Case 1: Manifest just below the limit + manifestBelow := createLargeManifest(maxManifestSize - 100) + tdfBelow := createTestTDF(manifestBelow, []byte("payload")) + _, err := s.sdk.LoadTDF(tdfBelow, WithMaxManifestSize(maxManifestSize)) + s.Require().NoError(err, "Manifest below max size should load successfully") + + // Case 2: Manifest just above the limit + manifestAbove := createLargeManifest(maxManifestSize + 100) + tdfAbove := createTestTDF(manifestAbove, []byte("payload")) + _, err = s.sdk.LoadTDF(tdfAbove, WithMaxManifestSize(maxManifestSize)) + s.Require().Error(err, "Manifest above max size should fail to load") + s.Require().ErrorContains(err, "size too large") +} + // create tdf func (s *TDFSuite) testEncrypt(sdk *SDK, encryptOpts []TDFOption, plainTextFilename, tdfFileName string, test tdfTest) *TDFObject { // create a plain text file @@ -2576,23 +2796,26 @@ func (s *TDFSuite) startBackend() { fa := &FakeAttributes{s: s} kasesToMake := []struct { - url, private, public, kid string + url, private, public, kid, algorithm string }{ - {"http://localhost:65432/", mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID}, - {"http://[::1]:65432/", mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID}, - {"https://a.kas/", mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID}, - {"https://b.kas/", mockRSAPrivateKey2, mockRSAPublicKey2, defaultKID}, - {"https://c.kas/", mockRSAPrivateKey3, mockRSAPublicKey3, defaultKID}, - {"https://d.kas/", mockECPrivateKey1, mockECPublicKey1, "e1"}, - {"https://e.kas/", mockECPrivateKey2, mockECPublicKey2, defaultKID}, - {kasAu, mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID}, - {kasCa, mockRSAPrivateKey2, mockRSAPublicKey2, defaultKID}, - {kasUk, mockRSAPrivateKey2, mockRSAPublicKey2, defaultKID}, - {kasNz, mockRSAPrivateKey3, mockRSAPublicKey3, defaultKID}, - {kasUs, mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID}, - {baseKeyURL, mockRSAPrivateKey1, mockRSAPublicKey1, baseKeyKID}, - {evenMoreSpecificKas, mockRSAPrivateKey3, mockRSAPublicKey3, "r3"}, - {obligationKas, mockRSAPrivateKey3, mockRSAPublicKey3, "r3"}, + {"http://localhost:65432/", mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID, "rsa:2048"}, + {"http://[::1]:65432/", mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID, "rsa:2048"}, + {"https://a.kas/", mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID, "rsa:2048"}, + {"https://b.kas/", mockRSAPrivateKey2, mockRSAPublicKey2, defaultKID, "rsa:2048"}, + {"https://c.kas/", mockRSAPrivateKey3, mockRSAPublicKey3, defaultKID, "rsa:2048"}, + {"https://d.kas/", mockECPrivateKey1, mockECPublicKey1, "e1", string(ocrypto.EC256Key)}, + {"https://e.kas/", mockECPrivateKey2, mockECPublicKey2, defaultKID, string(ocrypto.EC256Key)}, + {kasAu, mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID, "rsa:2048"}, + {kasCa, mockRSAPrivateKey2, mockRSAPublicKey2, defaultKID, "rsa:2048"}, + {kasUk, mockRSAPrivateKey2, mockRSAPublicKey2, defaultKID, "rsa:2048"}, + {kasNz, mockRSAPrivateKey3, mockRSAPublicKey3, defaultKID, "rsa:2048"}, + {kasUs, mockRSAPrivateKey1, mockRSAPublicKey1, defaultKID, "rsa:2048"}, + {baseKeyURL, mockRSAPrivateKey1, mockRSAPublicKey1, baseKeyKID, "rsa:2048"}, + {evenMoreSpecificKas, mockRSAPrivateKey3, mockRSAPublicKey3, "r3", "rsa:2048"}, + {obligationKas, mockRSAPrivateKey3, mockRSAPublicKey3, "r3", "rsa:2048"}, + {"https://f.kas/", mockHybridP256MLKEM768PrivateKey, mockHybridP256MLKEM768PublicKey, "h1", string(ocrypto.HybridSecp256r1MLKEM768Key)}, + {"https://g.kas/", mockHybridP384MLKEM1024PrivateKey, mockHybridP384MLKEM1024PublicKey, "h2", string(ocrypto.HybridSecp384r1MLKEM1024Key)}, + {"https://h.kas/", mockHybridXWingPrivateKey, mockHybridXWingPublicKey, "h3", string(ocrypto.HybridXWingKey)}, } fkar := &FakeKASRegistry{kases: kasesToMake, s: s} @@ -2607,7 +2830,7 @@ func (s *TDFSuite) startBackend() { s.kases[i] = FakeKas{ s: s, privateKey: ki.private, KASInfo: KASInfo{ - URL: ki.url, PublicKey: ki.public, KID: ki.kid, Algorithm: "rsa:2048", + URL: ki.url, PublicKey: ki.public, KID: ki.kid, Algorithm: ki.algorithm, }, legakeys: map[string]keyInfo{}, attrToRequiredObligations: obligationMap, @@ -2696,7 +2919,7 @@ type FakeKASRegistry struct { kasregistryconnect.UnimplementedKeyAccessServerRegistryServiceHandler s *TDFSuite kases []struct { - url, private, public, kid string + url, private, public, kid, algorithm string } } @@ -2859,6 +3082,45 @@ func (f *FakeKas) getRewrapResponse(rewrapRequest string, fulfillableObligations entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey) f.s.Require().NoError(err, "ocrypto.AsymEncryption.encrypt failed") + case "hybrid-wrapped": + kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n") + if kao.GetKid() != "" && kao.GetKid() != f.KID { + lk, ok := f.legakeys[kaoReq.GetKeyAccessObject().GetKid()] + f.s.Require().True(ok, "unable to find key [%s]", kao.GetKid()) + kasPrivateKey = strings.ReplaceAll(lk.private, "\n\t", "\n") + } + + var symmetricKey []byte + switch ocrypto.KeyType(f.Algorithm) { //nolint:exhaustive // only handle hybrid types + case ocrypto.HybridSecp256r1MLKEM768Key: + privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(kasPrivateKey)) + f.s.Require().NoError(err, "failed to extract P256+ML-KEM-768 private key from PEM") + symmetricKey, err = ocrypto.P256MLKEM768UnwrapDEK(privateKey, wrappedKey) + f.s.Require().NoError(err, "failed to unwrap P256+ML-KEM-768 wrapped key") + case ocrypto.HybridSecp384r1MLKEM1024Key: + privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(kasPrivateKey)) + f.s.Require().NoError(err, "failed to extract P384+ML-KEM-1024 private key from PEM") + symmetricKey, err = ocrypto.P384MLKEM1024UnwrapDEK(privateKey, wrappedKey) + f.s.Require().NoError(err, "failed to unwrap P384+ML-KEM-1024 wrapped key") + case ocrypto.HybridXWingKey: + privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(kasPrivateKey)) + f.s.Require().NoError(err, "failed to extract X-Wing private key from PEM") + symmetricKey, err = ocrypto.XWingUnwrapDEK(privateKey, wrappedKey) + f.s.Require().NoError(err, "failed to unwrap X-Wing wrapped key") + default: + f.s.Require().Failf("unsupported hybrid algorithm", "algorithm: %s", f.Algorithm) + } + + asymEncrypt, err := ocrypto.FromPublicPEMWithSalt(bodyData.GetClientPublicKey(), tdfSalt(), nil) + f.s.Require().NoError(err, "ocrypto.FromPublicPEMWithSalt failed") + if e, found := asymEncrypt.(ocrypto.ECEncryptor); found { + sessionKey, err := e.PublicKeyInPemFormat() + f.s.Require().NoError(err, "unable to serialize ephemeral key") + resp.SessionPublicKey = sessionKey + } + entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey) + f.s.Require().NoError(err, "ocrypto.encrypt failed") + case "wrapped": kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n") if kao.GetKid() != "" && kao.GetKid() != f.KID { @@ -2872,8 +3134,8 @@ func (f *FakeKas) getRewrapResponse(rewrapRequest string, fulfillableObligations f.s.Require().NoError(err, "ocrypto.NewAsymDecryption failed") symmetricKey, err := asymDecrypt.Decrypt(wrappedKey) f.s.Require().NoError(err, "ocrypto.Decrypt failed for kao:[%s # %s (%s)] kas:[%s # %s (%s)]", kao.GetKasUrl(), kao.GetKid(), kao.GetSplitId(), f.URL, f.KID, f.Algorithm) - asymEncrypt, err := ocrypto.NewAsymEncryption(bodyData.GetClientPublicKey()) - f.s.Require().NoError(err, "ocrypto.NewAsymEncryption failed") + asymEncrypt, err := ocrypto.FromPublicPEM(bodyData.GetClientPublicKey()) + f.s.Require().NoError(err, "ocrypto.FromPublicPEM failed") entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey) f.s.Require().NoError(err, "ocrypto.encrypt failed") @@ -3081,17 +3343,62 @@ func TestIsLessThanSemver(t *testing.T) { func TestGetKasErrorToReturn(t *testing.T) { defaultError := errors.New("default KAS error") - t.Run("InvalidArgument error returns ErrRewrapBadRequest", func(t *testing.T) { - inputError := errors.New("rpc error: code = InvalidArgument desc = invalid request") + t.Run("generic InvalidArgument (bad request) is potential tamper", func(t *testing.T) { + inputError := errors.New("rpc error: code = InvalidArgument desc = bad request") result := getKasErrorToReturn(inputError, defaultError) require.ErrorIs(t, result, ErrRewrapBadRequest) + require.ErrorIs(t, result, ErrTampered, "generic bad request may be policy binding tamper") + require.NotErrorIs(t, result, ErrKASRequestError) require.ErrorIs(t, result, defaultError) }) + t.Run("specific InvalidArgument is misconfiguration", func(t *testing.T) { + for _, msg := range []string{ + "unsupported key type", + "key access object is nil", + "ec-wrapped not enabled", + "wrapped key is empty", + "invalid ephemeral public key", + "unsupported EC key size", + "invalid ephemeral public key PEM", + "ephemeral key is not EC", + "invalid EC public key", + "no legacy key IDs found", + "failed to get additional rewrap context", + } { + t.Run(msg, func(t *testing.T) { + inputError := errors.New("rpc error: code = InvalidArgument desc = " + msg) + result := getKasErrorToReturn(inputError, defaultError) + require.ErrorIs(t, result, ErrKASRequestError) + require.NotErrorIs(t, result, ErrTampered, "specific KAS 400 must not match ErrTampered") + require.ErrorIs(t, result, defaultError) + }) + } + }) + + t.Run("message containing bad request as substring is still tamper", func(t *testing.T) { + // "desc = bad request body" contains "desc = bad request" as a substring, + // so it should be classified as potential tamper (conservative approach). + inputError := errors.New("rpc error: code = InvalidArgument desc = bad request body") + result := getKasErrorToReturn(inputError, defaultError) + require.ErrorIs(t, result, ErrRewrapBadRequest) + require.ErrorIs(t, result, ErrTampered, "substring match should err on side of tamper") + }) + + t.Run("middleware injecting bad request without desc prefix is not tamper", func(t *testing.T) { + // A message like "bad request: unsupported key type" without the "desc = " + // prefix should NOT trigger tamper classification. + inputError := errors.New("rpc error: code = InvalidArgument bad request: unsupported key type") + result := getKasErrorToReturn(inputError, defaultError) + require.ErrorIs(t, result, ErrKASRequestError) + require.NotErrorIs(t, result, ErrTampered, "unanchored bad request should not match") + }) + t.Run("PermissionDenied error returns ErrRewrapForbidden", func(t *testing.T) { inputError := errors.New("rpc error: code = PermissionDenied desc = access denied") result := getKasErrorToReturn(inputError, defaultError) require.ErrorIs(t, result, ErrRewrapForbidden) + require.NotErrorIs(t, result, ErrKASRequestError, "403 is authorization, not misconfiguration") require.ErrorIs(t, result, defaultError) }) @@ -3101,54 +3408,3 @@ func TestGetKasErrorToReturn(t *testing.T) { require.Equal(t, defaultError, result) }) } - -func (s *TDFSuite) Test_LargeManifest_WithMaxManifest() { - const maxManifestSize = 1024 * 1024 // 1MB - - // Helper to create a large manifest JSON string - createLargeManifest := func(size int) []byte { - manifest := map[string]interface{}{ - "payload": map[string]interface{}{ - "data": string(bytes.Repeat([]byte{'a'}, size)), - }, - "tdf_spec_version": TDFSpecVersion, - } - b, err := json.Marshal(manifest) - s.Require().NoError(err) - return b - } - - // Helper to create a TDF file in memory for testing - createTestTDF := func(manifest []byte, payload []byte) *bytes.Reader { - tdfBuffer := new(bytes.Buffer) - tdfWriter := archive.NewTDFWriter(tdfBuffer) - - // Add payload - err := tdfWriter.SetPayloadSize(int64(len(payload))) - s.Require().NoError(err) - err = tdfWriter.AppendPayload(payload) - s.Require().NoError(err) - - // Add manifest - err = tdfWriter.AppendManifest(string(manifest)) - s.Require().NoError(err) - - _, err = tdfWriter.Finish() - s.Require().NoError(err) - - return bytes.NewReader(tdfBuffer.Bytes()) - } - - // Case 1: Manifest just below the limit - manifestBelow := createLargeManifest(maxManifestSize - 100) - tdfBelow := createTestTDF(manifestBelow, []byte("payload")) - _, err := s.sdk.LoadTDF(tdfBelow, WithMaxManifestSize(maxManifestSize)) - s.Require().NoError(err, "Manifest below max size should load successfully") - - // Case 2: Manifest just above the limit - manifestAbove := createLargeManifest(maxManifestSize + 100) - tdfAbove := createTestTDF(manifestAbove, []byte("payload")) - _, err = s.sdk.LoadTDF(tdfAbove, WithMaxManifestSize(maxManifestSize)) - s.Require().Error(err, "Manifest above max size should fail to load") - s.Require().ErrorContains(err, "size too large") -} diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index f19331fa82..37a69a23ac 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -18,9 +18,28 @@ var ( ErrSegSigValidation = fmt.Errorf("[%w] tdf: failed integrity check on segment hash", ErrTampered) ErrTDFPayloadReadFail = fmt.Errorf("[%w] tdf: fail to read payload from tdf", ErrTampered) ErrTDFPayloadInvalidOffset = fmt.Errorf("[%w] sdk.Reader.ReadAt: negative offset", ErrTampered) - ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrTampered) ErrRootSignatureFailure = fmt.Errorf("[%w] tdf: issue verifying root signature", ErrTampered) - ErrRewrapForbidden = errors.New("tdf: rewrap request 403") + ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrTampered) + + // kasGenericBadRequest is the substring the SDK looks for in serialized + // KAS 400 errors to identify potential tamper. KAS uses the generic message + // "bad request" for errors involving secret key material (policy binding, + // DEK decryption). Per-KAO errors are serialized as plain strings through + // the proto response (not as gRPC status errors), so substring matching is + // the only classification mechanism available. + // + // The "desc = " prefix anchors the match to the gRPC status description + // field, avoiding false positives from middleware or error wrapping that + // might incidentally contain "bad request". + // + // Must stay in sync with the "bad request" message in + // service/kas/access/rewrap.go — and descriptive KAS messages must NOT + // contain this substring. + kasGenericBadRequest = "desc = bad request" + + // KAS request errors — client/configuration issues, not integrity failures + ErrKASRequestError = errors.New("tdf: KAS request error") + ErrRewrapForbidden = errors.New("tdf: rewrap request 403") ) // Custom error struct for Assertion errors diff --git a/sdk/version.go b/sdk/version.go index c9a4209a05..1575ae1d4c 100644 --- a/sdk/version.go +++ b/sdk/version.go @@ -7,5 +7,5 @@ const ( TDFSpecVersion = "4.3.0" // The three-part semantic version number of this SDK - Version = "0.11.0" // x-release-please-version + Version = "0.21.0" // x-release-please-version ) diff --git a/service/CHANGELOG.md b/service/CHANGELOG.md index c86d7beb51..0aa5884a26 100644 --- a/service/CHANGELOG.md +++ b/service/CHANGELOG.md @@ -1,5 +1,203 @@ # Changelog +## [0.16.0](https://github.com/opentdf/platform/compare/service/v0.15.0...service/v0.16.0) (2026-06-01) + + +### Features + +* **core:** add hybrid NIST EC + ML-KEM key wrapping support ([#3276](https://github.com/opentdf/platform/issues/3276)) ([1209acc](https://github.com/opentdf/platform/commit/1209acc2f8ae24af121f6a2892817c20ebb14d25)) +* **policy:** Add FQN to RegisteredResourceValues ([#3446](https://github.com/opentdf/platform/issues/3446)) ([3199583](https://github.com/opentdf/platform/commit/3199583c4a6454ac7eabe1260a142e5c5ff067ad)) +* **policy:** Add resource mapping group FQNs ([#3447](https://github.com/opentdf/platform/issues/3447)) ([6a0b3c6](https://github.com/opentdf/platform/commit/6a0b3c63795cf79b4d87d561464101c7cd2cf351)) + + +### Bug Fixes + +* **core:** remove deprecated grpc-gateway ([#3479](https://github.com/opentdf/platform/issues/3479)) ([a4230a2](https://github.com/opentdf/platform/commit/a4230a215db71ff369d49216f0f9f61fdb6c042e)) +* **deps:** bump github.com/opentdf/platform/lib/ocrypto from 0.10.0 to 0.12.0 in /service ([#3524](https://github.com/opentdf/platform/issues/3524)) ([9836404](https://github.com/opentdf/platform/commit/9836404c6732a1e7eab20ed182ff0d8eb5820462)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.30.0 to 0.31.0 in /service ([#3497](https://github.com/opentdf/platform/issues/3497)) ([a29f108](https://github.com/opentdf/platform/commit/a29f10878bafaa78cf8ec8a68b1b84ab2c298721)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.31.0 to 0.32.0 in /service ([#3523](https://github.com/opentdf/platform/issues/3523)) ([5f316f0](https://github.com/opentdf/platform/commit/5f316f0c149097383a6c96ab902e44d7ee209cd1)) +* **deps:** bump github.com/opentdf/platform/sdk from 0.19.0 to 0.20.0 in /service ([#3467](https://github.com/opentdf/platform/issues/3467)) ([7045d6e](https://github.com/opentdf/platform/commit/7045d6ed2d5ec9e9748111afecd52cb2f02ca5a0)) +* **deps:** bump github.com/opentdf/platform/sdk from 0.20.0 to 0.21.0 in /service ([#3548](https://github.com/opentdf/platform/issues/3548)) ([09fff7f](https://github.com/opentdf/platform/commit/09fff7f4c016f510841989dd86fbe32388b77d7e)) +* **deps:** bump module protocol/go to v0.30.0 throughout ([#3459](https://github.com/opentdf/platform/issues/3459)) ([8eaa502](https://github.com/opentdf/platform/commit/8eaa502b0f949ddbe18a5a1dac0931b92eec2351)) +* **policy:** include action_attribute_values in GetRegisteredResource response ([#3472](https://github.com/opentdf/platform/issues/3472)) ([29eff55](https://github.com/opentdf/platform/commit/29eff55c5470e948088d768274a03da06e092a6e)) + +## [0.15.0](https://github.com/opentdf/platform/compare/service/v0.14.0...service/v0.15.0) (2026-05-06) + + +### Features + +* **core:** pass access token verifier down to registered services ([#3428](https://github.com/opentdf/platform/issues/3428)) ([b8abf17](https://github.com/opentdf/platform/commit/b8abf17a0b71b29468b10ae397c688dca0081149)) +* **policy:** add sort support to listkaskeys ([#3344](https://github.com/opentdf/platform/issues/3344)) ([de1fe92](https://github.com/opentdf/platform/commit/de1fe926e306a15ff50fa0042b4fee988b3be1e6)) +* **policy:** support inline obligation triggers on attribute value create ([#3432](https://github.com/opentdf/platform/issues/3432)) ([876f512](https://github.com/opentdf/platform/commit/876f512f9ff944cebd3b6d65c7937446a74ace87)) + + +### Bug Fixes + +* **core:** infer JWT algorithms for JWKS keys without alg ([#3434](https://github.com/opentdf/platform/issues/3434)) ([83285e7](https://github.com/opentdf/platform/commit/83285e74c4602ebd8b485c91e32985a9bbc985a2)) +* **deps:** bump github.com/Azure/go-ntlmssp from 0.0.0-20221128193559-754e69321358 to 0.1.1 in /service ([#3388](https://github.com/opentdf/platform/issues/3388)) ([ef79989](https://github.com/opentdf/platform/commit/ef79989261b287e4500ea81e8581ed3469fb993c)) +* **deps:** bump github.com/jackc/pgx/v5 from 5.9.0 to 5.9.2 in /service ([#3371](https://github.com/opentdf/platform/issues/3371)) ([ab0974b](https://github.com/opentdf/platform/commit/ab0974b99b8d03608ec603aa391cea506954225b)) +* **deps:** bump github.com/opentdf/platform/lib/identifier from 0.3.0 to 0.4.0 in /service ([#3366](https://github.com/opentdf/platform/issues/3366)) ([4650e9b](https://github.com/opentdf/platform/commit/4650e9b69a3e656df3a191603a5b2bbd0ae640d0)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.25.0 to 0.26.0 in /service ([#3381](https://github.com/opentdf/platform/issues/3381)) ([ebc65f6](https://github.com/opentdf/platform/commit/ebc65f6778b5faafaa7e893a72ab967577efce5c)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.26.0 to 0.27.0 in /service ([#3392](https://github.com/opentdf/platform/issues/3392)) ([0c36cfa](https://github.com/opentdf/platform/commit/0c36cfaaaa8f658ff94c778e6ea45939dfeb3c0d)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.27.0 to 0.28.0 in /service ([#3416](https://github.com/opentdf/platform/issues/3416)) ([bc137f6](https://github.com/opentdf/platform/commit/bc137f67b666b429306e0d63f37b7fe9b0673058)) +* **deps:** bump github.com/opentdf/platform/sdk from 0.16.0 to 0.17.0 in /service ([#3395](https://github.com/opentdf/platform/issues/3395)) ([0382742](https://github.com/opentdf/platform/commit/0382742ec7a6d501d41a0f7fd7e4441e70f5136a)) +* **deps:** bump github.com/opentdf/platform/sdk from 0.17.0 to 0.19.0 in /service ([#3423](https://github.com/opentdf/platform/issues/3423)) ([969ac33](https://github.com/opentdf/platform/commit/969ac339aafd859789683a32860f8e4092b563d5)) + +## [0.14.0](https://github.com/opentdf/platform/compare/service/v0.13.0...service/v0.14.0) (2026-04-21) + + +### ⚠ BREAKING CHANGES + +* **sdk:** reclassify KAS 400 errors — distinguish tamper from misconfiguration ([#3166](https://github.com/opentdf/platform/issues/3166)) +* **policy:** optional namespace for RRs ([#3165](https://github.com/opentdf/platform/issues/3165)) +* **policy:** Namespace subject mappings and subject condition sets. ([#3143](https://github.com/opentdf/platform/issues/3143)) +* **policy:** Optional namespace on actions protos, NamespacedPolicy feature flag ([#3155](https://github.com/opentdf/platform/issues/3155)) +* **policy:** add namespaced actions schema and namespace-aware action queries ([#3154](https://github.com/opentdf/platform/issues/3154)) +* **policy:** only require namespace on GetAction if no id provided ([#3144](https://github.com/opentdf/platform/issues/3144)) +* **policy:** add namespace field to Actions proto ([#3130](https://github.com/opentdf/platform/issues/3130)) +* **policy:** namespace Registered Resources ([#3111](https://github.com/opentdf/platform/issues/3111)) +* **policy:** add namespace field to RegisteredResource proto ([#3110](https://github.com/opentdf/platform/issues/3110)) + +### Features + +* **authz:** Namespaced policy in decisioning ([#3226](https://github.com/opentdf/platform/issues/3226)) ([0355934](https://github.com/opentdf/platform/commit/03559346f5da4b69671a9c4fbfd56058186102bd)) +* **cli:** migrate otdfctl into platform monorepo ([#3205](https://github.com/opentdf/platform/issues/3205)) ([5177bec](https://github.com/opentdf/platform/commit/5177bec0a2f67aa1395e45a1b8a72570910f6208)) +* fix tracing ([#3242](https://github.com/opentdf/platform/issues/3242)) ([57e5680](https://github.com/opentdf/platform/commit/57e5680f994df948d2bea8e803b69b394ab28d16)) +* **policy:** add GetObligationTrigger RPC ([#3318](https://github.com/opentdf/platform/issues/3318)) ([d68e39d](https://github.com/opentdf/platform/commit/d68e39d950d94dcbb98a2f16982ea57f28d9c550)) +* **policy:** add namespace field to Actions proto ([#3130](https://github.com/opentdf/platform/issues/3130)) ([bedc9b3](https://github.com/opentdf/platform/commit/bedc9b35366104460c5fa5965819578232a3cb01)) +* **policy:** add namespace field to RegisteredResource proto ([#3110](https://github.com/opentdf/platform/issues/3110)) ([04fd85d](https://github.com/opentdf/platform/commit/04fd85d4b69b320f4dad9d21905864fba6708956)) +* **policy:** add namespaced actions schema and namespace-aware action queries ([#3154](https://github.com/opentdf/platform/issues/3154)) ([c0443f1](https://github.com/opentdf/platform/commit/c0443f1a031c7daff41eace6d6506f663a6856c3)) +* **policy:** add sort ListSubjectMappings API ([#3255](https://github.com/opentdf/platform/issues/3255)) ([9d5d757](https://github.com/opentdf/platform/commit/9d5d7570e22c6227409b01292f03c0d0624c1ce7)) +* **policy:** Add sort support listregisteredresources api ([#3312](https://github.com/opentdf/platform/issues/3312)) ([91a3ff3](https://github.com/opentdf/platform/commit/91a3ff3686512353669e35e4884fde807d73d9b0)) +* **policy:** add sort support to ListAttributes API ([#3223](https://github.com/opentdf/platform/issues/3223)) ([ec3312f](https://github.com/opentdf/platform/commit/ec3312f622dec7ed18ffa6033c86b248b47a420a)) +* **policy:** add sort support to ListKeyAccessServer ([#3287](https://github.com/opentdf/platform/issues/3287)) ([7fae2d7](https://github.com/opentdf/platform/commit/7fae2d701f3967b5ea743d4dc5ce0d41eb4d5413)) +* **policy:** Add sort support to ListNamespaces API ([#3192](https://github.com/opentdf/platform/issues/3192)) ([aac86cd](https://github.com/opentdf/platform/commit/aac86cdfbfc422149b62f85bbd752260b3a3dcd0)) +* **policy:** add sort support to listobligations api ([#3300](https://github.com/opentdf/platform/issues/3300)) ([9221cac](https://github.com/opentdf/platform/commit/9221cac2f0a0c82847f0e7973b044f78a30450d8)) +* **policy:** add sort support to ListSubjectConditionSets API ([#3272](https://github.com/opentdf/platform/issues/3272)) ([9010f12](https://github.com/opentdf/platform/commit/9010f125eef244be2ac34906c59e68319d3b8f95)) +* **policy:** add SortField proto and update PageRequest for sort support ([#3187](https://github.com/opentdf/platform/issues/3187)) ([6cf1862](https://github.com/opentdf/platform/commit/6cf1862438c7e62fa676aa74160cfa533a1f6315)) +* **policy:** Enforce same namespace when actions referenced downstream ([#3206](https://github.com/opentdf/platform/issues/3206)) ([4b5463a](https://github.com/opentdf/platform/commit/4b5463adca2dd9c0a2c14928ed6bd2c82895e0bd)) +* **policy:** namespace Registered Resources ([#3111](https://github.com/opentdf/platform/issues/3111)) ([6db1883](https://github.com/opentdf/platform/commit/6db188380d3c44f578b6170f123cb9cb1597f4d8)) +* **policy:** Namespace subject mappings and condition sets ([#3172](https://github.com/opentdf/platform/issues/3172)) ([6deed50](https://github.com/opentdf/platform/commit/6deed5086eedc959cce674a7e17d7fa406371b10)) +* **policy:** Namespace subject mappings and subject condition sets. ([#3143](https://github.com/opentdf/platform/issues/3143)) ([3006780](https://github.com/opentdf/platform/commit/3006780fea56f85b36223c134ae63a8afe109908)) +* **policy:** optional namespace for RRs ([#3165](https://github.com/opentdf/platform/issues/3165)) ([8948018](https://github.com/opentdf/platform/commit/89480186006085d2f59ebaeca6be6582db0e67d9)) +* **policy:** rollback migration strategy for namespaced actions ([#3235](https://github.com/opentdf/platform/issues/3235)) ([f7e5e01](https://github.com/opentdf/platform/commit/f7e5e01655b34852131ff6e4ad48fed1cf30e95d)) +* **policy:** Seed existing namespaces with standard actions ([#3228](https://github.com/opentdf/platform/issues/3228)) ([12136b0](https://github.com/opentdf/platform/commit/12136b0e241f5cec9101d721734568728fd2d6f3)) +* **policy:** Seed namespaces with standard actions on creation + namespaced actions for obligation triggers ([#3161](https://github.com/opentdf/platform/issues/3161)) ([984d76b](https://github.com/opentdf/platform/commit/984d76bcbf645655b691cc3749b761ba1bb02f16)) + + +### Bug Fixes + +* **ci:** Upgrade toolchain version to 1.25.8 ([#3116](https://github.com/opentdf/platform/issues/3116)) ([e1b7882](https://github.com/opentdf/platform/commit/e1b78822c0380a106e6eec05af78dc1fc9e5701f)) +* **core:** do not concat slashes directly in url/file paths ([#3290](https://github.com/opentdf/platform/issues/3290)) ([114c2a7](https://github.com/opentdf/platform/commit/114c2a7523235d68ee1afeb8883d478541e11834)) +* **deps:** bump github.com/jackc/pgx/v5 from 5.7.5 to 5.9.0 in /service ([#3316](https://github.com/opentdf/platform/issues/3316)) ([017362e](https://github.com/opentdf/platform/commit/017362edefab1df25315d68e9dae3c1cf3cad0db)) +* **deps:** bump github.com/opentdf/platform/lib/identifier from 0.2.0 to 0.3.0 in /service ([#3162](https://github.com/opentdf/platform/issues/3162)) ([8bc5dcd](https://github.com/opentdf/platform/commit/8bc5dcd21b8b2948ffa91d060710c60da9eb0e8d)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.16.0 to 0.17.0 in /service ([#3125](https://github.com/opentdf/platform/issues/3125)) ([29fec61](https://github.com/opentdf/platform/commit/29fec6125c36c33c1f7a8b97d249a3203e241840)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.17.0 to 0.21.0 in /service ([#3220](https://github.com/opentdf/platform/issues/3220)) ([e63add2](https://github.com/opentdf/platform/commit/e63add24a548285569c3cd7accd01438be25b14e)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.21.0 to 0.22.0 in /service ([#3248](https://github.com/opentdf/platform/issues/3248)) ([1ebce73](https://github.com/opentdf/platform/commit/1ebce737e092552fa23d25ae3e0f88a7b47fcb45)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.22.0 to 0.23.0 in /service ([#3271](https://github.com/opentdf/platform/issues/3271)) ([3338b8e](https://github.com/opentdf/platform/commit/3338b8e3028c4db2eb2b6e4a3a5741ede4f210ff)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.23.0 to 0.24.0 in /service ([#3321](https://github.com/opentdf/platform/issues/3321)) ([78e6022](https://github.com/opentdf/platform/commit/78e60224652cd9351f705e50ea3a843620b814f4)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.24.0 to 0.25.0 in /service ([#3333](https://github.com/opentdf/platform/issues/3333)) ([3940bf8](https://github.com/opentdf/platform/commit/3940bf897027ec01359eea1f9759fe59a3005208)) +* **deps:** bump github.com/opentdf/platform/sdk from 0.13.0 to 0.16.0 in /service ([#3356](https://github.com/opentdf/platform/issues/3356)) ([5617077](https://github.com/opentdf/platform/commit/5617077462b96b3b13f7d2f4c834710df0b42096)) +* **deps:** bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.42.0 to 1.43.0 in /service ([#3282](https://github.com/opentdf/platform/issues/3282)) ([046374a](https://github.com/opentdf/platform/commit/046374a37a442bf0bb106ec31a80183c396bec7d)) +* **deps:** bump go.opentelemetry.io/otel/sdk from 1.42.0 to 1.43.0 in /service ([#3281](https://github.com/opentdf/platform/issues/3281)) ([56b33f2](https://github.com/opentdf/platform/commit/56b33f208c0de26fe6c02b502d462024883e0215)) +* **deps:** bump google.golang.org/grpc from 1.77.0 to 1.79.3 in /service ([#3176](https://github.com/opentdf/platform/issues/3176)) ([3289502](https://github.com/opentdf/platform/commit/3289502cd2b7048bce28634137c9e903e93a824b)) +* **deps:** remove direct github.com/docker/docker dependency ([#3229](https://github.com/opentdf/platform/issues/3229)) ([2becb27](https://github.com/opentdf/platform/commit/2becb27c63d7ef34dbbc631e657e26386700e345)) +* **deps:** upgrade testcontainers-go to resolve vulns ([#3299](https://github.com/opentdf/platform/issues/3299)) ([72c6f9b](https://github.com/opentdf/platform/commit/72c6f9bd8d3612163a3ed38795e51a4d58cfc76d)) +* **ers:** include standard JWT claims in claims mode entity resolution ([#3196](https://github.com/opentdf/platform/issues/3196)) ([6d50da1](https://github.com/opentdf/platform/commit/6d50da1a3aff0d99476a8ce02bfc4e4931b08d78)) +* **ers:** ldap multi-strategy ers ([#3117](https://github.com/opentdf/platform/issues/3117)) ([d3aaf1a](https://github.com/opentdf/platform/commit/d3aaf1a6bfafab2e4447a5f34b3b089f68dea14e)) +* **policy:** deprecate ListAttributeValues in favor of existing GetAttribute ([#3108](https://github.com/opentdf/platform/issues/3108)) ([7e17c2d](https://github.com/opentdf/platform/commit/7e17c2d5ade62fb3b13265d17d663f928ced2df5)) +* **policy:** make obligation trigger uniqueness client-aware ([#3114](https://github.com/opentdf/platform/issues/3114)) ([9265bc3](https://github.com/opentdf/platform/commit/9265bc3f2790cfc0f5ac1d33bc51bca95c522bcc)) +* **policy:** omit empty attribute values from create responses ([#3193](https://github.com/opentdf/platform/issues/3193)) ([d298378](https://github.com/opentdf/platform/commit/d2983786ff04c3fec673c518593ebbc6b96cd853)) +* **policy:** only require namespace on GetAction if no id provided ([#3144](https://github.com/opentdf/platform/issues/3144)) ([10d0c0f](https://github.com/opentdf/platform/commit/10d0c0f88cd7eff3620011bd75b6c2389aa4dfb8)) +* **policy:** Optional namespace on actions protos, NamespacedPolicy feature flag ([#3155](https://github.com/opentdf/platform/issues/3155)) ([c20f039](https://github.com/opentdf/platform/commit/c20f039c6dc72bb7627075cf3cb330a6f03f2fec)) +* **policy:** order List* results by created_at ([#3088](https://github.com/opentdf/platform/issues/3088)) ([ea90ac2](https://github.com/opentdf/platform/commit/ea90ac279abbdf796d1cbe8efd8bac9c8c62de85)) +* **sdk:** normalize issuer URL before OIDC discovery ([#3261](https://github.com/opentdf/platform/issues/3261)) ([61f98c9](https://github.com/opentdf/platform/commit/61f98c94deb9a1b88e62436b6598735479db6e63)) +* **sdk:** reclassify KAS 400 errors — distinguish tamper from misconfiguration ([#3166](https://github.com/opentdf/platform/issues/3166)) ([f04a385](https://github.com/opentdf/platform/commit/f04a3856f004f68df0bcf7e355867971c8df7fdc)) +* **sdk:** remove testcontainers from consumer dependency graph ([#3129](https://github.com/opentdf/platform/issues/3129)) ([f17dcdd](https://github.com/opentdf/platform/commit/f17dcdd77a0096eb3cfd9f7d15033e4f2074cc16)) + +## [0.13.0](https://github.com/opentdf/platform/compare/service/v0.12.0...service/v0.13.0) (2026-02-18) + + +### ⚠ BREAKING CHANGES + +* **policy:** remove namespace certificate feature ([#3051](https://github.com/opentdf/platform/issues/3051)) + +### Features + +* **authz:** add casbin roleprovider interface ([#3069](https://github.com/opentdf/platform/issues/3069)) ([9d6b3f3](https://github.com/opentdf/platform/commit/9d6b3f3cc7ef8065e719345ef073bf31d87f3e22)) +* **core:** add interceptors to start options ([#3031](https://github.com/opentdf/platform/issues/3031)) ([e0b4e93](https://github.com/opentdf/platform/commit/e0b4e93ec9aa7d62d531997c432e66b10bdcab9d)) + + +### Bug Fixes + +* **deps:** bump github.com/opentdf/platform/lib/fixtures from 0.4.0 to 0.5.0 in /service ([#3034](https://github.com/opentdf/platform/issues/3034)) ([66b61b1](https://github.com/opentdf/platform/commit/66b61b1c07fddff456a3f6cdfc834f257a16d589)) +* **deps:** bump github.com/opentdf/platform/lib/ocrypto from 0.9.0 to 0.10.0 in /service ([#3080](https://github.com/opentdf/platform/issues/3080)) ([49582f0](https://github.com/opentdf/platform/commit/49582f0d5b9d99e86fd7cc0c6ac7fb98cb2207b9)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.15.0 to 0.16.0 in /service ([#3083](https://github.com/opentdf/platform/issues/3083)) ([a332f95](https://github.com/opentdf/platform/commit/a332f95e228f9f8f397649d1ce0cadf6342c9d7e)) +* **deps:** vulnerability fix in connect-rpc validate and ristretto ([#3065](https://github.com/opentdf/platform/issues/3065)) ([8860fed](https://github.com/opentdf/platform/commit/8860fed95cd4dee60052bfeb0a3bfe7b609c455e)) +* Go 1.25 ([#3053](https://github.com/opentdf/platform/issues/3053)) ([65eb7c3](https://github.com/opentdf/platform/commit/65eb7c3d5fe1892de1e4fabb9b3b7894742c3f02)) +* **kas:** dont hardcode P-256 curve ([#3073](https://github.com/opentdf/platform/issues/3073)) ([826d857](https://github.com/opentdf/platform/commit/826d857cf11a1e83108e45773d794c334c2b2e09)) +* **kas:** Fix EC P-521 typo ([#3075](https://github.com/opentdf/platform/issues/3075)) ([abc088d](https://github.com/opentdf/platform/commit/abc088d6f5f55eab240813faad2e575d87df51c1)) +* **policy:** reject unencrypted private keys for modes 1/2 ([#3072](https://github.com/opentdf/platform/issues/3072)) ([e2dc6d8](https://github.com/opentdf/platform/commit/e2dc6d8d1e1d35ce6a241bce2a23fa2d128511fa)) + + +### Code Refactoring + +* **policy:** remove namespace certificate feature ([#3051](https://github.com/opentdf/platform/issues/3051)) ([48abb81](https://github.com/opentdf/platform/commit/48abb813ae7accbfcaa6e6ad4bb7071e3476716d)) + +## [0.12.0](https://github.com/opentdf/platform/compare/service/v0.11.0...service/v0.12.0) (2026-01-27) + + +### ⚠ BREAKING CHANGES + +* remove nanotdf support ([#3013](https://github.com/opentdf/platform/issues/3013)) +* **core:** DSPX-2090 Removes unnamed key mgrs ([#2952](https://github.com/opentdf/platform/issues/2952)) + +### Features + +* **core:** Actually use KeyManager ProviderConfig ([#2837](https://github.com/opentdf/platform/issues/2837)) ([65ba2e0](https://github.com/opentdf/platform/commit/65ba2e002e30ac6624982e15c995dbd228a93541)) +* **core:** add additive CORS configuration fields ([#2941](https://github.com/opentdf/platform/issues/2941)) ([d45a34b](https://github.com/opentdf/platform/commit/d45a34b614eceab97a92b615578e92af8a8fc551)) +* **core:** add direct entitlement support ([#2630](https://github.com/opentdf/platform/issues/2630)) ([cc8337a](https://github.com/opentdf/platform/commit/cc8337a4d4b6be4cb1f4117711109c2d8d599cb9)) +* **deps:** Bump ocrypto to v0.9.0 ([#3024](https://github.com/opentdf/platform/issues/3024)) ([cd79950](https://github.com/opentdf/platform/commit/cd799509b15516f840436e6af20a14eebaa0556d)) +* **kas:** add configurable SRT skew tolerance and diagnostics ([#2886](https://github.com/opentdf/platform/issues/2886)) ([1a57227](https://github.com/opentdf/platform/commit/1a57227f6c4d9a02aecf68ca1b1b88bd265e49e0)) +* **kas:** Add nano policy binding to rewrap audit. ([#2870](https://github.com/opentdf/platform/issues/2870)) ([a12d1d4](https://github.com/opentdf/platform/commit/a12d1d4a69533cac9ac5581964c3053855584eb9)) +* **policy:** add allow_traversal to attribute definitions ([#3014](https://github.com/opentdf/platform/issues/3014)) ([bbbe21b](https://github.com/opentdf/platform/commit/bbbe21bb671f5ffedd116a08ff15779ce7034fcb)) +* **policy:** Create/Update scs to use transaction. ([#2882](https://github.com/opentdf/platform/issues/2882)) ([7493941](https://github.com/opentdf/platform/commit/74939411fc6f87aa3314873cfe5b1eb42e6f3d51)) +* **policy:** Return definition when attr value is missing ([#3012](https://github.com/opentdf/platform/issues/3012)) ([3967377](https://github.com/opentdf/platform/commit/3967377728cfc9dc8922d9327cf13bab5de2c38b)) +* Update Go toolchain version to 1.24.11 across all modules ([#2943](https://github.com/opentdf/platform/issues/2943)) ([a960eca](https://github.com/opentdf/platform/commit/a960eca78ab8870599f0aa2a315dbada355adf20)) + + +### Bug Fixes + +* **authz:** deny resources granularly when attribute value FQNs not found ([#2896](https://github.com/opentdf/platform/issues/2896)) ([802db02](https://github.com/opentdf/platform/commit/802db02f7542d7b24d61448a84f3a8b0aa38a09a)) +* **authz:** handle individual resource edge cases in decisions ([#2835](https://github.com/opentdf/platform/issues/2835)) ([fad4437](https://github.com/opentdf/platform/commit/fad443714c28f190cde723e5307451f481befd12)) +* **authz:** if entity identifier results in multiple representations, treat with AND in resource decision results ([#2860](https://github.com/opentdf/platform/issues/2860)) ([e869b35](https://github.com/opentdf/platform/commit/e869b35024bc2c752dfb89e9e7ad8a82608d8398)) +* **authz:** obligations should be logged to audit but not returned when not entitled ([#2847](https://github.com/opentdf/platform/issues/2847)) ([35da5e3](https://github.com/opentdf/platform/commit/35da5e3170780534b09f84308dc59d8af87224f9)) +* Connect RPC v1.19.1 ([#3009](https://github.com/opentdf/platform/issues/3009)) ([c354fd3](https://github.com/opentdf/platform/commit/c354fd387f2e17f764feacf302488d9afdbac5f0)) +* **core:** add obligations X-Rewrap-Additional-Context to default CORS allowed headers ([#2901](https://github.com/opentdf/platform/issues/2901)) ([d86868d](https://github.com/opentdf/platform/commit/d86868d6edb9d87e7c22c552e07dd218db98bc8d)) +* **core:** Add stderr log output option ([#2989](https://github.com/opentdf/platform/issues/2989)) ([7e01b2b](https://github.com/opentdf/platform/commit/7e01b2bae63627e13859cf5ec901561fdbc201b8)) +* **core:** DSPX-1944 Fix service negation for extra services ([#2905](https://github.com/opentdf/platform/issues/2905)) ([b07a4fe](https://github.com/opentdf/platform/commit/b07a4fe8de9085b72dc7c9569e71298be849b23e)) +* **core:** DSPX-2090 Removes unnamed key mgrs ([#2952](https://github.com/opentdf/platform/issues/2952)) ([ddd98db](https://github.com/opentdf/platform/commit/ddd98dbd6499c949f0a5ae4da42f50137ad5528b)) +* **core:** Let default basic keymanager work again ([#2858](https://github.com/opentdf/platform/issues/2858)) ([fb0b99d](https://github.com/opentdf/platform/commit/fb0b99dc6b4fd0cc5c243de474a683672df77b78)) +* **core:** remove duplicate root-level trace configuration ([#2944](https://github.com/opentdf/platform/issues/2944)) ([d323e85](https://github.com/opentdf/platform/commit/d323e856ec7cdb83d00fb29070ef105a457c5f1f)) +* **core:** Support audit and warn log levels ([#2996](https://github.com/opentdf/platform/issues/2996)) ([e789a64](https://github.com/opentdf/platform/commit/e789a64d52792bead961b8ec918f620e7c7c96ce)) +* **core:** Updates audit events when cancelled ([#2954](https://github.com/opentdf/platform/issues/2954)) ([808457e](https://github.com/opentdf/platform/commit/808457e8c8945cfe9d0318a19f7217a97874dfcb)) +* **deps:** bump github.com/opentdf/platform/lib/fixtures from 0.3.0 to 0.4.0 in /service ([#2964](https://github.com/opentdf/platform/issues/2964)) ([58512e2](https://github.com/opentdf/platform/commit/58512e23b4d51e1525516ba5c4a1d267b0a34551)) +* **deps:** bump github.com/opentdf/platform/lib/ocrypto from 0.7.0 to 0.8.0 in /service ([#2976](https://github.com/opentdf/platform/issues/2976)) ([be970db](https://github.com/opentdf/platform/commit/be970db2cdd2c1c732e4d9ae3370b22aaf185b0d)) +* **deps:** bump github.com/opentdf/platform/protocol/go from 0.13.0 to 0.14.0 in /service ([#2965](https://github.com/opentdf/platform/issues/2965)) ([6672550](https://github.com/opentdf/platform/commit/66725508ac9d611f38a68eec4bef2888cedf9437)) +* **deps:** bump the external group across 1 directory with 5 updates ([#2950](https://github.com/opentdf/platform/issues/2950)) ([6dc3bca](https://github.com/opentdf/platform/commit/6dc3bca01facc22a51293292c337963feabdf417)) +* **deps:** bump toolchain to go1.24.9 for CVEs found by govulncheck ([#2849](https://github.com/opentdf/platform/issues/2849)) ([23f76c0](https://github.com/opentdf/platform/commit/23f76c034cfb4c325d868eb96c95ba616e362db4)) +* **ers:** Do not use auth header jwt in MultiStrategy ERS ([#2862](https://github.com/opentdf/platform/issues/2862)) ([dd6256e](https://github.com/opentdf/platform/commit/dd6256ea89ceee83c3da85cda5e258031c43a0ed)) +* **kas:** Do not log index object ([#2910](https://github.com/opentdf/platform/issues/2910)) ([4f9b8b9](https://github.com/opentdf/platform/commit/4f9b8b9cff189d59583033e6451ff63557038e67)) +* **kas:** document rewrap proto fields used in bulk flow ([#2826](https://github.com/opentdf/platform/issues/2826)) ([32a7e91](https://github.com/opentdf/platform/commit/32a7e919c57fd724f5c4f01148861ebccb1a9989)) +* **kas:** Ensure root key is not logged. ([#2918](https://github.com/opentdf/platform/issues/2918)) ([de9a76e](https://github.com/opentdf/platform/commit/de9a76e403377816949365c7ac52e08a1e10ee40)) +* **kas:** Fix kas panics on bad requests ([#2916](https://github.com/opentdf/platform/issues/2916)) ([182b463](https://github.com/opentdf/platform/commit/182b4635c6a96881361ad65a9f9aa478c08cfe57)) +* **kas:** populate rewrap audit log ([#2861](https://github.com/opentdf/platform/issues/2861)) ([4fe97fd](https://github.com/opentdf/platform/commit/4fe97fd1ca6c05fb488833efb1397ab64ea0cfdf)) +* **policy:** ListKeys 404 on missing KAS ([#3001](https://github.com/opentdf/platform/issues/3001)) ([65a228b](https://github.com/opentdf/platform/commit/65a228b9222a812e8d9ab689875ebbb25ccc15d4)) +* **policy:** Return the correct total during list responses. ([#2836](https://github.com/opentdf/platform/issues/2836)) ([5c1ec9c](https://github.com/opentdf/platform/commit/5c1ec9c088e714e7a7f6f678cded31e4942b0a83)) +* **policy:** wrap SQL optional param type casts in null checks ([#2977](https://github.com/opentdf/platform/issues/2977)) ([4f6825e](https://github.com/opentdf/platform/commit/4f6825e3370f0617f443659ddeec8b1b0f751b15)) +* remove lingering kas info endpoint definition ([#2997](https://github.com/opentdf/platform/issues/2997)) ([b7e7a66](https://github.com/opentdf/platform/commit/b7e7a66d8a88847ce5c853685b53b03696b719b8)) +* remove nanotdf support ([#3013](https://github.com/opentdf/platform/issues/3013)) ([90ff7ce](https://github.com/opentdf/platform/commit/90ff7ce50754a1f37ba1cc530507c1f6e15930a0)) + ## [0.11.0](https://github.com/opentdf/platform/compare/service/v0.10.0...service/v0.11.0) (2025-10-22) diff --git a/service/authorization/authorization.go b/service/authorization/authorization.go index 26f7673b3a..9d159c93fc 100644 --- a/service/authorization/authorization.go +++ b/service/authorization/authorization.go @@ -13,7 +13,8 @@ import ( "github.com/creasty/defaults" "github.com/go-playground/validator/v10" "github.com/go-viper/mapstructure/v2" - "github.com/open-policy-agent/opa/rego" + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/rego" "github.com/opentdf/platform/protocol/go/authorization" "github.com/opentdf/platform/protocol/go/authorization/authorizationconnect" "github.com/opentdf/platform/protocol/go/common" @@ -32,8 +33,6 @@ import ( "github.com/opentdf/platform/service/pkg/config" "github.com/opentdf/platform/service/pkg/db" "github.com/opentdf/platform/service/pkg/serviceregistry" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -85,11 +84,10 @@ func NewRegistration() *serviceregistry.Service[authorizationconnect.Authorizati return &serviceregistry.Service[authorizationconnect.AuthorizationServiceHandler]{ ServiceOptions: serviceregistry.ServiceOptions[authorizationconnect.AuthorizationServiceHandler]{ - Namespace: "authorization", - ServiceDesc: &authorization.AuthorizationService_ServiceDesc, - ConnectRPCFunc: authorizationconnect.NewAuthorizationServiceHandler, - GRPCGatewayFunc: authorization.RegisterAuthorizationServiceHandler, - OnConfigUpdate: onUpdateConfig, + Namespace: "authorization", + ServiceDesc: &authorization.AuthorizationService_ServiceDesc, + ConnectRPCFunc: authorizationconnect.NewAuthorizationServiceHandler, + OnConfigUpdate: onUpdateConfig, RegisterFunc: func(srp serviceregistry.RegistrationParams) (authorizationconnect.AuthorizationServiceHandler, serviceregistry.HandlerServer) { authZCfg := new(Config) @@ -153,10 +151,6 @@ func (as AuthorizationService) IsReady(ctx context.Context) error { } func (as *AuthorizationService) GetDecisionsByToken(ctx context.Context, req *connect.Request[authorization.GetDecisionsByTokenRequest]) (*connect.Response[authorization.GetDecisionsByTokenResponse], error) { - // Extract trace context from the incoming request - propagator := otel.GetTextMapPropagator() - ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - ctx, span := as.Start(ctx, "GetDecisionsByToken") defer span.End() @@ -502,6 +496,7 @@ func (as *AuthorizationService) loadRegoAndBuiltins(cfg *Config) error { as.eval, err = rego.New( rego.Query(cfg.Rego.Query), rego.Module("entitlements.rego", string(entitlementRego)), + rego.SetRegoVersion(ast.RegoV0), rego.StrictBuiltinErrors(true), ).PrepareForEval(context.Background()) if err != nil { @@ -735,7 +730,21 @@ func retrieveAttributeDefinitions(ctx context.Context, attrFqns []string, sdk *o if err != nil { return nil, err } - return resp.GetFqnAttributeValues(), nil + // If `allow_traversal` is true for an attribute definition + // it will return an attribute definition for a missing + // value. Where before you would receive a 404 error. + // Since v1 does not expect direct entitlements + // and expects a value, we fail if there is no + // value. + fqnAttrVals := resp.GetFqnAttributeValues() + for _, fqn := range attrFqns { + normalized := strings.ToLower(fqn) + attributeAndValue, ok := fqnAttrVals[normalized] + if !ok || attributeAndValue == nil || attributeAndValue.GetValue() == nil { + return nil, status.Error(codes.NotFound, db.ErrTextNotFound) + } + } + return fqnAttrVals, nil } func getComprehensiveHierarchy(attributesMap map[string]*policy.Attribute, avf *attr.GetAttributeValuesByFqnsResponse, entitlement string, as *AuthorizationService, entitlements []string) []string { diff --git a/service/authorization/authorization.proto b/service/authorization/authorization.proto index 058f1f18f3..1fdaf313bc 100644 --- a/service/authorization/authorization.proto +++ b/service/authorization/authorization.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package authorization; -import "google/api/annotations.proto"; import "google/protobuf/any.proto"; import "policy/objects.proto"; @@ -287,19 +286,7 @@ message GetDecisionsByTokenResponse { } service AuthorizationService { - rpc GetDecisions(GetDecisionsRequest) returns (GetDecisionsResponse) { - option (google.api.http) = { - post: "/v1/authorization" - body: "*" - }; - } - rpc GetDecisionsByToken(GetDecisionsByTokenRequest) returns (GetDecisionsByTokenResponse) { - option (google.api.http) = {post: "/v1/token/authorization"}; - } - rpc GetEntitlements(GetEntitlementsRequest) returns (GetEntitlementsResponse) { - option (google.api.http) = { - post: "/v1/entitlements" - body: "*" - }; - } + rpc GetDecisions(GetDecisionsRequest) returns (GetDecisionsResponse) {} + rpc GetDecisionsByToken(GetDecisionsByTokenRequest) returns (GetDecisionsByTokenResponse) {} + rpc GetEntitlements(GetEntitlementsRequest) returns (GetEntitlementsResponse) {} } diff --git a/service/authorization/authorization_test.go b/service/authorization/authorization_test.go index 2011ea3046..775165da7a 100644 --- a/service/authorization/authorization_test.go +++ b/service/authorization/authorization_test.go @@ -9,7 +9,8 @@ import ( "go.opentelemetry.io/otel/trace/noop" "connectrpc.com/connect" - "github.com/open-policy-agent/opa/rego" + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/rego" "github.com/opentdf/platform/protocol/go/authorization" "github.com/opentdf/platform/protocol/go/entityresolution" "github.com/opentdf/platform/protocol/go/policy" @@ -23,6 +24,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/wrapperspb" ) func TestGetComprehensiveHierarchy(t *testing.T) { @@ -146,6 +148,7 @@ func Test_GetDecisionsAllOf_Pass(t *testing.T) { } testrego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -245,6 +248,7 @@ func Test_GetDecisionsAllOf_Pass(t *testing.T) { }, } testrego = rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -335,6 +339,7 @@ func Test_GetDecisions_AllOf_Fail(t *testing.T) { } testrego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -413,6 +418,7 @@ func Test_GetDecisionsAllOfWithEnvironmental_Pass(t *testing.T) { } testrego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -511,6 +517,7 @@ func Test_GetDecisionsAllOfWithEnvironmental_Fail(t *testing.T) { } testrego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -610,6 +617,7 @@ func Test_GetEntitlementsSimple(t *testing.T) { } rego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -684,6 +692,7 @@ func Test_GetEntitlementsFqnCasing(t *testing.T) { } rego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -763,6 +772,7 @@ func Test_GetEntitlements_HandlesPagination(t *testing.T) { } rego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -856,6 +866,7 @@ func Test_GetEntitlementsWithComprehensiveHierarchy(t *testing.T) { } rego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -1097,6 +1108,7 @@ func Test_GetDecisions_RA_FQN_Edge_Cases(t *testing.T) { } testrego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -1191,7 +1203,50 @@ func Test_GetDecisions_RA_FQN_Edge_Cases(t *testing.T) { assert.Len(t, resp.Msg.GetDecisionResponses(), 1) assert.Equal(t, authorization.DecisionResponse_DECISION_DENY, resp.Msg.GetDecisionResponses()[0].GetDecision()) - ////////// TEST3: No FQNs in Resource Attribute ///////// + ////////// TEST4: FQN present but value missing ////////// + + // getAttributesByFQN with allow_travesal=true. Will return an attribute definition but no attribute value (bc it doesn't exist) + getAttributesByValueFqnsResponse = attr.GetAttributeValuesByFqnsResponse{FqnAttributeValues: map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue{ + "https://example.com/attr/foo/value/doesntexist_allow_traversal": { + Attribute: &policy.Attribute{ + AllowTraversal: wrapperspb.Bool(true), + }, + Value: nil, + }, + }} + errGetAttributesByValueFqns = nil + + // set the request + req = connect.Request[authorization.GetDecisionsRequest]{ + Msg: &authorization.GetDecisionsRequest{ + DecisionRequests: []*authorization.DecisionRequest{ + { + Actions: []*policy.Action{}, + EntityChains: []*authorization.EntityChain{ + { + Id: "ec1", + Entities: []*authorization.Entity{ + {Id: "e1", EntityType: &authorization.Entity_UserName{UserName: "bob.smith"}, Category: authorization.Entity_CATEGORY_SUBJECT}, + }, + }, + }, + ResourceAttributes: []*authorization.ResourceAttribute{ + {AttributeValueFqns: []string{"https://example.com/attr/foo/value/doesntexist_allow_traversal"}}, + }, + }, + }, + }, + } + + resp, err = as.GetDecisions(ctx, &req) + + require.NoError(t, err) + assert.NotNil(t, resp) + + assert.Len(t, resp.Msg.GetDecisionResponses(), 1) + assert.Equal(t, authorization.DecisionResponse_DECISION_DENY, resp.Msg.GetDecisionResponses()[0].GetDecision()) + + ////////// TEST5: No FQNs in Resource Attribute ///////// // should not hit get attributes by value fqns getAttributesByValueFqnsResponse = attr.GetAttributeValuesByFqnsResponse{} @@ -1272,6 +1327,7 @@ func Test_GetDecisionsAllOf_Pass_EC_RA_Length_Mismatch(t *testing.T) { /////// TEST1: Three entity chains, one resource attribute /////// testrego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -1394,6 +1450,7 @@ func Test_GetDecisionsAllOf_Pass_EC_RA_Length_Mismatch(t *testing.T) { } testrego = rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -1463,6 +1520,7 @@ func Test_GetDecisionsAllOf_Pass_EC_RA_Length_Mismatch(t *testing.T) { }, } testrego = rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example @@ -1586,6 +1644,7 @@ func Test_GetDecisions_Empty_EC_RA(t *testing.T) { } testrego := rego.New( + rego.SetRegoVersion(ast.RegoV0), rego.Query("data.example.p"), rego.Module("example.rego", `package example diff --git a/service/authorization/authorization_test_structures.go b/service/authorization/authorization_test_structures.go index e9f4d764f5..de0d1127cf 100644 --- a/service/authorization/authorization_test_structures.go +++ b/service/authorization/authorization_test_structures.go @@ -75,20 +75,20 @@ func (*myAttributesClient) DeactivateAttributeValue(_ context.Context, _ *attr.D return &attr.DeactivateAttributeValueResponse{}, nil } -func (*myAttributesClient) AssignKeyAccessServerToAttribute(_ context.Context, _ *attr.AssignKeyAccessServerToAttributeRequest) (*attr.AssignKeyAccessServerToAttributeResponse, error) { - return &attr.AssignKeyAccessServerToAttributeResponse{}, nil +func (*myAttributesClient) AssignKeyAccessServerToAttribute(_ context.Context, _ *attr.AssignKeyAccessServerToAttributeRequest) (*attr.AssignKeyAccessServerToAttributeResponse, error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + return &attr.AssignKeyAccessServerToAttributeResponse{}, nil //nolint:staticcheck // Deprecated response kept for compatibility tests. } -func (*myAttributesClient) RemoveKeyAccessServerFromAttribute(_ context.Context, _ *attr.RemoveKeyAccessServerFromAttributeRequest) (*attr.RemoveKeyAccessServerFromAttributeResponse, error) { - return &attr.RemoveKeyAccessServerFromAttributeResponse{}, nil +func (*myAttributesClient) RemoveKeyAccessServerFromAttribute(_ context.Context, _ *attr.RemoveKeyAccessServerFromAttributeRequest) (*attr.RemoveKeyAccessServerFromAttributeResponse, error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + return &attr.RemoveKeyAccessServerFromAttributeResponse{}, nil //nolint:staticcheck // Deprecated response kept for compatibility tests. } -func (*myAttributesClient) AssignKeyAccessServerToValue(_ context.Context, _ *attr.AssignKeyAccessServerToValueRequest) (*attr.AssignKeyAccessServerToValueResponse, error) { - return &attr.AssignKeyAccessServerToValueResponse{}, nil +func (*myAttributesClient) AssignKeyAccessServerToValue(_ context.Context, _ *attr.AssignKeyAccessServerToValueRequest) (*attr.AssignKeyAccessServerToValueResponse, error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + return &attr.AssignKeyAccessServerToValueResponse{}, nil //nolint:staticcheck // Deprecated response kept for compatibility tests. } -func (*myAttributesClient) RemoveKeyAccessServerFromValue(_ context.Context, _ *attr.RemoveKeyAccessServerFromValueRequest) (*attr.RemoveKeyAccessServerFromValueResponse, error) { - return &attr.RemoveKeyAccessServerFromValueResponse{}, nil +func (*myAttributesClient) RemoveKeyAccessServerFromValue(_ context.Context, _ *attr.RemoveKeyAccessServerFromValueRequest) (*attr.RemoveKeyAccessServerFromValueResponse, error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + return &attr.RemoveKeyAccessServerFromValueResponse{}, nil //nolint:staticcheck // Deprecated response kept for compatibility tests. } func (*myAttributesClient) AssignPublicKeyToAttribute(_ context.Context, _ *attr.AssignPublicKeyToAttributeRequest) (*attr.AssignPublicKeyToAttributeResponse, error) { @@ -301,20 +301,20 @@ func (*paginatedMockAttributesClient) DeactivateAttributeValue(_ context.Context return &attr.DeactivateAttributeValueResponse{}, nil } -func (*paginatedMockAttributesClient) AssignKeyAccessServerToAttribute(_ context.Context, _ *attr.AssignKeyAccessServerToAttributeRequest) (*attr.AssignKeyAccessServerToAttributeResponse, error) { - return &attr.AssignKeyAccessServerToAttributeResponse{}, nil +func (*paginatedMockAttributesClient) AssignKeyAccessServerToAttribute(_ context.Context, _ *attr.AssignKeyAccessServerToAttributeRequest) (*attr.AssignKeyAccessServerToAttributeResponse, error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + return &attr.AssignKeyAccessServerToAttributeResponse{}, nil //nolint:staticcheck // Deprecated response kept for compatibility tests. } -func (*paginatedMockAttributesClient) RemoveKeyAccessServerFromAttribute(_ context.Context, _ *attr.RemoveKeyAccessServerFromAttributeRequest) (*attr.RemoveKeyAccessServerFromAttributeResponse, error) { - return &attr.RemoveKeyAccessServerFromAttributeResponse{}, nil +func (*paginatedMockAttributesClient) RemoveKeyAccessServerFromAttribute(_ context.Context, _ *attr.RemoveKeyAccessServerFromAttributeRequest) (*attr.RemoveKeyAccessServerFromAttributeResponse, error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + return &attr.RemoveKeyAccessServerFromAttributeResponse{}, nil //nolint:staticcheck // Deprecated response kept for compatibility tests. } -func (*paginatedMockAttributesClient) AssignKeyAccessServerToValue(_ context.Context, _ *attr.AssignKeyAccessServerToValueRequest) (*attr.AssignKeyAccessServerToValueResponse, error) { - return &attr.AssignKeyAccessServerToValueResponse{}, nil +func (*paginatedMockAttributesClient) AssignKeyAccessServerToValue(_ context.Context, _ *attr.AssignKeyAccessServerToValueRequest) (*attr.AssignKeyAccessServerToValueResponse, error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + return &attr.AssignKeyAccessServerToValueResponse{}, nil //nolint:staticcheck // Deprecated response kept for compatibility tests. } -func (*paginatedMockAttributesClient) RemoveKeyAccessServerFromValue(_ context.Context, _ *attr.RemoveKeyAccessServerFromValueRequest) (*attr.RemoveKeyAccessServerFromValueResponse, error) { - return &attr.RemoveKeyAccessServerFromValueResponse{}, nil +func (*paginatedMockAttributesClient) RemoveKeyAccessServerFromValue(_ context.Context, _ *attr.RemoveKeyAccessServerFromValueRequest) (*attr.RemoveKeyAccessServerFromValueResponse, error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + return &attr.RemoveKeyAccessServerFromValueResponse{}, nil //nolint:staticcheck // Deprecated response kept for compatibility tests. } func (*paginatedMockAttributesClient) AssignPublicKeyToAttribute(_ context.Context, _ *attr.AssignPublicKeyToAttributeRequest) (*attr.AssignPublicKeyToAttributeResponse, error) { diff --git a/service/authorization/v2/authorization.go b/service/authorization/v2/authorization.go index 4fb8e18b55..b90b56db3d 100644 --- a/service/authorization/v2/authorization.go +++ b/service/authorization/v2/authorization.go @@ -19,8 +19,6 @@ import ( ctxAuth "github.com/opentdf/platform/service/pkg/auth" "github.com/opentdf/platform/service/pkg/cache" "github.com/opentdf/platform/service/pkg/serviceregistry" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -141,15 +139,11 @@ func (as *Service) GetEntitlements(ctx context.Context, req *connect.Request[aut ctx, span := as.Start(ctx, "GetEntitlements") defer span.End() - // Extract trace context from the incoming request - propagator := otel.GetTextMapPropagator() - ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - entityIdentifier := req.Msg.GetEntityIdentifier() withComprehensiveHierarchy := req.Msg.GetWithComprehensiveHierarchy() // When authorization service can consume cached policy, switch to the other PDP (process based on policy passed in) - pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements) + pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements) if err != nil { return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToGetEntitlements, ErrFailedToInitPDP, err)) } @@ -172,11 +166,7 @@ func (as *Service) GetDecision(ctx context.Context, req *connect.Request[authzV2 ctx, span := as.Start(ctx, "GetDecision") defer span.End() - // Extract trace context from the incoming request - propagator := otel.GetTextMapPropagator() - ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - - pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements) + pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements) if err != nil { return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err)) } @@ -222,11 +212,7 @@ func (as *Service) GetDecisionMultiResource(ctx context.Context, req *connect.Re ctx, span := as.Start(ctx, "GetDecisionMultiResource") defer span.End() - // Extract trace context from the incoming request - propagator := otel.GetTextMapPropagator() - ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - - pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements) + pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements) if err != nil { return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err)) } @@ -275,11 +261,7 @@ func (as *Service) GetDecisionBulk(ctx context.Context, req *connect.Request[aut ctx, span := as.Start(ctx, "GetDecisionBulk") defer span.End() - // Extract trace context from the incoming request - propagator := otel.GetTextMapPropagator() - ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - - pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements) + pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements) if err != nil { return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err)) } diff --git a/service/authorization/v2/config.go b/service/authorization/v2/config.go index 24cc4d3236..44e16a8c34 100644 --- a/service/authorization/v2/config.go +++ b/service/authorization/v2/config.go @@ -20,6 +20,9 @@ type Config struct { // enable entity direct entitlements that do not require subject mappings AllowDirectEntitlements bool `mapstructure:"allow_direct_entitlements" json:"allow_direct_entitlements" default:"false"` + + // enforce strict namespaced entitlement evaluation behavior in access decisioning + EnforceNamespacedEntitlements bool `mapstructure:"enforce_namespaced_entitlements" json:"enforce_namespaced_entitlements" default:"false"` } // Validate tests for a sensible configuration @@ -56,5 +59,7 @@ func (c *Config) LogValue() slog.Value { slog.String("refresh_interval", c.Cache.RefreshInterval), ), ), + slog.Bool("allow_direct_entitlements", c.AllowDirectEntitlements), + slog.Bool("enforce_namespaced_entitlements", c.EnforceNamespacedEntitlements), ) } diff --git a/service/cmd/keygen/main.go b/service/cmd/keygen/main.go new file mode 100644 index 0000000000..721191e286 --- /dev/null +++ b/service/cmd/keygen/main.go @@ -0,0 +1,116 @@ +// Package main generates hybrid post-quantum KAS key pairs (X-Wing, P256+ML-KEM-768, P384+ML-KEM-1024) +// as PEM files for use with the OpenTDF platform. +package main + +import ( + "flag" + "log" + "os" + "path/filepath" + + "github.com/opentdf/platform/lib/ocrypto" +) + +type keySpec struct { + name string + newKeyPair func() (privatePEM, publicPEM string, err error) + privateOut string + publicOut string +} + +func main() { + outputDir := flag.String("output", ".", "directory to write PEM files") + flag.Parse() + + if err := os.MkdirAll(*outputDir, 0o755); err != nil { + log.Fatalf("failed to create output directory: %v", err) + } + + specs := []keySpec{ + { + name: "X-Wing", + newKeyPair: generateXWing, + privateOut: "kas-xwing-private.pem", + publicOut: "kas-xwing-public.pem", + }, + { + name: "P256+ML-KEM-768", + newKeyPair: generateP256MLKEM768, + privateOut: "kas-p256mlkem768-private.pem", + publicOut: "kas-p256mlkem768-public.pem", + }, + { + name: "P384+ML-KEM-1024", + newKeyPair: generateP384MLKEM1024, + privateOut: "kas-p384mlkem1024-private.pem", + publicOut: "kas-p384mlkem1024-public.pem", + }, + } + + for _, s := range specs { + privatePEM, publicPEM, err := s.newKeyPair() + if err != nil { + log.Fatalf("failed to generate %s key pair: %v", s.name, err) + } + + privPath := filepath.Join(*outputDir, s.privateOut) + pubPath := filepath.Join(*outputDir, s.publicOut) + + if err := os.WriteFile(privPath, []byte(privatePEM), 0o600); err != nil { + log.Fatalf("failed to write %s: %v", privPath, err) + } + if err := os.WriteFile(pubPath, []byte(publicPEM), 0o600); err != nil { + log.Fatalf("failed to write %s: %v", pubPath, err) + } + + log.Printf("Generated %s key pair:\n - Private: %s\n - Public: %s", s.name, privPath, pubPath) + } +} + +func generateXWing() (string, string, error) { + kp, err := ocrypto.NewXWingKeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} + +func generateP256MLKEM768() (string, string, error) { + kp, err := ocrypto.NewP256MLKEM768KeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} + +func generateP384MLKEM1024() (string, string, error) { + kp, err := ocrypto.NewP384MLKEM1024KeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} diff --git a/service/cmd/version.go b/service/cmd/version.go index 549f71498a..ffe9c52d66 100644 --- a/service/cmd/version.go +++ b/service/cmd/version.go @@ -2,7 +2,7 @@ package cmd import "github.com/spf13/cobra" -const Version = "0.11.0" // Service Version // x-release-please-version +const Version = "0.16.0" // Service Version // x-release-please-version func init() { rootCmd.AddCommand(&cobra.Command{ diff --git a/service/entityresolution/claims/entity_resolution.go b/service/entityresolution/claims/entity_resolution.go index 76c7cd387e..2b664ea31a 100644 --- a/service/entityresolution/claims/entity_resolution.go +++ b/service/entityresolution/claims/entity_resolution.go @@ -110,6 +110,26 @@ func getEntitiesFromToken(jwtString string) ([]*authorization.Entity, error) { } claims := token.PrivateClaims() + // PrivateClaims() excludes standard registered JWT claims (sub, iss, aud, etc.) + // because the jwx library stores them as typed fields. Add them back so selectors + // like .sub work in subject mapping conditions. + if sub := token.Subject(); sub != "" { + claims["sub"] = sub + } + if iss := token.Issuer(); iss != "" { + claims["iss"] = iss + } + if jti := token.JwtID(); jti != "" { + claims["jti"] = jti + } + if aud := token.Audience(); len(aud) > 0 { + // Convert []string to []interface{} for structpb compatibility + audSlice := make([]interface{}, len(aud)) + for i, a := range aud { + audSlice[i] = a + } + claims["aud"] = audSlice + } entities := []*authorization.Entity{} // Convert map[string]interface{} to *structpb.Struct diff --git a/service/entityresolution/claims/entity_resolution_test.go b/service/entityresolution/claims/entity_resolution_test.go index cf16ac4359..895edbcff2 100644 --- a/service/entityresolution/claims/entity_resolution_test.go +++ b/service/entityresolution/claims/entity_resolution_test.go @@ -113,4 +113,8 @@ func Test_JWTToEntityChainClaims(t *testing.T) { claimsMap := unpackedStruct.AsMap() assert.Equal(t, "helloworld", claimsMap["name"]) + // Standard registered claims like "sub" must be included for subject mapping selectors + assert.Equal(t, "1234567890", claimsMap["sub"]) + // Time-based claims (iat, exp, nbf) are excluded — structpb cannot serialize time.Time + assert.NotContains(t, claimsMap, "iat") } diff --git a/service/entityresolution/claims/v2/entity_resolution.go b/service/entityresolution/claims/v2/entity_resolution.go index d8481f840c..41003d7ee5 100644 --- a/service/entityresolution/claims/v2/entity_resolution.go +++ b/service/entityresolution/claims/v2/entity_resolution.go @@ -110,6 +110,26 @@ func getEntitiesFromToken(jwtString string) ([]*entity.Entity, error) { } claims := token.PrivateClaims() + // PrivateClaims() excludes standard registered JWT claims (sub, iss, aud, etc.) + // because the jwx library stores them as typed fields. Add them back so selectors + // like .sub work in subject mapping conditions. + if sub := token.Subject(); sub != "" { + claims["sub"] = sub + } + if iss := token.Issuer(); iss != "" { + claims["iss"] = iss + } + if jti := token.JwtID(); jti != "" { + claims["jti"] = jti + } + if aud := token.Audience(); len(aud) > 0 { + // Convert []string to []interface{} for structpb compatibility + audSlice := make([]interface{}, len(aud)) + for i, a := range aud { + audSlice[i] = a + } + claims["aud"] = audSlice + } entities := []*entity.Entity{} // Convert map[string]interface{} to *structpb.Struct diff --git a/service/entityresolution/claims/v2/entity_resolution_test.go b/service/entityresolution/claims/v2/entity_resolution_test.go index b5ac14b369..644bdff4e5 100644 --- a/service/entityresolution/claims/v2/entity_resolution_test.go +++ b/service/entityresolution/claims/v2/entity_resolution_test.go @@ -113,4 +113,8 @@ func Test_JWTToEntityChainClaims(t *testing.T) { claimsMap := unpackedStruct.AsMap() assert.Equal(t, "helloworld", claimsMap["name"]) + // Standard registered claims like "sub" must be included for subject mapping selectors + assert.Equal(t, "1234567890", claimsMap["sub"]) + // Time-based claims (iat, exp, nbf) are excluded — structpb cannot serialize time.Time + assert.NotContains(t, claimsMap, "iat") } diff --git a/service/entityresolution/entity_resolution.proto b/service/entityresolution/entity_resolution.proto index 4b93ab2fad..fde907d142 100644 --- a/service/entityresolution/entity_resolution.proto +++ b/service/entityresolution/entity_resolution.proto @@ -3,9 +3,8 @@ syntax = "proto3"; package entityresolution; import "authorization/authorization.proto"; -import "google/protobuf/struct.proto"; import "google/protobuf/any.proto"; -import "google/api/annotations.proto"; +import "google/protobuf/struct.proto"; /* Example: Get idp attributes for bob and alice (both represented using an email address @@ -108,17 +107,7 @@ message CreateEntityChainFromJwtResponse { service EntityResolutionService { // Deprecated: use v2 ResolveEntities instead - rpc ResolveEntities(ResolveEntitiesRequest) returns (ResolveEntitiesResponse) { - option (google.api.http) = { - post: "/entityresolution/resolve" - body: "*"; - }; - } + rpc ResolveEntities(ResolveEntitiesRequest) returns (ResolveEntitiesResponse) {} // Deprecated: use v2 CreateEntityChainsFromTokens instead - rpc CreateEntityChainFromJwt(CreateEntityChainFromJwtRequest) returns (CreateEntityChainFromJwtResponse) { - option (google.api.http) = { - post: "/entityresolution/entitychain" - body: "*"; - }; - } + rpc CreateEntityChainFromJwt(CreateEntityChainFromJwtRequest) returns (CreateEntityChainFromJwtResponse) {} } diff --git a/service/entityresolution/entityresolution.go b/service/entityresolution/entityresolution.go index ab8a3d0674..cb1c58ef47 100644 --- a/service/entityresolution/entityresolution.go +++ b/service/entityresolution/entityresolution.go @@ -33,10 +33,9 @@ type EntityResolution struct { func NewRegistration() *serviceregistry.Service[entityresolutionconnect.EntityResolutionServiceHandler] { return &serviceregistry.Service[entityresolutionconnect.EntityResolutionServiceHandler]{ ServiceOptions: serviceregistry.ServiceOptions[entityresolutionconnect.EntityResolutionServiceHandler]{ - Namespace: "entityresolution", - ServiceDesc: &entityresolution.EntityResolutionService_ServiceDesc, - ConnectRPCFunc: entityresolutionconnect.NewEntityResolutionServiceHandler, - GRPCGatewayFunc: entityresolution.RegisterEntityResolutionServiceHandler, + Namespace: "entityresolution", + ServiceDesc: &entityresolution.EntityResolutionService_ServiceDesc, + ConnectRPCFunc: entityresolutionconnect.NewEntityResolutionServiceHandler, RegisterFunc: func(srp serviceregistry.RegistrationParams) (entityresolutionconnect.EntityResolutionServiceHandler, serviceregistry.HandlerServer) { var inputConfig ERSConfig diff --git a/service/entityresolution/integration/claims_attributes_test.go b/service/entityresolution/integration/claims_attributes_test.go new file mode 100644 index 0000000000..c601f026f3 --- /dev/null +++ b/service/entityresolution/integration/claims_attributes_test.go @@ -0,0 +1,120 @@ +package integration + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/entityresolution" + "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +// TestClaimsERSSubjectMapping verifies that when the ERS is configured with +// mode: "claims", subject mapping selectors match JWT private claim names +// directly at the top level of the entity — e.g. ".department" matches a token +// containing "department": "Finance". +// +// The correct selector also depends on the multi-valued setting of the Keycloak +// User Attribute mapper: +// - Multi-valued OFF → claim is emitted as a string → use ".department" +// - Multi-valued ON → claim is emitted as an array → use ".department[]" +// +// Contrast with TestKeycloakUserAttributeSubjectMapping: in Keycloak ERS mode +// (the default), the entity is a Keycloak user object where custom attributes +// are nested under ".attributes.[]", not at ".". +// +// See: https://github.com/opentdf/platform/blob/main/docs/Configuring.md#L479 +func TestClaimsERSSubjectMapping(t *testing.T) { + const attrFQN = "https://example.com/attr/department/value/finance" + + // Simulate what the Claims ERS does: token.PrivateClaims() returns a flat + // map of JWT claim name → value, which is converted to structpb.Struct and + // used as the entity directly (no Keycloak user object lookup). + + t.Run("multi-valued OFF: claim is a string, selector is .department", func(t *testing.T) { + // Keycloak User Attribute mapper with multi-valued OFF emits a plain string. + entityStruct, err := structpb.NewStruct(map[string]interface{}{ + "department": "Finance", + "sub": "jen", + }) + require.NoError(t, err) + entity := &entityresolution.EntityRepresentation{ + OriginalId: "jen", + AdditionalProps: []*structpb.Struct{entityStruct}, + } + + t.Run("string selector matches", func(t *testing.T) { + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappings( + buildAttributeSubjectMapping(attrFQN, ".department", "Finance"), + entity, + ) + require.NoError(t, err) + assert.Equal(t, []string{attrFQN}, entitlements, + "selector '.department' should match a string-valued JWT claim") + }) + + t.Run("array selector does not match", func(t *testing.T) { + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappings( + buildAttributeSubjectMapping(attrFQN, ".department[]", "Finance"), + entity, + ) + require.NoError(t, err) + assert.Empty(t, entitlements, + "selector '.department[]' should NOT match when the claim is a string, not an array") + }) + }) + + t.Run("multi-valued ON: claim is an array, selector is .department[]", func(t *testing.T) { + // Keycloak User Attribute mapper with multi-valued ON emits an array. + entityStruct, err := structpb.NewStruct(map[string]interface{}{ + "department": []interface{}{"Finance"}, + "sub": "jen", + }) + require.NoError(t, err) + entity := &entityresolution.EntityRepresentation{ + OriginalId: "jen", + AdditionalProps: []*structpb.Struct{entityStruct}, + } + + t.Run("array selector matches", func(t *testing.T) { + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappings( + buildAttributeSubjectMapping(attrFQN, ".department[]", "Finance"), + entity, + ) + require.NoError(t, err) + assert.Equal(t, []string{attrFQN}, entitlements, + "selector '.department[]' should match an array-valued JWT claim") + }) + + t.Run("string selector does not match", func(t *testing.T) { + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappings( + buildAttributeSubjectMapping(attrFQN, ".department", "Finance"), + entity, + ) + require.NoError(t, err) + assert.Empty(t, entitlements, + "selector '.department' should NOT match when the claim is an array, not a string") + }) + }) + + t.Run("Keycloak-style nested selector never matches claims-mode entity", func(t *testing.T) { + entityStruct, err := structpb.NewStruct(map[string]interface{}{ + "department": "Finance", + "sub": "jen", + }) + require.NoError(t, err) + entity := &entityresolution.EntityRepresentation{ + OriginalId: "jen", + AdditionalProps: []*structpb.Struct{entityStruct}, + } + + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappings( + buildAttributeSubjectMapping(attrFQN, ".attributes.department[]", "Finance"), + entity, + ) + require.NoError(t, err) + assert.Empty(t, entitlements, + "selector '.attributes.department[]' should NOT match: claims entities are flat JWT claims, not Keycloak user objects") + }) +} diff --git a/service/entityresolution/integration/claims_test.go b/service/entityresolution/integration/claims_test.go index 076ba55f72..945715ffd3 100644 --- a/service/entityresolution/integration/claims_test.go +++ b/service/entityresolution/integration/claims_test.go @@ -377,10 +377,8 @@ func (a *ClaimsTestAdapter) createTestJWT(clientID, username, email string, addi _ = token.Set("email", email) // Additional custom claims - if additionalClaims != nil { - for key, value := range additionalClaims { - _ = token.Set(key, value) - } + for key, value := range additionalClaims { + _ = token.Set(key, value) } // Create JWK key @@ -417,10 +415,8 @@ func (a *ClaimsTestAdapter) createUnsignedTestJWT(clientID, username, email stri } // Add additional claims - if additionalClaims != nil { - for key, value := range additionalClaims { - claims[key] = value - } + for key, value := range additionalClaims { + claims[key] = value } // Create header and payload diff --git a/service/entityresolution/integration/internal/chain_contract_tests.go b/service/entityresolution/integration/internal/chain_contract_tests.go index 31189a7bd4..c298973982 100644 --- a/service/entityresolution/integration/internal/chain_contract_tests.go +++ b/service/entityresolution/integration/internal/chain_contract_tests.go @@ -182,9 +182,7 @@ func (suite *ChainContractTestSuite) executeChainRequest(t *testing.T, implement return nil, err } - if suite.handleConnectionErrors(t, err) { - return nil, err - } + suite.handleConnectionErrors(t, err) require.NoError(t, err, "Unexpected error: %v", err) require.NotNil(t, resp, "Response should not be nil") @@ -193,18 +191,18 @@ func (suite *ChainContractTestSuite) executeChainRequest(t *testing.T, implement } // handleConnectionErrors checks for connection-related errors and skips tests if service unavailable -func (suite *ChainContractTestSuite) handleConnectionErrors(t *testing.T, err error) bool { +func (suite *ChainContractTestSuite) handleConnectionErrors(t *testing.T, err error) { if err == nil { - return false + return } var connectErr *connect.Error if !errors.As(err, &connectErr) { - return false + return } if connectErr.Code() != connect.CodeInternal { - return false + return } errorMsg := connectErr.Message() @@ -212,10 +210,8 @@ func (suite *ChainContractTestSuite) handleConnectionErrors(t *testing.T, err er strings.Contains(errorMsg, "could not get token") || strings.Contains(errorMsg, "failed to login") { t.Skipf("Service unavailable (likely connection issue): %v", errorMsg) - return true + return } - - return false } // validateSingleChain validates a single entity chain according to the validation rule diff --git a/service/entityresolution/integration/internal/container_helpers.go b/service/entityresolution/integration/internal/container_helpers.go index e29c2fe32e..252e7c70ba 100644 --- a/service/entityresolution/integration/internal/container_helpers.go +++ b/service/entityresolution/integration/internal/container_helpers.go @@ -7,7 +7,6 @@ import ( "log/slog" "time" - "github.com/docker/go-connections/nat" tc "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) @@ -84,12 +83,12 @@ func (cm *ContainerManager) GetMappedPort(ctx context.Context, containerPort str return 0, errors.New("container not started") } - mappedPort, err := cm.Container.MappedPort(ctx, nat.Port(containerPort)) + mappedPort, err := cm.Container.MappedPort(ctx, containerPort) if err != nil { return 0, fmt.Errorf("failed to get mapped port for %s: %w", containerPort, err) } - return mappedPort.Int(), nil + return int(mappedPort.Num()), nil } // GetHost returns the container host (typically localhost) diff --git a/service/entityresolution/integration/keycloak_attributes_test.go b/service/entityresolution/integration/keycloak_attributes_test.go new file mode 100644 index 0000000000..44d2ea1cd3 --- /dev/null +++ b/service/entityresolution/integration/keycloak_attributes_test.go @@ -0,0 +1,153 @@ +package integration + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/Nerzal/gocloak/v13" + "github.com/opentdf/platform/lib/flattening" + "github.com/opentdf/platform/protocol/go/entityresolution" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/service/entityresolution/integration/internal" + "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +// TestKeycloakUserAttributeSubjectMapping verifies that when using the Keycloak ERS, +// subject mapping selectors must match the Keycloak user object structure, NOT raw JWT +// claim names. Custom Keycloak user attributes are nested under `.attributes.[]`, +// so a selector like `.department` will never match even if the JWT contains +// `"department": "Finance"`. The correct selector is `.attributes.department[]`. +// +// This test exists to document and verify actual Keycloak ERS behavior, and to serve +// as a reference for community members debugging subject mapping failures. +// See: https://github.com/orgs/opentdf/discussions/3115 +func TestKeycloakUserAttributeSubjectMapping(t *testing.T) { + if testing.Short() { + t.Skip("Skipping Keycloak integration tests in short mode") + } + + defer func() { + if r := recover(); r != nil { + if panicStr := fmt.Sprintf("%v", r); strings.Contains(panicStr, "Docker") || strings.Contains(panicStr, "docker") { + t.Skipf("Docker not available for Keycloak container tests: %v", r) + } else { + panic(r) + } + } + }() + + ctx := context.Background() + + adapter := NewKeycloakTestAdapter() + + require.NoError(t, adapter.SetupTestData(ctx, &internal.ContractTestDataSet{})) + defer adapter.TeardownTestData(ctx) //nolint:errcheck // teardown is best-effort; failure doesn't affect test outcome + + // Create a user with a 'department' Keycloak attribute — mirroring the scenario + // in https://github.com/orgs/opentdf/discussions/3115 + dept := map[string][]string{"department": {"Finance"}} + keycloakUser := gocloak.User{ + Username: gocloak.StringP("jen"), + Email: gocloak.StringP("jen@email.com"), + FirstName: gocloak.StringP("Jen Z"), + Enabled: gocloak.BoolP(true), + EmailVerified: gocloak.BoolP(true), + Attributes: &dept, + } + _, err := adapter.keycloakClient.CreateUser(ctx, adapter.adminToken.AccessToken, adapter.config.Realm, keycloakUser) + require.NoError(t, err) + + // Retrieve the user exactly as the Keycloak ERS does via GetUsers + exactMatch := true + users, err := adapter.keycloakClient.GetUsers(ctx, adapter.adminToken.AccessToken, adapter.config.Realm, gocloak.GetUsersParams{ + Username: gocloak.StringP("jen"), + Exact: &exactMatch, + }) + require.NoError(t, err) + require.Len(t, users, 1) + + // Serialize the user exactly as typeToGenericJSONMap does in the Keycloak ERS + userJSON, err := json.Marshal(users[0]) + require.NoError(t, err) + t.Logf("Keycloak user object JSON: %s", userJSON) + + var genericMap map[string]interface{} + err = json.Unmarshal(userJSON, &genericMap) + require.NoError(t, err) + + entityStruct, err := structpb.NewStruct(genericMap) + require.NoError(t, err) + + entityRepresentation := &entityresolution.EntityRepresentation{ + OriginalId: "jen", + AdditionalProps: []*structpb.Struct{entityStruct}, + } + + // Log all flattened keys so the structure is visible in test output + flattenedEntity, err := flattening.Flatten(genericMap) + require.NoError(t, err) + t.Log("Flattened Keycloak user keys:") + for _, item := range flattenedEntity.Items { + t.Logf(" key=%q value=%v", item.Key, item.Value) + } + + const attrFQN = "https://example.com/attr/department/value/finance" + + t.Run("correct selector matches Keycloak user attribute", func(t *testing.T) { + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappings( + buildAttributeSubjectMapping(attrFQN, ".attributes.department[]", "Finance"), + entityRepresentation, + ) + require.NoError(t, err) + assert.Equal(t, []string{attrFQN}, entitlements, + "selector '.attributes.department[]' should match Keycloak user attribute") + }) + + t.Run("JWT claim name selector does not match Keycloak user object", func(t *testing.T) { + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappings( + buildAttributeSubjectMapping(attrFQN, ".department", "Finance"), + entityRepresentation, + ) + require.NoError(t, err) + assert.Empty(t, entitlements, + "selector '.department' should NOT match: Keycloak ERS resolves user objects, not JWT claims") + }) +} + +func buildAttributeSubjectMapping(attrFQN, selector, value string) map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue { //nolint:unparam // attrFQN is parameterized so callers can specify any attribute FQN + return map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue{ + attrFQN: { + Value: &policy.Value{ + SubjectMappings: []*policy.SubjectMapping{ + { + SubjectConditionSet: &policy.SubjectConditionSet{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: selector, + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{value}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/service/entityresolution/integration/keycloak_test.go b/service/entityresolution/integration/keycloak_test.go index b0315672ab..d23cdcbdea 100644 --- a/service/entityresolution/integration/keycloak_test.go +++ b/service/entityresolution/integration/keycloak_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/Nerzal/gocloak/v13" - "github.com/docker/docker/api/types/container" "github.com/opentdf/platform/service/entityresolution/integration/internal" keycloakv2 "github.com/opentdf/platform/service/entityresolution/keycloak/v2" "github.com/opentdf/platform/service/logger" @@ -202,9 +201,7 @@ func (a *KeycloakTestAdapter) setupKeycloakContainer(ctx context.Context) error Env: containerConfig.Env, Cmd: containerConfig.Cmd, WaitingFor: containerConfig.WaitStrategy, - HostConfigModifier: func(hostConfig *container.HostConfig) { - hostConfig.AutoRemove = true - }, + AutoRemove: true, } container, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{ @@ -227,7 +224,7 @@ func (a *KeycloakTestAdapter) setupKeycloakContainer(ctx context.Context) error } // Update config with actual container details - a.config.Port = mappedPort.Int() + a.config.Port = int(mappedPort.Num()) a.config.URL = "http://" + net.JoinHostPort(a.config.Host, strconv.Itoa(a.config.Port)) a.config.AdminURL = a.config.URL diff --git a/service/entityresolution/integration/multistrategy_comprehensive_test.go b/service/entityresolution/integration/multistrategy_comprehensive_test.go index 9b09b625b4..70bb710cff 100644 --- a/service/entityresolution/integration/multistrategy_comprehensive_test.go +++ b/service/entityresolution/integration/multistrategy_comprehensive_test.go @@ -1,10 +1,14 @@ package integration import ( + "context" "database/sql" "encoding/base64" "fmt" "net" + "path/filepath" + "reflect" + "runtime" "strings" "testing" "time" @@ -187,7 +191,7 @@ func TestMultiStrategy_SQLOnly(t *testing.T) { Connection: map[string]interface{}{ "driver": "postgres", "host": host, - "port": mappedPort.Int(), + "port": int(mappedPort.Num()), "database": "testdb", "username": "testuser", "password": "testpass", @@ -319,7 +323,7 @@ func TestMultiStrategy_LDAPOnly(t *testing.T) { Type: "ldap", Connection: map[string]interface{}{ "host": host, - "port": mappedPort.Int(), + "port": int(mappedPort.Num()), "use_tls": false, "bind_dn": "cn=admin,dc=test,dc=local", "bind_password": "admin123", @@ -374,11 +378,6 @@ func TestMultiStrategy_LDAPOnly(t *testing.T) { ers, err := multistrategyv2.NewERSV2(ctx, config, logger.CreateTestLogger()) if err != nil { - // Check if this is the expected LDAP stub error - if strings.Contains(err.Error(), "LDAP not implemented - stub function") { - t.Skipf("LDAP provider is not fully implemented yet (stub implementation): %v", err) - return - } t.Fatalf("Failed to create multi-strategy ERS: %v", err) } @@ -398,11 +397,6 @@ func TestMultiStrategy_LDAPOnly(t *testing.T) { resp, err := ers.CreateEntityChainsFromTokens(ctx, connect.NewRequest(req)) if err != nil { - // LDAP lookup may fail if user doesn't exist, which is expected - if strings.Contains(err.Error(), "LDAP not implemented - stub function") { - t.Skipf("LDAP provider is stubbed and not fully implemented: %v", err) - return - } t.Logf("LDAP lookup failed as expected (no test data): %v", err) return } @@ -412,6 +406,162 @@ func TestMultiStrategy_LDAPOnly(t *testing.T) { } } +// Test 3b: Claims strategy fails, LDAP strategy succeeds +func TestMultiStrategy_ClaimsThenLDAPFallback(t *testing.T) { + if testing.Short() { + t.Skip("Skipping LDAP container tests in short mode") + } + testcontainers.SkipIfProviderIsNotHealthy(t) + + ctx := t.Context() + ldapContainer, host, port := startSeededLDAPContainer(ctx, t) + defer func() { _ = ldapContainer.Terminate(ctx) }() + + config := types.MultiStrategyConfig{ + FailureStrategy: types.FailureStrategyContinue, + Providers: map[string]types.ProviderConfig{ + "jwt_claims": { + Type: "claims", + Connection: map[string]interface{}{}, + }, + "ldap_directory": { + Type: "ldap", + Connection: map[string]interface{}{ + "host": host, + "port": port, + "use_tls": false, + "bind_dn": "cn=admin,dc=opentdf,dc=test", + "bind_password": "admin123", + "timeout": "30s", + }, + }, + }, + MappingStrategies: []types.MappingStrategy{ + { + Name: "claims_direct_lookup", + Provider: "jwt_claims", + EntityType: types.EntityTypeSubject, + Conditions: types.StrategyConditions{ + JWTClaims: []types.JWTClaimCondition{ + { + Claim: "sub", + Operator: "exists", + Values: []string{}, + }, + }, + }, + InputMapping: []types.InputMapping{ + { + JWTClaim: "username", + Parameter: "username", + Required: true, + }, + }, + OutputMapping: []types.OutputMapping{ + { + SourceClaim: "username", + ClaimName: "username", + }, + { + SourceClaim: "email", + ClaimName: "email_address", + }, + }, + }, + { + Name: "ldap_directory_lookup", + Provider: "ldap_directory", + EntityType: types.EntityTypeSubject, + Conditions: types.StrategyConditions{ + JWTClaims: []types.JWTClaimCondition{ + { + Claim: "sub", + Operator: "exists", + Values: []string{}, + }, + }, + }, + InputMapping: []types.InputMapping{ + { + JWTClaim: "sub", + Parameter: "username", + Required: true, + }, + }, + LDAPSearch: &types.LDAPSearchConfig{ + BaseDN: "ou=users,dc=opentdf,dc=test", + Filter: "(&(objectClass=inetOrgPerson)(uid={username}))", + Scope: "subtree", + Attributes: []string{"uid", "mail", "cn"}, + }, + OutputMapping: []types.OutputMapping{ + { + SourceAttribute: "uid", + ClaimName: "username", + }, + { + SourceAttribute: "mail", + ClaimName: "email_address", + }, + { + SourceAttribute: "cn", + ClaimName: "display_name", + }, + }, + }, + }, + } + + service, err := multistrategyv2.NewERSV2(ctx, config, logger.CreateTestLogger()) + if err != nil { + t.Fatalf("Failed to create multi-strategy ERS with LDAP fallback: %v", err) + } + + jwtClaims := types.JWTClaims{ + "sub": "alice", + } + ctxWithClaims := context.WithValue(ctx, types.JWTClaimsContextKey, jwtClaims) + + result, err := service.GetService().ResolveEntity(ctxWithClaims, "claims-ldap-failover", jwtClaims) + if err != nil { + t.Fatalf("Expected LDAP fallback to succeed, got error: %v", err) + } + + if got := result.Claims["username"]; got != "alice" { + t.Fatalf("Expected username 'alice', got %v", got) + } + + if got := result.Claims["email_address"]; got != "alice@opentdf.test" { + t.Fatalf("Expected email_address 'alice@opentdf.test', got %v", got) + } + + if got := result.Claims["display_name"]; got != "Alice Johnson" { + t.Fatalf("Expected display_name 'Alice Johnson', got %v", got) + } + + if got := result.Metadata["provider_type"]; got != "ldap" { + t.Fatalf("Expected provider_type 'ldap', got %v", got) + } + + if got := result.Metadata["strategy_name"]; got != "ldap_directory_lookup" { + t.Fatalf("Expected strategy_name 'ldap_directory_lookup', got %v", got) + } + + if got := result.Metadata["search_filter"]; got != "(&(objectClass=inetOrgPerson)(uid=alice))" { + t.Fatalf("Expected LDAP search_filter for alice, got %v", got) + } + + attemptedStrategies, ok := result.Metadata["attempted_strategies"].([]string) + if !ok { + t.Fatalf("Expected attempted_strategies to be []string, got %T", result.Metadata["attempted_strategies"]) + } + + expectedStrategies := []string{"claims_direct_lookup", "ldap_directory_lookup"} + if !reflect.DeepEqual(attemptedStrategies, expectedStrategies) { + t.Fatalf("Expected attempted_strategies %v, got %v", expectedStrategies, attemptedStrategies) + } +} + // Test 4: Multi-provider failover test (fallback near end) func TestMultiStrategy_MultiProviderFailover(t *testing.T) { if testing.Short() { @@ -812,3 +962,63 @@ func createSQLTestData(db *sql.DB) error { return nil } + +func startSeededLDAPContainer(ctx context.Context, t *testing.T) (testcontainers.Container, string, int) { + t.Helper() + + containerRequest := testcontainers.ContainerRequest{ + Image: "osixia/openldap:1.5.0", + ExposedPorts: []string{"389/tcp"}, + Env: map[string]string{ + "LDAP_ORGANISATION": "OpenTDF Test", + "LDAP_DOMAIN": "opentdf.test", + "LDAP_ADMIN_PASSWORD": "admin123", + }, + Cmd: []string{"--copy-service"}, + Files: []testcontainers.ContainerFile{ + { + HostFilePath: ldapFixturePath("01_organizational_units.ldif"), + ContainerFilePath: "/container/service/slapd/assets/config/bootstrap/ldif/custom/01_organizational_units.ldif", + FileMode: 0o644, + }, + { + HostFilePath: ldapFixturePath("02_test_users.ldif"), + ContainerFilePath: "/container/service/slapd/assets/config/bootstrap/ldif/custom/02_test_users.ldif", + FileMode: 0o644, + }, + }, + WaitingFor: wait.ForListeningPort("389/tcp").WithStartupTimeout(60 * time.Second), + } + + ldapContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: containerRequest, + Started: true, + }) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "docker") { + t.Skipf("Docker not available for LDAP container test: %v", err) + } + t.Fatalf("Failed to start LDAP container: %v", err) + } + + host, err := ldapContainer.Host(ctx) + if err != nil { + t.Fatalf("Failed to get LDAP container host: %v", err) + } + + mappedPort, err := ldapContainer.MappedPort(ctx, "389") + if err != nil { + t.Fatalf("Failed to get LDAP container port: %v", err) + } + + return ldapContainer, host, int(mappedPort.Num()) +} + +func ldapFixturePath(name string) string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + panic("failed to determine LDAP fixture path") + } + + return filepath.Join(filepath.Dir(filename), "ldap_test_data", name) +} diff --git a/service/entityresolution/keycloak/entity_resolution_test.go b/service/entityresolution/keycloak/entity_resolution_test.go index 2875439d6a..1c8172ae14 100644 --- a/service/entityresolution/keycloak/entity_resolution_test.go +++ b/service/entityresolution/keycloak/entity_resolution_test.go @@ -18,7 +18,7 @@ import ( "github.com/opentdf/platform/service/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" "google.golang.org/grpc/codes" ) @@ -678,7 +678,7 @@ func Test_GetConnectorTokenRefresh(t *testing.T) { service := &KeycloakEntityResolutionService{ idpConfig: kcconfig, logger: logger.CreateTestLogger(), - Tracer: trace.NewNoopTracerProvider().Tracer("test"), + Tracer: noop.NewTracerProvider().Tracer("test"), } req := &connect.Request[entityresolution.ResolveEntitiesRequest]{ diff --git a/service/entityresolution/keycloak/v2/entity_resolution_test.go b/service/entityresolution/keycloak/v2/entity_resolution_test.go index b4ecc07099..f3091e7d83 100644 --- a/service/entityresolution/keycloak/v2/entity_resolution_test.go +++ b/service/entityresolution/keycloak/v2/entity_resolution_test.go @@ -18,7 +18,7 @@ import ( "github.com/opentdf/platform/service/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" "google.golang.org/grpc/codes" ) @@ -166,7 +166,7 @@ func Test_GetConnectorTokenRefresh(t *testing.T) { service := &EntityResolutionServiceV2{ idpConfig: testConfig(server), logger: logger.CreateTestLogger(), - Tracer: trace.NewNoopTracerProvider().Tracer("test"), + Tracer: noop.NewTracerProvider().Tracer("test"), } // First call to trigger initial token acquisition diff --git a/service/entityresolution/multi-strategy/providers/ldap/ldap_backend.go b/service/entityresolution/multi-strategy/providers/ldap/ldap_backend.go new file mode 100644 index 0000000000..85f1a7dbbc --- /dev/null +++ b/service/entityresolution/multi-strategy/providers/ldap/ldap_backend.go @@ -0,0 +1,44 @@ +package ldap + +import ( + "crypto/tls" + "time" +) + +type Conn interface { + Bind(username, password string) error + Search(request SearchRequest) (*SearchResult, error) + Close() error + SetTimeout(timeout time.Duration) +} + +type SearchRequest struct { + BaseDN string + Scope int + DerefAliases int + SizeLimit int + TimeLimit int + TypesOnly bool + Filter string + Attributes []string +} + +type Backend interface { + Dial(network, addr string) (Conn, error) + DialTLS(network, addr string, config *tls.Config) (Conn, error) + EscapeFilter(filter string) string +} + +type SearchResult struct { + Entries []*Entry +} + +type Entry struct { + DN string + Attributes []*Attribute +} + +type Attribute struct { + Name string + Values []string +} diff --git a/service/entityresolution/multi-strategy/providers/ldap/ldap_backend_go_ldap.go b/service/entityresolution/multi-strategy/providers/ldap/ldap_backend_go_ldap.go new file mode 100644 index 0000000000..bb0dfbdec7 --- /dev/null +++ b/service/entityresolution/multi-strategy/providers/ldap/ldap_backend_go_ldap.go @@ -0,0 +1,129 @@ +package ldap + +import ( + "crypto/tls" + "fmt" + "net/url" + "time" + + golangldap "github.com/go-ldap/ldap/v3" +) + +type goLDAPBackend struct{} + +func NewGoLDAPBackend() Backend { + return goLDAPBackend{} +} + +func (goLDAPBackend) Dial(network, addr string) (Conn, error) { + conn, err := dialLDAPURL("ldap", network, addr) + if err != nil { + return nil, err + } + return &goLDAPConn{conn: conn}, nil +} + +func (goLDAPBackend) DialTLS(network, addr string, config *tls.Config) (Conn, error) { + opts := []golangldap.DialOpt{} + if config != nil { + opts = append(opts, golangldap.DialWithTLSConfig(config)) + } + + conn, err := dialLDAPURL("ldaps", network, addr, opts...) + if err != nil { + return nil, err + } + return &goLDAPConn{conn: conn}, nil +} + +func dialLDAPURL(scheme, network, addr string, opts ...golangldap.DialOpt) (*golangldap.Conn, error) { + target, err := dialTargetURL(scheme, network, addr) + if err != nil { + return nil, err + } + + conn, err := golangldap.DialURL(target, opts...) + if err != nil { + return nil, err + } + + return conn, nil +} + +func dialTargetURL(scheme, network, addr string) (string, error) { + if network != "tcp" { + return "", fmt.Errorf("unsupported LDAP network %q", network) + } + + return (&url.URL{ + Scheme: scheme, + Host: addr, + }).String(), nil +} + +func (goLDAPBackend) EscapeFilter(filter string) string { + return golangldap.EscapeFilter(filter) +} + +type goLDAPConn struct { + conn *golangldap.Conn +} + +func (c *goLDAPConn) SetTimeout(timeout time.Duration) { + c.conn.SetTimeout(timeout) +} + +func (c *goLDAPConn) Bind(username, password string) error { + return c.conn.Bind(username, password) +} + +func (c *goLDAPConn) Search(request SearchRequest) (*SearchResult, error) { + ldapReq := golangldap.NewSearchRequest( + request.BaseDN, + request.Scope, + request.DerefAliases, + request.SizeLimit, + request.TimeLimit, + request.TypesOnly, + request.Filter, + request.Attributes, + nil, + ) + + res, err := c.conn.Search(ldapReq) + if err != nil { + return nil, err + } + + return convertSearchResult(res), nil +} + +func (c *goLDAPConn) Close() error { + return c.conn.Close() +} + +func convertSearchResult(res *golangldap.SearchResult) *SearchResult { + if res == nil { + return &SearchResult{} + } + + out := &SearchResult{Entries: make([]*Entry, 0, len(res.Entries))} + for _, entry := range res.Entries { + outEntry := &Entry{ + DN: entry.DN, + } + if len(entry.Attributes) > 0 { + outEntry.Attributes = make([]*Attribute, 0, len(entry.Attributes)) + for _, attr := range entry.Attributes { + values := append([]string(nil), attr.Values...) + outEntry.Attributes = append(outEntry.Attributes, &Attribute{ + Name: attr.Name, + Values: values, + }) + } + } + out.Entries = append(out.Entries, outEntry) + } + + return out +} diff --git a/service/entityresolution/multi-strategy/providers/ldap/ldap_constants.go b/service/entityresolution/multi-strategy/providers/ldap/ldap_constants.go new file mode 100644 index 0000000000..acc6377851 --- /dev/null +++ b/service/entityresolution/multi-strategy/providers/ldap/ldap_constants.go @@ -0,0 +1,8 @@ +package ldap + +const ( + ScopeBaseObject = 0 + ScopeSingleLevel = 1 + ScopeWholeSubtree = 2 + NeverDerefAliases = 0 +) diff --git a/service/entityresolution/multi-strategy/providers/ldap/ldap_mapper.go b/service/entityresolution/multi-strategy/providers/ldap/ldap_mapper.go index 690aedfc89..9929db275f 100644 --- a/service/entityresolution/multi-strategy/providers/ldap/ldap_mapper.go +++ b/service/entityresolution/multi-strategy/providers/ldap/ldap_mapper.go @@ -39,14 +39,6 @@ func (m *Mapper) ExtractParameters(jwtClaims types.JWTClaims, inputMapping []typ params[mapping.Parameter] = claimValue } - // LDAP-specific parameter validation and sanitization - for paramName, paramValue := range params { - // Escape LDAP filter metacharacters in parameter values - if str, ok := paramValue.(string); ok { - params[paramName] = m.escapeLDAPFilter(str) - } - } - return params, nil } @@ -146,14 +138,14 @@ func isValidTemplateVariable(name string) bool { } // Must start with letter or underscore - if !((name[0] >= 'a' && name[0] <= 'z') || (name[0] >= 'A' && name[0] <= 'Z') || name[0] == '_') { + if !isASCIIAlpha(name[0]) && name[0] != '_' { return false } // Rest must be letters, digits, or underscores for i := 1; i < len(name); i++ { char := name[i] - if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_') { + if !isASCIIAlphaNumeric(char) && char != '_' { return false } } @@ -169,13 +161,13 @@ func isValidLDAPAttribute(name string) bool { // LDAP attribute names can contain letters, digits, and hyphens // Must start with a letter - if !((name[0] >= 'a' && name[0] <= 'z') || (name[0] >= 'A' && name[0] <= 'Z')) { + if !isASCIIAlpha(name[0]) { return false } for i := 1; i < len(name); i++ { char := name[i] - if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '-') { + if !isASCIIAlphaNumeric(char) && char != '-' { return false } } @@ -187,3 +179,11 @@ func isValidLDAPAttribute(name string) bool { func (m *Mapper) isTransformationSupported(transformationName string) bool { return transformation.IsSupportedByProvider(transformationName, "ldap") } + +func isASCIIAlpha(char byte) bool { + return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') +} + +func isASCIIAlphaNumeric(char byte) bool { + return isASCIIAlpha(char) || (char >= '0' && char <= '9') +} diff --git a/service/entityresolution/multi-strategy/providers/ldap/ldap_mapper_test.go b/service/entityresolution/multi-strategy/providers/ldap/ldap_mapper_test.go index 5a8f8332e9..ab2d2f9514 100644 --- a/service/entityresolution/multi-strategy/providers/ldap/ldap_mapper_test.go +++ b/service/entityresolution/multi-strategy/providers/ldap/ldap_mapper_test.go @@ -34,7 +34,7 @@ func TestLDAPMapper_ExtractParameters(t *testing.T) { expectError: false, }, { - name: "LDAP filter escaping", + name: "LDAP values remain raw until filter construction", jwtClaims: types.JWTClaims{ "username": "test(user)*", }, @@ -42,7 +42,7 @@ func TestLDAPMapper_ExtractParameters(t *testing.T) { {JWTClaim: "username", Parameter: "username", Required: true}, }, expectedParams: map[string]interface{}{ - "username": "test\\28user\\29\\2a", // Should be escaped + "username": "test(user)*", }, expectError: false, }, diff --git a/service/entityresolution/multi-strategy/providers/ldap/ldap_provider.go b/service/entityresolution/multi-strategy/providers/ldap/ldap_provider.go index 1c101e12d7..704dbb89a4 100644 --- a/service/entityresolution/multi-strategy/providers/ldap/ldap_provider.go +++ b/service/entityresolution/multi-strategy/providers/ldap/ldap_provider.go @@ -6,24 +6,32 @@ import ( "fmt" "strings" - // "github.com/go-ldap/ldap/v3" // LDAP client library - using stubs for now - "github.com/opentdf/platform/service/entityresolution/multi-strategy/types" ) // Provider implements the Provider interface for LDAP directories type Provider struct { - name string - config Config - mapper types.Mapper + name string + config Config + mapper types.Mapper + backend Backend } // NewProvider creates a new LDAP provider func NewProvider(ctx context.Context, name string, config Config) (*Provider, error) { + return newProviderWithBackend(ctx, name, config, NewGoLDAPBackend()) +} + +func newProviderWithBackend(ctx context.Context, name string, config Config, backend Backend) (*Provider, error) { + if backend == nil { + backend = NewGoLDAPBackend() + } + provider := &Provider{ - name: name, - config: config, - mapper: NewMapper(), + name: name, + config: config, + mapper: NewMapper(), + backend: backend, } // Test the connection during initialization @@ -111,18 +119,16 @@ func (p *Provider) ResolveEntity(ctx context.Context, strategy types.MappingStra ) } - // Create search request - searchRequest := NewSearchRequest( - strategy.LDAPSearch.BaseDN, - scope, - NeverDerefAliases, - 1, // Size limit - expect single entity - int(p.config.RequestTimeout.Seconds()), - false, // Types only - searchFilter, - strategy.LDAPSearch.Attributes, - nil, // Controls - ) + searchRequest := SearchRequest{ + BaseDN: strategy.LDAPSearch.BaseDN, + Scope: scope, + DerefAliases: NeverDerefAliases, + SizeLimit: 1, + TimeLimit: int(p.config.RequestTimeout.Seconds()), + TypesOnly: false, + Filter: searchFilter, + Attributes: append([]string(nil), strategy.LDAPSearch.Attributes...), + } // Execute search with context timeout searchCtx, cancel := context.WithTimeout(ctx, p.config.RequestTimeout) @@ -247,19 +253,19 @@ func (p *Provider) Close() error { } // getConnection establishes an LDAP connection -func (p *Provider) getConnection() (*Conn, error) { +func (p *Provider) getConnection() (Conn, error) { address := fmt.Sprintf("%s:%d", p.config.Host, p.config.Port) - var conn *Conn + var conn Conn var err error if p.config.UseTLS { tlsConfig := &tls.Config{ InsecureSkipVerify: p.config.SkipVerify, //nolint:gosec // TLS verification can be disabled via configuration for testing environments } - conn, err = DialTLS("tcp", address, tlsConfig) + conn, err = p.backend.DialTLS("tcp", address, tlsConfig) } else { - conn, err = Dial("tcp", address) + conn, err = p.backend.Dial("tcp", address) } if err != nil { @@ -275,7 +281,7 @@ func (p *Provider) getConnection() (*Conn, error) { if p.config.BindDN != "" { err = conn.Bind(p.config.BindDN, p.config.BindPassword) if err != nil { - conn.Close() + _ = conn.Close() return nil, err } } @@ -293,7 +299,7 @@ func (p *Provider) buildSearchFilter(filterTemplate string, params map[string]in // Convert parameter value to string and escape for LDAP valueStr := fmt.Sprintf("%v", paramValue) - escapedValue := EscapeFilter(valueStr) + escapedValue := p.backend.EscapeFilter(valueStr) filter = strings.ReplaceAll(filter, placeholder, escapedValue) } diff --git a/service/entityresolution/multi-strategy/providers/ldap/ldap_provider_test.go b/service/entityresolution/multi-strategy/providers/ldap/ldap_provider_test.go new file mode 100644 index 0000000000..5518e273d0 --- /dev/null +++ b/service/entityresolution/multi-strategy/providers/ldap/ldap_provider_test.go @@ -0,0 +1,189 @@ +package ldap + +import ( + "context" + "crypto/tls" + "errors" + "testing" + "time" + + "github.com/opentdf/platform/service/entityresolution/multi-strategy/transformation" + "github.com/opentdf/platform/service/entityresolution/multi-strategy/types" + "github.com/stretchr/testify/suite" +) + +type ProviderSuite struct { + suite.Suite +} + +type mockBackend struct{} + +type escapingBackend struct { + mockBackend +} + +func (mockBackend) Dial(_, _ string) (Conn, error) { + return nil, errors.New("mock LDAP backend not configured") +} + +func (mockBackend) DialTLS(_, _ string, _ *tls.Config) (Conn, error) { + return nil, errors.New("mock LDAP backend not configured") +} + +func (escapingBackend) EscapeFilter(filter string) string { + return transformation.EscapeLDAPFilter(filter) +} + +func (mockBackend) EscapeFilter(filter string) string { + return filter +} + +func TestProviderSuite(t *testing.T) { + suite.Run(t, new(ProviderSuite)) +} + +func (s *ProviderSuite) TestBuildSearchFilter() { + provider := &Provider{ + backend: escapingBackend{}, + } + + tests := []struct { + name string + filterTemplate string + params map[string]interface{} + expected string + expectError bool + }{ + { + name: "escapes raw parameter values once", + filterTemplate: "(&(objectClass=person)(uid={username}))", + params: map[string]interface{}{ + "username": "test(user)*", + }, + expected: "(&(objectClass=person)(uid=test\\28user\\29\\2a))", + }, + { + name: "formats non string parameters before escaping", + filterTemplate: "(&(objectClass=person)(employeeNumber={employee_number}))", + params: map[string]interface{}{ + "employee_number": 12345, + }, + expected: "(&(objectClass=person)(employeeNumber=12345))", + }, + { + name: "fails when placeholders remain", + filterTemplate: "(&(objectClass=person)(uid={username})(mail={email}))", + params: map[string]interface{}{ + "username": "testuser", + }, + expectError: true, + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + filter, err := provider.buildSearchFilter(tt.filterTemplate, tt.params) + + if tt.expectError { + s.Require().Error(err) + return + } + + s.Require().NoError(err) + s.Equal(tt.expected, filter) + }) + } +} + +type recordingBackend struct { + escapingBackend + conn *recordingConn +} + +func (b recordingBackend) Dial(_, _ string) (Conn, error) { + return b.conn, nil +} + +func (b recordingBackend) DialTLS(_, _ string, _ *tls.Config) (Conn, error) { + return b.conn, nil +} + +type recordingConn struct { + request SearchRequest + result *SearchResult +} + +func (c *recordingConn) Bind(_, _ string) error { + return nil +} + +func (c *recordingConn) Search(request SearchRequest) (*SearchResult, error) { + c.request = request + if c.result != nil { + return c.result, nil + } + return &SearchResult{}, nil +} + +func (c *recordingConn) Close() error { + return nil +} + +func (c *recordingConn) SetTimeout(time.Duration) {} + +func (s *ProviderSuite) TestResolveEntityBuildsSearchRequest() { + conn := &recordingConn{ + result: &SearchResult{ + Entries: []*Entry{ + { + DN: "uid=alice,ou=users,dc=opentdf,dc=test", + Attributes: []*Attribute{ + {Name: "uid", Values: []string{"alice"}}, + {Name: "mail", Values: []string{"alice@opentdf.test"}}, + }, + }, + }, + }, + } + provider := &Provider{ + name: "ldap", + config: Config{ + Host: "localhost", + Port: 389, + RequestTimeout: 30 * time.Second, + }, + backend: recordingBackend{conn: conn}, + } + + strategy := types.MappingStrategy{ + Name: "ldap_lookup", + LDAPSearch: &types.LDAPSearchConfig{ + BaseDN: "ou=users,dc=opentdf,dc=test", + Filter: "(&(objectClass=inetOrgPerson)(uid={username}))", + Scope: "subtree", + Attributes: []string{"uid", "mail"}, + }, + } + + result, err := provider.ResolveEntity(context.Background(), strategy, map[string]interface{}{ + "username": "alice", + }) + s.Require().NoError(err) + + s.Equal("ou=users,dc=opentdf,dc=test", conn.request.BaseDN) + s.Equal(ScopeWholeSubtree, conn.request.Scope) + s.Equal(NeverDerefAliases, conn.request.DerefAliases) + s.Equal(1, conn.request.SizeLimit) + s.Equal(30, conn.request.TimeLimit) + s.Equal("(&(objectClass=inetOrgPerson)(uid=alice))", conn.request.Filter) + + expectedAttrs := []string{"uid", "mail"} + s.Len(conn.request.Attributes, len(expectedAttrs)) + + for index, attr := range expectedAttrs { + s.Equal(attr, conn.request.Attributes[index]) + } + + s.Equal("alice", result.Data["uid"]) + s.Equal("alice@opentdf.test", result.Data["mail"]) +} diff --git a/service/entityresolution/multi-strategy/providers/ldap/ldap_stubs.go b/service/entityresolution/multi-strategy/providers/ldap/ldap_stubs.go deleted file mode 100644 index 3e2ad63d6b..0000000000 --- a/service/entityresolution/multi-strategy/providers/ldap/ldap_stubs.go +++ /dev/null @@ -1,65 +0,0 @@ -package ldap - -// LDAP stubs for building without the actual LDAP library -// In production, remove this file and import "github.com/go-ldap/ldap/v3" - -import ( - "crypto/tls" - "errors" -) - -// LDAP constants (stubs) -const ( - ScopeBaseObject = 0 - ScopeSingleLevel = 1 - ScopeWholeSubtree = 2 - NeverDerefAliases = 0 -) - -// LDAP types (stubs) -type ( - Conn struct{} - SearchRequest struct{} - SearchResult struct { - Entries []*Entry - } -) - -type Entry struct { - DN string - Attributes []*Attribute -} -type Attribute struct { - Name string - Values []string -} - -// LDAP functions (stubs) -func Dial(_, _ string) (*Conn, error) { - return nil, errors.New("LDAP not implemented - stub function") -} - -func DialTLS(_, _ string, _ *tls.Config) (*Conn, error) { - return nil, errors.New("LDAP not implemented - stub function") -} - -func NewSearchRequest(_ string, _, _, _, _ int, _ bool, _ string, _ []string, _ []interface{}) *SearchRequest { - return &SearchRequest{} -} - -func EscapeFilter(filter string) string { - return filter -} - -func (c *Conn) SetTimeout(_ interface{}) {} -func (c *Conn) Bind(_, _ string) error { - return errors.New("LDAP not implemented - stub function") -} - -func (c *Conn) Search(_ *SearchRequest) (*SearchResult, error) { - return &SearchResult{Entries: []*Entry{}}, errors.New("LDAP not implemented - stub function") -} - -func (c *Conn) Close() error { - return nil -} diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_mapper.go b/service/entityresolution/multi-strategy/providers/sql/sql_mapper.go index a629a7820d..4cd968ed7e 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_mapper.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_mapper.go @@ -155,14 +155,14 @@ func isValidSQLIdentifier(name string) bool { } // Must start with letter or underscore - if !((name[0] >= 'a' && name[0] <= 'z') || (name[0] >= 'A' && name[0] <= 'Z') || name[0] == '_') { + if (name[0] < 'a' || name[0] > 'z') && (name[0] < 'A' || name[0] > 'Z') && name[0] != '_' { return false } // Rest must be letters, digits, or underscores for i := 1; i < len(name); i++ { char := name[i] - if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_') { + if (char < 'a' || char > 'z') && (char < 'A' || char > 'Z') && (char < '0' || char > '9') && char != '_' { return false } } diff --git a/service/entityresolution/multi-strategy/types/errors.go b/service/entityresolution/multi-strategy/types/errors.go index 77c5713009..3651edc396 100644 --- a/service/entityresolution/multi-strategy/types/errors.go +++ b/service/entityresolution/multi-strategy/types/errors.go @@ -1,3 +1,4 @@ +//revive:disable:var-naming package types import ( diff --git a/service/entityresolution/multi-strategy/types/types.go b/service/entityresolution/multi-strategy/types/types.go index f92213b95a..68ffffd5eb 100644 --- a/service/entityresolution/multi-strategy/types/types.go +++ b/service/entityresolution/multi-strategy/types/types.go @@ -1,3 +1,4 @@ +//revive:disable:var-naming package types import ( diff --git a/service/go.mod b/service/go.mod index a115f8e4c9..77e1e68237 100644 --- a/service/go.mod +++ b/service/go.mod @@ -1,75 +1,81 @@ module github.com/opentdf/platform/service -go 1.24.0 - -toolchain go1.24.11 +go 1.25.0 require ( - buf.build/go/protovalidate v0.13.1 - connectrpc.com/connect v1.18.1 + buf.build/go/protovalidate v1.0.0 + connectrpc.com/connect v1.19.2 connectrpc.com/grpchealth v1.4.0 connectrpc.com/grpcreflect v1.3.0 - connectrpc.com/validate v0.3.0 + connectrpc.com/otelconnect v0.9.0 + connectrpc.com/validate v0.6.0 github.com/Masterminds/squirrel v1.5.4 github.com/Nerzal/gocloak/v13 v13.9.0 github.com/bmatcuk/doublestar v1.3.4 github.com/casbin/casbin/v2 v2.108.0 github.com/creasty/defaults v1.8.0 - github.com/dgraph-io/ristretto v0.2.0 - github.com/docker/docker v28.3.3+incompatible - github.com/docker/go-connections v0.5.0 + github.com/dgraph-io/ristretto/v2 v2.4.0 github.com/eko/gocache/lib/v4 v4.2.0 - github.com/eko/gocache/store/ristretto/v4 v4.2.2 + github.com/eko/gocache/store/ristretto/v4 v4.3.2 github.com/fsnotify/fsnotify v1.9.0 github.com/go-chi/cors v1.2.1 github.com/go-playground/validator/v10 v10.26.0 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/google/uuid v1.6.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa - github.com/jackc/pgx/v5 v5.7.5 + github.com/jackc/pgx/v5 v5.9.2 github.com/lestrrat-go/jwx/v2 v2.1.6 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.29 github.com/open-policy-agent/opa v1.5.1 - github.com/opentdf/platform/lib/fixtures v0.4.0 + github.com/opentdf/platform/lib/fixtures v0.5.0 github.com/opentdf/platform/lib/flattening v0.1.3 - github.com/opentdf/platform/lib/identifier v0.2.0 - github.com/opentdf/platform/lib/ocrypto v0.8.0 - github.com/opentdf/platform/protocol/go v0.14.0 - github.com/opentdf/platform/sdk v0.10.1 + github.com/opentdf/platform/lib/identifier v0.4.0 + github.com/opentdf/platform/lib/ocrypto v0.12.0 + github.com/opentdf/platform/protocol/go v0.32.0 + github.com/opentdf/platform/sdk v0.21.0 github.com/pressly/goose/v3 v3.24.3 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.37.0 - go.opentelemetry.io/otel v1.39.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 - go.opentelemetry.io/otel/sdk v1.39.0 - go.opentelemetry.io/otel/trace v1.39.0 - golang.org/x/net v0.47.0 - google.golang.org/grpc v1.77.0 - google.golang.org/protobuf v1.36.10 + github.com/testcontainers/testcontainers-go v0.42.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 + golang.org/x/net v0.54.0 + google.golang.org/grpc v1.81.0 + google.golang.org/protobuf v1.36.11 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1 // indirect - cel.dev/expr v0.24.0 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 // indirect + cel.dev/expr v0.25.1 // indirect + github.com/Azure/go-ntlmssp v0.1.1 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect - github.com/ebitengine/purego v0.8.4 // indirect - github.com/moby/go-archive v0.1.0 // indirect - github.com/shirou/gopsutil/v4 v4.25.5 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect + github.com/go-ldap/ldap/v3 v3.4.12 + github.com/moby/go-archive v0.2.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/vektah/gqlparser/v2 v2.5.26 // indirect ) @@ -103,18 +109,17 @@ require ( github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/mock v1.6.0 // indirect - github.com/google/cel-go v0.25.0 // indirect + github.com/google/cel-go v0.26.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gowebpki/jcs v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -127,12 +132,11 @@ require ( github.com/magiconair/properties v1.8.10 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -150,16 +154,16 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/ksuid v1.0.4 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tchap/go-patricia/v2 v2.3.2 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect @@ -167,17 +171,17 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect - go.uber.org/mock v0.4.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect - golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.31.0 - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/service/go.sum b/service/go.sum index 1af85034cd..05edfa4fb7 100644 --- a/service/go.sum +++ b/service/go.sum @@ -1,25 +1,29 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1 h1:AUL6VF5YWL01j/1H/DQbPUSDkEwYqwVCNw7yhbpOxSQ= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= -buf.build/go/protovalidate v0.13.1 h1:6loHDTWdY/1qmqmt1MijBIKeN4T9Eajrqb9isT1W1s8= -buf.build/go/protovalidate v0.13.1/go.mod h1:C/QcOn/CjXRn5udUwYBiLs8y1TGy7RS+GOSKqjS77aU= -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= -connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 h1:DQLS/rRxLHuugVzjJU5AvOwD57pdFl9he/0O7e5P294= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1/go.mod h1:aY3zbkNan5F+cGm9lITDP6oxJIwu0dn9KjJuJjWaHkg= +buf.build/go/protovalidate v1.0.0 h1:IAG1etULddAy93fiBsFVhpj7es5zL53AfB/79CVGtyY= +buf.build/go/protovalidate v1.0.0/go.mod h1:KQmEUrcQuC99hAw+juzOEAmILScQiKBP1Oc36vvCLW8= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= +connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= connectrpc.com/grpchealth v1.4.0 h1:MJC96JLelARPgZTiRF9KRfY/2N9OcoQvF2EWX07v2IE= connectrpc.com/grpchealth v1.4.0/go.mod h1:WhW6m1EzTmq3Ky1FE8EfkIpSDc6TfUx2M2KqZO3ts/Q= connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= -connectrpc.com/validate v0.3.0 h1:eMPASBQM+ztVzuLSXddB61zwJKzvWWZ6RLdIwTgh9Wo= -connectrpc.com/validate v0.3.0/go.mod h1:QLGN/m+oDeI4zaDAANK1L1G5K4i8gg6CUUwyl3HAG4A= +connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= +connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= +connectrpc.com/validate v0.6.0 h1:DcrgDKt2ZScrUs/d/mh9itD2yeEa0UbBBa+i0mwzx+4= +connectrpc.com/validate v0.6.0/go.mod h1:ihrpI+8gVbLH1fvVWJL1I3j0CfWnF8P/90LsmluRiZs= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= +github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -28,6 +32,8 @@ github.com/Nerzal/gocloak/v13 v13.9.0 h1:YWsJsdM5b0yhM2Ba3MLydiOlujkBry4TtdzfIzS github.com/Nerzal/gocloak/v13 v13.9.0/go.mod h1:YYuDcXZ7K2zKECyVP7pPqjKxx2AzYSpKDj8d6GuyM10= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -52,6 +58,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -63,8 +71,8 @@ github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLV github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -75,30 +83,26 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= -github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= -github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= -github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= -github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= +github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU= +github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw= github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M= -github.com/eko/gocache/store/ristretto/v4 v4.2.2 h1:lXFzoZ5ck6Gy6ON7f5DHSkNt122qN7KoroCVgVwF7oo= -github.com/eko/gocache/store/ristretto/v4 v4.2.2/go.mod h1:uIvBVJzqRepr5L0RsbkfQ2iYfbyos2fuji/s4yM+aUM= +github.com/eko/gocache/store/ristretto/v4 v4.3.2 h1:DfvjqmB6hPHJ9oduReMohe8rZCVtxmY8OqTkmIu+dk0= +github.com/eko/gocache/store/ristretto/v4 v4.3.2/go.mod h1:1F6nJFAY6fTx/UVd66iYr26V2GzZbVJqQJSl+CkRGh4= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -111,10 +115,14 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= +github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -139,8 +147,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= @@ -148,8 +154,8 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= -github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -161,10 +167,12 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= @@ -173,14 +181,24 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= -github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -221,12 +239,14 @@ github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -235,8 +255,6 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -247,18 +265,18 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opentdf/platform/lib/fixtures v0.4.0 h1:p3Y5MLJEBaWiSmo+QyRNTirvI8LqYDj+HtaE9vYrEJ8= -github.com/opentdf/platform/lib/fixtures v0.4.0/go.mod h1:ctyrVn+eTObHAPy3vrdPO0O1mc3vgQ6lc9pBTdhBAfo= +github.com/opentdf/platform/lib/fixtures v0.5.0 h1:uXLZF0xuc0fSssXkcKHL0onN/0aYk1HkkarBj6zGflQ= +github.com/opentdf/platform/lib/fixtures v0.5.0/go.mod h1:ctyrVn+eTObHAPy3vrdPO0O1mc3vgQ6lc9pBTdhBAfo= github.com/opentdf/platform/lib/flattening v0.1.3 h1:IuOm/wJVXNrzOV676Ticgr0wyBkL+lVjsoSfh+WSkNo= github.com/opentdf/platform/lib/flattening v0.1.3/go.mod h1:Gs/T+6FGZKk9OAdz2Jf1R8CTGeNRYrq1lZGDeYT3hrY= -github.com/opentdf/platform/lib/identifier v0.2.0 h1:lpz/QmkGwlli8PmBvDH2bPqWvpna0n0lbEX0+bH3P0o= -github.com/opentdf/platform/lib/identifier v0.2.0/go.mod h1:/tHnLlSVOq3qmbIYSvKrtuZchQfagenv4wG5twl4oRs= -github.com/opentdf/platform/lib/ocrypto v0.8.0 h1:rit/59go69mRHS3kAJQfX4zSbEgK7j5RMRCrx8UX6JA= -github.com/opentdf/platform/lib/ocrypto v0.8.0/go.mod h1:/TtiJldbP/LO1cvX8bwhnd7SVHSUImBt1EfjG9qEo78= -github.com/opentdf/platform/protocol/go v0.14.0 h1:gOG+VXCD5yb8p/uFDpEfsJndcV9wyzuRewioB8xCWJk= -github.com/opentdf/platform/protocol/go v0.14.0/go.mod h1:/lCXN+m/XCd63fSsSYWA4hGaZXCHPk5drdD0oPx+woc= -github.com/opentdf/platform/sdk v0.10.1 h1:kBrTK48xle7mdGc+atlr4kDh94f6kVj+0OB76K8rozI= -github.com/opentdf/platform/sdk v0.10.1/go.mod h1:+yaTi/c/GWHZPPmO27sq2s7Tcb2P/USkK8LuW1krhI8= +github.com/opentdf/platform/lib/identifier v0.4.0 h1:gJf4FqHxqpMdMdMwhI9QmvfHEfMLW4KvEr/qjk7hnio= +github.com/opentdf/platform/lib/identifier v0.4.0/go.mod h1:+gONr5mVf1YlLorZUeRefxiudYfC6JeQN7EwrKMk4g8= +github.com/opentdf/platform/lib/ocrypto v0.12.0 h1:N449KWy7VdMO0JwfsrG0kM6Uy8VrEnVvBciwzRHwnlg= +github.com/opentdf/platform/lib/ocrypto v0.12.0/go.mod h1:51UTmAWO6C8ghuMXiktpn63N+fLUQxY6zo8D65Ly0wQ= +github.com/opentdf/platform/protocol/go v0.32.0 h1:XdH/MscjqpESzmfNHSlC3/b84KDRJWrKSoRjbTTfKh4= +github.com/opentdf/platform/protocol/go v0.32.0/go.mod h1:GCiAAv0I8tkQDA2j9FuWzmK78OtIZSl+eAxAf2WHG+4= +github.com/opentdf/platform/sdk v0.21.0 h1:Q+oz/SU4L+ssqeIxkFISFnS4x2GAT03jI1LcLn4eO8k= +github.com/opentdf/platform/sdk v0.21.0/go.mod h1:1LcAnUbgVwJkX+T8hj24bcAQm91pYEbL2EiOdV+fLJ4= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -297,10 +315,10 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= -github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= -github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -313,13 +331,13 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -333,12 +351,12 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= -github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= -github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/vektah/gqlparser/v2 v2.5.26 h1:REqqFkO8+SOEgZHR/eHScjjVjGS8Nk3RMO/juiTobN4= github.com/vektah/gqlparser/v2 v2.5.26/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -350,8 +368,6 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -359,104 +375,92 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= +golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -477,5 +481,7 @@ modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/service/health/health.go b/service/health/health.go index fc6ab4d73a..e5bc666de0 100644 --- a/service/health/health.go +++ b/service/health/health.go @@ -10,7 +10,6 @@ import ( "connectrpc.com/connect" "connectrpc.com/grpchealth" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/serviceregistry" healthpb "google.golang.org/grpc/health/grpc_health_v1" @@ -36,9 +35,9 @@ func NewRegistration() *serviceregistry.Service[grpchealth.Checker] { srp.Logger.Error("failed to set well-known config", slog.String("error", err.Error())) } hs := HealthService{logger: srp.Logger} - return hs, func(_ context.Context, mux *runtime.ServeMux) error { - err := mux.HandlePath(http.MethodGet, "/healthz", func(w http.ResponseWriter, r *http.Request, _ map[string]string) { //nolint:contextcheck // check is not relevant here - resp, err := hs.Check(context.Background(), &grpchealth.CheckRequest{ + return hs, func(_ context.Context, mux *http.ServeMux) error { + mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { + resp, err := hs.Check(r.Context(), &grpchealth.CheckRequest{ Service: r.URL.Query().Get("service"), }) if err != nil { @@ -59,10 +58,6 @@ func NewRegistration() *serviceregistry.Service[grpchealth.Checker] { srp.Logger.Error("failed to encode health status", slog.String("error", err.Error())) } }) - if err != nil { - panic(errors.Join(errors.New("failed to register healthz endpoint"), err)) - } - return nil } }, diff --git a/service/integration/actions_test.go b/service/integration/actions_test.go index ce9ce597ce..b1e08bf6b7 100644 --- a/service/integration/actions_test.go +++ b/service/integration/actions_test.go @@ -2,13 +2,16 @@ package integration import ( "context" + "fmt" "log/slog" "strings" "testing" + "time" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/namespaces" "github.com/opentdf/platform/service/internal/fixtures" "github.com/opentdf/platform/service/pkg/db" policydb "github.com/opentdf/platform/service/policy/db" @@ -39,10 +42,22 @@ func (s *ActionsSuite) TearDownSuite() { } func (s *ActionsSuite) Test_ListActions_NoPagination_Succeeds() { - fixtureCustomAction1 := s.f.GetCustomActionKey("custom_action_1") - fixtureCustomAction2 := s.f.GetCustomActionKey("other_special_action") + name1 := fmt.Sprintf("scoped-list-nopage-1-%d", time.Now().UnixNano()) + name2 := fmt.Sprintf("scoped-list-nopage-2-%d", time.Now().UnixNano()) - list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{}) + created1, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name1, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + + created2, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name2, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) s.NotNil(list) s.Require().NoError(err) @@ -54,10 +69,10 @@ func (s *ActionsSuite) Test_ListActions_NoPagination_Succeeds() { foundDelete := false for _, action := range list.GetActionsCustom() { - switch action.GetName() { - case fixtureCustomAction1.Name: + switch action.GetId() { + case created1.GetId(): foundCustomAction1 = true - case fixtureCustomAction2.Name: + case created2.GetId(): foundCustomAction2 = true } } @@ -81,14 +96,41 @@ func (s *ActionsSuite) Test_ListActions_NoPagination_Succeeds() { s.True(foundDelete) } +func (s *ActionsSuite) Test_ListActions_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + create := func(i int) string { + name := fmt.Sprintf("order-test-action-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + s.Require().NotNil(created) + return created.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) + s.Require().NoError(err) + s.NotNil(list) + + assertIDsInOrder(s.T(), list.GetActionsCustom(), func(a *policy.Action) string { return a.GetId() }, thirdID, secondID, firstID) +} + func (s *ActionsSuite) Test_ListActions_Pagination_Succeeds() { - list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{}) + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) s.NotNil(list) s.Require().NoError(err) total := list.GetPagination().GetTotal() higherOffsetThanListCount := total + 1 list, err = s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{ + NamespaceId: s.defaultNamespaceID(), Pagination: &policy.PageRequest{ Offset: higherOffsetThanListCount, }, @@ -99,6 +141,7 @@ func (s *ActionsSuite) Test_ListActions_Pagination_Succeeds() { s.Equal(higherOffsetThanListCount, list.GetPagination().GetCurrentOffset()) list, err = s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{ + NamespaceId: s.defaultNamespaceID(), Pagination: &policy.PageRequest{ Offset: 0, Limit: total - 1, @@ -113,6 +156,7 @@ func (s *ActionsSuite) Test_ListActions_Pagination_Succeeds() { func (s *ActionsSuite) Test_ListActions_LimitLargerThanConfigured_Fails() { list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{ + NamespaceId: s.defaultNamespaceID(), Pagination: &policy.PageRequest{ Limit: s.db.LimitMax + 1, }, @@ -122,6 +166,155 @@ func (s *ActionsSuite) Test_ListActions_LimitLargerThanConfigured_Fails() { s.Require().ErrorIs(err, db.ErrListLimitTooLarge) } +func (s *ActionsSuite) Test_ListActions_FiltersCustomActionsByNamespace_Succeeds() { + name := fmt.Sprintf("scoped-list-action-%d", time.Now().UnixNano()) + + first, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + + second, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.otherNamespaceID(), + }) + s.Require().NoError(err) + + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) + s.Require().NoError(err) + + foundFirst := false + foundSecond := false + for _, action := range list.GetActionsCustom() { + if action.GetId() == first.GetId() { + foundFirst = true + s.Equal(s.defaultNamespaceID(), action.GetNamespace().GetId()) + } + if action.GetId() == second.GetId() { + foundSecond = true + } + } + + s.True(foundFirst) + s.False(foundSecond) +} + +func (s *ActionsSuite) Test_ListActions_WithNamespace_DoesNotLeakStandardActionsFromOtherNamespaces() { + createdNamespace, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: fmt.Sprintf("list-actions-standard-scope-%d", time.Now().UnixNano()), + }) + s.Require().NoError(err) + s.Require().NotNil(createdNamespace) + + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: createdNamespace.GetId()}) + s.Require().NoError(err) + s.Require().NotNil(list) + + expected := map[string]bool{ + "create": false, + "read": false, + "update": false, + "delete": false, + } + + for _, action := range list.GetActionsStandard() { + s.Require().NotNil(action.GetNamespace()) + s.Equal(createdNamespace.GetId(), action.GetNamespace().GetId()) + if _, ok := expected[action.GetName()]; ok { + expected[action.GetName()] = true + } + } + + for name, found := range expected { + s.True(found, "expected standard action %s scoped to namespace", name) + } +} + +func (s *ActionsSuite) Test_ListActions_WithoutNamespace_ReturnsAcrossNamespaces_Succeeds() { + name := fmt.Sprintf("global-list-action-%d", time.Now().UnixNano()) + + inDefault, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + + inOther, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.otherNamespaceID(), + }) + s.Require().NoError(err) + + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{}) + s.Require().NoError(err) + + foundDefault := false + foundOther := false + for _, action := range list.GetActionsCustom() { + if action.GetId() == inDefault.GetId() { + foundDefault = true + s.Equal(s.defaultNamespaceID(), action.GetNamespace().GetId()) + } + if action.GetId() == inOther.GetId() { + foundOther = true + s.Equal(s.otherNamespaceID(), action.GetNamespace().GetId()) + } + } + + s.True(foundDefault) + s.True(foundOther) +} + +func (s *ActionsSuite) Test_ListActions_LegacyCustomAction_ScopedExcluded_UnscopedIncluded_Succeeds() { + legacy := s.f.GetCustomActionKey("custom_action_1") + scopedName := fmt.Sprintf("scoped-list-action-%d", time.Now().UnixNano()) + scoped, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: scopedName, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + s.NotNil(scoped) + + assertLegacyAbsent := func(list *actions.ListActionsResponse) { + s.T().Helper() + found := false + for _, action := range list.GetActionsCustom() { + if action.GetId() == legacy.ID { + found = true + } + } + s.False(found) + } + + listByID, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) + s.Require().NoError(err) + assertLegacyAbsent(listByID) + + listByFQN, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceFqn: s.defaultNamespaceFQN()}) + s.Require().NoError(err) + assertLegacyAbsent(listByFQN) + + listUnscoped, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{}) + s.Require().NoError(err) + + foundUnscoped := false + foundScoped := false + for _, action := range listUnscoped.GetActionsCustom() { + if action.GetId() == legacy.ID { + foundUnscoped = true + s.Nil(action.GetNamespace()) + } + if action.GetId() == scoped.GetId() { + foundScoped = true + s.Require().NotNil(action.GetNamespace()) + s.Equal(s.defaultNamespaceID(), action.GetNamespace().GetId()) + } + } + s.True(foundUnscoped) + s.True(foundScoped) +} + func (s *ActionsSuite) Test_GetAction_Id_Succeeds() { fixtureCustomAction1 := s.f.GetCustomActionKey("custom_action_1") actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) @@ -151,29 +344,137 @@ func (s *ActionsSuite) Test_GetAction_Id_Succeeds() { } func (s *ActionsSuite) Test_GetAction_Name_Succeeds() { - customAction := s.f.GetCustomActionKey("other_special_action") - actionCreate := s.f.GetStandardAction(policydb.ActionCreate.String()) + name := fmt.Sprintf("get-by-name-action-%d", time.Now().UnixNano()) + customAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + s.NotNil(customAction) + action, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ Identifier: &actions.GetActionRequest_Name{ - Name: customAction.Name, + Name: customAction.GetName(), }, + NamespaceId: s.defaultNamespaceID(), }) s.NotNil(action) s.Require().NoError(err) - s.Equal(customAction.ID, action.GetId()) - s.Equal(customAction.Name, action.GetName()) + s.Equal(customAction.GetId(), action.GetId()) + s.Equal(customAction.GetName(), action.GetName()) + s.Require().NotNil(action.GetNamespace()) + s.Equal(s.defaultNamespaceID(), action.GetNamespace().GetId()) s.NotNil(action.GetMetadata()) +} - action, err = s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ - Identifier: &actions.GetActionRequest_Name{ - Name: actionCreate.GetName(), - }, +func (s *ActionsSuite) Test_GetAction_Name_ResolvesByNamespace_Succeeds() { + name := fmt.Sprintf("scoped-get-action-%d", time.Now().UnixNano()) + + inDefaultNs, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), }) - s.NotNil(action) s.Require().NoError(err) - s.Equal(actionCreate.GetName(), action.GetName()) - s.Equal(actionCreate.GetId(), action.GetId()) - s.NotNil(action.GetMetadata()) + + inOtherNs, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.otherNamespaceID(), + }) + s.Require().NoError(err) + + gotDefault, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: name}, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + s.Equal(inDefaultNs.GetId(), gotDefault.GetId()) + s.Equal(s.defaultNamespaceID(), gotDefault.GetNamespace().GetId()) + + gotOther, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: name}, + NamespaceId: s.otherNamespaceID(), + }) + s.Require().NoError(err) + s.Equal(inOtherNs.GetId(), gotOther.GetId()) + s.Equal(s.otherNamespaceID(), gotOther.GetNamespace().GetId()) +} + +func (s *ActionsSuite) Test_GetAction_Name_LegacyCustomAction_UnscopedSucceeds_ScopedFails() { + legacy := s.f.GetCustomActionKey("other_special_action") + + byUnscoped, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: legacy.Name}, + }) + s.Require().NoError(err) + s.NotNil(byUnscoped) + s.Equal(legacy.ID, byUnscoped.GetId()) + s.Nil(byUnscoped.GetNamespace()) + + assertLegacyGet := func(action *policy.Action, err error) { + s.T().Helper() + s.Nil(action) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrNotFound) + } + + byID, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: legacy.Name}, + NamespaceId: s.defaultNamespaceID(), + }) + assertLegacyGet(byID, err) + + byFQN, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: legacy.Name}, + NamespaceFqn: s.defaultNamespaceFQN(), + }) + assertLegacyGet(byFQN, err) + + byOtherID, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: legacy.Name}, + NamespaceId: s.otherNamespaceID(), + }) + assertLegacyGet(byOtherID, err) + + byOtherFQN, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: legacy.Name}, + NamespaceFqn: s.otherNamespaceFQN(), + }) + assertLegacyGet(byOtherFQN, err) +} + +func (s *ActionsSuite) Test_CreateListGetAction_WithNamespaceFQN_Succeeds() { + name := fmt.Sprintf("fqn-scoped-action-%d", time.Now().UnixNano()) + + created, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceFqn: s.otherNamespaceFQN(), + }) + s.Require().NoError(err) + s.Equal(name, created.GetName()) + s.Equal(s.otherNamespaceID(), created.GetNamespace().GetId()) + s.Equal(s.otherNamespaceFQN(), created.GetNamespace().GetFqn()) + + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceFqn: s.otherNamespaceFQN()}) + s.Require().NoError(err) + + found := false + for _, action := range list.GetActionsCustom() { + if action.GetId() == created.GetId() { + found = true + s.Equal(s.otherNamespaceID(), action.GetNamespace().GetId()) + s.Equal(s.otherNamespaceFQN(), action.GetNamespace().GetFqn()) + } + } + s.True(found) + + got, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: name}, + NamespaceFqn: s.otherNamespaceFQN(), + }) + s.Require().NoError(err) + s.Equal(created.GetId(), got.GetId()) + s.Equal(s.otherNamespaceID(), got.GetNamespace().GetId()) + s.Equal(s.otherNamespaceFQN(), got.GetNamespace().GetFqn()) } func (s *ActionsSuite) Test_GetAction_NonExistent_Fails() { @@ -190,6 +491,7 @@ func (s *ActionsSuite) Test_GetAction_NonExistent_Fails() { Identifier: &actions.GetActionRequest_Name{ Name: "totally_unknown_action", }, + NamespaceId: s.defaultNamespaceID(), }) s.Nil(action) s.Require().Error(err) @@ -199,7 +501,8 @@ func (s *ActionsSuite) Test_GetAction_NonExistent_Fails() { func (s *ActionsSuite) Test_CreateAction_Succeeds() { newName := "new_custom_action_createaction" action, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: newName, + Name: newName, + NamespaceId: s.defaultNamespaceID(), Metadata: &common.MetadataMutable{ Labels: map[string]string{ "label1": "value1", @@ -217,20 +520,36 @@ func (s *ActionsSuite) Test_CreateAction_Succeeds() { } func (s *ActionsSuite) Test_CreateAction_Conflict_Fails() { - fixtureCustomAction := s.f.GetCustomActionKey("custom_action_1") + name := fmt.Sprintf("create-conflict-action-%d", time.Now().UnixNano()) + _, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + action, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: fixtureCustomAction.Name, - }, - ) + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) s.Nil(action) s.Require().Error(err) s.Require().ErrorIs(err, db.ErrUniqueConstraintViolation) } +func (s *ActionsSuite) Test_CreateAction_MissingNamespace_SucceedsInLegacyMode() { + action, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: fmt.Sprintf("missing-namespace-%d", time.Now().UnixNano()), + }) + s.Require().NoError(err) + s.NotNil(action) + s.Nil(action.GetNamespace()) +} + func (s *ActionsSuite) Test_CreateAction_NormalizesToLowerCase() { newName := "New_Custom_Action_CreateAction_UPPER" action, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: newName, + Name: newName, + NamespaceId: s.defaultNamespaceID(), }, ) s.NotNil(action) @@ -240,7 +559,8 @@ func (s *ActionsSuite) Test_CreateAction_NormalizesToLowerCase() { func (s *ActionsSuite) Test_UpdateAction_Succeeds() { newAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: "new_custom_action_updateaction", + Name: "new_custom_action_updateaction", + NamespaceId: s.defaultNamespaceID(), Metadata: &common.MetadataMutable{ Labels: map[string]string{ "original": "original_value", @@ -279,7 +599,8 @@ func (s *ActionsSuite) Test_UpdateAction_Succeeds() { func (s *ActionsSuite) Test_UpdateAction_NormalizesToLowerCase() { newAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: "testing_update_action_casing", + Name: "testing_update_action_casing", + NamespaceId: s.defaultNamespaceID(), }) s.NotNil(newAction) s.Require().NoError(err) @@ -328,7 +649,8 @@ func (s *ActionsSuite) Test_UpdateAction_NonExistent_Fails() { func (s *ActionsSuite) Test_DeleteAction_Succeeds() { created, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: "new_custom_action_deleteaction", + Name: "new_custom_action_deleteaction", + NamespaceId: s.defaultNamespaceID(), }) s.NotNil(created) s.Require().NoError(err) @@ -371,6 +693,22 @@ func (s *ActionsSuite) Test_DeleteAction_StandardAction_Fails() { s.Contains(err.Error(), actionRead.GetName()) } +func (s *ActionsSuite) defaultNamespaceID() string { + return s.f.GetNamespaceKey("example.com").ID +} + +func (s *ActionsSuite) otherNamespaceID() string { + return s.f.GetNamespaceKey("example.net").ID +} + +func (s *ActionsSuite) defaultNamespaceFQN() string { + return "https://" + s.f.GetNamespaceKey("example.com").Name +} + +func (s *ActionsSuite) otherNamespaceFQN() string { + return "https://" + s.f.GetNamespaceKey("example.net").Name +} + func TestActionsSuite(t *testing.T) { if testing.Short() { t.Skip("skipping actions integration tests") diff --git a/service/integration/attribute_fqns_test.go b/service/integration/attribute_fqns_test.go index c06e5d4ca5..d38c39650b 100644 --- a/service/integration/attribute_fqns_test.go +++ b/service/integration/attribute_fqns_test.go @@ -19,6 +19,7 @@ import ( policydb "github.com/opentdf/platform/service/policy/db" "github.com/stretchr/testify/suite" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/wrapperspb" ) type AttributeFqnSuite struct { @@ -1028,6 +1029,62 @@ func (s *AttributeFqnSuite) TestGetAttributesByValueFqns() { } } +func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_FiltersInactiveValues_FromDefinition() { + namespace := "filter_inactive_values.get" + attr := "test_attr" + value1 := "test_value" + value2 := "test_value_2" + fqn1 := fqnBuilder(namespace, attr, value1) + + // Create namespace + n, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: namespace, + }) + s.Require().NoError(err) + + // Create attribute + a, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: n.GetId(), + Name: attr, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + AllowTraversal: wrapperspb.Bool(true), + }) + s.Require().NoError(err) + + // Create attribute values + v1, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, a.GetId(), &attributes.CreateAttributeValueRequest{ + Value: value1, + }) + s.Require().NoError(err) + + v2, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, a.GetId(), &attributes.CreateAttributeValueRequest{ + Value: value2, + }) + s.Require().NoError(err) + + // Deactivate the second value + deactivated, err := s.db.PolicyClient.DeactivateAttributeValue(s.ctx, v2.GetId()) + s.Require().NoError(err) + s.NotNil(deactivated) + + // Get attributes by FQN of the active value + attributeAndValue, err := s.db.PolicyClient.GetAttributesByValueFqns(s.ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: []string{fqn1}, + }) + s.Require().NoError(err) + s.Len(attributeAndValue, 1) + + val, ok := attributeAndValue[fqn1] + s.True(ok) + s.Equal(a.GetId(), val.GetAttribute().GetId()) + s.Equal(v1.GetId(), val.GetValue().GetId()) + + values := val.GetAttribute().GetValues() + s.Len(values, 1) + s.Equal(v1.GetId(), values[0].GetId()) + s.Equal(v1.GetValue(), values[0].GetValue()) +} + func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_NormalizesLowerCase() { namespace := "TESTLOWERCASE.get" attr := "test_attr" @@ -1311,6 +1368,182 @@ func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_Fails_WithDeactivatedAt s.Require().ErrorIs(err, db.ErrNotFound) } +func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_MixedFoundAndMissingValue_DifferentDefinitions_Succeeds() { + // create a new namespace + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-fqn-namespace.active", + }) + s.Require().NoError(err) + + // create attribute with one value (found) + attrFound, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: ns.GetId(), + Name: "mixed_attr_found", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + Values: []string{"value1"}, + AllowTraversal: wrapperspb.Bool(true), + }) + s.Require().NoError(err) + + gotFound, err := s.db.PolicyClient.GetAttribute(s.ctx, attrFound.GetId()) + s.Require().NoError(err) + s.Len(gotFound.GetValues(), 1) + foundValue := gotFound.GetValues()[0] + + // create attribute with no values (missing) + attrMissing, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: ns.GetId(), + Name: "mixed_attr_missing", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + AllowTraversal: wrapperspb.Bool(true), + }) + s.Require().NoError(err) + + foundFqn := fqnBuilder(ns.GetName(), attrFound.GetName(), foundValue.GetValue()) + missingFqn := fqnBuilder(ns.GetName(), attrMissing.GetName(), "missing_value") + + retrieved, err := s.db.PolicyClient.GetAttributesByValueFqns(s.ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: []string{foundFqn, missingFqn}, + }) + s.Require().NoError(err) + s.Len(retrieved, 2) + + found, ok := retrieved[foundFqn] + s.True(ok) + s.NotNil(found) + s.Equal(attrFound.GetId(), found.GetAttribute().GetId()) + s.NotNil(found.GetValue()) + s.Equal(foundValue.GetId(), found.GetValue().GetId()) + + missing, ok := retrieved[missingFqn] + s.True(ok) + s.NotNil(missing) + s.Equal(attrMissing.GetId(), missing.GetAttribute().GetId()) + s.Nil(missing.GetValue()) +} + +func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_MixedMissingValue_InactiveDefinition_Fails() { + // create a new namespace + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-fqn-namespace.inactive", + }) + s.Require().NoError(err) + + // give it an attribute with two values + attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: ns.GetId(), + Name: "inactive_attr", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + Values: []string{"value1", "value2"}, + AllowTraversal: wrapperspb.Bool(true), + }) + s.Require().NoError(err) + got, err := s.db.PolicyClient.GetAttribute(s.ctx, attr.GetId()) + s.Require().NoError(err) + s.Len(got.GetValues(), 2) + v1 := got.GetValues()[0] + + // deactivate the attribute definition + _, err = s.db.PolicyClient.DeactivateAttribute(s.ctx, attr.GetId()) + s.Require().NoError(err) + + foundFqn := fqnBuilder(ns.GetName(), attr.GetName(), v1.GetValue()) + missingFqn := fqnBuilder(ns.GetName(), attr.GetName(), "missing_value") + + retrieved, err := s.db.PolicyClient.GetAttributesByValueFqns(s.ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: []string{foundFqn, missingFqn}, + }) + s.Require().Error(err) + s.Nil(retrieved) + s.Require().ErrorIs(err, db.ErrNotFound) +} + +func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_MixedMissingValue_AllowTraversalMismatch_Fails() { + // create a new namespace + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-fqn-namespace.mixed-traversal", + }) + s.Require().NoError(err) + + // create attribute with allow_traversal=true + attrAllow, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: ns.GetId(), + Name: "mixed_attr_allow", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + AllowTraversal: wrapperspb.Bool(true), + }) + s.Require().NoError(err) + + // create attribute with allow_traversal=false + attrDeny, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: ns.GetId(), + Name: "mixed_attr_deny", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + }) + s.Require().NoError(err) + + allowMissingFqn := fqnBuilder(ns.GetName(), attrAllow.GetName(), "missing_value") + denyMissingFqn := fqnBuilder(ns.GetName(), attrDeny.GetName(), "missing_value") + + retrieved, err := s.db.PolicyClient.GetAttributesByValueFqns(s.ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: []string{allowMissingFqn, denyMissingFqn}, + }) + s.Require().Error(err) + s.Nil(retrieved) + s.Require().ErrorIs(err, db.ErrNotFound) +} + +func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_Fails_MissingValueAndDefinition() { + // create a new namespace + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-missing-attr.example", + }) + s.Require().NoError(err) + + missingFqn := fqnBuilder(ns.GetName(), "missing_attr", "missing_value") + retrieved, err := s.db.PolicyClient.GetAttributesByValueFqns(s.ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: []string{missingFqn}, + }) + s.Require().Error(err) + s.Nil(retrieved) + s.Require().ErrorIs(err, db.ErrNotFound) +} + +func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_Fails_WithInactiveValue_ActiveDefinition() { + // create a new namespace + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-fqn-namespace.inactive-value", + }) + s.Require().NoError(err) + + // create attribute with two values + attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: ns.GetId(), + Name: "inactive_value_attr", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + Values: []string{"value1", "value2"}, + AllowTraversal: wrapperspb.Bool(true), + }) + s.Require().NoError(err) + + got, err := s.db.PolicyClient.GetAttribute(s.ctx, attr.GetId()) + s.Require().NoError(err) + s.Len(got.GetValues(), 2) + valueToDeactivate := got.GetValues()[0] + + // deactivate a value while leaving the definition active + _, err = s.db.PolicyClient.DeactivateAttributeValue(s.ctx, valueToDeactivate.GetId()) + s.Require().NoError(err) + + fqn := fqnBuilder(ns.GetName(), attr.GetName(), valueToDeactivate.GetValue()) + retrieved, err := s.db.PolicyClient.GetAttributesByValueFqns(s.ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: []string{fqn}, + }) + s.Require().Error(err) + s.Nil(retrieved) + s.Require().ErrorIs(err, db.ErrAttributeValueInactive) +} + func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_Fails_WithDeactivatedAttributeValue() { // create a new namespace ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ @@ -1343,7 +1576,7 @@ func (s *AttributeFqnSuite) TestGetAttributesByValueFqns_Fails_WithDeactivatedAt }) s.Require().Error(err) s.Nil(v) - s.Require().ErrorIs(err, db.ErrNotFound) + s.Require().ErrorIs(err, db.ErrAttributeValueInactive) // get the attribute by the value fqn for v2 v, err = s.db.PolicyClient.GetAttributesByValueFqns(s.ctx, &attributes.GetAttributeValuesByFqnsRequest{ diff --git a/service/integration/attribute_values_test.go b/service/integration/attribute_values_test.go index e8c8cb992a..58f3ea7ff6 100644 --- a/service/integration/attribute_values_test.go +++ b/service/integration/attribute_values_test.go @@ -9,6 +9,7 @@ import ( "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/opentdf/platform/protocol/go/policy/kasregistry" "github.com/opentdf/platform/protocol/go/policy/namespaces" @@ -17,7 +18,6 @@ import ( "github.com/opentdf/platform/service/internal/fixtures" "github.com/opentdf/platform/service/pkg/db" "github.com/stretchr/testify/suite" - "google.golang.org/protobuf/proto" ) var absentAttributeValueUUID = "78909865-8888-9999-9999-000000000000" @@ -68,228 +68,6 @@ func (s *AttributeValuesSuite) TearDownSuite() { s.f.TearDown(s.ctx) } -func (s *AttributeValuesSuite) Test_ListAttributeValues_WithAttributeID_Succeeds() { - attrID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").AttributeDefinitionID - - listRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - AttributeId: attrID, - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE, - }) - s.Require().NoError(err) - s.NotNil(listRsp) - listed := listRsp.GetValues() - - // ensure list contains the two test fixtures and that response matches expected data - f1 := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1") - f2 := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value2") - - for _, val := range listed { - if val.GetId() == f1.ID { - s.Equal(f1.ID, val.GetId()) - s.Equal(f1.Value, val.GetValue()) - s.Equal(f1.AttributeDefinitionID, val.GetAttribute().GetId()) - } else if val.GetId() == f2.ID { - s.Equal(f2.ID, val.GetId()) - s.Equal(f2.Value, val.GetValue()) - s.Equal(f2.AttributeDefinitionID, val.GetAttribute().GetId()) - } - } -} - -func (s *AttributeValuesSuite) Test_ListAttributeValues_NoPagination_Succeeds() { - allFixtureValueFqns := map[string]bool{ - "https://example.com/attr/attr1/value/value1": false, - "https://example.com/attr/attr1/value/value2": false, - "https://example.com/attr/attr2/value/value1": false, - "https://example.com/attr/attr2/value/value2": false, - "https://example.net/attr/attr1/value/value1": false, - "https://example.net/attr/attr1/value/value2": false, - "https://scenario.com/attr/working_group/value/blue": false, - "https://deactivated.io/attr/deactivated_attr/value/deactivated_value": false, - } - listRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, - }) - s.Require().NoError(err) - s.NotNil(listRsp) - // mark every listed value true - for _, val := range listRsp.GetValues() { - allFixtureValueFqns[val.GetFqn()] = true - } - // ensure all fixtures were found by unbounded list - for fqn, found := range allFixtureValueFqns { - if !found { - s.Failf("failed to list fixture", fqn) - } - } -} - -func (s *AttributeValuesSuite) Test_ListAttributeValues_Limit_Succeeds() { - var limit int32 = 2 - listRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, - Pagination: &policy.PageRequest{ - Limit: limit, - }, - }) - s.Require().NoError(err) - s.NotNil(listRsp) - listed := listRsp.GetValues() - s.Equal(len(listed), int(limit)) - - for _, val := range listed { - s.NotEmpty(val.GetFqn()) - s.NotEmpty(val.GetId()) - s.NotEmpty(val.GetValue()) - } - - // request with one below maximum - listRsp, err = s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, - Pagination: &policy.PageRequest{ - Limit: s.db.LimitMax - 1, - }, - }) - s.Require().NoError(err) - s.NotNil(listRsp) - - // request with exactly maximum - listRsp, err = s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, - Pagination: &policy.PageRequest{ - Limit: s.db.LimitMax, - }, - }) - s.Require().NoError(err) - s.NotNil(listRsp) -} - -func (s *NamespacesSuite) Test_ListAttributeValues_Limit_TooLarge_Fails() { - listRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, - Pagination: &policy.PageRequest{ - Limit: s.db.LimitMax + 1, - }, - }) - s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrListLimitTooLarge) - s.Nil(listRsp) -} - -func (s *AttributeValuesSuite) Test_ListAttributeValues_Offset_Succeeds() { - req := &attributes.ListAttributeValuesRequest{ - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, - } - // make initial list request to compare against - listRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, req) - s.Require().NoError(err) - s.NotNil(listRsp) - listed := listRsp.GetValues() - - // set the offset pagination - offset := 5 - req.Pagination = &policy.PageRequest{ - Offset: int32(offset), - } - offsetListRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, req) - s.Require().NoError(err) - s.NotNil(offsetListRsp) - offsetListed := offsetListRsp.GetValues() - - // length is reduced by the offset amount - s.Equal(len(offsetListed), len(listed)-offset) - - // objects are equal between offset and original list beginning at offset index - for i, val := range offsetListed { - s.True(proto.Equal(val, listed[i+offset])) - } -} - -func (s *AttributeValuesSuite) Test_ListAttributeValues_AttributeDefID_Succeeds() { - // Create a namespace - ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ - Name: "test-pagination.com", - }) - s.Require().NoError(err) - s.NotNil(ns) - s.namespaces = append(s.namespaces, ns) - - // Create an attribute definition - attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ - Name: "test-attr-pagination", - NamespaceId: ns.GetId(), - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, - }) - s.Require().NoError(err) - s.NotNil(attr) - - // Create multiple attribute values - expectedValues := make([]string, 5) - createdValueIDs := make([]string, 5) - for i := 0; i < 5; i++ { - value := fmt.Sprintf("test-value-%d", i+1) - expectedValues[i] = value - - req := &attributes.CreateAttributeValueRequest{ - Value: value, - } - createdValue, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attr.GetId(), req) - s.Require().NoError(err) - s.NotNil(createdValue) - createdValueIDs[i] = createdValue.GetId() - s.Equal(value, createdValue.GetValue()) - } - - // Test listing all values without pagination - listRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - AttributeId: attr.GetId(), - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE, - }) - s.Require().NoError(err) - s.NotNil(listRsp) - s.Len(listRsp.GetValues(), 5) - - // Verify all created values are in the response - foundValues := make(map[string]bool) - for _, val := range listRsp.GetValues() { - foundValues[val.GetValue()] = true - s.Equal(attr.GetId(), val.GetAttribute().GetId()) - } - for _, expectedValue := range expectedValues { - s.True(foundValues[expectedValue], "Expected value %s not found in list", expectedValue) - } - s.Require().Equal(int32(5), listRsp.GetPagination().GetTotal()) - s.Require().Equal(int32(0), listRsp.GetPagination().GetCurrentOffset()) - s.Require().Equal(int32(0), listRsp.GetPagination().GetNextOffset()) - - // Deactivate one of the attribute values - deactivated, err := s.db.PolicyClient.DeactivateAttributeValue(s.ctx, createdValueIDs[2]) // deactivate "test-value-3" - s.Require().NoError(err) - s.Require().NotNil(deactivated) - s.Require().False(deactivated.GetActive().GetValue()) - - // Test listing only active values after deactivation - activeListRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - AttributeId: attr.GetId(), - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE, - }) - s.Require().NoError(err) - s.NotNil(activeListRsp) - s.Len(activeListRsp.GetValues(), 4) // should be 4 active values now - - // Verify that the deactivated value is not in the active list - activeFoundValues := make(map[string]bool) - for _, val := range activeListRsp.GetValues() { - activeFoundValues[val.GetValue()] = true - s.True(val.GetActive().GetValue(), "All values in active list should be active") - s.Equal(attr.GetId(), val.GetAttribute().GetId()) - } - s.False(activeFoundValues["test-value-3"], "Deactivated value should not be in active list") - s.Require().Equal(int32(4), activeListRsp.GetPagination().GetTotal()) - s.Require().Equal(int32(0), activeListRsp.GetPagination().GetCurrentOffset()) - s.Require().Equal(int32(0), activeListRsp.GetPagination().GetNextOffset()) -} - func (s *AttributeValuesSuite) Test_GetAttributeValue() { f := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1") @@ -768,16 +546,22 @@ func (s *AttributeValuesSuite) Test_DeactivateAttribute_Cascades_List() { } listValues := func(state common.ActiveStateEnum) bool { - listedValsRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - State: state, - AttributeId: stillActiveAttributeID, - }) + gotAttr, err := s.db.PolicyClient.GetAttribute(s.ctx, stillActiveAttributeID) s.Require().NoError(err) - s.NotNil(listedValsRsp) - listedVals := listedValsRsp.GetValues() - for _, v := range listedVals { - if deactivatedAttrValueID == v.GetId() { + s.NotNil(gotAttr) + for _, v := range gotAttr.GetValues() { + if deactivatedAttrValueID != v.GetId() { + continue + } + switch state { + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE: + return v.GetActive().GetValue() + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE: + return !v.GetActive().GetValue() + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY: return true + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_UNSPECIFIED: + return v.GetActive().GetValue() } } return false @@ -1175,8 +959,8 @@ func (s *AttributeValuesSuite) Test_GetAttributeValue_With_Two_Obligations_Succe s.obligations = append(s.obligations, obl1) // Create first obligation value with two triggers - readAction := s.f.GetStandardAction("read") - updateAction := s.f.GetStandardAction("update") + readAction := s.getActionByNameInNamespace("read", ns.GetId()) + updateAction := s.getActionByNameInNamespace("update", ns.GetId()) obl1Val1, err := s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ ObligationId: obl1.GetId(), @@ -1259,6 +1043,254 @@ func (s *AttributeValuesSuite) Test_GetAttributeValue_With_Two_Obligations_Succe s.assertObligations([]*policy.Obligation{obl1, obl2}, retrievedValue.GetObligations()) } +func (s *AttributeValuesSuite) Test_CreateAttributeValue_WithObligationTriggers_Succeeds() { + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-inline-obligation-triggers.com", + }) + s.Require().NoError(err) + s.NotNil(ns) + s.namespaces = append(s.namespaces, ns) + + attrDef, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: "test-inline-obligation-triggers-attr", + NamespaceId: ns.GetId(), + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + s.NotNil(attrDef) + + obl1, err := s.db.PolicyClient.CreateObligation(s.ctx, &obligations.CreateObligationRequest{ + NamespaceId: ns.GetId(), + Name: "test_inline_obligation_1", + }) + s.Require().NoError(err) + s.obligations = append(s.obligations, obl1) + + obl2, err := s.db.PolicyClient.CreateObligation(s.ctx, &obligations.CreateObligationRequest{ + NamespaceId: ns.GetId(), + Name: "test_inline_obligation_2", + }) + s.Require().NoError(err) + s.obligations = append(s.obligations, obl2) + + obl1Val1, err := s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ + ObligationId: obl1.GetId(), + Value: "inline_obligation_value_1", + }) + s.Require().NoError(err) + + obl1Val2, err := s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ + ObligationId: obl1.GetId(), + Value: "inline_obligation_value_2", + }) + s.Require().NoError(err) + + obl2Val1, err := s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ + ObligationId: obl2.GetId(), + Value: "inline_obligation_value_3", + }) + s.Require().NoError(err) + + readAction := s.getActionByNameInNamespace("read", ns.GetId()) + updateAction := s.getActionByNameInNamespace("update", ns.GetId()) + + createdValue, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attrDef.GetId(), &attributes.CreateAttributeValueRequest{ + Value: "test_value_with_inline_triggers", + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + ObligationValue: &common.IdFqnIdentifier{Id: obl1Val1.GetId()}, + Action: &common.IdNameIdentifier{Id: readAction.GetId()}, + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"source": "inline-trigger-1"}, + }, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: "test-client-1", + }, + }, + }, + { + ObligationValue: &common.IdFqnIdentifier{Fqn: obl1Val2.GetFqn()}, + Action: &common.IdNameIdentifier{Name: updateAction.GetName()}, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: "test-client-2", + }, + }, + }, + { + ObligationValue: &common.IdFqnIdentifier{Id: obl2Val1.GetId()}, + Action: &common.IdNameIdentifier{Name: readAction.GetName()}, + }, + }, + }) + s.Require().NoError(err) + s.NotNil(createdValue) + s.Require().Len(createdValue.GetObligations(), 2) + + retrievedValue, err := s.db.PolicyClient.GetAttributeValue(s.ctx, createdValue.GetId()) + s.Require().NoError(err) + s.NotNil(retrievedValue) + s.Require().Len(retrievedValue.GetObligations(), 2) + + obligationsByID := make(map[string]*policy.Obligation) + for _, obligation := range retrievedValue.GetObligations() { + obligationsByID[obligation.GetId()] = obligation + } + + retrievedObl1, found := obligationsByID[obl1.GetId()] + s.Require().True(found) + s.Require().Len(retrievedObl1.GetValues(), 2) + + retrievedObl1Values := make(map[string]*policy.ObligationValue) + for _, value := range retrievedObl1.GetValues() { + retrievedObl1Values[value.GetId()] = value + } + + s.assertObligationValueHasSingleTrigger(retrievedObl1Values[obl1Val1.GetId()], obl1Val1, readAction, createdValue, "test-client-1") + s.assertObligationValueHasSingleTrigger(retrievedObl1Values[obl1Val2.GetId()], obl1Val2, updateAction, createdValue, "test-client-2") + + triggerWithMetadata, err := s.db.PolicyClient.GetObligationTrigger(s.ctx, &obligations.GetObligationTriggerRequest{ + Id: retrievedObl1Values[obl1Val1.GetId()].GetTriggers()[0].GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(triggerWithMetadata.GetMetadata()) + s.Equal("inline-trigger-1", triggerWithMetadata.GetMetadata().GetLabels()["source"]) + + retrievedObl2, found := obligationsByID[obl2.GetId()] + s.Require().True(found) + s.Require().Len(retrievedObl2.GetValues(), 1) + s.assertObligationValueHasSingleTrigger(retrievedObl2.GetValues()[0], obl2Val1, readAction, createdValue, "") +} + +func (s *AttributeValuesSuite) Test_CreateAttributeValue_WithObligationTriggers_CrossNamespaceObligationValue_Succeeds() { + sourceNamespace, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-inline-cross-namespace-trigger-source.com", + }) + s.Require().NoError(err) + s.NotNil(sourceNamespace) + s.namespaces = append(s.namespaces, sourceNamespace) + + attrDef, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: "test-inline-cross-namespace-trigger-attr", + NamespaceId: sourceNamespace.GetId(), + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + s.NotNil(attrDef) + + targetNamespace, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-inline-cross-namespace-trigger-target.com", + }) + s.Require().NoError(err) + s.NotNil(targetNamespace) + s.namespaces = append(s.namespaces, targetNamespace) + + targetObligation, err := s.db.PolicyClient.CreateObligation(s.ctx, &obligations.CreateObligationRequest{ + NamespaceId: targetNamespace.GetId(), + Name: "test_inline_cross_namespace_obligation", + }) + s.Require().NoError(err) + s.obligations = append(s.obligations, targetObligation) + + targetObligationValue, err := s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ + ObligationId: targetObligation.GetId(), + Value: "inline_cross_namespace_obligation_value", + }) + s.Require().NoError(err) + + customAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "inline-cross-namespace-trigger-action", + NamespaceId: sourceNamespace.GetId(), + }) + s.Require().NoError(err) + + createdValue, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attrDef.GetId(), &attributes.CreateAttributeValueRequest{ + Value: "test_value_with_cross_namespace_inline_trigger", + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + ObligationValue: &common.IdFqnIdentifier{Fqn: targetObligationValue.GetFqn()}, + Action: &common.IdNameIdentifier{Name: customAction.GetName()}, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: "cross-namespace-inline-client", + }, + }, + }, + }, + }) + s.Require().NoError(err) + s.NotNil(createdValue) + s.Require().Len(createdValue.GetObligations(), 1) + + retrievedValue, err := s.db.PolicyClient.GetAttributeValue(s.ctx, createdValue.GetId()) + s.Require().NoError(err) + s.NotNil(retrievedValue) + s.Require().Len(retrievedValue.GetObligations(), 1) + + retrievedObligation := retrievedValue.GetObligations()[0] + s.Equal(targetObligation.GetId(), retrievedObligation.GetId()) + s.Equal(targetObligation.GetName(), retrievedObligation.GetName()) + s.Require().NotNil(retrievedObligation.GetNamespace()) + s.Equal(targetNamespace.GetFqn(), retrievedObligation.GetNamespace().GetFqn()) + s.Require().Len(retrievedObligation.GetValues(), 1) + + s.assertObligationValueHasSingleTrigger( + retrievedObligation.GetValues()[0], + targetObligationValue, + customAction, + createdValue, + "cross-namespace-inline-client", + ) +} + +func (s *AttributeValuesSuite) Test_CreateAttributeValue_WithObligationTriggers_ObligationValueFQNNotFound_Fails() { + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "test-inline-obligation-trigger-missing-obligation-fqn.com", + }) + s.Require().NoError(err) + s.NotNil(ns) + s.namespaces = append(s.namespaces, ns) + + attrDef, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: "test-inline-obligation-trigger-missing-obligation-fqn-attr", + NamespaceId: ns.GetId(), + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + s.NotNil(attrDef) + + obl, err := s.db.PolicyClient.CreateObligation(s.ctx, &obligations.CreateObligationRequest{ + NamespaceId: ns.GetId(), + Name: "test_inline_missing_obligation_fqn", + }) + s.Require().NoError(err) + s.obligations = append(s.obligations, obl) + + _, err = s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ + ObligationId: obl.GetId(), + Value: "test_inline_missing_obligation_fqn_value", + }) + s.Require().NoError(err) + + readAction := s.getActionByNameInNamespace("read", ns.GetId()) + + createdValue, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attrDef.GetId(), &attributes.CreateAttributeValueRequest{ + Value: "test_value_missing_obligation_fqn", + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + ObligationValue: &common.IdFqnIdentifier{ + Fqn: ns.GetFqn() + "/obl/" + obl.GetName() + "/value/missing_obligation_value", + }, + Action: &common.IdNameIdentifier{Id: readAction.GetId()}, + }, + }, + }) + s.Require().Error(err) + s.Nil(createdValue) + s.Require().ErrorIs(err, db.ErrNotFound) +} + func TestAttributeValuesSuite(t *testing.T) { if testing.Short() { t.Skip("skipping attribute values integration tests") @@ -1266,6 +1298,64 @@ func TestAttributeValuesSuite(t *testing.T) { suite.Run(t, new(AttributeValuesSuite)) } +func (s *AttributeValuesSuite) getActionByNameInNamespace(name string, namespaceID string) *policy.Action { + action, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: name}, + NamespaceId: namespaceID, + }) + s.Require().NoError(err) + return action +} + +func (s *AttributeValuesSuite) assertObligationValueHasSingleTrigger( + actualObligationValue *policy.ObligationValue, + expectedObligationValue *policy.ObligationValue, + expectedAction *policy.Action, + expectedAttributeValue *policy.Value, + expectedClientID string, +) { + s.Require().NotNil(actualObligationValue) + s.Equal(expectedObligationValue.GetId(), actualObligationValue.GetId()) + s.Equal(expectedObligationValue.GetValue(), actualObligationValue.GetValue()) + s.Equal(expectedObligationValue.GetFqn(), actualObligationValue.GetFqn()) + s.Require().Len(actualObligationValue.GetTriggers(), 1) + + trigger := actualObligationValue.GetTriggers()[0] + s.Require().NotEmpty(trigger.GetId()) + + if trigger.GetObligationValue() != nil { + s.Equal(expectedObligationValue.GetId(), trigger.GetObligationValue().GetId()) + s.Equal(expectedObligationValue.GetValue(), trigger.GetObligationValue().GetValue()) + s.Equal(expectedObligationValue.GetFqn(), trigger.GetObligationValue().GetFqn()) + s.Require().NotNil(trigger.GetObligationValue().GetObligation()) + s.Equal(expectedObligationValue.GetObligation().GetId(), trigger.GetObligationValue().GetObligation().GetId()) + s.Equal(expectedObligationValue.GetObligation().GetName(), trigger.GetObligationValue().GetObligation().GetName()) + s.Equal(expectedObligationValue.GetObligation().GetNamespace().GetFqn(), trigger.GetObligationValue().GetObligation().GetNamespace().GetFqn()) + } + + s.Require().NotNil(trigger.GetAction()) + s.Equal(expectedAction.GetId(), trigger.GetAction().GetId()) + s.Equal(expectedAction.GetName(), trigger.GetAction().GetName()) + s.Require().NotNil(trigger.GetNamespace()) + s.NotEmpty(trigger.GetNamespace().GetId()) + s.Equal(strings.Split(expectedAttributeValue.GetFqn(), "/attr/")[0], trigger.GetNamespace().GetFqn()) + + s.Require().NotNil(trigger.GetAttributeValue()) + s.Equal(expectedAttributeValue.GetId(), trigger.GetAttributeValue().GetId()) + s.Equal(expectedAttributeValue.GetFqn(), trigger.GetAttributeValue().GetFqn()) + if trigger.GetAttributeValue().GetValue() != "" { + s.Equal(expectedAttributeValue.GetValue(), trigger.GetAttributeValue().GetValue()) + } + + if expectedClientID == "" { + s.Empty(trigger.GetContext()) + return + } + + s.Require().Len(trigger.GetContext(), 1) + s.Equal(expectedClientID, trigger.GetContext()[0].GetPep().GetClientId()) +} + func (s *AttributeValuesSuite) assertObligations(expected, actual []*policy.Obligation) { s.Require().Len(actual, len(expected), "number of obligations does not match") diff --git a/service/integration/attributes_test.go b/service/integration/attributes_test.go index 1e66e0576c..2e2c29c32b 100644 --- a/service/integration/attributes_test.go +++ b/service/integration/attributes_test.go @@ -7,6 +7,7 @@ import ( "slices" "strings" "testing" + "time" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" @@ -19,6 +20,7 @@ import ( policydb "github.com/opentdf/platform/service/policy/db" "github.com/stretchr/testify/suite" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/wrapperspb" ) type AttributesSuite struct { @@ -64,6 +66,42 @@ func (s *AttributesSuite) Test_CreateAttribute_NoMetadataSucceeds() { s.NotNil(createdAttr) } +func (s *AttributesSuite) Test_CreateAttribute_WithoutValues_DoesNotReturnEmptyValue() { + attr := &attributes.CreateAttributeRequest{ + Name: "test__create_attribute_without_values", + NamespaceId: fixtureNamespaceID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + } + + createdAttr, err := s.db.PolicyClient.CreateAttribute(s.ctx, attr) + s.Require().NoError(err) + s.Require().NotNil(createdAttr) + s.Empty(createdAttr.GetValues()) + + gotAttr, err := s.db.PolicyClient.GetAttribute(s.ctx, createdAttr.GetId()) + s.Require().NoError(err) + s.Require().NotNil(gotAttr) + s.Empty(gotAttr.GetValues()) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: fixtureNamespaceID, + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + }) + s.Require().NoError(err) + s.Require().NotNil(listRsp) + + for _, listedAttr := range listRsp.GetAttributes() { + if listedAttr.GetId() != createdAttr.GetId() { + continue + } + + s.Empty(listedAttr.GetValues()) + return + } + + s.Failf("created attribute not found in list response", "attribute_id=%s", createdAttr.GetId()) +} + func (s *AttributesSuite) Test_CreateAttribute_NormalizeName() { name := "NaMe_12_ShOuLdBe-NoRmAlIzEd" attr := &attributes.CreateAttributeRequest{ @@ -100,6 +138,26 @@ func (s *AttributesSuite) Test_CreateAttribute_WithValueSucceeds() { s.NotNil(createdAttr) } +func (s *AttributesSuite) Test_CreateAttribute_WithAllowTraversal_Succeeds() { + attr := &attributes.CreateAttributeRequest{ + Name: "test__create_attribute_allow_traversal", + NamespaceId: fixtureNamespaceID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + AllowTraversal: &wrapperspb.BoolValue{Value: true}, + } + createdAttr, err := s.db.PolicyClient.CreateAttribute(s.ctx, attr) + s.Require().NoError(err) + s.NotNil(createdAttr) + s.Require().NotNil(createdAttr.GetAllowTraversal()) + s.True(createdAttr.GetAllowTraversal().GetValue()) + + got, err := s.db.PolicyClient.GetAttribute(s.ctx, createdAttr.GetId()) + s.Require().NoError(err) + s.NotNil(got) + s.Require().NotNil(got.GetAllowTraversal()) + s.True(got.GetAllowTraversal().GetValue()) +} + func (s *AttributesSuite) Test_CreateAttribute_WithMetadataSucceeds() { attr := &attributes.CreateAttributeRequest{ Name: "test__create_attribute_with_metadata", @@ -272,6 +330,32 @@ func (s *AttributesSuite) Test_GetAttribute_OrderOfValuesIsPreserved() { s.Equal(values[3], gotVals[2].GetValue()) } +func (s *AttributesSuite) Test_GetAttributesByValueFqns_IncludesAllowTraversal() { + attr := &attributes.CreateAttributeRequest{ + Name: "test__get_attributes_by_value_fqns_allow_traversal", + NamespaceId: fixtureNamespaceID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + AllowTraversal: &wrapperspb.BoolValue{Value: true}, + Values: []string{"allow_traversal_value"}, + } + createdAttr, err := s.db.PolicyClient.CreateAttribute(s.ctx, attr) + s.Require().NoError(err) + s.NotNil(createdAttr) + + valueFqn := fmt.Sprintf("https://%s/attr/%s/value/%s", createdAttr.GetNamespace().GetName(), createdAttr.GetName(), createdAttr.GetValues()[0].GetValue()) + resp, err := s.db.PolicyClient.GetAttributesByValueFqns(s.ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: []string{valueFqn}, + }) + s.Require().NoError(err) + s.NotNil(resp) + s.Contains(resp, valueFqn) + + gotAttr := resp[valueFqn].GetAttribute() + s.Require().NotNil(gotAttr) + s.Require().NotNil(gotAttr.GetAllowTraversal()) + s.True(gotAttr.GetAllowTraversal().GetValue()) +} + func (s *AttributesSuite) Test_GetAttribute() { fixtures := s.getAttributeFixtures() @@ -390,6 +474,271 @@ func (s *AttributesSuite) Test_ListAttributes_NoPagination_Succeeds() { } } +func (s *AttributesSuite) Test_ListAttributes_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + nsName := fmt.Sprintf("order-test-attrs-%d.com", suffix) + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: nsName}) + s.Require().NoError(err) + s.Require().NotNil(ns) + + create := func(i int) string { + name := fmt.Sprintf("order-test-attr-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: name, + NamespaceId: ns.GetId(), + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + s.Require().NotNil(created) + return created.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: ns.GetId(), + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, thirdID, secondID, firstID) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByName_ASC() { + nsID := s.createSortTestNamespace("sort-name-asc") + ids := s.createSortTestAttributes(nsID, []string{"aaa-sort", "bbb-sort", "ccc-sort"}) + defer s.deleteSortTestAttributes(ids) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // aaa < bbb < ccc in ASC order + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByName_DESC() { + nsID := s.createSortTestNamespace("sort-name-desc") + ids := s.createSortTestAttributes(nsID, []string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) + defer s.deleteSortTestAttributes(ids) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // ccc > bbb > aaa in DESC order + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByCreatedAt_ASC() { + nsID := s.createSortTestNamespace("sort-created-asc") + ids := s.createSortTestAttributes(nsID, []string{"createdasc-attr-0", "createdasc-attr-1", "createdasc-attr-2"}) + defer s.deleteSortTestAttributes(ids) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // oldest first in ASC order + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByCreatedAt_DESC() { + nsID := s.createSortTestNamespace("sort-created-desc") + ids := s.createSortTestAttributes(nsID, []string{"createddesc-attr-0", "createddesc-attr-1", "createddesc-attr-2"}) + defer s.deleteSortTestAttributes(ids) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // newest first in DESC order + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByUpdatedAt_DESC() { + nsID := s.createSortTestNamespace("sort-updated-desc") + ids := s.createSortTestAttributes(nsID, []string{"upd-sort-attr-0", "upd-sort-attr-1", "upd-sort-attr-2"}) + defer s.deleteSortTestAttributes(ids) + + // Update the first attribute so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateAttribute(s.ctx, ids[0], &attributes.UpdateAttributeRequest{ + Id: ids[0], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated attribute (ids[0]) should appear before the others + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[0], ids[2], ids[1]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByUpdatedAt_ASC() { + nsID := s.createSortTestNamespace("sort-updated-asc") + ids := s.createSortTestAttributes(nsID, []string{"upd-sort-asc-attr-0", "upd-sort-asc-attr-1", "upd-sort-asc-attr-2"}) + defer s.deleteSortTestAttributes(ids) + + // Update the last attribute so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateAttribute(s.ctx, ids[2], &attributes.UpdateAttributeRequest{ + Id: ids[2], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated attribute (ids[2]) should appear last in ASC order + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortTieBreaker_CreatedAtWithIDFallback() { + nsID := s.createSortTestNamespace("sort-tiebreaker") + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-attr-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: name, + NamespaceId: nsID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestAttributes(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "attribute_definitions", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, sorted[0], sorted[1], sorted[2]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByUnspecifiedField_DefaultsToCreatedAt() { + nsID := s.createSortTestNamespace("sort-unspecified-field") + ids := s.createSortTestAttributes(nsID, []string{"unspecified-field-attr-0", "unspecified-field-attr-1", "unspecified-field-attr-2"}) + defer s.deleteSortTestAttributes(ids) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Field defaults to created_at, explicit ASC is preserved + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByUnspecifiedDirection_DefaultsToDESC() { + nsID := s.createSortTestNamespace("sort-unspecified-dir") + ids := s.createSortTestAttributes(nsID, []string{"unspecified-dir-attr-0", "unspecified-dir-attr-1", "unspecified-dir-attr-2"}) + defer s.deleteSortTestAttributes(ids) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Direction defaults to DESC, explicit created_at field is preserved + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortByBothUnspecified_DefaultsToCreatedAtDESC() { + nsID := s.createSortTestNamespace("sort-both-unspecified") + ids := s.createSortTestAttributes(nsID, []string{"both-unspecified-attr-0", "both-unspecified-attr-1", "both-unspecified-attr-2"}) + defer s.deleteSortTestAttributes(ids) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Both default: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *AttributesSuite) Test_ListAttributes_SortOmitted() { + nsID := s.createSortTestNamespace("sort-omitted") + ids := s.createSortTestAttributes(nsID, []string{"omitted-sort-attr-0", "omitted-sort-attr-1", "omitted-sort-attr-2"}) + defer s.deleteSortTestAttributes(ids) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // No sort provided: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[2], ids[1], ids[0]) +} + func (s *AttributesSuite) Test_ListAttributes_Limit_Succeeds() { var limit int32 = 2 listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ @@ -508,6 +857,47 @@ func (s *AttributesSuite) Test_ListAttributes_FqnsIncluded() { } } +func (s *AttributesSuite) Test_ListAttributes_IncludesAllowTraversal() { + attrTrue := &attributes.CreateAttributeRequest{ + Name: "test__list_attributes_allow_traversal_true", + NamespaceId: fixtureNamespaceID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + AllowTraversal: &wrapperspb.BoolValue{Value: true}, + } + attrFalse := &attributes.CreateAttributeRequest{ + Name: "test__list_attributes_allow_traversal_false", + NamespaceId: fixtureNamespaceID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + } + createdTrue, err := s.db.PolicyClient.CreateAttribute(s.ctx, attrTrue) + s.Require().NoError(err) + createdFalse, err := s.db.PolicyClient.CreateAttribute(s.ctx, attrFalse) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: fixtureNamespaceID, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + var foundTrue, foundFalse *policy.Attribute + for _, attr := range listRsp.GetAttributes() { + switch attr.GetId() { + case createdTrue.GetId(): + foundTrue = attr + case createdFalse.GetId(): + foundFalse = attr + } + } + + s.Require().NotNil(foundTrue) + s.Require().NotNil(foundFalse) + s.Require().NotNil(foundTrue.GetAllowTraversal()) + s.True(foundTrue.GetAllowTraversal().GetValue()) + s.Require().NotNil(foundFalse.GetAllowTraversal()) + s.False(foundFalse.GetAllowTraversal().GetValue()) +} + func (s *AttributesSuite) Test_ListAttributes_ByNamespaceIdOrName() { // get all unique namespace_ids namespaces := map[string]string{} @@ -808,6 +1198,34 @@ func (s *AttributesSuite) Test_UnsafeUpdateAttribute_WithRule() { s.Equal(policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, updated.GetRule()) } +func (s *AttributesSuite) Test_UnsafeUpdateAttribute_WithAllowTraversal() { + attr := &attributes.CreateAttributeRequest{ + Name: "test__unsafe_update_attribute_allow_traversal", + NamespaceId: fixtureNamespaceID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + } + created, err := s.db.PolicyClient.CreateAttribute(s.ctx, attr) + s.Require().NoError(err) + s.NotNil(created) + s.Require().NotNil(created.GetAllowTraversal()) + s.False(created.GetAllowTraversal().GetValue()) + + updated, err := s.db.PolicyClient.UnsafeUpdateAttribute(s.ctx, &unsafe.UnsafeUpdateAttributeRequest{ + Id: created.GetId(), + AllowTraversal: &wrapperspb.BoolValue{Value: true}, + }) + s.Require().NoError(err) + s.NotNil(updated) + s.Require().NotNil(updated.GetAllowTraversal()) + s.True(updated.GetAllowTraversal().GetValue()) + + got, err := s.db.PolicyClient.GetAttribute(s.ctx, created.GetId()) + s.Require().NoError(err) + s.NotNil(got) + s.Require().NotNil(got.GetAllowTraversal()) + s.True(got.GetAllowTraversal().GetValue()) +} + func (s *AttributesSuite) Test_UnsafeUpdateAttribute_WithNewName() { originalName := "test__update_attribute_with_new_name" newName := originalName + "updated" @@ -1114,16 +1532,22 @@ func (s *AttributesSuite) Test_DeactivateAttribute_Cascades_List() { } listValues := func(state common.ActiveStateEnum) bool { - valsListRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - AttributeId: deactivatedAttrID, - State: state, - }) + gotAttr, err := s.db.PolicyClient.GetAttribute(s.ctx, deactivatedAttrID) s.Require().NoError(err) - s.NotNil(valsListRsp) - listed := valsListRsp.GetValues() - for _, v := range listed { - if deactivatedAttrValueID == v.GetId() { + s.NotNil(gotAttr) + for _, v := range gotAttr.GetValues() { + if deactivatedAttrValueID != v.GetId() { + continue + } + switch state { + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE: + return v.GetActive().GetValue() + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE: + return !v.GetActive().GetValue() + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY: return true + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_UNSPECIFIED: + return v.GetActive().GetValue() } } return false @@ -1436,19 +1860,6 @@ func (s *AttributesSuite) Test_RemovePublicKeyFromAttribute_Not_Found_Fails() { s.NotNil(resp) } -func (s *AttributesSuite) getAttributeFixtures() map[string]fixtures.FixtureDataAttribute { - return map[string]fixtures.FixtureDataAttribute{ - "example.com/attr/attr1": s.f.GetAttributeKey("example.com/attr/attr1"), - "example.com/attr/attr2": s.f.GetAttributeKey("example.com/attr/attr2"), - "example.net/attr/attr1": s.f.GetAttributeKey("example.net/attr/attr1"), - "example.net/attr/attr2": s.f.GetAttributeKey("example.net/attr/attr2"), - "example.net/attr/attr3": s.f.GetAttributeKey("example.net/attr/attr3"), - "example.org/attr/attr1": s.f.GetAttributeKey("example.org/attr/attr1"), - "example.org/attr/attr2": s.f.GetAttributeKey("example.org/attr/attr2"), - "example.org/attr/attr3": s.f.GetAttributeKey("example.org/attr/attr3"), - } -} - // - Test that a Get/List attribute returns the Asymmetric Keys with the provider configs / add a key with no provider config // Test_GetAttribute_ByIdAndFqn_ReturnSameResult validates that getAttribute works correctly @@ -1472,6 +1883,52 @@ func (s *AttributesSuite) Test_GetAttribute_ByIdAndFqn_ReturnSameResult() { } } +// createSortTestNamespace creates an isolated namespace for sort testing. +func (s *AttributesSuite) createSortTestNamespace(label string) string { + nsName := fmt.Sprintf("%s-%d.com", label, time.Now().UnixNano()) + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: nsName}) + s.Require().NoError(err) + return ns.GetId() +} + +// createSortTestAttributes creates attributes in the given namespace with the given prefixes, +// adding 5ms gaps between creations for distinct timestamps. Returns the attribute IDs in creation order. +func (s *AttributesSuite) createSortTestAttributes(nsID string, prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + name := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) + created, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: name, + NamespaceId: nsID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + return ids +} + +// deleteSortTestAttributes deactivates attributes created by sort tests. +func (s *AttributesSuite) deleteSortTestAttributes(ids []string) { + s.Require().NoError(forceDeleteRows(s.ctx, s.db, "attribute_definitions", ids)) +} + +func (s *AttributesSuite) getAttributeFixtures() map[string]fixtures.FixtureDataAttribute { + return map[string]fixtures.FixtureDataAttribute{ + "example.com/attr/attr1": s.f.GetAttributeKey("example.com/attr/attr1"), + "example.com/attr/attr2": s.f.GetAttributeKey("example.com/attr/attr2"), + "example.net/attr/attr1": s.f.GetAttributeKey("example.net/attr/attr1"), + "example.net/attr/attr2": s.f.GetAttributeKey("example.net/attr/attr2"), + "example.net/attr/attr3": s.f.GetAttributeKey("example.net/attr/attr3"), + "example.org/attr/attr1": s.f.GetAttributeKey("example.org/attr/attr1"), + "example.org/attr/attr2": s.f.GetAttributeKey("example.org/attr/attr2"), + "example.org/attr/attr3": s.f.GetAttributeKey("example.org/attr/attr3"), + } +} + func TestAttributesSuite(t *testing.T) { if testing.Short() { t.Skip("skipping attributes integration tests") diff --git a/service/integration/kas_registry_key_test.go b/service/integration/kas_registry_key_test.go index ca798f5d55..819ce9ff59 100644 --- a/service/integration/kas_registry_key_test.go +++ b/service/integration/kas_registry_key_test.go @@ -3,8 +3,11 @@ package integration import ( "context" "encoding/base64" + "fmt" "log/slog" + "slices" "testing" + "time" "github.com/google/uuid" "github.com/opentdf/platform/protocol/go/common" @@ -466,6 +469,67 @@ func (s *KasRegistryKeySuite) Test_ListKeys_InvalidLimit_Fail() { s.Require().ErrorContains(err, db.ErrListLimitTooLarge.Error()) } +func (s *KasRegistryKeySuite) Test_ListKeys_NoKasFilter_Success() { + req := kasregistry.ListKeysRequest{} + resp, err := s.db.PolicyClient.ListKeys(s.ctx, &req) + s.Require().NoError(err) + s.NotNil(resp) + s.NotEmpty(resp.GetKasKeys()) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_OrdersByCreatedAt_Succeeds() { + kasReq := kasregistry.CreateKeyAccessServerRequest{ + Name: "order-test-kas-" + uuid.NewString(), + Uri: "https://order-test-kas-" + uuid.NewString() + ".opentdf.io", + } + kas, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasReq) + s.Require().NoError(err) + s.NotNil(kas) + + keyIDs := make([]string, 0, 3) + kasIDs := []string{kas.GetId()} + defer func() { + s.cleanupKeys(keyIDs, kasIDs) + }() + + createKey := func() string { + keyReq := kasregistry.CreateKeyRequest{ + KasId: kas.GetId(), + KeyId: uuid.NewString(), + KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, + KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, + PrivateKeyCtx: &policy.PrivateKeyCtx{ + KeyId: validKeyID1, + WrappedKey: keyCtx, + }, + } + resp, err := s.db.PolicyClient.CreateKey(s.ctx, &keyReq) + s.Require().NoError(err) + s.NotNil(resp) + return resp.GetKasKey().GetKey().GetId() + } + + firstID := createKey() + keyIDs = append(keyIDs, firstID) + time.Sleep(5 * time.Millisecond) + secondID := createKey() + keyIDs = append(keyIDs, secondID) + time.Sleep(5 * time.Millisecond) + thirdID := createKey() + keyIDs = append(keyIDs, thirdID) + + resp, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{ + KasId: kas.GetId(), + }, + }) + s.Require().NoError(err) + s.NotNil(resp) + + assertIDsInOrder(s.T(), resp.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, thirdID, secondID, firstID) +} + func (s *KasRegistryKeySuite) Test_ListKeys_KasID_Success() { req := kasregistry.ListKeysRequest{ KasFilter: &kasregistry.ListKeysRequest_KasId{ @@ -496,6 +560,55 @@ func (s *KasRegistryKeySuite) Test_ListKeys_KasURI_Success() { s.validateListKeysResponse(resp, 2, err) } +func (s *KasRegistryKeySuite) Test_ListKeys_KasFilter_NotFound_Fails() { + tests := []struct { + name string + makeReq func() *kasregistry.ListKeysRequest + }{ + { + name: "by_kas_id", + makeReq: func() *kasregistry.ListKeysRequest { + return &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{ + KasId: uuid.NewString(), + }, + } + }, + }, + { + name: "by_kas_name", + makeReq: func() *kasregistry.ListKeysRequest { + return &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasName{ + KasName: "kas-name-does-not-exist", + }, + } + }, + }, + { + name: "by_kas_uri", + makeReq: func() *kasregistry.ListKeysRequest { + return &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasUri{ + KasUri: "https://kas-uri-does-not-exist.opentdf.io", + }, + } + }, + }, + } + + for _, tc := range tests { + tc := tc + s.Run(tc.name, func() { + req := tc.makeReq() + resp, err := s.db.PolicyClient.ListKeys(s.ctx, req) + s.Require().Error(err) + s.Nil(resp) + s.Require().ErrorContains(err, db.ErrNotFound.Error()) + }) + } +} + func (s *KasRegistryKeySuite) Test_ListKeys_FilterAlgo_NoKeysWithAlgo_Success() { req := kasregistry.ListKeysRequest{ KasFilter: &kasregistry.ListKeysRequest_KasId{ @@ -1360,6 +1473,48 @@ func (s *KasRegistryKeySuite) Test_ListKeyMappings_InvalidLimit_Fail() { s.Nil(resp) } +func (s *KasRegistryKeySuite) Test_ListKeyMappings_OrdersByCreatedAt_Succeeds() { + kasKeys := make([]*policy.KasKey, 0, 2) + kasIDs := make([]string, 0, 2) + namespaces := make([]*policy.Namespace, 0, 1) + defer func() { + keyIDs := make([]string, 0, len(kasKeys)) + for _, key := range kasKeys { + keyIDs = append(keyIDs, key.GetKey().GetId()) + } + s.cleanupKeys(keyIDs, kasIDs) + s.cleanupNamespacesAndAttrs(namespaces) + }() + + kasKey1 := s.createKeyAndKas() + kasKeys = append(kasKeys, kasKey1) + kasIDs = append(kasIDs, kasKey1.GetKasId()) + time.Sleep(5 * time.Millisecond) + kasKey2 := s.createKeyAndKas() + kasKeys = append(kasKeys, kasKey2) + kasIDs = append(kasIDs, kasKey2.GetKasId()) + + ns := s.createNamespace() + namespaces = append(namespaces, ns) + attrDef := s.createAttrDef(ns.GetId()) + val := s.createValue(attrDef.GetId()) + + s.createValueMapping(kasKey1.GetKey().GetId(), val.GetId()) + s.createValueMapping(kasKey2.GetKey().GetId(), val.GetId()) + + resp, err := s.db.PolicyClient.ListKeyMappings(s.ctx, &kasregistry.ListKeyMappingsRequest{}) + s.Require().NoError(err) + s.NotNil(resp) + + assertIDsInOrder( + s.T(), + resp.GetKeyMappings(), + func(m *kasregistry.KeyMapping) string { return m.GetKid() }, + kasKey2.GetKey().GetKeyId(), + kasKey1.GetKey().GetKeyId(), + ) +} + func (s *KasRegistryKeySuite) Test_ListKeyMappings_ByID_Invalid_UUID_Fail() { req := kasregistry.ListKeyMappingsRequest{ Identifier: &kasregistry.ListKeyMappingsRequest_Id{ @@ -1671,8 +1826,8 @@ func (s *KasRegistryKeySuite) Test_ListKeyMappings_Multiple_Keys_Pagination_Succ s.Require().NoError(err) s.NotNil(mappingsResp) s.Len(mappingsResp.GetKeyMappings(), 2) - s.validateKeyMapping(mappingsResp.GetKeyMappings()[0], kasKeys[0], []*policy.Namespace{namespaces[0]}, []*policy.Attribute{attributeDefs[0]}, []*policy.Value{attrValues[0]}) - s.validateKeyMapping(mappingsResp.GetKeyMappings()[1], kasKeys[1], []*policy.Namespace{namespaces[1]}, []*policy.Attribute{attributeDefs[1]}, []*policy.Value{attrValues[1]}) + s.validateKeyMapping(mappingsResp.GetKeyMappings()[0], kasKeys[1], []*policy.Namespace{namespaces[1]}, []*policy.Attribute{attributeDefs[1]}, []*policy.Value{attrValues[1]}) + s.validateKeyMapping(mappingsResp.GetKeyMappings()[1], kasKeys[0], []*policy.Namespace{namespaces[0]}, []*policy.Attribute{attributeDefs[0]}, []*policy.Value{attrValues[0]}) s.NotNil(mappingsResp.GetPagination()) s.Equal(int32(2), mappingsResp.GetPagination().GetTotal()) s.Equal(int32(0), mappingsResp.GetPagination().GetCurrentOffset()) @@ -1687,7 +1842,7 @@ func (s *KasRegistryKeySuite) Test_ListKeyMappings_Multiple_Keys_Pagination_Succ s.Require().NoError(err) s.NotNil(mappingsResp) s.Len(mappingsResp.GetKeyMappings(), 1) - s.validateKeyMapping(mappingsResp.GetKeyMappings()[0], kasKeys[0], []*policy.Namespace{namespaces[0]}, []*policy.Attribute{attributeDefs[0]}, []*policy.Value{attrValues[0]}) + s.validateKeyMapping(mappingsResp.GetKeyMappings()[0], kasKeys[1], []*policy.Namespace{namespaces[1]}, []*policy.Attribute{attributeDefs[1]}, []*policy.Value{attrValues[1]}) s.NotNil(mappingsResp.GetPagination()) s.Equal(int32(2), mappingsResp.GetPagination().GetTotal()) s.Equal(int32(0), mappingsResp.GetPagination().GetCurrentOffset()) @@ -1703,7 +1858,7 @@ func (s *KasRegistryKeySuite) Test_ListKeyMappings_Multiple_Keys_Pagination_Succ s.Require().NoError(err) s.NotNil(mappingsResp) s.Len(mappingsResp.GetKeyMappings(), 1) - s.validateKeyMapping(mappingsResp.GetKeyMappings()[0], kasKeys[1], []*policy.Namespace{namespaces[1]}, []*policy.Attribute{attributeDefs[1]}, []*policy.Value{attrValues[1]}) + s.validateKeyMapping(mappingsResp.GetKeyMappings()[0], kasKeys[0], []*policy.Namespace{namespaces[0]}, []*policy.Attribute{attributeDefs[0]}, []*policy.Value{attrValues[0]}) s.NotNil(mappingsResp.GetPagination()) s.Equal(int32(2), mappingsResp.GetPagination().GetTotal()) s.Equal(int32(1), mappingsResp.GetPagination().GetCurrentOffset()) @@ -1744,8 +1899,8 @@ func (s *KasRegistryKeySuite) Test_ListKeyMappings_Multiple_Mixed_Mappings() { s.Require().NoError(err) s.NotNil(mappedResponse) s.Len(mappedResponse.GetKeyMappings(), 2) - s.validateKeyMapping(mappedResponse.GetKeyMappings()[0], kasKeys[0], namespaces, []*policy.Attribute{}, attrValues) - s.validateKeyMapping(mappedResponse.GetKeyMappings()[1], kasKeys[1], []*policy.Namespace{}, attributeDefs, []*policy.Value{}) + s.validateKeyMapping(mappedResponse.GetKeyMappings()[0], kasKeys[1], []*policy.Namespace{}, attributeDefs, []*policy.Value{}) + s.validateKeyMapping(mappedResponse.GetKeyMappings()[1], kasKeys[0], namespaces, []*policy.Attribute{}, attrValues) s.NotNil(mappedResponse.GetPagination()) s.Equal(int32(2), mappedResponse.GetPagination().GetTotal()) s.Equal(int32(0), mappedResponse.GetPagination().GetCurrentOffset()) @@ -1835,6 +1990,75 @@ func (s *KasRegistryKeySuite) Test_DeleteKey_Success() { s.Require().ErrorContains(err, db.ErrNotFound.Error()) } +func validatePublicKeyCtx(s *suite.Suite, expectedPubCtx []byte, actual *policy.SimpleKasKey) { + decodedExpectedPubCtx, err := base64.StdEncoding.DecodeString(string(expectedPubCtx)) + s.Require().NoError(err) + + var expectedPub policy.PublicKeyCtx + err = protojson.Unmarshal(decodedExpectedPubCtx, &expectedPub) + s.Require().NoError(err) + s.Equal(expectedPub.GetPem(), actual.GetPublicKey().GetPem()) +} + +func validatePrivatePublicCtx(s *suite.Suite, expectedPrivCtx, expectedPubCtx []byte, actual *policy.KasKey) { + decodedExpectedPrivCtx, err := base64.StdEncoding.DecodeString(string(expectedPrivCtx)) + s.Require().NoError(err) + + var expectedPriv policy.PrivateKeyCtx + err = protojson.Unmarshal(decodedExpectedPrivCtx, &expectedPriv) + s.Require().NoError(err) + + s.Equal(expectedPriv.GetKeyId(), actual.GetKey().GetPrivateKeyCtx().GetKeyId()) + s.Equal(expectedPriv.GetWrappedKey(), actual.GetKey().GetPrivateKeyCtx().GetWrappedKey()) + validatePublicKeyCtx(s, expectedPubCtx, &policy.SimpleKasKey{ + KasUri: actual.GetKasUri(), + PublicKey: &policy.SimpleKasPublicKey{ + Pem: actual.GetKey().GetPublicKeyCtx().GetPem(), + }, + }) +} + +// Test_ListKeyMappings_AllParameterCombinations validates that listKeyMappings works correctly +// with various combinations of optional parameters +func (s *KasRegistryKeySuite) Test_ListKeyMappings_AllParameterCombinations() { + kas1 := s.kasFixtures[0] + key1 := s.kasKeys[0] + + // Test 1: No parameters - should return all mappings (may be 0) + allMappings, err := s.db.PolicyClient.ListKeyMappings(s.ctx, &kasregistry.ListKeyMappingsRequest{}) + s.Require().NoError(err) + s.NotNil(allMappings) + // No assertion on count - fixtures may not have any mappings + + // Test 2: Filter by key with KAS URI + mappingsByKey, err := s.db.PolicyClient.ListKeyMappings(s.ctx, &kasregistry.ListKeyMappingsRequest{ + Identifier: &kasregistry.ListKeyMappingsRequest_Key{ + Key: &kasregistry.KasKeyIdentifier{ + Identifier: &kasregistry.KasKeyIdentifier_Uri{ + Uri: kas1.URI, + }, + Kid: key1.KeyID, + }, + }, + }) + s.Require().NoError(err, "Should successfully query with KAS URI and key ID") + s.NotNil(mappingsByKey) + + // Test 3: Filter by key with KAS ID (validates alternative params path) + mappingsByKeyID, err := s.db.PolicyClient.ListKeyMappings(s.ctx, &kasregistry.ListKeyMappingsRequest{ + Identifier: &kasregistry.ListKeyMappingsRequest_Key{ + Key: &kasregistry.KasKeyIdentifier{ + Identifier: &kasregistry.KasKeyIdentifier_KasId{ + KasId: key1.KeyAccessServerID, + }, + Kid: key1.KeyID, + }, + }, + }) + s.Require().NoError(err, "Should successfully query with KAS ID and key ID") + s.NotNil(mappingsByKeyID) +} + func (s *KasRegistryKeySuite) validateKeyMapping(mapping *kasregistry.KeyMapping, expectedKey *policy.KasKey, expectedNamespace []*policy.Namespace, expectedAttrDef []*policy.Attribute, expectedValue []*policy.Value) { s.Equal(expectedKey.GetKey().GetKeyId(), mapping.GetKid()) s.Equal(expectedKey.GetKasUri(), mapping.GetKasUri()) @@ -2137,34 +2361,6 @@ func (s *KasRegistryKeySuite) validateListKeysResponse(resp *kasregistry.ListKey } } -func validatePublicKeyCtx(s *suite.Suite, expectedPubCtx []byte, actual *policy.SimpleKasKey) { - decodedExpectedPubCtx, err := base64.StdEncoding.DecodeString(string(expectedPubCtx)) - s.Require().NoError(err) - - var expectedPub policy.PublicKeyCtx - err = protojson.Unmarshal(decodedExpectedPubCtx, &expectedPub) - s.Require().NoError(err) - s.Equal(expectedPub.GetPem(), actual.GetPublicKey().GetPem()) -} - -func validatePrivatePublicCtx(s *suite.Suite, expectedPrivCtx, expectedPubCtx []byte, actual *policy.KasKey) { - decodedExpectedPrivCtx, err := base64.StdEncoding.DecodeString(string(expectedPrivCtx)) - s.Require().NoError(err) - - var expectedPriv policy.PrivateKeyCtx - err = protojson.Unmarshal(decodedExpectedPrivCtx, &expectedPriv) - s.Require().NoError(err) - - s.Equal(expectedPriv.GetKeyId(), actual.GetKey().GetPrivateKeyCtx().GetKeyId()) - s.Equal(expectedPriv.GetWrappedKey(), actual.GetKey().GetPrivateKeyCtx().GetWrappedKey()) - validatePublicKeyCtx(s, expectedPubCtx, &policy.SimpleKasKey{ - KasUri: actual.GetKasUri(), - PublicKey: &policy.SimpleKasPublicKey{ - Pem: actual.GetKey().GetPublicKeyCtx().GetPem(), - }, - }) -} - // cascade delete will remove namespaces and all associated attributes and values func (s *KasRegistryKeySuite) cleanupNamespacesAndAttrsByIDs(namespaceIDs []string) { namespaces := make([]*policy.Namespace, len(namespaceIDs)) @@ -2278,43 +2474,285 @@ func (s *KasRegistryKeySuite) createKeyAndKas() *policy.KasKey { return keyResp.GetKasKey() } -// Test_ListKeyMappings_AllParameterCombinations validates that listKeyMappings works correctly -// with various combinations of optional parameters -func (s *KasRegistryKeySuite) Test_ListKeyMappings_AllParameterCombinations() { - kas1 := s.kasFixtures[0] - key1 := s.kasKeys[0] +func (s *KasRegistryKeySuite) Test_ListKeys_SortByKeyId_ASC() { + ids, kasID := s.createSortTestKasKeys([]string{"aaa-kksort", "bbb-kksort", "ccc-kksort"}) + defer s.deleteSortTestKasKeys(ids, kasID) - // Test 1: No parameters - should return all mappings (may be 0) - allMappings, err := s.db.PolicyClient.ListKeyMappings(s.ctx, &kasregistry.ListKeyMappingsRequest{}) + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) s.Require().NoError(err) - s.NotNil(allMappings) - // No assertion on count - fixtures may not have any mappings + s.NotNil(list) - // Test 2: Filter by key with KAS URI - mappingsByKey, err := s.db.PolicyClient.ListKeyMappings(s.ctx, &kasregistry.ListKeyMappingsRequest{ - Identifier: &kasregistry.ListKeyMappingsRequest_Key{ - Key: &kasregistry.KasKeyIdentifier{ - Identifier: &kasregistry.KasKeyIdentifier_Uri{ - Uri: kas1.URI, - }, - Kid: key1.KeyID, - }, + // aaa < bbb < ccc in ASC order + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortByKeyId_DESC() { + ids, kasID := s.createSortTestKasKeys([]string{"aaa-kksortdesc", "bbb-kksortdesc", "ccc-kksortdesc"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, }, }) - s.Require().NoError(err, "Should successfully query with KAS URI and key ID") - s.NotNil(mappingsByKey) + s.Require().NoError(err) + s.NotNil(list) - // Test 3: Filter by key with KAS ID (validates alternative params path) - mappingsByKeyID, err := s.db.PolicyClient.ListKeyMappings(s.ctx, &kasregistry.ListKeyMappingsRequest{ - Identifier: &kasregistry.ListKeyMappingsRequest_Key{ - Key: &kasregistry.KasKeyIdentifier{ - Identifier: &kasregistry.KasKeyIdentifier_KasId{ - KasId: key1.KeyAccessServerID, - }, - Kid: key1.KeyID, + // ccc > bbb > aaa in DESC order + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortByCreatedAt_ASC() { + ids, kasID := s.createSortTestKasKeys([]string{"createdasc-kk-0", "createdasc-kk-1", "createdasc-kk-2"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // oldest first in ASC order + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortByCreatedAt_DESC() { + ids, kasID := s.createSortTestKasKeys([]string{"createddesc-kk-0", "createddesc-kk-1", "createddesc-kk-2"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // newest first in DESC order + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortByUpdatedAt_DESC() { + ids, kasID := s.createSortTestKasKeys([]string{"upd-sort-kk-0", "upd-sort-kk-1", "upd-sort-kk-2"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + // Update the first key so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateKey(s.ctx, &kasregistry.UpdateKeyRequest{ + Id: ids[0], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // The updated key (ids[0]) should appear before the others + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[0], ids[2], ids[1]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortByUpdatedAt_ASC() { + ids, kasID := s.createSortTestKasKeys([]string{"updasc-kk-0", "updasc-kk-1", "updasc-kk-2"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + // Update the last key so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateKey(s.ctx, &kasregistry.UpdateKeyRequest{ + Id: ids[2], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // The updated key (ids[2]) should appear last in ASC order + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortTieBreaker_CreatedAtWithIDFallback() { + kasReq := kasregistry.CreateKeyAccessServerRequest{ + Name: "tiebreaker-kk-kas-" + uuid.NewString(), + Uri: "https://tiebreaker-kk-kas-" + uuid.NewString() + ".opentdf.io", + } + kas, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasReq) + s.Require().NoError(err) + + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + keyReq := kasregistry.CreateKeyRequest{ + KasId: kas.GetId(), + KeyId: fmt.Sprintf("tiebreaker-kk-%d-%d", i, suffix), + KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, + KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, + PrivateKeyCtx: &policy.PrivateKeyCtx{ + KeyId: fmt.Sprintf("tiebreaker-kk-priv-%d-%d", i, suffix), + WrappedKey: keyCtx, }, + } + resp, err := s.db.PolicyClient.CreateKey(s.ctx, &keyReq) + s.Require().NoError(err) + ids[i] = resp.GetKasKey().GetKey().GetId() + } + defer s.deleteSortTestKasKeys(ids, kas.GetId()) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "key_access_server_keys", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{ + KasId: kas.GetId(), + }, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, }, }) - s.Require().NoError(err, "Should successfully query with KAS ID and key ID") - s.NotNil(mappingsByKeyID) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, sorted[0], sorted[1], sorted[2]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortByUnspecifiedField_DefaultsToCreatedAt() { + ids, kasID := s.createSortTestKasKeys([]string{"unsf-kk-0", "unsf-kk-1", "unsf-kk-2"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // Field defaults to created_at, explicit ASC is preserved + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortByUnspecifiedDirection_DefaultsToDESC() { + ids, kasID := s.createSortTestKasKeys([]string{"unsd-kk-0", "unsd-kk-1", "unsd-kk-2"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // Direction defaults to DESC, explicit created_at field is preserved + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortByBothUnspecified_DefaultsToCreatedAtDESC() { + ids, kasID := s.createSortTestKasKeys([]string{"unsb-kk-0", "unsb-kk-1", "unsb-kk-2"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // Both default: created_at DESC + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistryKeySuite) Test_ListKeys_SortOmitted() { + ids, kasID := s.createSortTestKasKeys([]string{"omit-kk-0", "omit-kk-1", "omit-kk-2"}) + defer s.deleteSortTestKasKeys(ids, kasID) + + list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{KasId: kasID}, + }) + s.Require().NoError(err) + s.NotNil(list) + + // No sort provided: created_at DESC + assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[2], ids[1], ids[0]) +} + +// Sort test helpers + +// createSortTestKasKeys creates kas keys with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the key IDs in creation order and the parent KAS ID. +func (s *KasRegistryKeySuite) createSortTestKasKeys(prefixes []string) ([]string, string) { + label := "kas" + if len(prefixes) > 0 { + label = prefixes[0] + } + kasUUID := uuid.NewString() + kasReq := kasregistry.CreateKeyAccessServerRequest{ + Name: label + "-kas-" + kasUUID, + Uri: "https://" + label + "-kas-" + kasUUID + ".opentdf.io", + } + kas, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasReq) + s.Require().NoError(err) + s.NotNil(kas) + + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + ts := time.Now().UnixNano() + keyReq := kasregistry.CreateKeyRequest{ + KasId: kas.GetId(), + KeyId: fmt.Sprintf("%s-%d", prefix, ts), + KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, + KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, + PrivateKeyCtx: &policy.PrivateKeyCtx{ + KeyId: fmt.Sprintf("%s-priv-%d", prefix, ts), + WrappedKey: keyCtx, + }, + } + resp, err := s.db.PolicyClient.CreateKey(s.ctx, &keyReq) + s.Require().NoError(err) + s.NotNil(resp) + ids[i] = resp.GetKasKey().GetKey().GetId() + } + return ids, kas.GetId() +} + +// deleteSortTestKasKeys cleans up kas keys and their parent KAS created by sort tests. +func (s *KasRegistryKeySuite) deleteSortTestKasKeys(keyIDs []string, kasID string) { + s.cleanupKeys(keyIDs, []string{kasID}) } diff --git a/service/integration/kas_registry_key_unencrypted_test.go b/service/integration/kas_registry_key_unencrypted_test.go new file mode 100644 index 0000000000..569d160237 --- /dev/null +++ b/service/integration/kas_registry_key_unencrypted_test.go @@ -0,0 +1,46 @@ +package integration + +import ( + "encoding/base64" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/opentdf/platform/service/pkg/db" +) + +func (s *KasRegistryKeySuite) Test_CreateKasKey_PEMPrivateKey_Fail() { + pemPrivateKey := "-----BEGIN PRIVATE KEY-----\nZg==\n-----END PRIVATE KEY-----\n" + encodedPem := base64.StdEncoding.EncodeToString([]byte(pemPrivateKey)) + req := kasregistry.CreateKeyRequest{ + KasId: s.kasKeys[0].KeyAccessServerID, + KeyId: validKeyID1, + KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, + KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, + PrivateKeyCtx: &policy.PrivateKeyCtx{ + WrappedKey: encodedPem, + KeyId: validKeyID1, + }, + } + resp, err := s.db.PolicyClient.CreateKey(s.ctx, &req) + s.Require().Error(err) + s.Require().ErrorContains(err, db.ErrUnencryptedPrivateKey.Error()) + s.Nil(resp) +} + +func (s *KasRegistryKeySuite) Test_RotateKey_PEMPrivateKey_Fail() { + keyMap := s.setupKeysForRotate(s.kasKeys[0].KeyAccessServerID) + pemPrivateKey := "-----BEGIN PRIVATE KEY-----\nZg==\n-----END PRIVATE KEY-----\n" + encodedPem := base64.StdEncoding.EncodeToString([]byte(pemPrivateKey)) + newKey := kasregistry.RotateKeyRequest_NewKey{ + KeyId: validKeyID1, + Algorithm: policy.Algorithm_ALGORITHM_EC_P256, + KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, + PrivateKeyCtx: &policy.PrivateKeyCtx{WrappedKey: encodedPem, KeyId: validKeyID1}, + } + rotatedInKey, err := s.db.PolicyClient.RotateKey(s.ctx, keyMap[rotateKey], &newKey) + s.Require().Error(err) + s.Require().ErrorContains(err, db.ErrUnencryptedPrivateKey.Error()) + s.Nil(rotatedInKey) +} diff --git a/service/integration/kas_registry_test.go b/service/integration/kas_registry_test.go index 8257280a3b..ec35952569 100644 --- a/service/integration/kas_registry_test.go +++ b/service/integration/kas_registry_test.go @@ -2,9 +2,12 @@ package integration import ( "context" + "fmt" "log/slog" + "slices" "strings" "testing" + "time" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" @@ -64,6 +67,40 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_NoPagination_Succeeds() { } } +func (s *KasRegistrySuite) Test_ListKeyAccessServers_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + create := func(i int) string { + uri := fmt.Sprintf("https://order-test-kas-%d-%d.example.com", i, suffix) + name := fmt.Sprintf("order-test-kas-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasregistry.CreateKeyAccessServerRequest{ + Uri: uri, + PublicKey: &policy.PublicKey{ + PublicKey: &policy.PublicKey_Remote{ + Remote: fmt.Sprintf("https://order-test-key-%d-%d.example.com/key", i, suffix), + }, + }, + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"name": name}, + }, + }) + s.Require().NoError(err) + s.Require().NotNil(created) + return created.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{}) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, thirdID, secondID, firstID) +} + func (s *KasRegistrySuite) Test_ListKeyAccessServers_Limit_Succeeds() { var limit int32 = 2 listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ @@ -816,6 +853,260 @@ func (s *KasRegistrySuite) Test_DeleteKeyAccessServer_WithInvalidId_Fails() { s.Require().ErrorIs(err, db.ErrUUIDInvalid) } +// Test_GetKeyAccessServer_ByIdNameUri_ReturnSameResult validates that getKeyAccessServer works correctly +// with ID, name, and URI lookups +func (s *KasRegistrySuite) Test_GetKeyAccessServer_ByIdNameUri_ReturnSameResult() { + remoteFixture := s.f.GetKasRegistryKey("key_access_server_1") + + // Get by ID + kasByID, err := s.db.PolicyClient.GetKeyAccessServer(s.ctx, remoteFixture.ID) + s.Require().NoError(err, "Failed to get KAS by ID") + s.Require().NotNil(kasByID) + + // Get by Name + kasByName, err := s.db.PolicyClient.GetKeyAccessServer(s.ctx, &kasregistry.GetKeyAccessServerRequest_Name{Name: remoteFixture.Name}) + s.Require().NoError(err, "Failed to get KAS by Name") + s.Require().NotNil(kasByName) + + // Get by URI + kasByURI, err := s.db.PolicyClient.GetKeyAccessServer(s.ctx, &kasregistry.GetKeyAccessServerRequest_Uri{Uri: remoteFixture.URI}) + s.Require().NoError(err, "Failed to get KAS by URI") + s.Require().NotNil(kasByURI) + + // Verify all three return the same KAS + s.True(proto.Equal(kasByID, kasByName)) + s.True(proto.Equal(kasByID, kasByURI)) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByCreatedAt_ASC() { + ids := s.createSortTestKeyAccessServers([]string{"sort-kas-created-asc-0", "sort-kas-created-asc-1", "sort-kas-created-asc-2"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByCreatedAt_DESC() { + ids := s.createSortTestKeyAccessServers([]string{"sort-kas-created-desc-0", "sort-kas-created-desc-1", "sort-kas-created-desc-2"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUpdatedAt_DESC() { + ids := s.createSortTestKeyAccessServers([]string{"sort-kas-updated-desc-0", "sort-kas-updated-desc-1", "sort-kas-updated-desc-2"}) + defer s.deleteSortTestKeyAccessServers(ids) + + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateKeyAccessServer(s.ctx, ids[0], &kasregistry.UpdateKeyAccessServerRequest{ + Id: ids[0], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[0], ids[2], ids[1]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUpdatedAt_ASC() { + ids := s.createSortTestKeyAccessServers([]string{"sort-kas-updated-asc-0", "sort-kas-updated-asc-1", "sort-kas-updated-asc-2"}) + defer s.deleteSortTestKeyAccessServers(ids) + + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateKeyAccessServer(s.ctx, ids[2], &kasregistry.UpdateKeyAccessServerRequest{ + Id: ids[2], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByName_ASC() { + ids := s.createSortTestKeyAccessServers([]string{"aaa-kas-sort", "bbb-kas-sort", "ccc-kas-sort"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByName_DESC() { + ids := s.createSortTestKeyAccessServers([]string{"aaa-kas-sortdesc", "bbb-kas-sortdesc", "ccc-kas-sortdesc"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUri_ASC() { + ids := s.createSortTestKeyAccessServers([]string{"aaa-kas-uri", "bbb-kas-uri", "ccc-kas-uri"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_URI, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUri_DESC() { + ids := s.createSortTestKeyAccessServers([]string{"aaa-kas-uridesc", "bbb-kas-uridesc", "ccc-kas-uridesc"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_URI, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortTieBreaker_CreatedAtWithIDFallback() { + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-kas-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasregistry.CreateKeyAccessServerRequest{ + Uri: fmt.Sprintf("https://%s.example.com", name), + Name: name, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestKeyAccessServers(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "key_access_servers", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, sorted[0], sorted[1], sorted[2]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUnspecifiedField_DefaultsToCreatedAt() { + ids := s.createSortTestKeyAccessServers([]string{"unspecified-field-kas-0", "unspecified-field-kas-1", "unspecified-field-kas-2"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Field defaults to created_at, explicit ASC is preserved + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUnspecifiedDirection_DefaultsToDESC() { + ids := s.createSortTestKeyAccessServers([]string{"unspecified-dir-kas-0", "unspecified-dir-kas-1", "unspecified-dir-kas-2"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Direction defaults to DESC, explicit created_at field is preserved + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByBothUnspecified_DefaultsToCreatedAtDESC() { + ids := s.createSortTestKeyAccessServers([]string{"both-unspecified-kas-0", "both-unspecified-kas-1", "both-unspecified-kas-2"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Both default: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortOmitted() { + ids := s.createSortTestKeyAccessServers([]string{"sort-omitted-kas-0", "sort-omitted-kas-1", "sort-omitted-kas-2"}) + defer s.deleteSortTestKeyAccessServers(ids) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{}) + s.Require().NoError(err) + s.NotNil(listRsp) + + // No sort provided: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[2], ids[1], ids[0]) +} + func (s *KasRegistrySuite) getKasRegistryFixtures() []fixtures.FixtureDataKasRegistry { return []fixtures.FixtureDataKasRegistry{ s.f.GetKasRegistryKey("key_access_server_1"), @@ -862,29 +1153,31 @@ func (s *KasRegistrySuite) validateKasRegistryKeys(kasr *policy.KeyAccessServer) s.Len(expectedKasKeys, matchingKeysCount) } -// Test_GetKeyAccessServer_ByIdNameUri_ReturnSameResult validates that getKeyAccessServer works correctly -// with ID, name, and URI lookups -func (s *KasRegistrySuite) Test_GetKeyAccessServer_ByIdNameUri_ReturnSameResult() { - remoteFixture := s.f.GetKasRegistryKey("key_access_server_1") - - // Get by ID - kasByID, err := s.db.PolicyClient.GetKeyAccessServer(s.ctx, remoteFixture.ID) - s.Require().NoError(err, "Failed to get KAS by ID") - s.Require().NotNil(kasByID) - - // Get by Name - kasByName, err := s.db.PolicyClient.GetKeyAccessServer(s.ctx, &kasregistry.GetKeyAccessServerRequest_Name{Name: remoteFixture.Name}) - s.Require().NoError(err, "Failed to get KAS by Name") - s.Require().NotNil(kasByName) - - // Get by URI - kasByURI, err := s.db.PolicyClient.GetKeyAccessServer(s.ctx, &kasregistry.GetKeyAccessServerRequest_Uri{Uri: remoteFixture.URI}) - s.Require().NoError(err, "Failed to get KAS by URI") - s.Require().NotNil(kasByURI) +// createSortTestKeyAccessServers creates KAS entries with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the KAS IDs in creation order. +func (s *KasRegistrySuite) createSortTestKeyAccessServers(prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + suffix := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) + created, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasregistry.CreateKeyAccessServerRequest{ + Uri: fmt.Sprintf("https://%s.example.com", suffix), + Name: suffix, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + return ids +} - // Verify all three return the same KAS - s.True(proto.Equal(kasByID, kasByName)) - s.True(proto.Equal(kasByID, kasByURI)) +// deleteSortTestKeyAccessServers cleans up KAS entries created by sort tests. +func (s *KasRegistrySuite) deleteSortTestKeyAccessServers(ids []string) { + for _, id := range ids { + _, err := s.db.PolicyClient.DeleteKeyAccessServer(s.ctx, id) + s.Require().NoError(err) + } } func TestKasRegistrySuite(t *testing.T) { diff --git a/service/integration/keymanagement_test.go b/service/integration/keymanagement_test.go index 66e79ed65f..3ac4b6c96e 100644 --- a/service/integration/keymanagement_test.go +++ b/service/integration/keymanagement_test.go @@ -5,6 +5,7 @@ import ( "log/slog" "strings" "testing" + "time" "github.com/google/uuid" "github.com/opentdf/platform/protocol/go/common" @@ -198,6 +199,28 @@ func (s *KeyManagementSuite) Test_ListProviderConfig_No_Pagination_Succeeds() { s.NotEmpty(resp.GetProviderConfigs()) } +func (s *KeyManagementSuite) Test_ListProviderConfig_OrdersByCreatedAt_Succeeds() { + pcIDs := make([]string, 0) + defer func() { + s.deleteTestProviderConfigs(pcIDs) + }() + + pc1 := s.createTestProviderConfig(s.getUniqueProviderName("order-test-provider-1"), validProviderConfig, nil) + pcIDs = append(pcIDs, pc1.GetId()) + time.Sleep(5 * time.Millisecond) + pc2 := s.createTestProviderConfig(s.getUniqueProviderName("order-test-provider-2"), validProviderConfig, nil) + pcIDs = append(pcIDs, pc2.GetId()) + time.Sleep(5 * time.Millisecond) + pc3 := s.createTestProviderConfig(s.getUniqueProviderName("order-test-provider-3"), validProviderConfig, nil) + pcIDs = append(pcIDs, pc3.GetId()) + + resp, err := s.db.PolicyClient.ListProviderConfigs(s.ctx, &policy.PageRequest{}) + s.Require().NoError(err) + s.NotNil(resp) + + assertIDsInOrder(s.T(), resp.GetProviderConfigs(), func(pc *policy.KeyProviderConfig) string { return pc.GetId() }, pc3.GetId(), pc2.GetId(), pc1.GetId()) +} + func (s *KeyManagementSuite) Test_ListProviderConfig_PaginationLimit_Succeeds() { pcIDs := make([]string, 0) defer func() { @@ -208,6 +231,7 @@ func (s *KeyManagementSuite) Test_ListProviderConfig_PaginationLimit_Succeeds() pc := s.createTestProviderConfig(s.testProvider, validProviderConfig, nil) pcIDs = append(pcIDs, pc.GetId()) + pc2 := s.createTestProviderConfig(testProvider2, validProviderConfig, nil) pcIDs = append(pcIDs, pc2.GetId()) diff --git a/service/integration/main_test.go b/service/integration/main_test.go index a3e3b1b3b5..ac45fc4460 100644 --- a/service/integration/main_test.go +++ b/service/integration/main_test.go @@ -6,11 +6,11 @@ import ( "log/slog" "net" "os" + "strings" "testing" "time" "github.com/creasty/defaults" - "github.com/docker/go-connections/nat" "github.com/google/uuid" "github.com/opentdf/platform/service/internal/fixtures" tc "github.com/testcontainers/testcontainers-go" @@ -83,14 +83,16 @@ func TestMain(m *testing.M) { "POSTGRES_PASSWORD": conf.DB.Password, "POSTGRES_DB": conf.DB.Database, }, - WaitingFor: wait.ForSQL(nat.Port("5432/tcp"), "pgx", func(host string, port nat.Port) string { + WaitingFor: wait.ForSQL("5432/tcp", "pgx", func(host string, port string) string { + // port is Port.String() which includes protocol (e.g. "5432/tcp"); strip it + portNum, _, _ := strings.Cut(port, "/") return fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", conf.DB.User, conf.DB.Password, - net.JoinHostPort(host, port.Port()), + net.JoinHostPort(host, portNum), conf.DB.Database, ) - }).WithStartupTimeout(time.Second * 60).WithQuery("SELECT 1"), // Increased timeout and simplified query + }).WithStartupTimeout(time.Second * 60).WithQuery("SELECT 1"), }, Started: true, } @@ -121,7 +123,7 @@ func TestMain(m *testing.M) { panic(err) } - conf.DB.Port = port.Int() + conf.DB.Port = int(port.Num()) //nolint:sloglint // emoji slog.Info("🏠 loading fixtures") diff --git a/service/integration/migration_test.go b/service/integration/migration_test.go new file mode 100644 index 0000000000..4a8529308f --- /dev/null +++ b/service/integration/migration_test.go @@ -0,0 +1,577 @@ +package integration + +import ( + "context" + "database/sql" + "testing" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5/stdlib" + "github.com/opentdf/platform/service/pkg/db" + "github.com/opentdf/platform/service/policy" + "github.com/pressly/goose/v3" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" +) + +// migrationTestHarness provides direct access to the goose provider for +// fine-grained migration control (UpTo, DownTo, ApplyVersion) and raw +// SQL execution between migration steps. +type migrationTestHarness struct { + t *testing.T + ctx context.Context //nolint:containedctx // context is used across test helper methods + dbClient *db.Client + provider *goose.Provider + schema string + sqlDB *sql.DB +} + +func newMigrationTestHarness(t *testing.T, schema string) *migrationTestHarness { + t.Helper() + ctx := context.Background() + c := *Config + c.DB.Schema = schema + + tracer := otel.Tracer("") + dbClient, err := db.New(ctx, c.DB, c.Logger, &tracer) + require.NoError(t, err, "failed to create db client") + + // Create schema + q := "CREATE SCHEMA IF NOT EXISTS " + pgx.Identifier{schema}.Sanitize() + _, err = dbClient.Pgx.Exec(ctx, q) + require.NoError(t, err, "failed to create schema") + + // Build goose provider directly for fine-grained control + pool, ok := dbClient.Pgx.(*pgxpool.Pool) + require.True(t, ok, "expected pgxpool.Pool") + sqlDB := stdlib.OpenDBFromPool(pool) + + provider, err := goose.NewProvider(goose.DialectPostgres, sqlDB, policy.Migrations) + require.NoError(t, err, "failed to create goose provider") + + h := &migrationTestHarness{ + t: t, + ctx: ctx, + dbClient: dbClient, + provider: provider, + schema: schema, + sqlDB: sqlDB, + } + + t.Cleanup(func() { + sqlDB.Close() + dropSchema(ctx, t, dbClient, schema) + dbClient.Pgx.Close() + }) + + return h +} + +func (h *migrationTestHarness) upTo(version int64) { + h.t.Helper() + results, err := h.provider.UpTo(h.ctx, version) + require.NoError(h.t, err, "migration UpTo(%d) failed", version) + for _, r := range results { + require.NoError(h.t, r.Error, "migration %d up error", r.Source.Version) + } +} + +func (h *migrationTestHarness) downTo(version int64) { + h.t.Helper() + results, err := h.provider.DownTo(h.ctx, version) + require.NoError(h.t, err, "migration DownTo(%d) failed", version) + for _, r := range results { + require.NoError(h.t, r.Error, "migration %d down error", r.Source.Version) + } +} + +func (h *migrationTestHarness) exec(query string, args ...any) { + h.t.Helper() + _, err := h.dbClient.Pgx.Exec(h.ctx, query, args...) + require.NoError(h.t, err, "exec failed: %s", query) +} + +func (h *migrationTestHarness) queryRow(query string, args ...any) pgx.Row { + h.t.Helper() + return h.dbClient.Pgx.QueryRow(h.ctx, query, args...) +} + +func dropSchema(ctx context.Context, t *testing.T, client *db.Client, schema string) { + t.Helper() + q := "DROP SCHEMA IF EXISTS " + pgx.Identifier{schema}.Sanitize() + " CASCADE" + if _, err := client.Pgx.Exec(ctx, q); err != nil { + t.Logf("warning: failed to drop schema %s: %v", schema, err) + } +} + +// TestMigrationUpDownUp validates that all migrations can be applied forward, +// rolled back completely, and re-applied. This catches broken Down migrations +// before they're needed in a production rollback. +func TestMigrationUpDownUp(t *testing.T) { + if testing.Short() { + t.Skip("skipping migration roundtrip test") + } + + h := newMigrationTestHarness(t, "test_opentdf_migration_roundtrip") + + // Phase 1: Apply all migrations up + upResults, err := h.provider.Up(h.ctx) + require.NoError(t, err, "migration up failed") + require.NotEmpty(t, upResults, "expected at least one migration applied") + for _, r := range upResults { + require.NoError(t, r.Error, "migration %d up error", r.Source.Version) + } + t.Logf("phase 1 (up): applied %d migrations", len(upResults)) + + // Phase 2: Roll back all migrations + downResults, err := h.provider.DownTo(h.ctx, 0) + require.NoError(t, err, "migration down failed") + require.NotEmpty(t, downResults, "expected at least one migration rolled back") + for _, r := range downResults { + require.NoError(t, r.Error, "migration %d down error", r.Source.Version) + } + require.Len(t, downResults, len(upResults), "rollback count should match applied count") + t.Logf("phase 2 (down): rolled back %d migrations", len(downResults)) + + // Phase 3: Re-apply all migrations + reupResults, err := h.provider.Up(h.ctx) + require.NoError(t, err, "migration re-up failed after rollback") + require.NotEmpty(t, reupResults, "expected at least one migration re-applied") + for _, r := range reupResults { + require.NoError(t, r.Error, "migration %d re-up error", r.Source.Version) + } + require.Len(t, reupResults, len(upResults), "re-applied count should match initial count") + t.Logf("phase 3 (re-up): applied %d migrations", len(reupResults)) +} + +// TestMigrationData_SelectorFieldRename tests the JSONB field rename migration +// (20240405000000_update_selector_field_name) to verify data integrity through +// the up and down transitions. +// +// The migration renames subject_external_field -> subject_external_selector_value +// inside the condition JSONB column of subject_condition_set. +func TestMigrationData_SelectorFieldRename(t *testing.T) { + if testing.Short() { + t.Skip("skipping data migration test") + } + + h := newMigrationTestHarness(t, "test_opentdf_selector_rename") + + // Migrate to the version just before the rename migration + const preMigration int64 = 20240402000000 + const renameMigration int64 = 20240405000000 + h.upTo(preMigration) + + // Insert test data using the old field name (subject_external_field) + h.exec(` + INSERT INTO subject_condition_set (id, condition) VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + '[{ + "condition_groups": [{ + "boolean_operator": "AND", + "conditions": [{ + "operator": "IN", + "subject_external_field": "team_name", + "subject_external_values": ["engineering", "platform"] + }] + }] + }]'::jsonb + ) + `) + + // Apply the rename migration + h.upTo(renameMigration) + + // Verify the field was renamed to subject_external_selector_value + var fieldValue string + row := h.queryRow(` + SELECT condition->0->'condition_groups'->0->'conditions'->0->>'subject_external_selector_value' + FROM subject_condition_set + WHERE id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + `) + require.NoError(t, row.Scan(&fieldValue)) + require.Equal(t, "team_name", fieldValue, "field should be renamed to subject_external_selector_value after up") + + // Verify old field name is gone + var oldFieldValue *string + row = h.queryRow(` + SELECT condition->0->'condition_groups'->0->'conditions'->0->>'subject_external_field' + FROM subject_condition_set + WHERE id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + `) + require.NoError(t, row.Scan(&oldFieldValue)) + require.Nil(t, oldFieldValue, "old field name should not exist after up migration") + + // Roll back the rename migration + h.downTo(preMigration) + + // Verify the field was renamed back to subject_external_field + row = h.queryRow(` + SELECT condition->0->'condition_groups'->0->'conditions'->0->>'subject_external_field' + FROM subject_condition_set + WHERE id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + `) + require.NoError(t, row.Scan(&fieldValue)) + require.Equal(t, "team_name", fieldValue, "field should be restored to subject_external_field after down") + + // Verify the new field name is gone after rollback + row = h.queryRow(` + SELECT condition->0->'condition_groups'->0->'conditions'->0->>'subject_external_selector_value' + FROM subject_condition_set + WHERE id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + `) + require.NoError(t, row.Scan(&oldFieldValue)) + require.Nil(t, oldFieldValue, "new field name should not exist after down migration") +} + +// TestMigrationData_ActionsNamespaceDownRemapsAndDedupes verifies that +// 20260312000000_add_namespace_to_actions down migration remaps namespaced +// action references to canonical global actions and deduplicates rows across +// referencing tables. +func TestMigrationData_ActionsNamespaceDownRemapsAndDedupes(t *testing.T) { + if testing.Short() { + t.Skip("skipping data migration test") + } + + h := newMigrationTestHarness(t, "test_opentdf_actions_namespace_down") + + const ( + preNamespaceRollback int64 = 20260318000000 + postNamespaceRollback int64 = 20260306000000 + + namespaceID = "11111111-1111-1111-1111-111111111111" + attributeDefID = "22222222-2222-2222-2222-222222222222" + attributeValueID = "33333333-3333-3333-3333-333333333333" + subjectSetID = "44444444-4444-4444-4444-444444444444" + subjectMappingID = "55555555-5555-5555-5555-555555555555" + subjectSetTieID = "55666666-6666-6666-6666-666666666666" + subjectMappingTie = "55777777-7777-7777-7777-777777777777" + registeredResID = "66666666-6666-6666-6666-666666666666" + registeredValueID = "77777777-7777-7777-7777-777777777777" + registeredValueTie = "77888888-8888-8888-8888-888888888888" + + obligationDefID = "88888888-8888-8888-8888-888888888888" + obligationValID = "99999999-9999-9999-9999-999999999999" + obligationValTie = "99999999-9999-9999-9999-999999999998" + + namespacedCreateID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + globalCustomID = "abababab-abab-abab-abab-abababababab" + namespaceCustomID = "acacacac-acac-acac-acac-acacacacacac" + namespaceTwoID = "adadadad-adad-adad-adad-adadadadadad" + nsOnlyOlderID = "afafafaf-afaf-afaf-afaf-afafafafafaf" + nsOnlyNewerID = "aeaeaeae-aeae-aeae-aeae-aeaeaeaeaeae" + + smaRowGlobalID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + smaRowNamespaceID = "cccccccc-cccc-cccc-cccc-cccccccccccc" + otRowGlobalID = "dddddddd-dddd-dddd-dddd-dddddddddddd" + otRowNamespaceID = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" + + smaCustomGlobalID = "f1f1f1f1-f1f1-f1f1-f1f1-f1f1f1f1f1f1" + smaCustomNamespaceID = "f2f2f2f2-f2f2-f2f2-f2f2-f2f2f2f2f2f2" + rrCustomGlobalID = "f3f3f3f3-f3f3-f3f3-f3f3-f3f3f3f3f3f3" + rrCustomNamespaceID = "f4f4f4f4-f4f4-f4f4-f4f4-f4f4f4f4f4f4" + otCustomGlobalID = "f5f5f5f5-f5f5-f5f5-f5f5-f5f5f5f5f5f5" + otCustomNamespaceID = "f6f6f6f6-f6f6-f6f6-f6f6-f6f6f6f6f6f6" + rrTieOlderID = "f7f7f7f7-f7f7-f7f7-f7f7-f7f7f7f7f7f7" + rrTieNewerID = "f8f8f8f8-f8f8-f8f8-f8f8-f8f8f8f8f8f8" + otTieOlderID = "f9f9f9f9-f9f9-f9f9-f9f9-f9f9f9f9f9f9" + otTieNewerID = "fafafafa-fafa-fafa-fafa-fafafafafafa" + ) + + h.upTo(preNamespaceRollback) + + // Global canonical action id for create should win remap selection. + var globalCreateID string + row := h.queryRow(` + SELECT id FROM actions + WHERE name = 'create' AND namespace_id IS NULL + `) + require.NoError(t, row.Scan(&globalCreateID)) + + // Seed minimal dependency graph. + h.exec(`INSERT INTO attribute_namespaces (id, name, active) VALUES ($1, 'migration-test.example', true)`, namespaceID) + h.exec(`INSERT INTO attribute_namespaces (id, name, active) VALUES ($1, 'migration-test-two.example', true)`, namespaceTwoID) + h.exec(` + INSERT INTO attribute_definitions (id, namespace_id, name, rule, active) + VALUES ($1, $2, 'department', 'ALL_OF', true) + `, attributeDefID, namespaceID) + h.exec(` + INSERT INTO attribute_values (id, attribute_definition_id, value, active) + VALUES ($1, $2, 'engineering', true) + `, attributeValueID, attributeDefID) + h.exec(` + INSERT INTO subject_condition_set (id, condition) + VALUES ($1, '[{"condition_groups":[{"boolean_operator":"AND","conditions":[]}]}]'::jsonb) + `, subjectSetID) + h.exec(` + INSERT INTO subject_mappings (id, attribute_value_id, subject_condition_set_id) + VALUES ($1, $2, $3) + `, subjectMappingID, attributeValueID, subjectSetID) + h.exec(` + INSERT INTO subject_condition_set (id, condition) + VALUES ($1, '[{"condition_groups":[{"boolean_operator":"AND","conditions":[]}]}]'::jsonb) + `, subjectSetTieID) + h.exec(` + INSERT INTO subject_mappings (id, attribute_value_id, subject_condition_set_id) + VALUES ($1, $2, $3) + `, subjectMappingTie, attributeValueID, subjectSetTieID) + h.exec(` + INSERT INTO registered_resources (id, name) + VALUES ($1, 'migration-test-resource') + `, registeredResID) + h.exec(` + INSERT INTO registered_resource_values (id, registered_resource_id, value) + VALUES ($1, $2, 'migration-test-resource-value') + `, registeredValueID, registeredResID) + h.exec(` + INSERT INTO registered_resource_values (id, registered_resource_id, value) + VALUES ($1, $2, 'migration-test-resource-value-tie') + `, registeredValueTie, registeredResID) + h.exec(` + INSERT INTO obligation_definitions (id, namespace_id, name) + VALUES ($1, $2, 'migration-test-obligation') + `, obligationDefID, namespaceID) + h.exec(` + INSERT INTO obligation_values_standard (id, obligation_definition_id, value) + VALUES ($1, $2, 'migration-test-obligation-value') + `, obligationValID, obligationDefID) + h.exec(` + INSERT INTO obligation_values_standard (id, obligation_definition_id, value) + VALUES ($1, $2, 'migration-test-obligation-value-tie') + `, obligationValTie, obligationDefID) + + // Namespaced duplicate of standard create action. + h.exec(` + INSERT INTO actions (id, name, is_standard, namespace_id) + VALUES ($1, 'create', true, $2) + `, namespacedCreateID, namespaceID) + h.exec(` + INSERT INTO actions (id, name, is_standard, namespace_id) + VALUES ($1, 'migration-custom-merge', false, NULL), ($2, 'migration-custom-merge', false, $3) + `, globalCustomID, namespaceCustomID, namespaceID) + h.exec(` + INSERT INTO actions (id, name, is_standard, namespace_id, created_at) + VALUES + ($1, 'migration-ns-only', false, $2, '2026-01-01T00:00:00Z'::timestamp), + ($3, 'migration-ns-only', false, $4, '2026-01-01T00:00:01Z'::timestamp) + `, nsOnlyOlderID, namespaceID, nsOnlyNewerID, namespaceTwoID) + + // Two references in each table that collapse to one after remap. + h.exec(` + INSERT INTO subject_mapping_actions (subject_mapping_id, action_id) + VALUES ($1, $2), ($1, $3) + `, subjectMappingID, globalCreateID, namespacedCreateID) + h.exec(` + INSERT INTO subject_mapping_actions (subject_mapping_id, action_id, created_at) + VALUES ($1, $2, NOW()), ($1, $3, NOW() + interval '1 second') + `, subjectMappingID, globalCustomID, namespaceCustomID) + h.exec(` + INSERT INTO registered_resource_action_attribute_values (id, registered_resource_value_id, action_id, attribute_value_id) + VALUES ($1, $2, $3, $4), ($5, $2, $6, $4) + `, smaRowGlobalID, registeredValueID, globalCreateID, attributeValueID, smaRowNamespaceID, namespacedCreateID) + h.exec(` + INSERT INTO registered_resource_action_attribute_values (id, registered_resource_value_id, action_id, attribute_value_id) + VALUES ($1, $2, $3, $4), ($5, $2, $6, $4) + `, rrCustomGlobalID, registeredValueID, globalCustomID, attributeValueID, rrCustomNamespaceID, namespaceCustomID) + h.exec(` + INSERT INTO obligation_triggers (id, obligation_value_id, action_id, attribute_value_id) + VALUES ($1, $2, $3, $4), ($5, $2, $6, $4) + `, otRowGlobalID, obligationValID, globalCreateID, attributeValueID, otRowNamespaceID, namespacedCreateID) + h.exec(` + INSERT INTO obligation_triggers (id, obligation_value_id, action_id, attribute_value_id) + VALUES ($1, $2, $3, $4), ($5, $2, $6, $4) + `, otCustomGlobalID, obligationValID, globalCustomID, attributeValueID, otCustomNamespaceID, namespaceCustomID) + + // Namespaced-only duplicate branch (no global action exists for this name). + h.exec(` + INSERT INTO subject_mapping_actions (subject_mapping_id, action_id) + VALUES ($1, $2), ($1, $3) + `, subjectMappingTie, nsOnlyOlderID, nsOnlyNewerID) + h.exec(` + INSERT INTO registered_resource_action_attribute_values (id, registered_resource_value_id, action_id, attribute_value_id) + VALUES ($1, $2, $3, $4), ($5, $2, $6, $4) + `, rrTieOlderID, registeredValueTie, nsOnlyOlderID, attributeValueID, rrTieNewerID, nsOnlyNewerID) + h.exec(` + INSERT INTO obligation_triggers (id, obligation_value_id, action_id, attribute_value_id) + VALUES ($1, $2, $3, $4), ($5, $2, $6, $4) + `, otTieOlderID, obligationValTie, nsOnlyOlderID, attributeValueID, otTieNewerID, nsOnlyNewerID) + + // Sanity precondition: namespaced action exists and ref tables have 2 rows for test key. + var count int + row = h.queryRow(`SELECT COUNT(*) FROM actions WHERE name = 'create'`) + require.NoError(t, row.Scan(&count)) + require.GreaterOrEqual(t, count, 2) + + row = h.queryRow(`SELECT COUNT(*) FROM subject_mapping_actions WHERE subject_mapping_id = $1`, subjectMappingID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 4, count) + + row = h.queryRow(`SELECT COUNT(*) FROM registered_resource_action_attribute_values WHERE registered_resource_value_id = $1 AND attribute_value_id = $2`, registeredValueID, attributeValueID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 4, count) + + row = h.queryRow(`SELECT COUNT(*) FROM obligation_triggers WHERE obligation_value_id = $1 AND attribute_value_id = $2`, obligationValID, attributeValueID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 4, count) + + row = h.queryRow(`SELECT COUNT(*) FROM actions WHERE name = 'migration-ns-only'`) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 2, count) + + row = h.queryRow(`SELECT COUNT(*) FROM subject_mapping_actions WHERE subject_mapping_id = $1`, subjectMappingTie) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 2, count) + + row = h.queryRow(`SELECT COUNT(*) FROM registered_resource_action_attribute_values WHERE registered_resource_value_id = $1 AND attribute_value_id = $2`, registeredValueTie, attributeValueID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 2, count) + + row = h.queryRow(`SELECT COUNT(*) FROM obligation_triggers WHERE obligation_value_id = $1 AND attribute_value_id = $2`, obligationValTie, attributeValueID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 2, count) + + h.downTo(postNamespaceRollback) + + // actions.namespace_id should be gone. + row = h.queryRow(` + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = current_schema() + AND table_name = 'actions' + AND column_name = 'namespace_id' + `) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 0, count) + + // No duplicate action names after restoring global unique(name). + row = h.queryRow(` + SELECT COUNT(*) + FROM ( + SELECT name FROM actions GROUP BY name HAVING COUNT(*) > 1 + ) d + `) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 0, count) + + // All references should now point at canonical global create action. + var resolvedActionID string + row = h.queryRow(` + SELECT sma.action_id + FROM subject_mapping_actions sma + JOIN actions a ON a.id = sma.action_id + WHERE sma.subject_mapping_id = $1 AND a.name = 'create' + `, subjectMappingID) + require.NoError(t, row.Scan(&resolvedActionID)) + require.Equal(t, globalCreateID, resolvedActionID) + + row = h.queryRow(`SELECT COUNT(*) FROM subject_mapping_actions WHERE subject_mapping_id = $1`, subjectMappingID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 2, count) + + row = h.queryRow(`SELECT COUNT(*) FROM subject_mapping_actions WHERE subject_mapping_id = $1 AND action_id = $2`, subjectMappingID, globalCreateID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(`SELECT COUNT(*) FROM subject_mapping_actions WHERE subject_mapping_id = $1 AND action_id = $2`, subjectMappingID, globalCustomID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(` + SELECT rr.action_id + FROM registered_resource_action_attribute_values rr + JOIN actions a ON a.id = rr.action_id + WHERE rr.registered_resource_value_id = $1 AND rr.attribute_value_id = $2 AND a.name = 'create' + `, registeredValueID, attributeValueID) + require.NoError(t, row.Scan(&resolvedActionID)) + require.Equal(t, globalCreateID, resolvedActionID) + + row = h.queryRow(`SELECT COUNT(*) FROM registered_resource_action_attribute_values WHERE registered_resource_value_id = $1 AND attribute_value_id = $2`, registeredValueID, attributeValueID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 2, count) + + row = h.queryRow(`SELECT COUNT(*) FROM registered_resource_action_attribute_values WHERE registered_resource_value_id = $1 AND attribute_value_id = $2 AND action_id = $3`, registeredValueID, attributeValueID, globalCreateID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(`SELECT COUNT(*) FROM registered_resource_action_attribute_values WHERE registered_resource_value_id = $1 AND attribute_value_id = $2 AND action_id = $3`, registeredValueID, attributeValueID, globalCustomID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(` + SELECT ot.action_id + FROM obligation_triggers ot + JOIN actions a ON a.id = ot.action_id + WHERE ot.obligation_value_id = $1 AND ot.attribute_value_id = $2 AND a.name = 'create' + `, obligationValID, attributeValueID) + require.NoError(t, row.Scan(&resolvedActionID)) + require.Equal(t, globalCreateID, resolvedActionID) + + row = h.queryRow(`SELECT COUNT(*) FROM obligation_triggers WHERE obligation_value_id = $1 AND attribute_value_id = $2`, obligationValID, attributeValueID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 2, count) + + row = h.queryRow(`SELECT COUNT(*) FROM obligation_triggers WHERE obligation_value_id = $1 AND attribute_value_id = $2 AND action_id = $3`, obligationValID, attributeValueID, globalCreateID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(`SELECT COUNT(*) FROM obligation_triggers WHERE obligation_value_id = $1 AND attribute_value_id = $2 AND action_id = $3`, obligationValID, attributeValueID, globalCustomID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(`SELECT COUNT(*) FROM actions WHERE name = 'migration-custom-merge'`) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + // Namespaced-only duplicates are canonicalized by created_at ASC, then id ASC. + row = h.queryRow(`SELECT COUNT(*) FROM actions WHERE name = 'migration-ns-only'`) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(` + SELECT action_id FROM subject_mapping_actions WHERE subject_mapping_id = $1 + `, subjectMappingTie) + require.NoError(t, row.Scan(&resolvedActionID)) + require.Equal(t, nsOnlyOlderID, resolvedActionID) + + row = h.queryRow(`SELECT COUNT(*) FROM subject_mapping_actions WHERE subject_mapping_id = $1`, subjectMappingTie) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(` + SELECT action_id + FROM registered_resource_action_attribute_values + WHERE registered_resource_value_id = $1 AND attribute_value_id = $2 + `, registeredValueTie, attributeValueID) + require.NoError(t, row.Scan(&resolvedActionID)) + require.Equal(t, nsOnlyOlderID, resolvedActionID) + + row = h.queryRow(`SELECT COUNT(*) FROM registered_resource_action_attribute_values WHERE registered_resource_value_id = $1 AND attribute_value_id = $2`, registeredValueTie, attributeValueID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + row = h.queryRow(` + SELECT action_id + FROM obligation_triggers + WHERE obligation_value_id = $1 AND attribute_value_id = $2 + `, obligationValTie, attributeValueID) + require.NoError(t, row.Scan(&resolvedActionID)) + require.Equal(t, nsOnlyOlderID, resolvedActionID) + + row = h.queryRow(`SELECT COUNT(*) FROM obligation_triggers WHERE obligation_value_id = $1 AND attribute_value_id = $2`, obligationValTie, attributeValueID) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 1, count) + + // No orphan action refs. + row = h.queryRow(`SELECT COUNT(*) FROM subject_mapping_actions sma LEFT JOIN actions a ON a.id = sma.action_id WHERE a.id IS NULL`) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 0, count) + + row = h.queryRow(`SELECT COUNT(*) FROM registered_resource_action_attribute_values rr LEFT JOIN actions a ON a.id = rr.action_id WHERE a.id IS NULL`) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 0, count) + + row = h.queryRow(`SELECT COUNT(*) FROM obligation_triggers ot LEFT JOIN actions a ON a.id = ot.action_id WHERE a.id IS NULL`) + require.NoError(t, row.Scan(&count)) + require.Equal(t, 0, count) +} diff --git a/service/integration/namespaces_test.go b/service/integration/namespaces_test.go index 9dc734d549..e4e9dbd985 100644 --- a/service/integration/namespaces_test.go +++ b/service/integration/namespaces_test.go @@ -4,11 +4,14 @@ import ( "context" "fmt" "log/slog" + "slices" "strings" "testing" + "time" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/opentdf/platform/protocol/go/policy/kasregistry" "github.com/opentdf/platform/protocol/go/policy/namespaces" @@ -83,6 +86,50 @@ func (s *NamespacesSuite) Test_CreateNamespace_NormalizeCasing() { s.Equal(strings.ToLower(name), got.GetName(), createdNamespace.GetName()) } +func (s *NamespacesSuite) Test_CreateNamespace_SeedsStandardActions() { + createdNamespace, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: fmt.Sprintf("seed-actions-%d.com", time.Now().UnixNano()), + }) + s.Require().NoError(err) + s.NotNil(createdNamespace) + + listed, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: createdNamespace.GetId()}) + s.Require().NoError(err) + s.NotNil(listed) + + scopedNames := make([]string, 0, 4) + for _, action := range listed.GetActionsStandard() { + if action.GetNamespace().GetId() == createdNamespace.GetId() { + scopedNames = append(scopedNames, action.GetName()) + } + } + + s.ElementsMatch([]string{"create", "read", "update", "delete"}, scopedNames) +} + +func (s *NamespacesSuite) Test_CreateNamespace_WithoutPublicKeys_DoesNotReturnKeys() { + name := fmt.Sprintf("no-namespace-children-%d.com", time.Now().UnixNano()) + createdNamespace, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: name}) + s.Require().NoError(err) + s.Require().NotNil(createdNamespace) + defer func() { + _, err := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, createdNamespace, createdNamespace.GetFqn()) + s.Require().NoError(err) + }() + + s.Empty(createdNamespace.GetKasKeys()) + + gotByID, err := s.db.PolicyClient.GetNamespace(s.ctx, createdNamespace.GetId()) + s.Require().NoError(err) + s.Require().NotNil(gotByID) + s.Empty(gotByID.GetKasKeys()) + + gotByFQN, err := s.db.PolicyClient.GetNamespace(s.ctx, &namespaces.GetNamespaceRequest_Fqn{Fqn: createdNamespace.GetName()}) + s.Require().NoError(err) + s.Require().NotNil(gotByFQN) + s.Empty(gotByFQN.GetKasKeys()) +} + func (s *NamespacesSuite) Test_GetNamespace() { testData := s.getActiveNamespaceFixtures() @@ -199,6 +246,281 @@ func (s *NamespacesSuite) Test_ListNamespaces_NoPagination_Succeeds() { } } +func (s *NamespacesSuite) Test_ListNamespaces_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + create := func(i int) string { + name := fmt.Sprintf("order-test-ns-%d-%d.com", i, suffix) + created, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: name}) + s.Require().NoError(err) + s.Require().NotNil(created) + return created.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + listNamespacesRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + }) + s.Require().NoError(err) + s.NotNil(listNamespacesRsp) + + assertIDsInOrder(s.T(), listNamespacesRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, thirdID, secondID, firstID) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByName_ASC() { + ids := s.createSortTestNamespaces([]string{"aaa-sort", "bbb-sort", "ccc-sort"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // aaa < bbb < ccc in ASC order + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByName_DESC() { + ids := s.createSortTestNamespaces([]string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // ccc > bbb > aaa in DESC order + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByCreatedAt_ASC() { + ids := s.createSortTestNamespaces([]string{"createdasc-ns-0", "createdasc-ns-1", "createdasc-ns-2"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // oldest first in ASC order + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByCreatedAt_DESC() { + ids := s.createSortTestNamespaces([]string{"createddesc-ns-0", "createddesc-ns-1", "createddesc-ns-2"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // newest first in DESC order + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByFqn_ASC() { + ids := s.createSortTestNamespaces([]string{"aaa-fqnsort", "bbb-fqnsort", "ccc-fqnsort"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_FQN, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // fqn is https://, so aaa < bbb < ccc in ASC order + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByFqn_DESC() { + ids := s.createSortTestNamespaces([]string{"aaa-fqnsortdesc", "bbb-fqnsortdesc", "ccc-fqnsortdesc"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_FQN, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // fqn is https://, so ccc > bbb > aaa in DESC order + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByUpdatedAt_DESC() { + ids := s.createSortTestNamespaces([]string{"upd-sort-ns-0", "upd-sort-ns-1", "upd-sort-ns-2"}) + defer s.deleteSortTestNamespaces(ids) + + // Update the first namespace so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateNamespace(s.ctx, ids[0], &namespaces.UpdateNamespaceRequest{ + Id: ids[0], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated namespace (ids[0]) should appear before the others + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[0], ids[2], ids[1]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByUpdatedAt_ASC() { + ids := s.createSortTestNamespaces([]string{"upd-sort-asc-ns-0", "upd-sort-asc-ns-1", "upd-sort-asc-ns-2"}) + defer s.deleteSortTestNamespaces(ids) + + // Update the last namespace so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateNamespace(s.ctx, ids[2], &namespaces.UpdateNamespaceRequest{ + Id: ids[2], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated namespace (ids[2]) should appear last in ASC order + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortTieBreaker_CreatedAtWithIDFallback() { + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-ns-%d-%d.com", i, suffix) + created, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: name}) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestNamespaces(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "attribute_namespaces", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, sorted[0], sorted[1], sorted[2]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByUnspecifiedField_DefaultsToCreatedAt() { + ids := s.createSortTestNamespaces([]string{"unspecified-sort-ns-0", "unspecified-sort-ns-1", "unspecified-sort-ns-2"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Field defaults to created_at, explicit ASC is preserved + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByUnspecifiedDirection_DefaultsToDESC() { + ids := s.createSortTestNamespaces([]string{"unspecified-dir-ns-0", "unspecified-dir-ns-1", "unspecified-dir-ns-2"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Direction defaults to DESC, explicit created_at field is preserved + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortByBothUnspecified_DefaultsToCreatedAtDESC() { + ids := s.createSortTestNamespaces([]string{"both-unspecified-ns-0", "both-unspecified-ns-1", "both-unspecified-ns-2"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Both default: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *NamespacesSuite) Test_ListNamespaces_SortOmitted() { + ids := s.createSortTestNamespaces([]string{"sort-omitted-ns-0", "sort-omitted-ns-1", "sort-omitted-ns-2"}) + defer s.deleteSortTestNamespaces(ids) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // No sort provided: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[2], ids[1], ids[0]) +} + func (s *NamespacesSuite) Test_ListNamespaces_Limit_Succeeds() { var limit int32 = 2 listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ @@ -432,16 +754,22 @@ func (s *NamespacesSuite) Test_DeactivateNamespace_Cascades_List() { } listValues := func(state common.ActiveStateEnum) bool { - listedValsRsp, err := s.db.PolicyClient.ListAttributeValues(s.ctx, &attributes.ListAttributeValuesRequest{ - AttributeId: deactivatedAttrID, - State: state, - }) + gotAttr, err := s.db.PolicyClient.GetAttribute(s.ctx, deactivatedAttrID) s.Require().NoError(err) - s.NotNil(listedValsRsp) - listed := listedValsRsp.GetValues() - for _, v := range listed { - if deactivatedAttrValueID == v.GetId() { + s.NotNil(gotAttr) + for _, v := range gotAttr.GetValues() { + if deactivatedAttrValueID != v.GetId() { + continue + } + switch state { + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE: + return v.GetActive().GetValue() + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_INACTIVE: + return !v.GetActive().GetValue() + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY: return true + case common.ActiveStateEnum_ACTIVE_STATE_ENUM_UNSPECIFIED: + return v.GetActive().GetValue() } } return false @@ -1116,6 +1444,27 @@ func (s *NamespacesSuite) Test_GetNamespace_ByIdAndName_ReturnSameResult() { } } +// createSortTestNamespaces creates namespaces with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the namespace IDs in creation order. +func (s *NamespacesSuite) createSortTestNamespaces(prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + name := fmt.Sprintf("%s-%d.com", prefix, time.Now().UnixNano()) + created, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: name}) + s.Require().NoError(err) + ids[i] = created.GetId() + } + return ids +} + +// deleteSortTestNamespaces deactivates namespaces created by sort tests. +func (s *NamespacesSuite) deleteSortTestNamespaces(ids []string) { + s.Require().NoError(forceDeleteRows(s.ctx, s.db, "attribute_namespaces", ids)) +} + func (s *NamespacesSuite) getActiveNamespaceFixtures() []fixtures.FixtureDataNamespace { return []fixtures.FixtureDataNamespace{ s.f.GetNamespaceKey("example.com"), diff --git a/service/integration/obligation_triggers_test.go b/service/integration/obligation_triggers_test.go index 7502cb29b4..1d18a6c2a1 100644 --- a/service/integration/obligation_triggers_test.go +++ b/service/integration/obligation_triggers_test.go @@ -2,9 +2,12 @@ package integration import ( "context" + "errors" "fmt" "log/slog" + "strings" "testing" + "time" "github.com/google/uuid" "github.com/opentdf/platform/protocol/go/common" @@ -26,6 +29,7 @@ const ( obligationName = "test-obligation" obligationValue = "test-obligation-value" clientID = "test-client-id" + secondClientID = "test-client-id-2" ) type ObligationTriggersSuite struct { @@ -88,7 +92,8 @@ func (s *ObligationTriggersSuite) SetupSuite() { // Create an action s.action, err = s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: actionName, + Name: actionName, + NamespaceId: s.namespace.GetId(), }) s.Require().NoError(err) @@ -127,16 +132,26 @@ func (s *ObligationTriggersSuite) TearDownSuite() { func (s *ObligationTriggersSuite) TearDownTest() { for _, triggerID := range s.triggerIDsToClean { + if triggerID == "" { + continue + } _, err := s.db.PolicyClient.DeleteObligationTrigger(s.ctx, &obligations.RemoveObligationTriggerRequest{ Id: triggerID, }) - s.Require().NoError(err) + if err != nil && !errors.Is(err, db.ErrNotFound) { + s.Require().NoError(err) + } } for _, obligationValueID := range s.obligationValueIDsToClean { + if obligationValueID == "" { + continue + } _, err := s.db.PolicyClient.DeleteObligationValue(s.ctx, &obligations.DeleteObligationValueRequest{ Id: obligationValueID, }) - s.Require().NoError(err) + if err != nil && !errors.Is(err, db.ErrNotFound) { + s.Require().NoError(err) + } } s.triggerIDsToClean = nil s.obligationValueIDsToClean = nil @@ -170,6 +185,54 @@ func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_WithIDs_Success() s.Require().Equal("test", trigger.GetMetadata().GetLabels()["source"]) } +func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_SameTupleDifferentClients_Success() { + req := &obligations.AddObligationTriggerRequest{ + ObligationValue: &common.IdFqnIdentifier{Id: s.obligationValue.GetId()}, + AttributeValue: &common.IdFqnIdentifier{Id: s.attributeValue.GetId()}, + Action: &common.IdNameIdentifier{Id: s.action.GetId()}, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: clientID, + }, + }, + } + + firstTrigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, req) + s.Require().NoError(err) + s.triggerIDsToClean = append(s.triggerIDsToClean, firstTrigger.GetId()) + s.validateTriggerWithDefaults(firstTrigger, true) + + req.Context.Pep.ClientId = secondClientID + secondTrigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, req) + s.Require().NoError(err) + s.triggerIDsToClean = append(s.triggerIDsToClean, secondTrigger.GetId()) + s.Require().NotEqual(firstTrigger.GetId(), secondTrigger.GetId()) + s.Require().Len(secondTrigger.GetContext(), 1) + s.Require().Equal(secondClientID, secondTrigger.GetContext()[0].GetPep().GetClientId()) +} + +func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_SameTupleSameClient_Fails() { + req := &obligations.AddObligationTriggerRequest{ + ObligationValue: &common.IdFqnIdentifier{Id: s.obligationValue.GetId()}, + AttributeValue: &common.IdFqnIdentifier{Id: s.attributeValue.GetId()}, + Action: &common.IdNameIdentifier{Id: s.action.GetId()}, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: clientID, + }, + }, + } + + firstTrigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, req) + s.Require().NoError(err) + s.triggerIDsToClean = append(s.triggerIDsToClean, firstTrigger.GetId()) + + duplicateTrigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, req) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrUniqueConstraintViolation) + s.Nil(duplicateTrigger) +} + func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_NoCtx_Success() { trigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, &obligations.AddObligationTriggerRequest{ ObligationValue: &common.IdFqnIdentifier{Id: s.obligationValue.GetId()}, @@ -201,6 +264,120 @@ func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_WithNameFQN_Succe s.Require().Equal("test", trigger.GetMetadata().GetLabels()["source"]) } +func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_WithStandardActionName_PrefersNamespaceScopedAction() { + globalRead := s.f.GetStandardAction("read") + + listed, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.namespace.GetId()}) + s.Require().NoError(err) + + namespaceReadID := "" + for _, act := range listed.GetActionsStandard() { + if act.GetName() == "read" && act.GetId() != globalRead.GetId() && act.GetNamespace().GetId() == s.namespace.GetId() { + namespaceReadID = act.GetId() + break + } + } + s.Require().NotEmpty(namespaceReadID, "expected a namespace-scoped standard read action") + + uniqueValue := fmt.Sprintf("trigger-read-action-%d", time.Now().UnixNano()) + ov, err := s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ + ObligationId: s.obligation.GetId(), + Value: uniqueValue, + }) + s.Require().NoError(err) + s.obligationValueIDsToClean = append(s.obligationValueIDsToClean, ov.GetId()) + + trigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, &obligations.AddObligationTriggerRequest{ + ObligationValue: &common.IdFqnIdentifier{Id: ov.GetId()}, + AttributeValue: &common.IdFqnIdentifier{Id: s.attributeValue.GetId()}, + Action: &common.IdNameIdentifier{Name: "read"}, + }) + s.Require().NoError(err) + s.triggerIDsToClean = append(s.triggerIDsToClean, trigger.GetId()) + + s.Equal(namespaceReadID, trigger.GetAction().GetId()) + s.Equal("read", trigger.GetAction().GetName()) +} + +func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_ObligationValueDifferentNamespace_Succeeds() { + targetNamespace, _, targetObligationValue := s.createCrossNamespaceObligationValue("different-obligation-ns") + + customActionName := fmt.Sprintf("cross-namespace-trigger-action-%d", time.Now().UnixNano()) + sourceAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: customActionName, + NamespaceId: s.namespace.GetId(), + }) + s.Require().NoError(err) + + var triggerID string + defer func() { + if triggerID != "" { + _, err := s.db.PolicyClient.DeleteObligationTrigger(s.ctx, &obligations.RemoveObligationTriggerRequest{ + Id: triggerID, + }) + if err != nil && !errors.Is(err, db.ErrNotFound) { + s.Require().NoError(err) + } + } + + _, err := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, targetNamespace, targetNamespace.GetFqn()) + s.Require().NoError(err) + }() + + trigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, &obligations.AddObligationTriggerRequest{ + ObligationValue: &common.IdFqnIdentifier{Id: targetObligationValue.GetId()}, + AttributeValue: &common.IdFqnIdentifier{Id: s.attributeValue.GetId()}, + Action: &common.IdNameIdentifier{Name: sourceAction.GetName()}, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: clientID, + }, + }, + }) + s.Require().NoError(err) + triggerID = trigger.GetId() + + s.validateTrigger(trigger, targetObligationValue, s.attributeValue, sourceAction, true) +} + +func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_CrossNamespaceActionName_UsesSourceNamespaceAction() { + targetNamespace, _, targetObligationValue := s.createCrossNamespaceObligationValue("cross-namespace-action-resolution") + + sourceRead := s.getActionByNameInNamespace("read", s.namespace.GetId()) + targetRead := s.getActionByNameInNamespace("read", targetNamespace.GetId()) + s.Require().NotEqual(sourceRead.GetId(), targetRead.GetId()) + + var triggerID string + defer func() { + if triggerID != "" { + _, err := s.db.PolicyClient.DeleteObligationTrigger(s.ctx, &obligations.RemoveObligationTriggerRequest{ + Id: triggerID, + }) + if err != nil && !errors.Is(err, db.ErrNotFound) { + s.Require().NoError(err) + } + } + + _, err := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, targetNamespace, targetNamespace.GetFqn()) + s.Require().NoError(err) + }() + + trigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, &obligations.AddObligationTriggerRequest{ + ObligationValue: &common.IdFqnIdentifier{Fqn: targetObligationValue.GetFqn()}, + AttributeValue: &common.IdFqnIdentifier{Fqn: s.attributeValue.GetFqn()}, + Action: &common.IdNameIdentifier{Name: "read"}, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: clientID, + }, + }, + }) + s.Require().NoError(err) + triggerID = trigger.GetId() + + s.validateTrigger(trigger, targetObligationValue, s.attributeValue, sourceRead, true) +} + func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_ObligationValueNotFound_Fails() { randomID := uuid.NewString() trigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, &obligations.AddObligationTriggerRequest{ @@ -231,7 +408,7 @@ func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_AttributeValueNot }, }) s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrNotNullViolation) + s.Require().ErrorIs(err, db.ErrNotFound) s.Nil(trigger) } @@ -248,7 +425,7 @@ func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_ActionNotFound_Fa }, }) s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrInvalidOblTriParam) + s.Require().ErrorIs(err, db.ErrNotFound) s.Nil(trigger) } @@ -290,10 +467,64 @@ func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_AttributeValueDif }, }) s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrInvalidOblTriParam) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) s.Nil(trigger) } +func (s *ObligationTriggersSuite) Test_CreateObligationTrigger_ActionIDDifferentNamespace_Fails() { + differentNamespace, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "different-action-ns", + }) + s.Require().NoError(err) + defer func() { + _, deleteErr := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, differentNamespace, differentNamespace.GetFqn()) + s.Require().NoError(deleteErr) + }() + + differentAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "action-different-ns", + NamespaceId: differentNamespace.GetId(), + }) + s.Require().NoError(err) + + trigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, &obligations.AddObligationTriggerRequest{ + ObligationValue: &common.IdFqnIdentifier{Id: s.obligationValue.GetId()}, + AttributeValue: &common.IdFqnIdentifier{Id: s.attributeValue.GetId()}, + Action: &common.IdNameIdentifier{Id: differentAction.GetId()}, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: clientID, + }, + }, + }) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) + s.Nil(trigger) +} + +func (s *ObligationTriggersSuite) Test_GetObligationTrigger_Success() { + trigger := s.createGenericTrigger() + s.triggerIDsToClean = append(s.triggerIDsToClean, trigger.GetId()) + + retrievedTrigger, err := s.db.PolicyClient.GetObligationTrigger(s.ctx, &obligations.GetObligationTriggerRequest{ + Id: trigger.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(retrievedTrigger) + s.Require().Equal(trigger.GetId(), retrievedTrigger.GetId()) + s.validateTrigger(retrievedTrigger, trigger.GetObligationValue(), trigger.GetAttributeValue(), trigger.GetAction(), true) + s.Require().NotNil(retrievedTrigger.GetMetadata()) +} + +func (s *ObligationTriggersSuite) Test_GetObligationTrigger_NotFound_Fails() { + randomID := uuid.NewString() + _, err := s.db.PolicyClient.GetObligationTrigger(s.ctx, &obligations.GetObligationTriggerRequest{ + Id: randomID, + }) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrNotFound) +} + func (s *ObligationTriggersSuite) Test_DeleteObligationTrigger_Success() { trigger := s.createGenericTrigger() deletedTrigger, err := s.db.PolicyClient.DeleteObligationTrigger(s.ctx, &obligations.RemoveObligationTriggerRequest{ @@ -322,6 +553,28 @@ func (s *ObligationTriggersSuite) Test_ListObligationTriggers_NoTriggersNoFilter s.validatePageResponses(pageResult, 0, 0, 0) } +func (s *ObligationTriggersSuite) Test_ListObligationTriggers_OrdersByCreatedAt_Succeeds() { + first := s.createUniqueTrigger("ordered-first") + s.triggerIDsToClean = append(s.triggerIDsToClean, first.GetId()) + s.obligationValueIDsToClean = append(s.obligationValueIDsToClean, first.GetObligationValue().GetId()) + time.Sleep(5 * time.Millisecond) + second := s.createUniqueTrigger("ordered-second") + s.triggerIDsToClean = append(s.triggerIDsToClean, second.GetId()) + s.obligationValueIDsToClean = append(s.obligationValueIDsToClean, second.GetObligationValue().GetId()) + time.Sleep(5 * time.Millisecond) + third := s.createUniqueTrigger("ordered-third") + s.triggerIDsToClean = append(s.triggerIDsToClean, third.GetId()) + s.obligationValueIDsToClean = append(s.obligationValueIDsToClean, third.GetObligationValue().GetId()) + + triggers, _, err := s.db.PolicyClient.ListObligationTriggers(s.ctx, &obligations.ListObligationTriggersRequest{ + NamespaceId: s.namespace.GetId(), + }) + s.Require().NoError(err) + s.NotNil(triggers) + + assertIDsInOrder(s.T(), triggers, func(t *policy.ObligationTrigger) string { return t.GetId() }, third.GetId(), second.GetId(), first.GetId()) +} + func (s *ObligationTriggersSuite) Test_ListObligationTriggers_NoTriggersWithNamespaceId_Success() { triggers, pageResult, err := s.db.PolicyClient.ListObligationTriggers(s.ctx, &obligations.ListObligationTriggersRequest{ NamespaceId: s.namespace.GetId(), @@ -383,7 +636,6 @@ func (s *ObligationTriggersSuite) Test_ListObligationTriggers_MultipleTriggersWi foundTriggers := make(map[string]bool) for _, t := range triggers { - s.Require().Equal(s.namespace.GetId(), t.GetObligationValue().GetObligation().GetNamespace().GetId()) createdTrigger, ok := createdTriggersMap[t.GetId()] s.Require().True(ok) s.validateTrigger(t, createdTrigger.GetObligationValue(), createdTrigger.GetAttributeValue(), createdTrigger.GetAction(), true) @@ -410,7 +662,6 @@ func (s *ObligationTriggersSuite) Test_ListObligationTriggers_MultipleTriggersWi foundTriggers := make(map[string]bool) for _, t := range triggers { - s.Require().Equal(s.namespace.GetFqn(), t.GetObligationValue().GetObligation().GetNamespace().GetFqn()) createdTrigger, ok := createdTriggersMap[t.GetId()] s.Require().True(ok) s.validateTrigger(t, createdTrigger.GetObligationValue(), createdTrigger.GetAttributeValue(), createdTrigger.GetAction(), true) @@ -471,10 +722,78 @@ func (s *ObligationTriggersSuite) Test_ListObligationTriggers_WithNamespaceAndPa s.Require().NoError(err) s.Require().Len(triggers, 1) s.validatePageResponses(pageRes, 1, 0, 0) - s.Require().Equal(s.namespace.GetId(), triggers[0].GetObligationValue().GetObligation().GetNamespace().GetId()) s.validateTriggerWithDefaults(triggers[0], true) } +func (s *ObligationTriggersSuite) Test_ListObligationTriggers_CrossNamespaceObligationUsesSourceNamespaceFilter_Success() { + targetNamespace, _, targetObligationValue := s.createCrossNamespaceObligationValue("cross-namespace-list-test") + + customActionName := fmt.Sprintf("cross-namespace-list-action-%d", time.Now().UnixNano()) + sourceAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: customActionName, + NamespaceId: s.namespace.GetId(), + }) + s.Require().NoError(err) + + var triggerID string + defer func() { + if triggerID != "" { + _, err := s.db.PolicyClient.DeleteObligationTrigger(s.ctx, &obligations.RemoveObligationTriggerRequest{ + Id: triggerID, + }) + if err != nil && !errors.Is(err, db.ErrNotFound) { + s.Require().NoError(err) + } + } + + _, err := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, targetNamespace, targetNamespace.GetFqn()) + s.Require().NoError(err) + }() + + trigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, &obligations.AddObligationTriggerRequest{ + ObligationValue: &common.IdFqnIdentifier{Fqn: targetObligationValue.GetFqn()}, + AttributeValue: &common.IdFqnIdentifier{Id: s.attributeValue.GetId()}, + Action: &common.IdNameIdentifier{Name: sourceAction.GetName()}, + Context: &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: clientID, + }, + }, + }) + s.Require().NoError(err) + triggerID = trigger.GetId() + + sourceNamespaceTriggers, sourcePage, err := s.db.PolicyClient.ListObligationTriggers(s.ctx, &obligations.ListObligationTriggersRequest{ + NamespaceId: s.namespace.GetId(), + }) + s.Require().NoError(err) + s.Require().Len(sourceNamespaceTriggers, 1) + s.validatePageResponses(sourcePage, 1, 0, 0) + s.validateTrigger(sourceNamespaceTriggers[0], targetObligationValue, s.attributeValue, sourceAction, true) + + sourceNamespaceFqnTriggers, sourceFqnPage, err := s.db.PolicyClient.ListObligationTriggers(s.ctx, &obligations.ListObligationTriggersRequest{ + NamespaceFqn: s.namespace.GetFqn(), + }) + s.Require().NoError(err) + s.Require().Len(sourceNamespaceFqnTriggers, 1) + s.validatePageResponses(sourceFqnPage, 1, 0, 0) + s.validateTrigger(sourceNamespaceFqnTriggers[0], targetObligationValue, s.attributeValue, sourceAction, true) + + targetNamespaceTriggers, targetPage, err := s.db.PolicyClient.ListObligationTriggers(s.ctx, &obligations.ListObligationTriggersRequest{ + NamespaceId: targetNamespace.GetId(), + }) + s.Require().NoError(err) + s.Require().Empty(targetNamespaceTriggers) + s.validatePageResponses(targetPage, 0, 0, 0) + + targetNamespaceFqnTriggers, targetFqnPage, err := s.db.PolicyClient.ListObligationTriggers(s.ctx, &obligations.ListObligationTriggersRequest{ + NamespaceFqn: targetNamespace.GetFqn(), + }) + s.Require().NoError(err) + s.Require().Empty(targetNamespaceFqnTriggers) + s.validatePageResponses(targetFqnPage, 0, 0, 0) +} + func (s *ObligationTriggersSuite) Test_ListObligationTriggers_LimitToLarge() { triggers, pageRes, err := s.db.PolicyClient.ListObligationTriggers(s.ctx, &obligations.ListObligationTriggersRequest{ NamespaceId: s.namespace.GetId(), @@ -587,6 +906,11 @@ func (s *ObligationTriggersSuite) validateTrigger(actual *policy.ObligationTrigg s.Require().Equal(expectedAction.GetId(), actual.GetAction().GetId()) s.Require().Equal(expectedAction.GetName(), actual.GetAction().GetName()) + // Validate top-level trigger namespace + s.Require().NotNil(actual.GetNamespace()) + s.Require().NotEmpty(actual.GetNamespace().GetId()) + s.Require().Equal(strings.Split(expectedAttributeValue.GetFqn(), "/attr/")[0], actual.GetNamespace().GetFqn()) + // Validate context if shouldHaveCtx { s.Require().NotNil(actual.GetContext()) @@ -610,6 +934,15 @@ func (s *ObligationTriggersSuite) validateTriggerWithDefaults(trigger *policy.Ob s.validateTrigger(trigger, s.obligationValue, s.attributeValue, s.action, shouldHaveCtx) } +func (s *ObligationTriggersSuite) getActionByNameInNamespace(name string, namespaceID string) *policy.Action { + action, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: name}, + NamespaceId: namespaceID, + }) + s.Require().NoError(err) + return action +} + func (s *ObligationTriggersSuite) appendObligationValuesToClean(createdTriggers map[string]*policy.ObligationTrigger) { for _, trigger := range createdTriggers { s.obligationValueIDsToClean = append(s.obligationValueIDsToClean, trigger.GetObligationValue().GetId()) @@ -617,15 +950,23 @@ func (s *ObligationTriggersSuite) appendObligationValuesToClean(createdTriggers } func (s *ObligationTriggersSuite) createDifferentNamespaceWithTrigger(namespaceName string) *DifferentNamespaceEntities { + uniqueNamespaceName := fmt.Sprintf("%s-%d", namespaceName, time.Now().UnixNano()) + // Create a different namespace differentNamespace, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ - Name: namespaceName, + Name: uniqueNamespaceName, + }) + s.Require().NoError(err) + + differentAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "different-action-" + uniqueNamespaceName, + NamespaceId: differentNamespace.GetId(), }) s.Require().NoError(err) // Create obligation in different namespace differentObligation, err := s.db.PolicyClient.CreateObligation(s.ctx, &obligations.CreateObligationRequest{ - Name: "different-obligation-" + namespaceName, + Name: "different-obligation-" + uniqueNamespaceName, NamespaceId: differentNamespace.GetId(), }) s.Require().NoError(err) @@ -633,13 +974,13 @@ func (s *ObligationTriggersSuite) createDifferentNamespaceWithTrigger(namespaceN // Create obligation value in different namespace differentObligationValue, err := s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ ObligationId: differentObligation.GetId(), - Value: "different-obligation-value-" + namespaceName, + Value: "different-obligation-value-" + uniqueNamespaceName, }) s.Require().NoError(err) // Create attribute in different namespace differentAttribute, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ - Name: "different-attribute-" + namespaceName, + Name: "different-attribute-" + uniqueNamespaceName, NamespaceId: differentNamespace.GetId(), Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, }) @@ -647,7 +988,7 @@ func (s *ObligationTriggersSuite) createDifferentNamespaceWithTrigger(namespaceN // Create attribute value in different namespace differentAttributeValue, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, differentAttribute.GetId(), &attributes.CreateAttributeValueRequest{ - Value: "different-value-" + namespaceName, + Value: "different-value-" + uniqueNamespaceName, AttributeId: differentAttribute.GetId(), }) s.Require().NoError(err) @@ -656,7 +997,7 @@ func (s *ObligationTriggersSuite) createDifferentNamespaceWithTrigger(namespaceN differentTrigger, err := s.db.PolicyClient.CreateObligationTrigger(s.ctx, &obligations.AddObligationTriggerRequest{ ObligationValue: &common.IdFqnIdentifier{Id: differentObligationValue.GetId()}, AttributeValue: &common.IdFqnIdentifier{Id: differentAttributeValue.GetId()}, - Action: &common.IdNameIdentifier{Id: s.action.GetId()}, + Action: &common.IdNameIdentifier{Id: differentAction.GetId()}, Context: &policy.RequestContext{ Pep: &policy.PolicyEnforcementPoint{ ClientId: clientID, @@ -684,3 +1025,26 @@ func (s *ObligationTriggersSuite) createDifferentNamespaceWithTrigger(namespaceN }, } } + +func (s *ObligationTriggersSuite) createCrossNamespaceObligationValue(namespaceName string) (*policy.Namespace, *policy.Obligation, *policy.ObligationValue) { + uniqueNamespaceName := fmt.Sprintf("%s-%d", namespaceName, time.Now().UnixNano()) + + targetNamespace, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: uniqueNamespaceName, + }) + s.Require().NoError(err) + + targetObligation, err := s.db.PolicyClient.CreateObligation(s.ctx, &obligations.CreateObligationRequest{ + Name: "cross-namespace-obligation-" + uniqueNamespaceName, + NamespaceId: targetNamespace.GetId(), + }) + s.Require().NoError(err) + + targetObligationValue, err := s.db.PolicyClient.CreateObligationValue(s.ctx, &obligations.CreateObligationValueRequest{ + ObligationId: targetObligation.GetId(), + Value: "cross-namespace-obligation-value-" + uniqueNamespaceName, + }) + s.Require().NoError(err) + + return targetNamespace, targetObligation, targetObligationValue +} diff --git a/service/integration/obligations_test.go b/service/integration/obligations_test.go index 6b2d4814a1..f950788c35 100644 --- a/service/integration/obligations_test.go +++ b/service/integration/obligations_test.go @@ -2,7 +2,9 @@ package integration import ( "context" + "fmt" "log/slog" + "slices" "strconv" "testing" "time" @@ -10,6 +12,7 @@ import ( "github.com/opentdf/platform/lib/identifier" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" "github.com/opentdf/platform/protocol/go/policy/obligations" "github.com/opentdf/platform/service/internal/fixtures" "github.com/opentdf/platform/service/pkg/db" @@ -102,6 +105,44 @@ func (s *ObligationsSuite) Test_CreateObligation_Succeeds() { s.deleteObligations([]string{obl.GetId()}) } +func (s *ObligationsSuite) Test_CreateObligation_WithoutValues_DoesNotReturnValues() { + namespaceID, namespaceFQN, namespace := s.getNamespaceData(nsExampleCom) + name := fmt.Sprintf("%s-no-values-%d", oblName, time.Now().UnixNano()) + + createdObl := s.createObligation(namespaceID, name, nil) + defer s.deleteObligations([]string{createdObl.GetId()}) + + s.assertObligationBasics(createdObl, name, namespaceID, namespace.Name, namespaceFQN) + s.Empty(createdObl.GetValues()) + + gotObl, err := s.db.PolicyClient.GetObligation(s.ctx, &obligations.GetObligationRequest{ + Id: createdObl.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(gotObl) + s.assertObligationBasics(gotObl, name, namespaceID, namespace.Name, namespaceFQN) + s.Empty(gotObl.GetValues()) + + oblList, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + NamespaceId: namespaceID, + }) + s.Require().NoError(err) + s.Require().NotNil(oblList) + + found := false + for _, obl := range oblList { + if obl.GetId() != createdObl.GetId() { + continue + } + + found = true + s.assertObligationBasics(obl, name, namespaceID, namespace.Name, namespaceFQN) + s.Empty(obl.GetValues()) + break + } + s.True(found) +} + func (s *ObligationsSuite) Test_CreateObligation_Fails() { // Invalid namespace ID obl, err := s.db.PolicyClient.CreateObligation(s.ctx, &obligations.CreateObligationRequest{ @@ -459,6 +500,32 @@ func (s *ObligationsSuite) Test_ListObligations_Succeeds() { s.deleteObligations(createdOblIDs) } +func (s *ObligationsSuite) Test_ListObligations_OrdersByCreatedAt_Succeeds() { + namespaceID, _, _ := s.getNamespaceData(nsExampleCom) + suffix := time.Now().UnixNano() + + create := func(i int) *policy.Obligation { + name := fmt.Sprintf("order-test-obl-%d-%d", i, suffix) + return s.createObligation(namespaceID, name, nil) + } + + first := create(1) + time.Sleep(5 * time.Millisecond) + second := create(2) + time.Sleep(5 * time.Millisecond) + third := create(3) + + defer s.deleteObligations([]string{first.GetId(), second.GetId(), third.GetId()}) + + oblList, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + NamespaceId: namespaceID, + }) + s.Require().NoError(err) + s.NotNil(oblList) + + assertIDsInOrder(s.T(), oblList, func(obl *policy.Obligation) string { return obl.GetId() }, third.GetId(), second.GetId(), first.GetId()) +} + func (s *ObligationsSuite) Test_ListObligations_Fails() { // Attempt to list obligations with an invalid namespace ID oblList, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ @@ -1462,10 +1529,15 @@ func (s *ObligationsSuite) Test_UpdateObligationValue_WithTriggers_Succeeds() { }) s.Require().NoError(err) s.NotNil(updatedOblValue) + readAction, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: "read"}, + NamespaceId: triggerSetup.namespace.ID, + }) + s.Require().NoError(err) s.assertObligationValueBasics(updatedOblValue, oblValPrefix+"test-1-updated", triggerSetup.namespace.ID, triggerSetup.namespace.Name, httpsPrefix+triggerSetup.namespace.Name) s.assertTriggers(updatedOblValue, []*TriggerAssertion{ { - expectedAction: triggerSetup.action, + expectedAction: readAction, expectedObligation: triggerSetup.createdObl, expectedAttributeValue: triggerSetup.attributeValues[1], expectedAttributeValueFQN: "https://example.com/attr/attr1/value/value2", @@ -1590,6 +1662,328 @@ func (s *ObligationsSuite) Test_DeleteObligationValue_Fails() { s.deleteObligations([]string{createdObl.GetId()}) } +// Test_ListObligations_EmptyNamespaceId_ReturnsAll validates that empty string parameters +// are properly treated as NULL +func (s *ObligationsSuite) Test_ListObligations_EmptyNamespaceId_ReturnsAll() { + // Create obligations in different namespaces + namespaceID1, _, _ := s.getNamespaceData(nsExampleCom) + namespaceID2, _, _ := s.getNamespaceData(nsExampleNet) + + obl1 := s.createObligation(namespaceID1, oblName+"-empty-test-1", nil) + obl2 := s.createObligation(namespaceID2, oblName+"-empty-test-2", nil) + + defer s.deleteObligations([]string{obl1.GetId(), obl2.GetId()}) + + // List with empty namespace_id should return all obligations + allObligations, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + NamespaceId: "", // Empty string should be treated as NULL + }) + s.Require().NoError(err) + s.NotNil(allObligations) + s.GreaterOrEqual(len(allObligations), 2, "Should return at least our two test obligations") + + // Verify our test obligations are in the results + found1, found2 := false, false + for _, obl := range allObligations { + if obl.GetId() == obl1.GetId() { + found1 = true + } + if obl.GetId() == obl2.GetId() { + found2 = true + } + } + s.True(found1, "Should find obligation from namespace 1") + s.True(found2, "Should find obligation from namespace 2") +} + +// Test_GetObligation_ByIdAndFqn_ReturnSameResult validates that getObligation works correctly +// with both ID and FQN lookups +func (s *ObligationsSuite) Test_GetObligation_ByIdAndFqn_ReturnSameResult() { + namespaceID, namespaceFQN, _ := s.getNamespaceData(nsExampleCom) + createdObl := s.createObligation(namespaceID, oblName+"-dual-lookup-test", oblVals) + + defer s.deleteObligations([]string{createdObl.GetId()}) + + // Get by ID + oblByID, err := s.db.PolicyClient.GetObligation(s.ctx, &obligations.GetObligationRequest{ + Id: createdObl.GetId(), + }) + s.Require().NoError(err) + s.NotNil(oblByID) + + // Get by FQN + oblByFQN, err := s.db.PolicyClient.GetObligation(s.ctx, &obligations.GetObligationRequest{ + Fqn: namespaceFQN + "/obl/" + oblName + "-dual-lookup-test", + }) + s.Require().NoError(err) + s.NotNil(oblByFQN) + + // Verify both return the same obligation + s.True(proto.Equal(oblByID, oblByFQN)) +} + +// Sort by Name + +func (s *ObligationsSuite) Test_ListObligations_SortByName_ASC() { + ids := s.createSortTestObligations([]string{"aaa-sort", "bbb-sort", "ccc-sort"}) + defer s.deleteObligations(ids) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // aaa < bbb < ccc in ASC order + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *ObligationsSuite) Test_ListObligations_SortByName_DESC() { + ids := s.createSortTestObligations([]string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) + defer s.deleteObligations(ids) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // ccc > bbb > aaa in DESC order + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[2], ids[1], ids[0]) +} + +// Sort by FQN + +func (s *ObligationsSuite) Test_ListObligations_SortByFqn_ASC() { + // Create obligations across two namespaces to prove FQN sort uses the full + // constructed FQN (namespace_fqn/obl/name), not just the name. + // "example.com" < "example.net" lexicographically, so even zzz in example.com + // sorts before aaa in example.net. Within example.com, name breaks the tie. + comID, _, _ := s.getNamespaceData(nsExampleCom) + netID, _, _ := s.getNamespaceData(nsExampleNet) + suffix := fmt.Sprintf("fqnasc-%d", time.Now().UnixNano()) + + oblComAAA := s.createObligation(comID, "aaa-"+suffix, nil) + oblComZZZ := s.createObligation(comID, "zzz-"+suffix, nil) + oblNetAAA := s.createObligation(netID, "aaa-"+suffix, nil) + defer s.deleteObligations([]string{oblComAAA.GetId(), oblComZZZ.GetId(), oblNetAAA.GetId()}) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // example.com/obl/aaa < example.com/obl/zzz < example.net/obl/aaa + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, oblComAAA.GetId(), oblComZZZ.GetId(), oblNetAAA.GetId()) +} + +func (s *ObligationsSuite) Test_ListObligations_SortByFqn_DESC() { + comID, _, _ := s.getNamespaceData(nsExampleCom) + netID, _, _ := s.getNamespaceData(nsExampleNet) + suffix := fmt.Sprintf("fqndesc-%d", time.Now().UnixNano()) + + oblComAAA := s.createObligation(comID, "aaa-"+suffix, nil) + oblComZZZ := s.createObligation(comID, "zzz-"+suffix, nil) + oblNetAAA := s.createObligation(netID, "aaa-"+suffix, nil) + defer s.deleteObligations([]string{oblComAAA.GetId(), oblComZZZ.GetId(), oblNetAAA.GetId()}) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // example.net/obl/aaa > example.com/obl/zzz > example.com/obl/aaa + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, oblNetAAA.GetId(), oblComZZZ.GetId(), oblComAAA.GetId()) +} + +// Sort by CreatedAt + +func (s *ObligationsSuite) Test_ListObligations_SortByCreatedAt_ASC() { + ids := s.createSortTestObligations([]string{"createdasc-obl-0", "createdasc-obl-1", "createdasc-obl-2"}) + defer s.deleteObligations(ids) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // oldest first in ASC order + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *ObligationsSuite) Test_ListObligations_SortByCreatedAt_DESC() { + ids := s.createSortTestObligations([]string{"createddesc-obl-0", "createddesc-obl-1", "createddesc-obl-2"}) + defer s.deleteObligations(ids) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // newest first in DESC order + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[2], ids[1], ids[0]) +} + +// Sort by UpdatedAt + +func (s *ObligationsSuite) Test_ListObligations_SortByUpdatedAt_DESC() { + ids := s.createSortTestObligations([]string{"upd-sort-obl-0", "upd-sort-obl-1", "upd-sort-obl-2"}) + defer s.deleteObligations(ids) + + // Update the first obligation so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateObligation(s.ctx, &obligations.UpdateObligationRequest{ + Id: ids[0], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated obligation (ids[0]) should appear before the others + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[0], ids[2], ids[1]) +} + +func (s *ObligationsSuite) Test_ListObligations_SortByUpdatedAt_ASC() { + ids := s.createSortTestObligations([]string{"upd-sort-asc-obl-0", "upd-sort-asc-obl-1", "upd-sort-asc-obl-2"}) + defer s.deleteObligations(ids) + + // Update the last obligation so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateObligation(s.ctx, &obligations.UpdateObligationRequest{ + Id: ids[2], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated obligation (ids[2]) should appear last in ASC order + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[0], ids[1], ids[2]) +} + +// Sort by Unspecified (fallback to default) + +func (s *ObligationsSuite) Test_ListObligations_SortTieBreaker_CreatedAtWithIDFallback() { + namespaceID, _, _ := s.getNamespaceData(nsExampleCom) + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-obl-%d-%d", i, suffix) + obl := s.createObligation(namespaceID, name, nil) + ids[i] = obl.GetId() + } + defer s.deleteObligations(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "obligation_definitions", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, sorted[0], sorted[1], sorted[2]) +} + +func (s *ObligationsSuite) Test_ListObligations_SortByUnspecifiedField_DefaultsToCreatedAt() { + ids := s.createSortTestObligations([]string{"unspecified-field-obl-0", "unspecified-field-obl-1", "unspecified-field-obl-2"}) + defer s.deleteObligations(ids) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Field defaults to created_at, explicit ASC is preserved + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *ObligationsSuite) Test_ListObligations_SortByUnspecifiedDirection_DefaultsToDESC() { + ids := s.createSortTestObligations([]string{"unspecified-dir-obl-0", "unspecified-dir-obl-1", "unspecified-dir-obl-2"}) + defer s.deleteObligations(ids) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Direction defaults to DESC, explicit created_at field is preserved + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *ObligationsSuite) Test_ListObligations_SortByBothUnspecified_DefaultsToCreatedAtDESC() { + ids := s.createSortTestObligations([]string{"both-unspecified-obl-0", "both-unspecified-obl-1", "both-unspecified-obl-2"}) + defer s.deleteObligations(ids) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Both default: created_at DESC + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *ObligationsSuite) Test_ListObligations_SortOmitted() { + ids := s.createSortTestObligations([]string{"sort-omitted-obl-0", "sort-omitted-obl-1", "sort-omitted-obl-2"}) + defer s.deleteObligations(ids) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{}) + s.Require().NoError(err) + s.NotNil(listRsp) + + // No sort provided: created_at DESC + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, ids[2], ids[1], ids[0]) +} + // Helper functions for common operations func (s *ObligationsSuite) getNamespaceData(nsName string) (string, string, fixtures.FixtureDataNamespace) { @@ -1641,7 +2035,11 @@ func (s *ObligationsSuite) assertObligationValueBasics(oblValue *policy.Obligati func (s *ObligationsSuite) setupTriggerTests() *TriggerSetup { namespaceID, _, namespace := s.getNamespaceData(nsExampleCom) createdObl := s.createObligation(namespaceID, oblName, nil) - triggerAction := s.f.GetStandardAction("read") + triggerAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: fmt.Sprintf("trigger-action-%d", time.Now().UnixNano()), + NamespaceId: namespaceID, + }) + s.Require().NoError(err) triggerAttributeValue := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1") triggerAttributeValue2 := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value2") @@ -1726,7 +2124,14 @@ func (s *ObligationsSuite) createObligationValueWithTriggers(obligationID string triggers = customTriggers } else { // Default triggers for backward compatibility - triggerAction := s.f.GetStandardAction("read") + obl, err := s.db.PolicyClient.GetObligation(s.ctx, &obligations.GetObligationRequest{Id: obligationID}) + s.Require().NoError(err) + + triggerAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: fmt.Sprintf("trigger-action-%d", time.Now().UnixNano()), + NamespaceId: obl.GetNamespace().GetId(), + }) + s.Require().NoError(err) triggerAttributeValue := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1") triggerAttributeValue2 := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value2") @@ -1824,62 +2229,20 @@ func (s *ObligationsSuite) assertObligationValuesSpecificTriggers(obl *policy.Ob } } -// Test_ListObligations_EmptyNamespaceId_ReturnsAll validates that empty string parameters -// are properly treated as NULL -func (s *ObligationsSuite) Test_ListObligations_EmptyNamespaceId_ReturnsAll() { - // Create obligations in different namespaces - namespaceID1, _, _ := s.getNamespaceData(nsExampleCom) - namespaceID2, _, _ := s.getNamespaceData(nsExampleNet) +// Sort test helpers - obl1 := s.createObligation(namespaceID1, oblName+"-empty-test-1", nil) - obl2 := s.createObligation(namespaceID2, oblName+"-empty-test-2", nil) - - defer s.deleteObligations([]string{obl1.GetId(), obl2.GetId()}) - - // List with empty namespace_id should return all obligations - allObligations, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ - NamespaceId: "", // Empty string should be treated as NULL - }) - s.Require().NoError(err) - s.NotNil(allObligations) - s.GreaterOrEqual(len(allObligations), 2, "Should return at least our two test obligations") - - // Verify our test obligations are in the results - found1, found2 := false, false - for _, obl := range allObligations { - if obl.GetId() == obl1.GetId() { - found1 = true - } - if obl.GetId() == obl2.GetId() { - found2 = true +// createSortTestObligations creates obligations with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the obligation IDs in creation order. +func (s *ObligationsSuite) createSortTestObligations(prefixes []string) []string { + namespaceID, _, _ := s.getNamespaceData(nsExampleCom) + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) } + name := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) + obl := s.createObligation(namespaceID, name, nil) + ids[i] = obl.GetId() } - s.True(found1, "Should find obligation from namespace 1") - s.True(found2, "Should find obligation from namespace 2") -} - -// Test_GetObligation_ByIdAndFqn_ReturnSameResult validates that getObligation works correctly -// with both ID and FQN lookups -func (s *ObligationsSuite) Test_GetObligation_ByIdAndFqn_ReturnSameResult() { - namespaceID, namespaceFQN, _ := s.getNamespaceData(nsExampleCom) - createdObl := s.createObligation(namespaceID, oblName+"-dual-lookup-test", oblVals) - - defer s.deleteObligations([]string{createdObl.GetId()}) - - // Get by ID - oblByID, err := s.db.PolicyClient.GetObligation(s.ctx, &obligations.GetObligationRequest{ - Id: createdObl.GetId(), - }) - s.Require().NoError(err) - s.NotNil(oblByID) - - // Get by FQN - oblByFQN, err := s.db.PolicyClient.GetObligation(s.ctx, &obligations.GetObligationRequest{ - Fqn: namespaceFQN + "/obl/" + oblName + "-dual-lookup-test", - }) - s.Require().NoError(err) - s.NotNil(oblByFQN) - - // Verify both return the same obligation - s.True(proto.Equal(oblByID, oblByFQN)) + return ids } diff --git a/service/integration/registered_resources_test.go b/service/integration/registered_resources_test.go index d4bf030805..7165231274 100644 --- a/service/integration/registered_resources_test.go +++ b/service/integration/registered_resources_test.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "log/slog" + "slices" "strings" "testing" + "time" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" @@ -60,17 +62,21 @@ const invalidID = "00000000-0000-0000-0000-000000000000" func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_Succeeds() { req := ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_create_res", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_create_res", } created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) s.Require().NoError(err) s.NotNil(created) + s.NotNil(created.GetNamespace()) + s.Equal(s.getNamespaceID("example.com"), created.GetNamespace().GetId()) } func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_NormalizedName_Succeeds() { req := ®isteredresources.CreateRegisteredResourceRequest{ - Name: "TeST_CrEaTe_RES_NorMa-LiZeD", + NamespaceId: s.getNamespaceID("example.com"), + Name: "TeST_CrEaTe_RES_NorMa-LiZeD", } created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) @@ -85,8 +91,9 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithValues_Succ "test_create_res_values__value2", } req := ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_create_res_values", - Values: values, + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_create_res_values", + Values: values, } created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) @@ -100,7 +107,8 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithValues_Succ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithMetadata_Succeeds() { req := ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_create_res_metadata", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_create_res_metadata", Metadata: &common.MetadataMutable{ Labels: map[string]string{ "key1": "value1", @@ -115,16 +123,27 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithMetadata_Su s.Require().Len(created.GetMetadata().GetLabels(), 2) } -func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithNonUniqueName_Fails() { - existing := s.f.GetRegisteredResourceKey("res_with_values") +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithNonUniqueName_SameNamespace_Fails() { + // Create a resource in a namespace first + nsID := s.getNamespaceID("example.com") + name := "test_unique_ns_res" req := ®isteredresources.CreateRegisteredResourceRequest{ - Name: existing.Name, + NamespaceId: nsID, + Name: name, } - created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().NoError(err) + s.NotNil(created) + + // Try to create another with the same name in the same namespace + req2 := ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + } + dup, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req2) s.Require().Error(err) s.Require().ErrorIs(err, db.ErrUniqueConstraintViolation) - s.Nil(created) + s.Nil(dup) } // Get @@ -246,10 +265,37 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_NoPagination_Suc s.Equal(2, foundCount) } +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + create := func(i int) string { + name := fmt.Sprintf("order-test-resource-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: s.getNamespaceID("example.com"), + Name: name, + }) + s.Require().NoError(err) + s.Require().NotNil(created) + return created.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{}) + s.Require().NoError(err) + s.NotNil(list) + + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, thirdID, secondID, firstID) +} + func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_RegResValuesContainActionAttributeValues() { // Create a registered resource with values that have action attribute values newRegRes, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_list_reg_res_with_action_attr_values", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_list_reg_res_with_action_attr_values", }) s.Require().NoError(err) s.NotNil(newRegRes) @@ -295,35 +341,37 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_RegResValuesCont s.NotNil(list) foundRegRes := false - foundVal1 := false - foundVal2 := false + var foundVal1, foundVal2 bool for _, r := range list.GetResources() { - if r.GetId() == regResID { - s.Equal("test_list_reg_res_with_action_attr_values", r.GetName()) - values := r.GetValues() - s.Require().Len(values, 2) - foundRegRes = true - - // Check if action attribute values are present in the values - for _, v := range values { - if v.GetId() == val1.GetId() { - foundVal1 = true - actionAttrValues := v.GetActionAttributeValues() - s.Require().NotEmpty(actionAttrValues) - for _, aav := range actionAttrValues { - s.NotNil(aav.GetAction()) - s.NotNil(aav.GetAttributeValue()) - } - } - if v.GetId() == val2.GetId() { - foundVal2 = true - actionAttrValues := v.GetActionAttributeValues() - s.Require().NotEmpty(actionAttrValues) - for _, aav := range actionAttrValues { - s.NotNil(aav.GetAction()) - s.NotNil(aav.GetAttributeValue()) - } - } + if r.GetId() != regResID { + continue + } + s.Equal("test_list_reg_res_with_action_attr_values", r.GetName()) + values := r.GetValues() + s.Require().Len(values, 2) + foundRegRes = true + + for _, v := range values { + actionAttrValues := v.GetActionAttributeValues() + s.Require().Len(actionAttrValues, 1) + aav := actionAttrValues[0] + s.Require().NotNil(aav.GetAction()) + s.Require().NotNil(aav.GetAttributeValue()) + s.NotNil(aav.GetAction().GetNamespace(), "action namespace should be populated for namespaced RR") + s.Equal("example.com", aav.GetAction().GetNamespace().GetName()) + s.Equal("https://example.com", aav.GetAction().GetNamespace().GetFqn()) + + switch v.GetId() { + case val1.GetId(): + foundVal1 = true + s.Equal(actions.ActionNameCreate, aav.GetAction().GetName()) + s.Equal("https://example.com/attr/attr1/value/value1", aav.GetAttributeValue().GetFqn()) + case val2.GetId(): + foundVal2 = true + s.Equal(actions.ActionNameUpdate, aav.GetAction().GetName()) + s.Equal("https://example.com/attr/attr2/value/value2", aav.GetAttributeValue().GetFqn()) + default: + s.FailNow("unexpected value found", "value id: %s", v.GetId()) } } } @@ -332,6 +380,91 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_RegResValuesCont s.True(foundVal2, "Value 2 not found in registered resource values") } +func (s *RegisteredResourcesSuite) Test_GetRegisteredResource_RegResValuesContainActionAttributeValues() { + // Create a registered resource with values that have action attribute values + newRegRes, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_get_reg_res_with_action_attr_values", + }) + s.Require().NoError(err) + s.NotNil(newRegRes) + regResID := newRegRes.GetId() + + val1, err := s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: regResID, + Value: "test_value_1", + ActionAttributeValues: []*registeredresources.ActionAttributeValue{ + { + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: actions.ActionNameCreate, + }, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: "https://example.com/attr/attr1/value/value1", + }, + }, + }, + }) + s.Require().NoError(err) + s.NotNil(val1) + + val2, err := s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: regResID, + Value: "test_value_2", + ActionAttributeValues: []*registeredresources.ActionAttributeValue{ + { + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: actions.ActionNameUpdate, + }, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: "https://example.com/attr/attr2/value/value2", + }, + }, + }, + }) + s.Require().NoError(err) + s.NotNil(val2) + + // Get the registered resource and check if values contain action attribute values + got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Id{ + Id: regResID, + }, + }) + s.Require().NoError(err) + s.NotNil(got) + s.Equal("test_get_reg_res_with_action_attr_values", got.GetName()) + + values := got.GetValues() + s.Require().Len(values, 2) + + var foundVal1, foundVal2 bool + for _, v := range values { + actionAttrValues := v.GetActionAttributeValues() + s.Require().Len(actionAttrValues, 1) + aav := actionAttrValues[0] + s.Require().NotNil(aav.GetAction()) + s.Require().NotNil(aav.GetAttributeValue()) + s.NotNil(aav.GetAction().GetNamespace(), "action namespace should be populated for namespaced RR") + s.Equal("example.com", aav.GetAction().GetNamespace().GetName()) + s.Equal("https://example.com", aav.GetAction().GetNamespace().GetFqn()) + + switch v.GetId() { + case val1.GetId(): + foundVal1 = true + s.Equal(actions.ActionNameCreate, aav.GetAction().GetName()) + s.Equal("https://example.com/attr/attr1/value/value1", aav.GetAttributeValue().GetFqn()) + case val2.GetId(): + foundVal2 = true + s.Equal(actions.ActionNameUpdate, aav.GetAction().GetName()) + s.Equal("https://example.com/attr/attr2/value/value2", aav.GetAttributeValue().GetFqn()) + default: + s.FailNow("unexpected value found", "value id: %s", v.GetId()) + } + } + s.True(foundVal1, "Value 1 not found in registered resource values") + s.True(foundVal2, "Value 2 not found in registered resource values") +} + func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_Limit_Succeeds() { var limit int32 = 1 list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ @@ -424,7 +557,8 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResource_Succeeds() { } created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_update_res", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_update_res", Metadata: &common.MetadataMutable{ Labels: labels, }, @@ -482,7 +616,8 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResource_Succeeds() { func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResource_NormalizedName_Succeeds() { created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_update_res_normalized", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_update_res_normalized", }) s.Require().NoError(err) s.NotNil(created) @@ -514,17 +649,26 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResource_InvalidID_Fails s.Nil(updated) } -func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResource_NonUniqueName_Fails() { - created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_update_res_non_unique", +func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResource_NonUniqueName_SameNamespace_Fails() { + nsID := s.getNamespaceID("example.com") + created1, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: "test_update_res_non_unique_a", }) s.Require().NoError(err) - s.NotNil(created) + s.NotNil(created1) - existingRes := s.f.GetRegisteredResourceKey("res_only") + created2, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: "test_update_res_non_unique_b", + }) + s.Require().NoError(err) + s.NotNil(created2) + + // Try to rename created2 to created1's name in the same namespace updated, err := s.db.PolicyClient.UpdateRegisteredResource(s.ctx, ®isteredresources.UpdateRegisteredResourceRequest{ - Id: created.GetId(), - Name: existingRes.Name, + Id: created2.GetId(), + Name: created1.GetName(), }) s.Require().Error(err) s.Require().ErrorIs(err, db.ErrUniqueConstraintViolation) @@ -535,7 +679,8 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResource_NonUniqueName_F func (s *RegisteredResourcesSuite) Test_DeleteRegisteredResource_Succeeds() { created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_delete_res", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_delete_res", Values: []string{ "test_delete_value1", "test_delete_value2", @@ -594,7 +739,8 @@ func (s *RegisteredResourcesSuite) Test_DeleteRegisteredResource_WithInvalidID_F func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_Succeeds() { res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_create_res_value", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_create_res_value", }) s.Require().NoError(err) s.NotNil(res) @@ -607,11 +753,13 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_Succeeds() created, err := s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, req) s.Require().NoError(err) s.NotNil(created) + s.Equal("https://example.com/reg_res/test_create_res_value/value/value", created.GetFqn()) } func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_NormalizedName_Succeeds() { res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_create_res_value_normalized", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_create_res_value_normalized", }) s.Require().NoError(err) s.NotNil(res) @@ -629,7 +777,8 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_Normalized func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithMetadata_Succeeds() { res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_create_res_value_metadata", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_create_res_value_metadata", }) s.Require().NoError(err) s.NotNil(res) @@ -653,7 +802,8 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithMetada func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_With_ActionAttributeValues_Succeeds() { res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_create_res_value_action_attr_values", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_create_res_value_action_attr_values", }) s.Require().NoError(err) s.NotNil(res) @@ -736,7 +886,8 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithNonUni func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithInvalidActionAttributeValues_Fails() { res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_create_res_value_invalid_action_attr_values", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_create_res_value_invalid_action_attr_values", }) s.Require().NoError(err) s.NotNil(res) @@ -758,21 +909,7 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithInvali }, }, }, - err: db.ErrForeignKeyViolation, - }, - { - name: "Invalid Action Name", - actionAttrValues: []*registeredresources.ActionAttributeValue{ - { - ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionName{ - ActionName: "invalid_action_name", - }, - AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueFqn{ - AttributeValueFqn: "https://example.com/attr/attr1/value/value1", - }, - }, - }, - err: db.ErrNotFound, + err: db.ErrMissingValue, }, { name: "Invalid Attribute Value ID", @@ -786,7 +923,7 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithInvali }, }, }, - err: db.ErrForeignKeyViolation, + err: db.ErrNotFound, }, { name: "Invalid Attribute Value FQN", @@ -823,6 +960,7 @@ func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithInvali // Get func (s *RegisteredResourcesSuite) Test_GetRegisteredResourceValue_Valid_Succeeds() { + // Fixture registered resources are legacy rows without namespace_id, so the FQN case uses the legacy https://reg_res/... format. existingRes := s.f.GetRegisteredResourceKey("res_with_values") existingResValue1 := s.f.GetRegisteredResourceValueKey("res_with_values__value1") @@ -918,6 +1056,7 @@ func (s *RegisteredResourcesSuite) Test_GetRegisteredResourceValue_Invalid_Fails // Get By FQNs func (s *RegisteredResourcesSuite) TestGetRegisteredResourceValuesByFQNs_Valid_Succeeds() { + // Fixture registered resources are legacy rows without namespace_id, so these FQNs use the legacy https://reg_res/... format. existingRes := s.f.GetRegisteredResourceKey("res_with_values") existingResValue1 := s.f.GetRegisteredResourceValueKey("res_with_values__value1") existingResValue2 := s.f.GetRegisteredResourceValueKey("res_with_values__value2") @@ -948,6 +1087,7 @@ func (s *RegisteredResourcesSuite) TestGetRegisteredResourceValuesByFQNs_Valid_S } func (s *RegisteredResourcesSuite) TestGetRegisteredResourceValuesByFQNs_SomeInvalid_Fails() { + // Fixture registered resources are legacy rows without namespace_id, so the valid FQN uses the legacy https://reg_res/... format. existingRes := s.f.GetRegisteredResourceKey("res_with_values") existingResValue1 := s.f.GetRegisteredResourceValueKey("res_with_values__value1") fqns := []string{ @@ -1017,6 +1157,41 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResourceValues_NoPaginatio s.Equal(2, foundCount) } +func (s *RegisteredResourcesSuite) Test_ListRegisteredResourceValues_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + resource, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: s.getNamespaceID("example.com"), + Name: fmt.Sprintf("order-test-res-%d", suffix), + }) + s.Require().NoError(err) + s.Require().NotNil(resource) + + create := func(i int) string { + val := fmt.Sprintf("order-test-val-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: resource.GetId(), + Value: val, + }) + s.Require().NoError(err) + s.Require().NotNil(created) + return created.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + list, err := s.db.PolicyClient.ListRegisteredResourceValues(s.ctx, ®isteredresources.ListRegisteredResourceValuesRequest{ + ResourceId: resource.GetId(), + }) + s.Require().NoError(err) + s.NotNil(list) + + assertIDsInOrder(s.T(), list.GetValues(), func(v *policy.RegisteredResourceValue) string { return v.GetId() }, thirdID, secondID, firstID) +} + func (s *RegisteredResourcesSuite) Test_ListRegisteredResourceValues_Limit_Succeeds() { var limit int32 = 1 list, err := s.db.PolicyClient.ListRegisteredResourceValues(s.ctx, ®isteredresources.ListRegisteredResourceValuesRequest{ @@ -1087,6 +1262,7 @@ func (s *AttributesSuite) Test_ListRegisteredResourceValues_Offset_Succeeds() { } func (s *RegisteredResourcesSuite) Test_ListRegisteredResourceValues_ByResourceID_Succeeds() { + // Fixture registered resources are legacy rows without namespace_id, so listed values should expose legacy https://reg_res/... FQNs. existingRes := s.f.GetRegisteredResourceKey("res_with_values") existingResValue1 := s.f.GetRegisteredResourceValueKey("res_with_values__value1") existingResValue2 := s.f.GetRegisteredResourceValueKey("res_with_values__value2") @@ -1100,10 +1276,15 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResourceValues_ByResourceI s.Len(list.GetValues(), 2) foundCount := 0 + expectedFQNs := map[string]string{ + existingResValue1.ID: fmt.Sprintf("https://reg_res/%s/value/%s", existingRes.Name, existingResValue1.Value), + existingResValue2.ID: fmt.Sprintf("https://reg_res/%s/value/%s", existingRes.Name, existingResValue2.Value), + } for _, r := range list.GetValues() { if r.GetId() == existingResValue1.ID || r.GetId() == existingResValue2.ID { foundCount++ + s.Equal(expectedFQNs[r.GetId()], r.GetFqn()) } } @@ -1133,7 +1314,8 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_Succeeds() } res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_update_res_value", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_update_res_value", }) s.Require().NoError(err) s.NotNil(res) @@ -1157,6 +1339,7 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_Succeeds() }) s.Require().NoError(err) s.NotNil(created) + s.Equal("https://example.com/reg_res/test_update_res_value/value/value", created.GetFqn()) // update with no changes updated, err := s.db.PolicyClient.UpdateRegisteredResourceValue(s.ctx, ®isteredresources.UpdateRegisteredResourceValueRequest{ @@ -1164,6 +1347,7 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_Succeeds() }) s.Require().NoError(err) s.NotNil(updated) + s.Equal(created.GetFqn(), updated.GetFqn()) // verify resource value not updated got, err := s.db.PolicyClient.GetRegisteredResourceValue(s.ctx, ®isteredresources.GetRegisteredResourceValueRequest{ @@ -1174,6 +1358,7 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_Succeeds() s.Require().NoError(err) s.Require().NotNil(got) s.Equal(created.GetValue(), got.GetValue()) + s.Equal(created.GetFqn(), got.GetFqn()) s.Equal(labels, got.GetMetadata().GetLabels()) s.Require().Len(got.GetActionAttributeValues(), 1) @@ -1206,6 +1391,7 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_Succeeds() }) s.Require().NoError(err) s.NotNil(updated) + s.Equal("https://example.com/reg_res/test_update_res_value/value/updated_value", updated.GetFqn()) // verify resource updated got, err = s.db.PolicyClient.GetRegisteredResourceValue(s.ctx, ®isteredresources.GetRegisteredResourceValueRequest{ @@ -1216,6 +1402,7 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_Succeeds() s.Require().NoError(err) s.NotNil(got) s.Equal("updated_value", got.GetValue()) + s.Equal(updated.GetFqn(), got.GetFqn()) s.Equal(expectedLabels, got.GetMetadata().GetLabels()) metadata := got.GetMetadata() createdAt := metadata.GetCreatedAt() @@ -1237,7 +1424,8 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_Succeeds() func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_NormalizedName_Succeeds() { res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_update_res_value_normalized", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_update_res_value_normalized", }) s.Require().NoError(err) s.NotNil(res) @@ -1278,7 +1466,8 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_InvalidID_ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_NonUniqueResourceAndValue_Fails() { res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_update_res_value_non_unique", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_update_res_value_non_unique", }) s.Require().NoError(err) s.NotNil(res) @@ -1311,7 +1500,8 @@ func (s *RegisteredResourcesSuite) Test_UpdateRegisteredResourceValue_NonUniqueR func (s *RegisteredResourcesSuite) Test_DeleteRegisteredResourceValue_Succeeds() { res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_delete_res_value", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_delete_res_value", }) s.Require().NoError(err) s.NotNil(res) @@ -1336,6 +1526,7 @@ func (s *RegisteredResourcesSuite) Test_DeleteRegisteredResourceValue_Succeeds() deleted, err := s.db.PolicyClient.DeleteRegisteredResourceValue(s.ctx, created.GetId()) s.Require().NoError(err) s.Require().Equal(created.GetId(), deleted.GetId()) + s.Equal(created.GetFqn(), deleted.GetFqn()) // verify resource value deleted @@ -1377,12 +1568,14 @@ func (s *RegisteredResourcesSuite) Test_DeleteAction_CascadeDeleteActionAttribut // create action and resource value with action attribute values action, err := s.db.PolicyClient.CreateAction(s.ctx, &pbActions.CreateActionRequest{ - Name: "test_delete_action", + Name: "test_delete_action", + NamespaceId: s.getNamespaceID("example.com"), }) s.Require().NoError(err) res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_delete_action_res", + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_delete_action_res", }) s.Require().NoError(err) s.NotNil(res) @@ -1447,7 +1640,8 @@ func (s *RegisteredResourcesSuite) Test_DeleteAttributeValue_CascadeDeleteAction s.Require().NotNil(attrVal) res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - Name: "test_delete_attr_value_res", + NamespaceId: ns.GetId(), + Name: "test_delete_attr_value_res", }) s.Require().NoError(err) s.NotNil(res) @@ -1489,3 +1683,918 @@ func (s *RegisteredResourcesSuite) Test_DeleteAttributeValue_CascadeDeleteAction s.NotNil(resVal) s.Empty(resVal.GetActionAttributeValues()) } + +/// +/// Namespace-scoped Registered Resources +/// + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithNamespaceFQN_Succeeds() { + nsFQN := s.getNamespaceFQN("example.com") + req := ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceFqn: nsFQN, + Name: "test_create_res_ns_fqn", + } + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().NoError(err) + s.NotNil(created) + s.NotNil(created.GetNamespace()) + s.Equal(s.getNamespaceID("example.com"), created.GetNamespace().GetId()) + s.Equal("example.com", created.GetNamespace().GetName()) + s.Equal(nsFQN, created.GetNamespace().GetFqn()) +} + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_SameNameDifferentNamespaces_Succeeds() { + name := "test_same_name_diff_ns" + nsID1 := s.getNamespaceID("example.com") + nsID2 := s.getNamespaceID("example.net") + + created1, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID1, + Name: name, + }) + s.Require().NoError(err) + s.NotNil(created1) + s.Equal(nsID1, created1.GetNamespace().GetId()) + + created2, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID2, + Name: name, + }) + s.Require().NoError(err) + s.NotNil(created2) + s.Equal(nsID2, created2.GetNamespace().GetId()) + + // Both should exist with different IDs + s.NotEqual(created1.GetId(), created2.GetId()) +} + +func (s *RegisteredResourcesSuite) Test_GetRegisteredResource_ByNameWithNamespaceFQN_Succeeds() { + nsID := s.getNamespaceID("example.com") + nsFQN := s.getNamespaceFQN("example.com") + name := "test_get_by_name_ns" + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + }) + s.Require().NoError(err) + s.NotNil(created) + + got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: name, + }, + NamespaceFqn: nsFQN, + }) + s.Require().NoError(err) + s.NotNil(got) + s.Equal(created.GetId(), got.GetId()) + s.Equal(name, got.GetName()) + s.NotNil(got.GetNamespace()) + s.Equal(nsID, got.GetNamespace().GetId()) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_FilterByNamespaceID_Succeeds() { + nsID := s.getNamespaceID("example.net") + name := "test_list_ns_filter" + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + }) + s.Require().NoError(err) + s.NotNil(created) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + NamespaceId: nsID, + }) + s.Require().NoError(err) + s.NotNil(list) + + // Should find at least the one we just created + found := false + for _, r := range list.GetResources() { + s.Equal(nsID, r.GetNamespace().GetId(), "all listed resources should belong to the filtered namespace") + if r.GetId() == created.GetId() { + found = true + } + } + s.True(found, "created resource should be in the filtered list") +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_FilterByNamespaceFQN_Succeeds() { + nsID := s.getNamespaceID("example.net") + nsFQN := s.getNamespaceFQN("example.net") + name := "test_list_ns_fqn_filter" + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + }) + s.Require().NoError(err) + s.NotNil(created) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + NamespaceFqn: nsFQN, + }) + s.Require().NoError(err) + s.NotNil(list) + + found := false + for _, r := range list.GetResources() { + s.Equal(nsID, r.GetNamespace().GetId(), "all listed resources should belong to the filtered namespace") + if r.GetId() == created.GetId() { + found = true + } + } + s.True(found, "created resource should be in the filtered list") +} + +func (s *RegisteredResourcesSuite) Test_GetRegisteredResourceValue_NamespacedFQN_Succeeds() { + nsID := s.getNamespaceID("example.com") + name := "test_get_rrv_ns_fqn" + valueName := "test-value" + + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + Values: []string{valueName}, + }) + s.Require().NoError(err) + s.NotNil(res) + + // Get by namespaced FQN + fqn := fmt.Sprintf("https://example.com/reg_res/%s/value/%s", name, valueName) + gotByFQN, err := s.db.PolicyClient.GetRegisteredResourceValue(s.ctx, ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_Fqn{ + Fqn: fqn, + }, + }) + s.Require().NoError(err) + s.NotNil(gotByFQN) + s.Equal(valueName, gotByFQN.GetValue()) + s.Equal(fqn, gotByFQN.GetFqn()) + s.NotNil(gotByFQN.GetResource()) + s.NotNil(gotByFQN.GetResource().GetNamespace()) + s.Equal(nsID, gotByFQN.GetResource().GetNamespace().GetId()) + + gotByID, err := s.db.PolicyClient.GetRegisteredResourceValue(s.ctx, ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_Id{ + Id: gotByFQN.GetId(), + }, + }) + s.Require().NoError(err) + s.Equal(gotByFQN.GetFqn(), gotByID.GetFqn()) +} + +func (s *RegisteredResourcesSuite) Test_GetRegisteredResourceValuesByFQNs_NamespacedFormat_Succeeds() { + nsID := s.getNamespaceID("example.com") + name := "test_get_rrvs_ns_fqns" + val1 := "value1" + val2 := "value2" + + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + Values: []string{val1, val2}, + }) + s.Require().NoError(err) + s.NotNil(res) + + fqn1 := fmt.Sprintf("https://example.com/reg_res/%s/value/%s", name, val1) + fqn2 := fmt.Sprintf("https://example.com/reg_res/%s/value/%s", name, val2) + + fqnMap, err := s.db.PolicyClient.GetRegisteredResourceValuesByFQNs(s.ctx, ®isteredresources.GetRegisteredResourceValuesByFQNsRequest{ + Fqns: []string{fqn1, fqn2}, + }) + s.Require().NoError(err) + s.Require().Len(fqnMap, 2) + s.NotNil(fqnMap[fqn1]) + s.NotNil(fqnMap[fqn2]) + s.Equal(val1, fqnMap[fqn1].GetValue()) + s.Equal(val2, fqnMap[fqn2].GetValue()) + s.Equal(fqn1, fqnMap[fqn1].GetFqn()) + s.Equal(fqn2, fqnMap[fqn2].GetFqn()) +} + +func (s *RegisteredResourcesSuite) Test_RegisteredResource_NamespaceInResponses_Succeeds() { + nsID := s.getNamespaceID("example.com") + nsFQN := s.getNamespaceFQN("example.com") + name := "test_ns_in_responses" + + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + Values: []string{"resp-val"}, + }) + s.Require().NoError(err) + s.NotNil(res) + + // Verify namespace in Create response + s.NotNil(res.GetNamespace()) + s.Equal(nsID, res.GetNamespace().GetId()) + s.Equal("example.com", res.GetNamespace().GetName()) + s.Equal(nsFQN, res.GetNamespace().GetFqn()) + s.Require().Len(res.GetValues(), 1) + s.Equal("https://example.com/reg_res/test_ns_in_responses/value/resp-val", res.GetValues()[0].GetFqn()) + + // Verify namespace in Get response + got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Id{ + Id: res.GetId(), + }, + }) + s.Require().NoError(err) + s.NotNil(got.GetNamespace()) + s.Equal(nsID, got.GetNamespace().GetId()) + s.Require().Len(got.GetValues(), 1) + s.Equal(res.GetValues()[0].GetFqn(), got.GetValues()[0].GetFqn()) + + // Verify namespace in List response + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + NamespaceId: nsID, + }) + s.Require().NoError(err) + found := false + for _, r := range list.GetResources() { + if r.GetId() == res.GetId() { + found = true + s.NotNil(r.GetNamespace()) + s.Equal(nsID, r.GetNamespace().GetId()) + s.Require().Len(r.GetValues(), 1) + s.Equal(res.GetValues()[0].GetFqn(), r.GetValues()[0].GetFqn()) + } + } + s.True(found) + + // Verify namespace in Value response + valResp, err := s.db.PolicyClient.GetRegisteredResourceValue(s.ctx, ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_Id{ + Id: res.GetValues()[0].GetId(), + }, + }) + s.Require().NoError(err) + s.NotNil(valResp.GetResource()) + s.NotNil(valResp.GetResource().GetNamespace()) + s.Equal(nsID, valResp.GetResource().GetNamespace().GetId()) + s.Equal("https://example.com/reg_res/test_ns_in_responses/value/resp-val", valResp.GetFqn()) +} + +func (s *RegisteredResourcesSuite) Test_LegacyRegisteredResources_NoNamespace_StillAccessible() { + // Fixture resources are legacy (no namespace) - verify they're still accessible + existingRes := s.f.GetRegisteredResourceKey("res_only") + existingResWithValues := s.f.GetRegisteredResourceKey("res_with_values") + existingResValue := s.f.GetRegisteredResourceValueKey("res_with_values__value1") + + got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Id{ + Id: existingRes.ID, + }, + }) + s.Require().NoError(err) + s.NotNil(got) + s.Equal(existingRes.Name, got.GetName()) + // Legacy resources have nil namespace + s.Nil(got.GetNamespace()) + + gotValue, err := s.db.PolicyClient.GetRegisteredResourceValue(s.ctx, ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_Id{ + Id: existingResValue.ID, + }, + }) + s.Require().NoError(err) + s.NotNil(gotValue) + s.Nil(gotValue.GetResource().GetNamespace()) + // Legacy values preserve the pre-namespace FQN shape. + s.Equal(fmt.Sprintf("https://reg_res/%s/value/%s", existingResWithValues.Name, existingResValue.Value), gotValue.GetFqn()) +} + +func (s *RegisteredResourcesSuite) Test_SameNamespaceEnforcement_DifferentNamespace_Fails() { + // Create a resource in example.com namespace + nsID := s.getNamespaceID("example.com") + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: "test_same_ns_enforcement", + Values: []string{"enforce-val"}, + }) + s.Require().NoError(err) + s.NotNil(res) + + // Create an attribute in example.net namespace + otherNsID := s.getNamespaceID("example.net") + attrName := fmt.Sprintf("test_enforce_attr_%d", time.Now().UnixNano()) + attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: otherNsID, + Name: attrName, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + Values: []string{"val1"}, + }) + s.Require().NoError(err) + s.NotNil(attr) + s.Require().NotEmpty(attr.GetValues()) + + crossNsAttrValID := attr.GetValues()[0].GetId() + + // Try to create a value with action-attribute-value from different namespace -> should fail + _, err = s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: res.GetId(), + Value: "enforce-val2", + ActionAttributeValues: []*registeredresources.ActionAttributeValue{ + { + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: actions.ActionNameRead, + }, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueId{ + AttributeValueId: crossNsAttrValID, + }, + }, + }, + }) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *RegisteredResourcesSuite) Test_SameNamespaceEnforcement_SameNamespace_Succeeds() { + // Create a resource in example.com namespace + nsID := s.getNamespaceID("example.com") + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: "test_same_ns_enforcement_ok", + Values: []string{"enforce-ok-val"}, + }) + s.Require().NoError(err) + s.NotNil(res) + + // Create an attribute in the SAME namespace (example.com) + attrName := fmt.Sprintf("test_enforce_same_attr_%d", time.Now().UnixNano()) + attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + NamespaceId: nsID, + Name: attrName, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + Values: []string{"val1"}, + }) + s.Require().NoError(err) + s.NotNil(attr) + s.Require().NotEmpty(attr.GetValues()) + + sameNsAttrValID := attr.GetValues()[0].GetId() + + // Create a value with action-attribute-value from same namespace -> should succeed + resVal, err := s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: res.GetId(), + Value: "enforce-ok-val2", + ActionAttributeValues: []*registeredresources.ActionAttributeValue{ + { + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: actions.ActionNameRead, + }, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueId{ + AttributeValueId: sameNsAttrValID, + }, + }, + }, + }) + s.Require().NoError(err) + s.NotNil(resVal) + s.Require().Len(resVal.GetActionAttributeValues(), 1) +} + +func (s *RegisteredResourcesSuite) Test_SameNamespaceEnforcement_ActionIDDifferentNamespace_Fails() { + comNsID := s.getNamespaceID("example.com") + netNsID := s.getNamespaceID("example.net") + + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: comNsID, + Name: fmt.Sprintf("test_rr_action_id_ns_mismatch_%d", time.Now().UnixNano()), + }) + s.Require().NoError(err) + s.NotNil(res) + + wrongAction, err := s.db.PolicyClient.CreateAction(s.ctx, &pbActions.CreateActionRequest{ + Name: fmt.Sprintf("rr_wrong_action_id_%d", time.Now().UnixNano()), + NamespaceId: netNsID, + }) + s.Require().NoError(err) + + _, err = s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: res.GetId(), + Value: fmt.Sprintf("rr_val_action_id_mismatch_%d", time.Now().UnixNano()), + ActionAttributeValues: []*registeredresources.ActionAttributeValue{ + { + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionId{ActionId: wrongAction.GetId()}, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: "https://example.com/attr/attr1/value/value1", + }, + }, + }, + }) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *RegisteredResourcesSuite) Test_UnnamespacedResource_NamespacedActionID_Fails() { + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + Name: fmt.Sprintf("test_rr_unnamespaced_%d", time.Now().UnixNano()), + }) + s.Require().NoError(err) + s.NotNil(res) + s.Nil(res.GetNamespace()) + + namespacedAction, err := s.db.PolicyClient.CreateAction(s.ctx, &pbActions.CreateActionRequest{ + Name: fmt.Sprintf("rr_namespaced_action_%d", time.Now().UnixNano()), + NamespaceId: s.getNamespaceID("example.com"), + }) + s.Require().NoError(err) + + _, err = s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: res.GetId(), + Value: fmt.Sprintf("rr_val_unnamespaced_%d", time.Now().UnixNano()), + ActionAttributeValues: []*registeredresources.ActionAttributeValue{ + { + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionId{ActionId: namespacedAction.GetId()}, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: "https://example.com/attr/attr1/value/value1", + }, + }, + }, + }) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithNamespacedCustomActionName_Succeeds() { + nsID := s.getNamespaceID("example.com") + + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: fmt.Sprintf("test_rr_custom_action_name_%d", time.Now().UnixNano()), + }) + s.Require().NoError(err) + s.NotNil(res) + + customActionName := fmt.Sprintf("rr_custom_action_%d", time.Now().UnixNano()) + customAction, err := s.db.PolicyClient.CreateAction(s.ctx, &pbActions.CreateActionRequest{ + Name: customActionName, + NamespaceId: nsID, + }) + s.Require().NoError(err) + s.NotNil(customAction) + + resVal, err := s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: res.GetId(), + Value: fmt.Sprintf("test_rr_custom_action_name_value_%d", time.Now().UnixNano()), + ActionAttributeValues: []*registeredresources.ActionAttributeValue{ + { + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: customActionName, + }, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: "https://example.com/attr/attr1/value/value1", + }, + }, + }, + }) + s.Require().NoError(err) + s.NotNil(resVal) + s.Require().Len(resVal.GetActionAttributeValues(), 1) + s.Equal(customAction.GetId(), resVal.GetActionAttributeValues()[0].GetAction().GetId()) + s.Equal(customActionName, resVal.GetActionAttributeValues()[0].GetAction().GetName()) +} + +// ┌─────────────────────────────────────────────────────────────────────────────┐ +// │ namespace-optional tests │ +// │ Remove this section when enforce_namespace flag is phased out │ +// └─────────────────────────────────────────────────────────────────────────────┘ + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_Succeeds() { + req := ®isteredresources.CreateRegisteredResourceRequest{ + Name: "test_create_no_ns", + } + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().NoError(err) + s.NotNil(created) + s.NotEmpty(created.GetId()) + s.Equal("test_create_no_ns", created.GetName()) + s.Nil(created.GetNamespace()) +} + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_WithValues_Succeeds() { + req := ®isteredresources.CreateRegisteredResourceRequest{ + Name: "test_create_no_ns_vals", + Values: []string{"val1", "val2"}, + } + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().NoError(err) + s.NotNil(created) + s.Nil(created.GetNamespace()) + s.Require().Len(created.GetValues(), 2) +} + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_GetByID_Succeeds() { + req := ®isteredresources.CreateRegisteredResourceRequest{ + Name: "test_no_ns_get_by_id", + } + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().NoError(err) + s.NotNil(created) + + got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Id{ + Id: created.GetId(), + }, + }) + s.Require().NoError(err) + s.NotNil(got) + s.Equal(created.GetId(), got.GetId()) + s.Nil(got.GetNamespace()) +} + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_GetByName_Succeeds() { + name := "test_no_ns_get_by_name" + req := ®isteredresources.CreateRegisteredResourceRequest{ + Name: name, + } + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().NoError(err) + s.NotNil(created) + + got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: name, + }, + }) + s.Require().NoError(err) + s.NotNil(got) + s.Equal(created.GetId(), got.GetId()) + s.Nil(got.GetNamespace()) +} + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_DuplicateName_Fails() { + name := "test_no_ns_dup" + req := ®isteredresources.CreateRegisteredResourceRequest{ + Name: name, + } + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().NoError(err) + s.NotNil(created) + + _, err = s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrUniqueConstraintViolation) +} + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_ListIncluded_Succeeds() { + name := "test_no_ns_list" + req := ®isteredresources.CreateRegisteredResourceRequest{ + Name: name, + } + + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req) + s.Require().NoError(err) + s.NotNil(created) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{}) + s.Require().NoError(err) + s.NotNil(list) + + // Also create a namespaced resource to verify both types appear in unfiltered list + namespacedRes, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: s.getNamespaceID("example.com"), + Name: "test_ns_list", + }) + s.Require().NoError(err) + s.NotNil(namespacedRes) + + list, err = s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{}) + s.Require().NoError(err) + + foundNoNS := false + foundNamespaced := false + for _, r := range list.GetResources() { + if r.GetId() == created.GetId() { + foundNoNS = true + s.Nil(r.GetNamespace()) + } + if r.GetId() == namespacedRes.GetId() { + foundNamespaced = true + s.NotNil(r.GetNamespace()) + } + } + s.True(foundNoNS, "no-namespace resource should appear in unfiltered list") + s.True(foundNamespaced, "namespaced resource should also appear in unfiltered list") +} + +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_SameName_NoNamespaceAndNamespaced_Succeeds() { + name := "test_same_name_no_ns_and_ns" + + // Create non-namespaced resource + created1, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + Name: name, + }) + s.Require().NoError(err) + s.NotNil(created1) + s.Nil(created1.GetNamespace()) + + // Create namespaced resource with the same name + nsID := s.getNamespaceID("example.com") + created2, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + }) + s.Require().NoError(err) + s.NotNil(created2) + s.NotNil(created2.GetNamespace()) + s.Equal(nsID, created2.GetNamespace().GetId()) + + // Both should exist with different IDs + s.NotEqual(created1.GetId(), created2.GetId()) +} + +func (s *RegisteredResourcesSuite) Test_GetRegisteredResource_ByName_Ambiguous_ReturnsNonNamespaced() { + name := "test_ambiguous_name_lookup" + + // Create non-namespaced resource + createdNoNS, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + Name: name, + }) + s.Require().NoError(err) + s.NotNil(createdNoNS) + + // Create namespaced resource with the same name + nsID := s.getNamespaceID("example.com") + createdWithNS, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: name, + }) + s.Require().NoError(err) + s.NotNil(createdWithNS) + + // Get by name only (no namespace filter) — should deterministically return the non-namespaced one + got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: name, + }, + }) + s.Require().NoError(err) + s.NotNil(got) + s.Equal(createdNoNS.GetId(), got.GetId()) + s.Nil(got.GetNamespace()) +} + +// ┌─────────────────────────────────────────────────────────────────────────────┐ +// │ end namespace-optional tests │ +// └─────────────────────────────────────────────────────────────────────────────┘ + +// Sort tests + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByName_ASC() { + ids := s.createSortTestRegisteredResources([]string{"aaa-rrsort", "bbb-rrsort", "ccc-rrsort"}) + defer s.deleteSortTestRegisteredResources(ids) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // aaa < bbb < ccc in ASC order + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByName_DESC() { + ids := s.createSortTestRegisteredResources([]string{"aaa-rrsortdesc", "bbb-rrsortdesc", "ccc-rrsortdesc"}) + defer s.deleteSortTestRegisteredResources(ids) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // ccc > bbb > aaa in DESC order + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByCreatedAt_ASC() { + ids := s.createSortTestRegisteredResources([]string{"createdasc-rr-0", "createdasc-rr-1", "createdasc-rr-2"}) + defer s.deleteSortTestRegisteredResources(ids) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // oldest first in ASC order + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByCreatedAt_DESC() { + ids := s.createSortTestRegisteredResources([]string{"createddesc-rr-0", "createddesc-rr-1", "createddesc-rr-2"}) + defer s.deleteSortTestRegisteredResources(ids) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // newest first in DESC order + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUpdatedAt_DESC() { + ids := s.createSortTestRegisteredResources([]string{"upd-sort-rr-0", "upd-sort-rr-1", "upd-sort-rr-2"}) + defer s.deleteSortTestRegisteredResources(ids) + + // Update the first resource so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateRegisteredResource(s.ctx, ®isteredresources.UpdateRegisteredResourceRequest{ + Id: ids[0], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // The updated resource (ids[0]) should appear before the others + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[0], ids[2], ids[1]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUpdatedAt_ASC() { + ids := s.createSortTestRegisteredResources([]string{"upd-sort-asc-rr-0", "upd-sort-asc-rr-1", "upd-sort-asc-rr-2"}) + defer s.deleteSortTestRegisteredResources(ids) + + // Update the last resource so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateRegisteredResource(s.ctx, ®isteredresources.UpdateRegisteredResourceRequest{ + Id: ids[2], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // The updated resource (ids[2]) should appear last in ASC order + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortTieBreaker_CreatedAtWithIDFallback() { + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-rr-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: s.getNamespaceID("example.com"), + Name: name, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestRegisteredResources(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "registered_resources", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, sorted[0], sorted[1], sorted[2]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUnspecifiedField_DefaultsToCreatedAt() { + ids := s.createSortTestRegisteredResources([]string{"unspecified-field-rr-0", "unspecified-field-rr-1", "unspecified-field-rr-2"}) + defer s.deleteSortTestRegisteredResources(ids) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // Field defaults to created_at, explicit ASC is preserved + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUnspecifiedDirection_DefaultsToDESC() { + ids := s.createSortTestRegisteredResources([]string{"unspecified-dir-rr-0", "unspecified-dir-rr-1", "unspecified-dir-rr-2"}) + defer s.deleteSortTestRegisteredResources(ids) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // Direction defaults to DESC, explicit created_at field is preserved + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByBothUnspecified_DefaultsToCreatedAtDESC() { + ids := s.createSortTestRegisteredResources([]string{"both-unspecified-rr-0", "both-unspecified-rr-1", "both-unspecified-rr-2"}) + defer s.deleteSortTestRegisteredResources(ids) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + // Both default: created_at DESC + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortOmitted() { + ids := s.createSortTestRegisteredResources([]string{"sort-omitted-rr-0", "sort-omitted-rr-1", "sort-omitted-rr-2"}) + defer s.deleteSortTestRegisteredResources(ids) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{}) + s.Require().NoError(err) + s.NotNil(list) + + // No sort provided: created_at DESC + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[2], ids[1], ids[0]) +} + +// Sort test helpers + +// createSortTestRegisteredResources creates registered resources with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the resource IDs in creation order. +func (s *RegisteredResourcesSuite) createSortTestRegisteredResources(prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + name := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: s.getNamespaceID("example.com"), + Name: name, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + return ids +} + +// deleteSortTestRegisteredResources cleans up registered resources created by sort tests. +func (s *RegisteredResourcesSuite) deleteSortTestRegisteredResources(ids []string) { + for _, id := range ids { + _, err := s.db.PolicyClient.DeleteRegisteredResource(s.ctx, id) + s.Require().NoError(err) + } +} + +func (s *RegisteredResourcesSuite) getNamespaceID(key string) string { + ns := s.f.GetNamespaceKey(key) + return ns.ID +} + +func (s *RegisteredResourcesSuite) getNamespaceFQN(key string) string { + ns := s.f.GetNamespaceKey(key) + return "https://" + ns.Name +} diff --git a/service/integration/resource_mappings_test.go b/service/integration/resource_mappings_test.go index 4230bdf36a..b11c5b48b1 100644 --- a/service/integration/resource_mappings_test.go +++ b/service/integration/resource_mappings_test.go @@ -5,7 +5,9 @@ import ( "fmt" "log/slog" "testing" + "time" + "github.com/opentdf/platform/lib/identifier" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" @@ -60,6 +62,7 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappingGroups_NoPagination_Succ for _, rmGroup := range listed { if testRmGroup.ID == rmGroup.GetId() { found = true + s.Equal(s.resourceMappingGroupFqn(testRmGroup), rmGroup.GetFqn()) break } } @@ -67,6 +70,43 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappingGroups_NoPagination_Succ } } +func (s *ResourceMappingsSuite) Test_ListResourceMappingGroups_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: fmt.Sprintf("order-test-rmg-%d.com", suffix), + }) + s.Require().NoError(err) + s.Require().NotNil(ns) + defer func() { + _, err := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, ns, ns.GetFqn()) + s.Require().NoError(err) + }() + + create := func(i int) string { + group, err := s.db.PolicyClient.CreateResourceMappingGroup(s.ctx, &resourcemapping.CreateResourceMappingGroupRequest{ + Name: fmt.Sprintf("order-test-group-%d-%d", i, suffix), + NamespaceId: ns.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(group) + return group.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + listRsp, err := s.db.PolicyClient.ListResourceMappingGroups(s.ctx, &resourcemapping.ListResourceMappingGroupsRequest{ + NamespaceId: ns.GetId(), + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetResourceMappingGroups(), func(g *policy.ResourceMappingGroup) string { return g.GetId() }, thirdID, secondID, firstID) +} + func (s *ResourceMappingsSuite) Test_ListResourceMappingGroups_Limit_Succeeds() { var limit int32 = 2 listRsp, err := s.db.PolicyClient.ListResourceMappingGroups(s.ctx, &resourcemapping.ListResourceMappingGroupsRequest{ @@ -83,6 +123,7 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappingGroups_Limit_Succeeds() s.NotEmpty(rmg.GetNamespaceId()) s.NotEmpty(rmg.GetId()) s.NotEmpty(rmg.GetName()) + s.NotEmpty(rmg.GetFqn()) } } @@ -136,6 +177,7 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappingGroups_WithNamespaceId_S s.Equal(scenarioDotComRmGroup.ID, list[0].GetId()) s.Equal(scenarioDotComRmGroup.NamespaceID, list[0].GetNamespaceId()) s.Equal(scenarioDotComRmGroup.Name, list[0].GetName()) + s.Equal(s.resourceMappingGroupFqn(scenarioDotComRmGroup), list[0].GetFqn()) } func (s *ResourceMappingsSuite) Test_ListResourceMappingGroups_MultipleNamespaces_Succeeds() { @@ -278,6 +320,7 @@ func (s *ResourceMappingsSuite) Test_GetResourceMappingGroup() { s.Equal(testRmGroup.ID, rmGroup.GetId()) s.Equal(testRmGroup.NamespaceID, rmGroup.GetNamespaceId()) s.Equal(testRmGroup.Name, rmGroup.GetName()) + s.Equal(s.resourceMappingGroupFqn(testRmGroup), rmGroup.GetFqn()) metadata := rmGroup.GetMetadata() createdAt := metadata.GetCreatedAt() updatedAt := metadata.GetUpdatedAt() @@ -293,13 +336,20 @@ func (s *ResourceMappingsSuite) Test_GetResourceMappingGroupWithUnknownIdFails() } func (s *ResourceMappingsSuite) Test_CreateResourceMappingGroup() { + exampleCom := s.getExampleDotComNamespace() + groupName := "example.com_ns_new_group" req := &resourcemapping.CreateResourceMappingGroupRequest{ - NamespaceId: s.getExampleDotComNamespace().ID, - Name: "example.com_ns_new_group", + NamespaceId: exampleCom.ID, + Name: groupName, } rmGroup, err := s.db.PolicyClient.CreateResourceMappingGroup(s.ctx, req) s.Require().NoError(err) s.NotNil(rmGroup) + expectedFQN := (&identifier.FullyQualifiedResourceMappingGroup{ + Namespace: exampleCom.Name, + GroupName: groupName, + }).FQN() + s.Equal(expectedFQN, rmGroup.GetFqn()) } func (s *ResourceMappingsSuite) Test_CreateResourceMappingGroupWithUnknownNamespaceIdFails() { @@ -357,9 +407,11 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMappingGroup() { s.Require().NoError(err) s.NotNil(createdGroup) + updateName := "example.com_ns_group_updated" + scenarioCom := s.getScenarioDotComNamespace() updateReq := &resourcemapping.UpdateResourceMappingGroupRequest{ - NamespaceId: s.getScenarioDotComNamespace().ID, - Name: "example.com_ns_group_updated", + NamespaceId: scenarioCom.ID, + Name: updateName, Metadata: &common.MetadataMutable{ Labels: updateLabels, }, @@ -369,6 +421,11 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMappingGroup() { s.Require().NoError(err) s.NotNil(updatedGroup) s.Equal(createdGroup.GetId(), updatedGroup.GetId()) + expectedFQN := (&identifier.FullyQualifiedResourceMappingGroup{ + Namespace: scenarioCom.Name, + GroupName: updateName, + }).FQN() + s.Equal(expectedFQN, updatedGroup.GetFqn()) gotGroup, err := s.db.PolicyClient.GetResourceMappingGroup(s.ctx, createdGroup.GetId()) s.Require().NoError(err) @@ -377,6 +434,7 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMappingGroup() { s.Equal(createdGroup.GetId(), gotGroup.GetId()) s.Equal(updateReq.GetNamespaceId(), gotGroup.GetNamespaceId()) s.Equal(updateReq.GetName(), gotGroup.GetName()) + s.Equal(updatedGroup.GetFqn(), gotGroup.GetFqn()) metadata := gotGroup.GetMetadata() createdAt := metadata.GetCreatedAt() updatedAt := metadata.GetUpdatedAt() @@ -406,17 +464,19 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMappingGroupWithUnknownIdFail } func (s *ResourceMappingsSuite) Test_UpdateResourceMappingGroupWithNamespaceIdOnlySucceeds() { + groupName := "example.com_ns_group_created_nsidonly" req := &resourcemapping.CreateResourceMappingGroupRequest{ NamespaceId: s.getExampleDotComNamespace().ID, - Name: "example.com_ns_group_created_nsidonly", + Name: groupName, } rmGroup, err := s.db.PolicyClient.CreateResourceMappingGroup(s.ctx, req) s.Require().NoError(err) s.NotNil(rmGroup) + scenarioCom := s.getScenarioDotComNamespace() updateReq := &resourcemapping.UpdateResourceMappingGroupRequest{ Id: rmGroup.GetId(), - NamespaceId: s.getScenarioDotComNamespace().ID, + NamespaceId: scenarioCom.ID, } updatedRmGroup, err := s.db.PolicyClient.UpdateResourceMappingGroup(s.ctx, rmGroup.GetId(), updateReq) s.Require().NoError(err) @@ -428,20 +488,28 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMappingGroupWithNamespaceIdOn s.NotNil(gotUpdatedRmGroup) s.Equal(updateReq.GetNamespaceId(), gotUpdatedRmGroup.GetNamespaceId()) s.Equal(req.GetName(), gotUpdatedRmGroup.GetName()) + expectedFQN := (&identifier.FullyQualifiedResourceMappingGroup{ + Namespace: scenarioCom.Name, + GroupName: groupName, + }).FQN() + s.Equal(expectedFQN, updatedRmGroup.GetFqn()) + s.Equal(updatedRmGroup.GetFqn(), gotUpdatedRmGroup.GetFqn()) } func (s *ResourceMappingsSuite) Test_UpdateResourceMappingGroupWithNameOnlySucceeds() { + exampleCom := s.getExampleDotComNamespace() req := &resourcemapping.CreateResourceMappingGroupRequest{ - NamespaceId: s.getExampleDotComNamespace().ID, + NamespaceId: exampleCom.ID, Name: "example.com_ns_group_created_nameonly", } rmGroup, err := s.db.PolicyClient.CreateResourceMappingGroup(s.ctx, req) s.Require().NoError(err) s.NotNil(rmGroup) + updatedName := "example.com_ns_group_created_nameonly_updated" updateReq := &resourcemapping.UpdateResourceMappingGroupRequest{ Id: rmGroup.GetId(), - Name: "example.com_ns_group_created_nameonly_updated", + Name: updatedName, } updatedRmGroup, err := s.db.PolicyClient.UpdateResourceMappingGroup(s.ctx, rmGroup.GetId(), updateReq) s.Require().NoError(err) @@ -453,6 +521,12 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMappingGroupWithNameOnlySucce s.NotNil(gotUpdatedRmGroup) s.Equal(req.GetNamespaceId(), gotUpdatedRmGroup.GetNamespaceId()) s.Equal(updateReq.GetName(), gotUpdatedRmGroup.GetName()) + expectedFQN := (&identifier.FullyQualifiedResourceMappingGroup{ + Namespace: exampleCom.Name, + GroupName: updatedName, + }).FQN() + s.Equal(expectedFQN, updatedRmGroup.GetFqn()) + s.Equal(updatedRmGroup.GetFqn(), gotUpdatedRmGroup.GetFqn()) } func (s *ResourceMappingsSuite) Test_DeleteResourceMappingGroup() { @@ -483,6 +557,7 @@ func (s *ResourceMappingsSuite) Test_DeleteResourceMappingGroup() { s.Require().NoError(err) s.NotNil(deletedGroup) s.Equal(createdGroup.GetId(), deletedGroup.GetId()) + s.Equal(createdGroup.GetFqn(), deletedGroup.GetFqn()) // get the mapping to verify group id is cascade set to null gotMapping, err := s.db.PolicyClient.GetResourceMapping(s.ctx, createdMapping.GetId()) @@ -546,6 +621,7 @@ func (s *ResourceMappingsSuite) Test_CreateResourceMappingWithGroupIdSucceeds() s.Require().NoError(err) s.NotNil(createdMapping) s.Equal(rmGroup.ID, createdMapping.GetGroup().GetId()) + s.Equal(s.resourceMappingGroupFqn(rmGroup), createdMapping.GetGroup().GetFqn()) } func (s *ResourceMappingsSuite) Test_CreateResourceMappingWithUnknownGroupIdFails() { @@ -596,6 +672,14 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappings_NoPagination_Succeeds( testGroups[testGroup.ID] = testGroup } + ungroupedValue := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1") + ungroupedMapping, err := s.db.PolicyClient.CreateResourceMapping(s.ctx, &resourcemapping.CreateResourceMappingRequest{ + AttributeValueId: ungroupedValue.ID, + Terms: []string{"ungrouped-list-term"}, + }) + s.Require().NoError(err) + s.Require().NotNil(ungroupedMapping) + listRsp, err := s.db.PolicyClient.ListResourceMappings(s.ctx, &resourcemapping.ListResourceMappingsRequest{}) s.Require().NoError(err) s.NotNil(listRsp) @@ -605,8 +689,15 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappings_NoPagination_Succeeds( testMappingCount := len(testMappings) foundCount := 0 + foundUngroupedMapping := false for _, mapping := range list { + if mapping.GetId() == ungroupedMapping.GetId() { + foundUngroupedMapping = true + s.Nil(mapping.GetGroup()) + continue + } + testMapping, ok := testMappings[mapping.GetId()] if !ok { // only validating presence of all fixtures within the list response @@ -616,8 +707,10 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappings_NoPagination_Succeeds( s.Equal(testMapping.Terms, mapping.GetTerms()) s.Equal(testMapping.GroupID, mapping.GetGroup().GetId()) - s.Equal(testGroups[mapping.GetGroup().GetId()].Name, mapping.GetGroup().GetName()) - s.Equal(testGroups[mapping.GetGroup().GetId()].NamespaceID, mapping.GetGroup().GetNamespaceId()) + testGroup := testGroups[mapping.GetGroup().GetId()] + s.Equal(testGroup.Name, mapping.GetGroup().GetName()) + s.Equal(testGroup.NamespaceID, mapping.GetGroup().GetNamespaceId()) + s.Equal(s.resourceMappingGroupFqn(testGroup), mapping.GetGroup().GetFqn()) metadata := mapping.GetMetadata() createdAt := metadata.GetCreatedAt() updatedAt := metadata.GetUpdatedAt() @@ -633,6 +726,130 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappings_NoPagination_Succeeds( } s.Equal(testMappingCount, foundCount) + s.True(foundUngroupedMapping, "expected to find ungrouped mapping %s", ungroupedMapping.GetId()) +} + +func (s *ResourceMappingsSuite) Test_ListResourceMappings_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: fmt.Sprintf("order-test-rm-%d.com", suffix), + }) + s.Require().NoError(err) + s.Require().NotNil(ns) + defer func() { + _, err := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, ns, ns.GetFqn()) + s.Require().NoError(err) + }() + + attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: fmt.Sprintf("order-test-attr-%d", suffix), + NamespaceId: ns.GetId(), + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + s.Require().NotNil(attr) + + val, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attr.GetId(), &attributes.CreateAttributeValueRequest{ + Value: fmt.Sprintf("order-test-value-%d", suffix), + AttributeId: attr.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(val) + + group, err := s.db.PolicyClient.CreateResourceMappingGroup(s.ctx, &resourcemapping.CreateResourceMappingGroupRequest{ + Name: fmt.Sprintf("order-test-group-%d", suffix), + NamespaceId: ns.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(group) + + create := func(i int) string { + mapping, err := s.db.PolicyClient.CreateResourceMapping(s.ctx, &resourcemapping.CreateResourceMappingRequest{ + AttributeValueId: val.GetId(), + Terms: []string{fmt.Sprintf("term-%d-%d", i, suffix)}, + GroupId: group.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(mapping) + return mapping.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + listRsp, err := s.db.PolicyClient.ListResourceMappings(s.ctx, &resourcemapping.ListResourceMappingsRequest{ + GroupId: group.GetId(), + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetResourceMappings(), func(rm *policy.ResourceMapping) string { return rm.GetId() }, thirdID, secondID, firstID) +} + +func (s *ResourceMappingsSuite) Test_ListResourceMappingsByGroupFqns_OrdersByCreatedAt_Succeeds() { + suffix := time.Now().UnixNano() + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: fmt.Sprintf("order-test-rm-fqn-%d.com", suffix), + }) + s.Require().NoError(err) + s.Require().NotNil(ns) + defer func() { + _, err := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, ns, ns.GetFqn()) + s.Require().NoError(err) + }() + + attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: fmt.Sprintf("order-test-attr-fqn-%d", suffix), + NamespaceId: ns.GetId(), + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + s.Require().NotNil(attr) + + val, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attr.GetId(), &attributes.CreateAttributeValueRequest{ + Value: fmt.Sprintf("order-test-value-fqn-%d", suffix), + AttributeId: attr.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(val) + + group, err := s.db.PolicyClient.CreateResourceMappingGroup(s.ctx, &resourcemapping.CreateResourceMappingGroupRequest{ + Name: fmt.Sprintf("order-test-group-fqn-%d", suffix), + NamespaceId: ns.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(group) + + create := func(i int) string { + mapping, err := s.db.PolicyClient.CreateResourceMapping(s.ctx, &resourcemapping.CreateResourceMappingRequest{ + AttributeValueId: val.GetId(), + Terms: []string{fmt.Sprintf("term-fqn-%d-%d", i, suffix)}, + GroupId: group.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(mapping) + return mapping.GetId() + } + + firstID := create(1) + time.Sleep(5 * time.Millisecond) + secondID := create(2) + time.Sleep(5 * time.Millisecond) + thirdID := create(3) + + fqn := fmt.Sprintf("https://%s/resm/%s", ns.GetName(), group.GetName()) + mappingsByGroup, err := s.db.PolicyClient.ListResourceMappingsByGroupFqns(s.ctx, []string{fqn}) + s.Require().NoError(err) + s.NotNil(mappingsByGroup) + + groupMappings, ok := mappingsByGroup[fqn] + s.Require().True(ok) + s.Require().NotNil(groupMappings) + + assertIDsInOrder(s.T(), groupMappings.GetMappings(), func(rm *policy.ResourceMapping) string { return rm.GetId() }, thirdID, secondID, firstID) } func (s *ResourceMappingsSuite) Test_ListResourceMappings_Limit_Succeeds() { @@ -907,6 +1124,10 @@ func (s *ResourceMappingsSuite) Test_ListResourceMappings_ByGroupFqns_Succeeds() s.Equal(scenarioDotComGroup.ID, group.GetId()) s.Equal(scenarioDotComGroup.NamespaceID, group.GetNamespaceId()) s.Equal(scenarioDotComGroup.Name, group.GetName()) + s.Equal(groupFqn, group.GetFqn()) + groupByID, err := s.db.PolicyClient.GetResourceMappingGroup(s.ctx, scenarioDotComGroup.ID) + s.Require().NoError(err) + s.Equal(groupByID.GetFqn(), group.GetFqn()) groupMetadata := group.GetMetadata() createdAt := groupMetadata.GetCreatedAt() updatedAt := groupMetadata.GetUpdatedAt() @@ -1057,6 +1278,7 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMapping() { newLabel := "new label" rmGroup := s.getResourceMappingGroupFixtures()[0] + expectedGroupFqn := s.resourceMappingGroupFqn(rmGroup) labels := map[string]string{ "fixed": fixedLabel, @@ -1086,11 +1308,13 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMapping() { }) s.Require().NoError(err) s.NotNil(createdMapping) + s.Equal(expectedGroupFqn, createdMapping.GetGroup().GetFqn()) updateWithoutChange, err := s.db.PolicyClient.UpdateResourceMapping(s.ctx, createdMapping.GetId(), &resourcemapping.UpdateResourceMappingRequest{}) s.Require().NoError(err) s.NotNil(updateWithoutChange) s.Equal(createdMapping.GetId(), updateWithoutChange.GetId()) + s.Equal(expectedGroupFqn, updateWithoutChange.GetGroup().GetFqn()) // update the created with new metadata and terms updateWithChange, err := s.db.PolicyClient.UpdateResourceMapping(s.ctx, createdMapping.GetId(), &resourcemapping.UpdateResourceMappingRequest{ @@ -1109,6 +1333,7 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMapping() { s.Equal(updateTerms, updateWithChange.GetTerms()) s.Equal(expectedLabels, updateWithChange.GetMetadata().GetLabels()) s.Equal(createdMapping.GetGroup().GetId(), updateWithChange.GetGroup().GetId()) + s.Equal(expectedGroupFqn, updateWithChange.GetGroup().GetFqn()) // get after update to verify db reflects changes made got, err := s.db.PolicyClient.GetResourceMapping(s.ctx, createdMapping.GetId()) @@ -1125,6 +1350,7 @@ func (s *ResourceMappingsSuite) Test_UpdateResourceMapping() { s.False(updatedAt.AsTime().IsZero()) s.True(updatedAt.AsTime().After(createdAt.AsTime())) s.Equal(rmGroup.ID, got.GetGroup().GetId()) + s.Equal(expectedGroupFqn, got.GetGroup().GetFqn()) } func (s *ResourceMappingsSuite) Test_UpdateResourceMappingWithUnknownIdFails() { @@ -1262,6 +1488,16 @@ func (s *ResourceMappingsSuite) getResourceMappingGroupFixtures() []fixtures.Fix } } +func (s *ResourceMappingsSuite) resourceMappingGroupFqn(group fixtures.FixtureDataResourceMappingGroup) string { + namespace, err := s.db.PolicyClient.GetNamespace(s.ctx, group.NamespaceID) + s.Require().NoError(err) + + return (&identifier.FullyQualifiedResourceMappingGroup{ + Namespace: namespace.GetName(), + GroupName: group.Name, + }).FQN() +} + func (s *ResourceMappingsSuite) getResourceMappingFixtures() []fixtures.FixtureDataResourceMapping { return []fixtures.FixtureDataResourceMapping{ s.f.GetResourceMappingKey("resource_mapping_to_attribute_value1"), diff --git a/service/integration/subject_mappings_test.go b/service/integration/subject_mappings_test.go index ac631375a7..5376dc8be6 100644 --- a/service/integration/subject_mappings_test.go +++ b/service/integration/subject_mappings_test.go @@ -2,12 +2,17 @@ package integration import ( "context" + "fmt" "log/slog" + "slices" "strings" "testing" + "time" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/namespaces" "github.com/opentdf/platform/protocol/go/policy/subjectmapping" "github.com/opentdf/platform/service/internal/fixtures" "github.com/opentdf/platform/service/pkg/db" @@ -242,39 +247,6 @@ func (s *SubjectMappingsSuite) TestCreateSubjectMapping_BrandNewActionNames_Succ s.True(foundNewActionTwo) } -func (s *SubjectMappingsSuite) TestCreateSubjectMapping_DeprecatedProtoEnums_Fails() { - s.T().Skip("Skipping test while deprecation of proto actions is in flight") - - fixtureAttrVal := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1") - fixtureScs := s.f.GetSubjectConditionSetKey("subject_condition_set2") - - newSubjectMapping := &subjectmapping.CreateSubjectMappingRequest{ - AttributeValueId: fixtureAttrVal.ID, - Actions: []*policy.Action{ - { - Value: &policy.Action_Standard{ - Standard: policy.Action_STANDARD_ACTION_DECRYPT, - }, - }, - }, - ExistingSubjectConditionSetId: fixtureScs.ID, - } - - created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, newSubjectMapping) - s.Nil(created) - s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrMissingValue) - - newSubjectMapping.GetActions()[0].Value = &policy.Action_Standard{ - Standard: policy.Action_STANDARD_ACTION_TRANSMIT, - } - - created, err = s.db.PolicyClient.CreateSubjectMapping(s.ctx, newSubjectMapping) - s.Nil(created) - s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrMissingValue) -} - func (s *SubjectMappingsSuite) TestUpdateSubjectMapping_Actions() { // create a new one SM with actions, update it with different actions, and verify the update fixtureAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value2").ID @@ -386,50 +358,6 @@ func (s *SubjectMappingsSuite) TestUpdateSubjectMapping_Actions_NonExistentActio s.Require().ErrorIs(err, db.ErrForeignKeyViolation) } -func (s *SubjectMappingsSuite) TestUpdateSubjectMapping_Actions_DeprecatedProtoEnums_Fails() { - s.T().Skip("Skipping test while deprecation of proto actions is in flight") - - fixtureAttrVal := s.f.GetAttributeValueKey("example.com/attr/attr2/value/value1") - fixtureScs := s.f.GetSubjectConditionSetKey("subject_condition_set1") - - newSubjectMapping := &subjectmapping.CreateSubjectMappingRequest{ - AttributeValueId: fixtureAttrVal.ID, - Actions: []*policy.Action{ - {Name: policydb.ActionRead.String()}, - }, - ExistingSubjectConditionSetId: fixtureScs.ID, - } - - created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, newSubjectMapping) - s.NotNil(created) - s.Require().NoError(err) - - updateReq := &subjectmapping.UpdateSubjectMappingRequest{ - Id: created.GetId(), - Actions: []*policy.Action{ - { - Value: &policy.Action_Standard{ - Standard: policy.Action_STANDARD_ACTION_DECRYPT, - }, - }, - }, - } - - updated, err := s.db.PolicyClient.UpdateSubjectMapping(s.ctx, updateReq) - s.Nil(updated) - s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrMissingValue) - - updateReq.Actions[0].Value = &policy.Action_Standard{ - Standard: policy.Action_STANDARD_ACTION_TRANSMIT, - } - - updated, err = s.db.PolicyClient.UpdateSubjectMapping(s.ctx, updateReq) - s.Nil(updated) - s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrMissingValue) -} - func (s *SubjectMappingsSuite) TestUpdateSubjectMapping_SubjectConditionSetId() { // create a new one, update it, and verify the update fixtureAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value1").ID @@ -625,6 +553,249 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_NoPagination_Succeeds() s.True(found3) } +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_OrdersByCreatedAt_Succeeds() { + fixtureAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value2").ID + actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) + + createMapping := func(email string) string { + scs := &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".email", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{email}, + }, + }, + }, + }, + }, + }, + } + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fixtureAttrValID, + NewSubjectConditionSet: scs, + Actions: []*policy.Action{actionRead}, + }) + s.Require().NoError(err) + s.Require().NotEmpty(created.GetId()) + return created.GetId() + } + + firstID := createMapping("order-test-1@example.com") + time.Sleep(5 * time.Millisecond) + secondID := createMapping("order-test-2@example.com") + time.Sleep(5 * time.Millisecond) + thirdID := createMapping("order-test-3@example.com") + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(context.Background(), &subjectmapping.ListSubjectMappingsRequest{}) + s.Require().NoError(err) + + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, thirdID, secondID, firstID) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByCreatedAt_ASC() { + ids := s.createSortTestSubjectMappings([]string{"sort-created-asc-0", "sort-created-asc-1", "sort-created-asc-2"}) + defer s.deleteSortTestSubjectMappings(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // oldest first in ASC order + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByCreatedAt_DESC() { + ids := s.createSortTestSubjectMappings([]string{"sort-created-desc-0", "sort-created-desc-1", "sort-created-desc-2"}) + defer s.deleteSortTestSubjectMappings(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // newest first in DESC order + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUpdatedAt_DESC() { + ids := s.createSortTestSubjectMappings([]string{"sort-updated-desc-0", "sort-updated-desc-1", "sort-updated-desc-2"}) + defer s.deleteSortTestSubjectMappings(ids) + + // Update the first mapping so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateSubjectMapping(s.ctx, &subjectmapping.UpdateSubjectMappingRequest{ + Id: ids[0], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated mapping (ids[0]) should appear before the others + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[0], ids[2], ids[1]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUpdatedAt_ASC() { + ids := s.createSortTestSubjectMappings([]string{"sort-updated-asc-0", "sort-updated-asc-1", "sort-updated-asc-2"}) + defer s.deleteSortTestSubjectMappings(ids) + + // Update the last mapping so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateSubjectMapping(s.ctx, &subjectmapping.UpdateSubjectMappingRequest{ + Id: ids[2], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated mapping (ids[2]) should appear last in ASC order + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortTieBreaker_CreatedAtWithIDFallback() { + fixtureAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value2").ID + actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) + + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + email := fmt.Sprintf("tiebreaker-sm-%d-%d@example.com", i, suffix) + scs := &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".email", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{email}, + }, + }, + }, + }, + }, + }, + } + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fixtureAttrValID, + NewSubjectConditionSet: scs, + Actions: []*policy.Action{actionRead}, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestSubjectMappings(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "subject_mappings", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, sorted[0], sorted[1], sorted[2]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUnspecifiedField_DefaultsToCreatedAt() { + ids := s.createSortTestSubjectMappings([]string{"unspecified-field-sm-0", "unspecified-field-sm-1", "unspecified-field-sm-2"}) + defer s.deleteSortTestSubjectMappings(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Field defaults to created_at, explicit ASC is preserved + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUnspecifiedDirection_DefaultsToDESC() { + ids := s.createSortTestSubjectMappings([]string{"unspecified-dir-sm-0", "unspecified-dir-sm-1", "unspecified-dir-sm-2"}) + defer s.deleteSortTestSubjectMappings(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Direction defaults to DESC, explicit created_at field is preserved + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByBothUnspecified_DefaultsToCreatedAtDESC() { + ids := s.createSortTestSubjectMappings([]string{"both-unspecified-sm-0", "both-unspecified-sm-1", "both-unspecified-sm-2"}) + defer s.deleteSortTestSubjectMappings(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Both default: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortOmitted() { + ids := s.createSortTestSubjectMappings([]string{"sort-omitted-sm-0", "sort-omitted-sm-1", "sort-omitted-sm-2"}) + defer s.deleteSortTestSubjectMappings(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{}) + s.Require().NoError(err) + s.NotNil(listRsp) + + // No sort provided: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[2], ids[1], ids[0]) +} + func (s *SubjectMappingsSuite) Test_ListSubjectMappings_Limit_Succeeds() { var limit int32 = 3 listRsp, err := s.db.PolicyClient.ListSubjectMappings(context.Background(), &subjectmapping.ListSubjectMappingsRequest{ @@ -696,6 +867,179 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_Offset_Succeeds() { } } +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_ByNamespaceId_Succeeds() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + comAttrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + netAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value1").ID + comSCS := s.newSCSInNamespace(comNsID) + netSCS := s.newSCSInNamespace(netNsID) + + comSM, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: comAttrValID, + Actions: []*policy.Action{{Name: "list_by_ns_id_com"}}, + ExistingSubjectConditionSetId: comSCS.GetId(), + NamespaceId: comNsID, + }) + s.Require().NoError(err) + + netSM, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: netAttrValID, + Actions: []*policy.Action{{Name: "list_by_ns_id_net"}}, + ExistingSubjectConditionSetId: netSCS.GetId(), + NamespaceId: netNsID, + }) + s.Require().NoError(err) + defer func() { + _, _ = s.db.PolicyClient.DeleteSubjectMapping(s.ctx, comSM.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectMapping(s.ctx, netSM.GetId()) + }() + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: comNsID, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + listed := listRsp.GetSubjectMappings() + s.NotEmpty(listed) + + foundCom := false + for _, sm := range listed { + s.Equal(comNsID, sm.GetNamespace().GetId()) + if sm.GetId() == comSM.GetId() { + foundCom = true + } + s.NotEqual(netSM.GetId(), sm.GetId()) + } + s.True(foundCom) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_ByNamespaceFqn_Succeeds() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + comAttrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value2").ID + netAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value2").ID + comSCS := s.newSCSInNamespace(comNsID) + netSCS := s.newSCSInNamespace(netNsID) + + comSM, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: comAttrValID, + Actions: []*policy.Action{{Name: "list_by_ns_fqn_com"}}, + ExistingSubjectConditionSetId: comSCS.GetId(), + NamespaceFqn: "https://example.com", + }) + s.Require().NoError(err) + + netSM, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: netAttrValID, + Actions: []*policy.Action{{Name: "list_by_ns_fqn_net"}}, + ExistingSubjectConditionSetId: netSCS.GetId(), + NamespaceFqn: "https://example.net", + }) + s.Require().NoError(err) + defer func() { + _, _ = s.db.PolicyClient.DeleteSubjectMapping(s.ctx, comSM.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectMapping(s.ctx, netSM.GetId()) + }() + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceFqn: "https://example.com", + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + listed := listRsp.GetSubjectMappings() + s.NotEmpty(listed) + + foundCom := false + for _, sm := range listed { + s.Equal(comNsID, sm.GetNamespace().GetId()) + if sm.GetId() == comSM.GetId() { + foundCom = true + } + s.NotEqual(netSM.GetId(), sm.GetId()) + } + s.True(foundCom) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_ByNamespaceId_NoResults_Succeeds() { + emptyNs, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: "list-sm-no-results.example", + }) + s.Require().NoError(err) + s.Require().NotNil(emptyNs) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: emptyNs.GetId(), + }) + s.Require().NoError(err) + s.NotNil(listRsp) + s.Empty(listRsp.GetSubjectMappings()) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_NoNamespaceFilter_ReturnsAllNamespaces() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + comAttrValID := s.f.GetAttributeValueKey("example.com/attr/attr2/value/value1").ID + netAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value1").ID + comSCS := s.newSCSInNamespace(comNsID) + netSCS := s.newSCSInNamespace(netNsID) + fixtureScs := s.f.GetSubjectConditionSetKey("subject_condition_set1") + actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) + + comSM, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: comAttrValID, + Actions: []*policy.Action{{Name: "list_no_filter_com"}}, + ExistingSubjectConditionSetId: comSCS.GetId(), + NamespaceId: comNsID, + }) + s.Require().NoError(err) + + netSM, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: netAttrValID, + Actions: []*policy.Action{{Name: "list_no_filter_net"}}, + ExistingSubjectConditionSetId: netSCS.GetId(), + NamespaceId: netNsID, + }) + s.Require().NoError(err) + + unnamespacedSM, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: s.f.GetAttributeValueKey("example.com/attr/attr2/value/value2").ID, + Actions: []*policy.Action{actionRead}, + ExistingSubjectConditionSetId: fixtureScs.ID, + }) + s.Require().NoError(err) + defer func() { + _, _ = s.db.PolicyClient.DeleteSubjectMapping(s.ctx, comSM.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectMapping(s.ctx, netSM.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectMapping(s.ctx, unnamespacedSM.GetId()) + }() + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{}) + s.Require().NoError(err) + s.NotNil(listRsp) + + listed := listRsp.GetSubjectMappings() + foundCom, foundNet, foundUnnamespaced := false, false, false + for _, sm := range listed { + switch sm.GetId() { + case comSM.GetId(): + foundCom = true + s.Equal(comNsID, sm.GetNamespace().GetId()) + case netSM.GetId(): + foundNet = true + s.Equal(netNsID, sm.GetNamespace().GetId()) + case unnamespacedSM.GetId(): + foundUnnamespaced = true + s.Nil(sm.GetNamespace()) + } + } + s.True(foundCom) + s.True(foundNet) + s.True(foundUnnamespaced) +} + func (s *SubjectMappingsSuite) TestDeleteSubjectMapping() { // create a new subject mapping, delete it, and verify get fails with not found fixtureAttrValID := s.f.GetAttributeValueKey("example.com/attr/attr2/value/value1").ID @@ -799,7 +1143,7 @@ func (s *SubjectMappingsSuite) TestCreateSubjectConditionSet() { }, } - scs, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet) + scs, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet, "", "") s.Require().NoError(err) s.NotNil(scs) } @@ -824,7 +1168,7 @@ func (s *SubjectMappingsSuite) TestCreateSubjectConditionSetContains() { }, } - scs, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet) + scs, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet, "", "") s.Require().NoError(err) s.NotNil(scs) } @@ -891,6 +1235,50 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSet_NoPagination_Succeed s.True(found4) } +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSet_OrdersByCreatedAt_Succeeds() { + create := func(email string) string { + scs := &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".email", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{email}, + }, + }, + }, + }, + }, + }, + } + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, scs, "", "") + s.Require().NoError(err) + s.Require().NotNil(created) + return created.GetId() + } + + firstID := create("order-scs-1@example.com") + time.Sleep(5 * time.Millisecond) + secondID := create("order-scs-2@example.com") + time.Sleep(5 * time.Millisecond) + thirdID := create("order-scs-3@example.com") + defer func() { + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, firstID) + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, secondID) + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, thirdID) + }() + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(context.Background(), &subjectmapping.ListSubjectConditionSetsRequest{}) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, thirdID, secondID, firstID) +} + func (s *SubjectMappingsSuite) Test_ListSubjectConditionSet_Limit_Succeeds() { var limit int32 = 3 listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(context.Background(), &subjectmapping.ListSubjectConditionSetsRequest{ @@ -919,46 +1307,368 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSet_Limit_Succeeds() { s.NotNil(listRsp) } -func (s *NamespacesSuite) Test_ListSubjectConditionSets_Limit_TooLarge_Fails() { - listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(context.Background(), &subjectmapping.ListSubjectConditionSetsRequest{ - Pagination: &policy.PageRequest{ - Limit: s.db.LimitMax + 1, +func (s *NamespacesSuite) Test_ListSubjectConditionSets_Limit_TooLarge_Fails() { + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(context.Background(), &subjectmapping.ListSubjectConditionSetsRequest{ + Pagination: &policy.PageRequest{ + Limit: s.db.LimitMax + 1, + }, + }) + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrListLimitTooLarge) + s.Nil(listRsp) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSet_Offset_Succeeds() { + req := &subjectmapping.ListSubjectConditionSetsRequest{} + totalListRsp, err := s.db.PolicyClient.ListSubjectConditionSets(context.Background(), req) + s.Require().NoError(err) + s.NotNil(totalListRsp) + + totalList := totalListRsp.GetSubjectConditionSets() + s.NotEmpty(totalList) + + // set the offset pagination + offset := 5 + req.Pagination = &policy.PageRequest{ + Offset: int32(offset), + } + + offetListRsp, err := s.db.PolicyClient.ListSubjectConditionSets(context.Background(), req) + s.Require().NoError(err) + s.NotNil(offetListRsp) + + offsetList := offetListRsp.GetSubjectConditionSets() + s.NotEmpty(offsetList) + + // length is reduced by the offset amount + s.Equal(len(offsetList), len(totalList)-offset) + + // objects are equal between offset and original list beginning at offset index + for i, scs := range offsetList { + s.True(proto.Equal(scs, totalList[i+offset])) + } +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_ByNamespaceId_Succeeds() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + + comSCS1 := s.newSCSInNamespace(comNsID) + comSCS2 := s.newSCSInNamespace(comNsID) + netSCS := s.newSCSInNamespace(netNsID) + defer func() { + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, comSCS1.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, comSCS2.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, netSCS.GetId()) + }() + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceId: comNsID, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + listed := listRsp.GetSubjectConditionSets() + s.NotEmpty(listed) + + foundCom1, foundCom2 := false, false + for _, scs := range listed { + s.Equal(comNsID, scs.GetNamespace().GetId()) + switch scs.GetId() { + case comSCS1.GetId(): + foundCom1 = true + case comSCS2.GetId(): + foundCom2 = true + } + s.NotEqual(netSCS.GetId(), scs.GetId()) + } + s.True(foundCom1) + s.True(foundCom2) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_ByNamespaceFqn_Succeeds() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + + comSCS := s.newSCSInNamespace(comNsID) + netSCS := s.newSCSInNamespace(netNsID) + defer func() { + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, comSCS.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, netSCS.GetId()) + }() + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceFqn: "https://example.com", + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + listed := listRsp.GetSubjectConditionSets() + s.NotEmpty(listed) + + foundCom := false + for _, scs := range listed { + s.Equal(comNsID, scs.GetNamespace().GetId()) + if scs.GetId() == comSCS.GetId() { + foundCom = true + } + s.NotEqual(netSCS.GetId(), scs.GetId()) + } + s.True(foundCom) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_ByNamespaceId_ExcludesUnnamespaced() { + comNsID := s.exampleComNsID() + + namespacedSCS := s.newSCSInNamespace(comNsID) + unnamespacedSCS, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{{}}, + }, "", "") + s.Require().NoError(err) + defer func() { + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, namespacedSCS.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, unnamespacedSCS.GetId()) + }() + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceId: comNsID, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + for _, scs := range listRsp.GetSubjectConditionSets() { + s.NotEqual(unnamespacedSCS.GetId(), scs.GetId()) + s.NotNil(scs.GetNamespace()) + } +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_NoNamespaceFilter_ReturnsAllNamespaces() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + + comSCS := s.newSCSInNamespace(comNsID) + netSCS := s.newSCSInNamespace(netNsID) + unnamespacedSCS, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{{}}, + }, "", "") + s.Require().NoError(err) + defer func() { + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, comSCS.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, netSCS.GetId()) + _, _ = s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, unnamespacedSCS.GetId()) + }() + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{}) + s.Require().NoError(err) + s.NotNil(listRsp) + + listed := listRsp.GetSubjectConditionSets() + foundCom, foundNet, foundUnnamespaced := false, false, false + for _, scs := range listed { + switch scs.GetId() { + case comSCS.GetId(): + foundCom = true + s.Equal(comNsID, scs.GetNamespace().GetId()) + case netSCS.GetId(): + foundNet = true + s.Equal(netNsID, scs.GetNamespace().GetId()) + case unnamespacedSCS.GetId(): + foundUnnamespaced = true + s.Nil(scs.GetNamespace()) + } + } + s.True(foundCom) + s.True(foundNet) + s.True(foundUnnamespaced) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByCreatedAt_ASC() { + ids := s.createSortTestSubjectConditionSets([]string{"sort-scs-created-asc-0", "sort-scs-created-asc-1", "sort-scs-created-asc-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // oldest first in ASC order + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByCreatedAt_DESC() { + ids := s.createSortTestSubjectConditionSets([]string{"sort-scs-created-desc-0", "sort-scs-created-desc-1", "sort-scs-created-desc-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // newest first in DESC order + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[2], ids[1], ids[0]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUpdatedAt_DESC() { + ids := s.createSortTestSubjectConditionSets([]string{"sort-scs-updated-desc-0", "sort-scs-updated-desc-1", "sort-scs-updated-desc-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) + + // Update the first SCS so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateSubjectConditionSet(s.ctx, &subjectmapping.UpdateSubjectConditionSetRequest{ + Id: ids[0], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated SCS (ids[0]) should appear before the others + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[0], ids[2], ids[1]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUpdatedAt_ASC() { + ids := s.createSortTestSubjectConditionSets([]string{"sort-scs-updated-asc-0", "sort-scs-updated-asc-1", "sort-scs-updated-asc-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) + + // Update the last SCS so its updated_at is the most recent + time.Sleep(5 * time.Millisecond) + _, err := s.db.PolicyClient.UpdateSubjectConditionSet(s.ctx, &subjectmapping.UpdateSubjectConditionSetRequest{ + Id: ids[2], + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"updated": "true"}, + }, + MetadataUpdateBehavior: common.MetadataUpdateEnum_METADATA_UPDATE_ENUM_REPLACE, + }) + s.Require().NoError(err) + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // The updated SCS (ids[2]) should appear last in ASC order + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortTieBreaker_CreatedAtWithIDFallback() { + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + val := fmt.Sprintf("tiebreaker-scs-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".sort_test", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{val}, + }, + }, + }, + }, + }, + }, + }, "", "") + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestSubjectConditionSets(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "subject_condition_set", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, sorted[0], sorted[1], sorted[2]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUnspecifiedField_DefaultsToCreatedAt() { + ids := s.createSortTestSubjectConditionSets([]string{"unspecified-field-scs-0", "unspecified-field-scs-1", "unspecified-field-scs-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + // Field defaults to created_at, explicit ASC is preserved + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[0], ids[1], ids[2]) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUnspecifiedDirection_DefaultsToDESC() { + ids := s.createSortTestSubjectConditionSets([]string{"unspecified-dir-scs-0", "unspecified-dir-scs-1", "unspecified-dir-scs-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, }, }) - s.Require().Error(err) - s.Require().ErrorIs(err, db.ErrListLimitTooLarge) - s.Nil(listRsp) -} - -func (s *SubjectMappingsSuite) Test_ListSubjectConditionSet_Offset_Succeeds() { - req := &subjectmapping.ListSubjectConditionSetsRequest{} - totalListRsp, err := s.db.PolicyClient.ListSubjectConditionSets(context.Background(), req) s.Require().NoError(err) - s.NotNil(totalListRsp) + s.NotNil(listRsp) - totalList := totalListRsp.GetSubjectConditionSets() - s.NotEmpty(totalList) + // Direction defaults to DESC, explicit created_at field is preserved + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[2], ids[1], ids[0]) +} - // set the offset pagination - offset := 5 - req.Pagination = &policy.PageRequest{ - Offset: int32(offset), - } +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByBothUnspecified_DefaultsToCreatedAtDESC() { + ids := s.createSortTestSubjectConditionSets([]string{"both-unspecified-scs-0", "both-unspecified-scs-1", "both-unspecified-scs-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) - offetListRsp, err := s.db.PolicyClient.ListSubjectConditionSets(context.Background(), req) + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + }) s.Require().NoError(err) - s.NotNil(offetListRsp) + s.NotNil(listRsp) - offsetList := offetListRsp.GetSubjectConditionSets() - s.NotEmpty(offsetList) + // Both default: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[2], ids[1], ids[0]) +} - // length is reduced by the offset amount - s.Equal(len(offsetList), len(totalList)-offset) +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortOmitted() { + ids := s.createSortTestSubjectConditionSets([]string{"sort-omitted-scs-0", "sort-omitted-scs-1", "sort-omitted-scs-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) - // objects are equal between offset and original list beginning at offset index - for i, scs := range offsetList { - s.True(proto.Equal(scs, totalList[i+offset])) - } + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{}) + s.Require().NoError(err) + s.NotNil(listRsp) + + // No sort provided: created_at DESC + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[2], ids[1], ids[0]) } func (s *SubjectMappingsSuite) TestDeleteSubjectConditionSet() { @@ -982,7 +1692,7 @@ func (s *SubjectMappingsSuite) TestDeleteSubjectConditionSet() { }, } - created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet) + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet, "", "") s.Require().NoError(err) s.NotNil(created) @@ -1022,11 +1732,11 @@ func (s *SubjectMappingsSuite) TestDeleteAllUnmappedSubjectConditionSets() { }, } - unmapped, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newSCS) + unmapped, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newSCS, "", "") s.Require().NoError(err) s.NotNil(unmapped) - mapped, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newSCS) + mapped, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newSCS, "", "") s.Require().NoError(err) s.NotNil(mapped) @@ -1069,7 +1779,7 @@ func (s *SubjectMappingsSuite) TestUpdateSubjectConditionSet_NewSubjectSets() { {}, }, } - created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet) + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet, "", "") s.Require().NoError(err) s.NotNil(created) @@ -1125,7 +1835,7 @@ func (s *SubjectMappingsSuite) TestUpdateSubjectConditionSet_AllAllowedFields() }, } - created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet) + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet, "", "") s.Require().NoError(err) s.NotNil(created) @@ -1191,7 +1901,7 @@ func (s *SubjectMappingsSuite) TestUpdateSubjectConditionSet_ChangeOperator() { }, } - created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet) + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, newConditionSet, "", "") s.Require().NoError(err) s.NotNil(created) @@ -1477,7 +2187,7 @@ func (s *SubjectMappingsSuite) TestGetMatchedSubjectMappings_ConditionSetReusedB }, }, } - createdSCS, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, toCreate) + createdSCS, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, toCreate, "", "") s.Require().NoError(err) s.NotNil(createdSCS) @@ -1620,7 +2330,7 @@ func (s *SubjectMappingsSuite) TestGetMatchedSubjectMappings_ResponsiveToUpdatio }, } - createdSCS, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, subjectConditionSet) + createdSCS, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, subjectConditionSet, "", "") s.Require().NoError(err) s.NotNil(createdSCS) @@ -1730,7 +2440,7 @@ func (s *SubjectMappingsSuite) TestUpdateSubjectConditionSet_MetadataVariations( Metadata: &common.MetadataMutable{ Labels: labels, }, - }) + }, "", "") s.Require().NoError(err) s.NotNil(created) @@ -1776,3 +2486,461 @@ func (s *SubjectMappingsSuite) TestUpdateSubjectConditionSet_MetadataVariations( s.Equal(created.GetId(), got.GetId()) s.Equal(labels, got.GetMetadata().GetLabels()) } + +/*----------------------------------------------------------------- + *-------- Namespace Consistency Tests ---------------------------- + *----------------------------------------------------------------*/ + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_NamespacedById_AllSameNamespace_Succeeds() { + nsID := s.exampleComNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + scs := s.newSCSInNamespace(nsID) + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Name: "read"}}, + ExistingSubjectConditionSetId: scs.GetId(), + NamespaceId: nsID, + }) + s.Require().NoError(err) + s.NotNil(created) + + sm, err := s.db.PolicyClient.GetSubjectMapping(s.ctx, created.GetId()) + s.Require().NoError(err) + s.Equal(nsID, sm.GetNamespace().GetId()) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_NamespacedByFqn_Succeeds() { + nsID := s.exampleComNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + scs := s.newSCSInNamespace(nsID) + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Name: "read"}}, + ExistingSubjectConditionSetId: scs.GetId(), + NamespaceFqn: "https://example.com", + }) + s.Require().NoError(err) + s.NotNil(created) + + sm, err := s.db.PolicyClient.GetSubjectMapping(s.ctx, created.GetId()) + s.Require().NoError(err) + s.Equal(nsID, sm.GetNamespace().GetId()) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_AttributeValueWrongNamespace_Fails() { + nsID := s.exampleComNsID() + attrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value1").ID + scs := s.newSCSInNamespace(nsID) + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Name: "read"}}, + ExistingSubjectConditionSetId: scs.GetId(), + NamespaceId: nsID, + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_ExistingSCSWrongNamespace_Fails() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + scsInNet := s.newSCSInNamespace(netNsID) + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Name: "read"}}, + ExistingSubjectConditionSetId: scsInNet.GetId(), + NamespaceId: comNsID, + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_CustomActionWrongNamespace_Fails() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + scs := s.newSCSInNamespace(comNsID) + + customAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "wrong_ns_action", + NamespaceId: netNsID, + }) + s.Require().NoError(err) + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Id: customAction.GetId()}}, + ExistingSubjectConditionSetId: scs.GetId(), + NamespaceId: comNsID, + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_StandardActionById_WrongNamespace_Fails() { + nsID := s.exampleComNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + scs := s.newSCSInNamespace(nsID) + actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{actionRead}, + ExistingSubjectConditionSetId: scs.GetId(), + NamespaceId: nsID, + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_NamespacedSM_MixedExistingAndNewActions_Succeeds() { + nsID := s.exampleComNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + scs := s.newSCSInNamespace(nsID) + + // Create two actions in the correct namespace (will be referenced by ID) + existingAction1, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "mixed_existing_one", + NamespaceId: nsID, + }) + s.Require().NoError(err) + + existingAction2, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "mixed_existing_two", + NamespaceId: nsID, + }) + s.Require().NoError(err) + + // Third action is passed by name — should be created in the SM's namespace + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{ + {Id: existingAction1.GetId()}, + {Id: existingAction2.GetId()}, + {Name: "mixed_new_by_name"}, + }, + ExistingSubjectConditionSetId: scs.GetId(), + NamespaceId: nsID, + }) + s.Require().NoError(err) + s.NotNil(created) + + sm, err := s.db.PolicyClient.GetSubjectMapping(s.ctx, created.GetId()) + s.Require().NoError(err) + s.Equal(nsID, sm.GetNamespace().GetId()) + s.Require().Len(sm.GetActions(), 3) + + // Verify every action — including the one created by name — is in the correct namespace + for _, a := range sm.GetActions() { + s.Equal(nsID, a.GetNamespace().GetId(), "action %s should be in namespace %s", a.GetId(), nsID) + } +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_UnnamespacedSM_NamespacedAttributeValue_Succeeds() { + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) + fixtureScs := s.f.GetSubjectConditionSetKey("subject_condition_set1") + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{actionRead}, + ExistingSubjectConditionSetId: fixtureScs.ID, + }) + s.Require().NoError(err) + s.NotNil(created) + s.Nil(created.GetNamespace()) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectConditionSet_WithNamespaceId_Succeeds() { + nsID := s.exampleComNsID() + scs := s.newSCSInNamespace(nsID) + s.NotNil(scs) + s.Equal(nsID, scs.GetNamespace().GetId()) + s.Equal("https://example.com", scs.GetNamespace().GetFqn()) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectConditionSet_WithNamespaceFqn_Succeeds() { + netNsID := s.exampleNetNsID() + scs, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{{}}, + }, "", "https://example.net") + s.Require().NoError(err) + s.NotNil(scs) + s.Equal(netNsID, scs.GetNamespace().GetId()) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectConditionSet_WithoutNamespace_Succeeds() { + scs, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{{}}, + }, "", "") + s.Require().NoError(err) + s.NotNil(scs) + s.Nil(scs.GetNamespace()) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_NamespacedSM_UnnamespacedSCS_Fails() { + nsID := s.exampleComNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + + // Create an un-namespaced SCS + unnamespacedSCS, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".test_field", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"test_value"}, + }, + }, + }, + }, + }, + }, + }, "", "") + s.Require().NoError(err) + s.Nil(unnamespacedSCS.GetNamespace()) + + // Attempt to create a namespaced SM with the un-namespaced SCS + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Name: "read"}}, + ExistingSubjectConditionSetId: unnamespacedSCS.GetId(), + NamespaceId: nsID, + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_UnnamespacedSM_NamespacedSCS_Fails() { + nsID := s.exampleComNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + namespacedSCS := s.newSCSInNamespace(nsID) + + // Un-namespaced SM with a namespaced SCS should fail: SCS must be unnamespaced + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Name: "read"}}, + ExistingSubjectConditionSetId: namespacedSCS.GetId(), + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_UnnamespacedSM_NamespacedCustomAction_Fails() { + nsID := s.exampleComNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + fixtureScs := s.f.GetSubjectConditionSetKey("subject_condition_set1") + + // Create a namespaced custom action + customAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "unnamespaced_sm_ns_action", + NamespaceId: nsID, + }) + s.Require().NoError(err) + + // Un-namespaced SM with a namespaced action should fail + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Id: customAction.GetId()}}, + ExistingSubjectConditionSetId: fixtureScs.ID, + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_NamespacedSM_MultipleActions_OneMismatch_Fails() { + comNsID := s.exampleComNsID() + netNsID := s.exampleNetNsID() + attrValID := s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID + scs := s.newSCSInNamespace(comNsID) + + // Create one action in the correct namespace and one in the wrong namespace + goodAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "good_action_multi", + NamespaceId: comNsID, + }) + s.Require().NoError(err) + + badAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: "bad_action_multi", + NamespaceId: netNsID, + }) + s.Require().NoError(err) + + // Should fail because one of the actions belongs to a different namespace + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attrValID, + Actions: []*policy.Action{{Id: goodAction.GetId()}, {Id: badAction.GetId()}}, + ExistingSubjectConditionSetId: scs.GetId(), + NamespaceId: comNsID, + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNamespaceMismatch) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectConditionSet_InvalidNamespaceId_Fails() { + _, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{{}}, + }, "not-a-uuid", "") + s.Require().Error(err) + s.Require().ErrorIs(err, db.ErrUUIDInvalid) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectConditionSet_InvalidNamespaceFqn_FailsWithoutInsert() { + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{{}}, + }, "", "https://does-not-exist.example") + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNotFound) +} + +func (s *SubjectMappingsSuite) TestCreateSubjectMapping_InvalidNamespaceFqn_FailsWithoutInsert() { + actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) + + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: s.f.GetAttributeValueKey("example.com/attr/attr1/value/value1").ID, + Actions: []*policy.Action{actionRead}, + NamespaceFqn: "https://does-not-exist.example", + NewSubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{{}}, + }, + }) + s.Require().Error(err) + s.Nil(created) + s.Require().ErrorIs(err, db.ErrNotFound) +} + +func (s *SubjectMappingsSuite) exampleComNsID() string { + return s.f.GetNamespaceKey("example.com").ID +} + +func (s *SubjectMappingsSuite) exampleNetNsID() string { + return s.f.GetNamespaceKey("example.net").ID +} + +func (s *SubjectMappingsSuite) newSCSInNamespace(nsID string) *policy.SubjectConditionSet { + scs, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".test_field", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"test_value"}, + }, + }, + }, + }, + }, + }, + }, nsID, "") + s.Require().NoError(err) + return scs +} + +func (s *SubjectMappingsSuite) createSortTestSubjectMappings(prefixes []string) []string { + fixtureAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value2").ID + actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) + + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + email := fmt.Sprintf("%s-%d@example.com", prefix, time.Now().UnixNano()) + scs := &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".email", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{email}, + }, + }, + }, + }, + }, + }, + } + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fixtureAttrValID, + NewSubjectConditionSet: scs, + Actions: []*policy.Action{actionRead}, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + return ids +} + +func (s *SubjectMappingsSuite) createSortTestSubjectConditionSets(prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + val := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".sort_test", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{val}, + }, + }, + }, + }, + }, + }, + }, "", "") + s.Require().NoError(err) + ids[i] = created.GetId() + } + return ids +} + +// deleteSortTestSubjectMappings cleans up subject mappings created by sort tests. +func (s *SubjectMappingsSuite) deleteSortTestSubjectMappings(ids []string) { + for _, id := range ids { + _, err := s.db.PolicyClient.DeleteSubjectMapping(s.ctx, id) + s.Require().NoError(err) + } +} + +// deleteSortTestSubjectConditionSets cleans up subject condition sets created by sort tests. +func (s *SubjectMappingsSuite) deleteSortTestSubjectConditionSets(ids []string) { + for _, id := range ids { + _, err := s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, id) + s.Require().NoError(err) + } +} diff --git a/service/integration/utils.go b/service/integration/utils.go new file mode 100644 index 0000000000..27b5121906 --- /dev/null +++ b/service/integration/utils.go @@ -0,0 +1,56 @@ +package integration + +import ( + "context" + "fmt" + "testing" + + "github.com/opentdf/platform/service/internal/fixtures" + "github.com/stretchr/testify/require" +) + +// assertIDsInOrder verifies that the given IDs appear in the expected relative +// order within items, tolerating extra rows that don't match any target ID. +func assertIDsInOrder[T any](tb testing.TB, items []T, getID func(T) string, ids ...string) { + tb.Helper() + + targets := make(map[string]struct{}, len(ids)) + for _, id := range ids { + targets[id] = struct{}{} + } + + positions := make(map[string]int, len(ids)) + for i, item := range items { + id := getID(item) + if _, ok := targets[id]; ok { + positions[id] = i + } + } + + require.Len(tb, positions, len(ids)) + for i := 0; i < len(ids)-1; i++ { + require.Less(tb, positions[ids[i]], positions[ids[i+1]]) + } +} + +// forceDeleteRows hard-deletes rows by ID via raw SQL, bypassing the API's +// soft-delete/deactivate limitation for resources like namespaces and attributes. +func forceDeleteRows(ctx context.Context, db fixtures.DBInterface, table string, ids []string) error { + sql := fmt.Sprintf( + `DELETE FROM %s WHERE id = ANY($1::uuid[])`, + db.TableName(table), + ) + _, err := db.Client.Pgx.Exec(ctx, sql, ids) + return err +} + +// forceCreatedAtTie sets created_at to a fixed timestamp for the given IDs, +// guaranteeing that the ORDER BY tiebreaker (id ASC) determines sort order. +func forceCreatedAtTie(ctx context.Context, db fixtures.DBInterface, table string, ids []string) error { + sql := fmt.Sprintf( + `UPDATE %s SET created_at = '2000-01-01T00:00:00Z' WHERE id = ANY($1::uuid[])`, + db.TableName(table), + ) + _, err := db.Client.Pgx.Exec(ctx, sql, ids) + return err +} diff --git a/service/internal/access/v2/evaluate.go b/service/internal/access/v2/evaluate.go index e787fa62ec..3700916b65 100644 --- a/service/internal/access/v2/evaluate.go +++ b/service/internal/access/v2/evaluate.go @@ -8,6 +8,7 @@ import ( "slices" "strings" + "github.com/opentdf/platform/lib/identifier" authz "github.com/opentdf/platform/protocol/go/authorization/v2" "github.com/opentdf/platform/protocol/go/policy" attrs "github.com/opentdf/platform/protocol/go/policy/attributes" @@ -34,10 +35,12 @@ func getResourceDecision( entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, action *policy.Action, resource *authz.Resource, + namespacedPolicy bool, ) (*ResourceDecision, error) { var ( resourceID = resource.GetEphemeralId() registeredResourceValueFQN string + requiredNamespaceFqn *identifier.FullyQualifiedAttribute resourceAttributeValues *authz.Resource_AttributeValues failure = &ResourceDecision{ Entitled: false, @@ -45,7 +48,7 @@ func getResourceDecision( ResourceName: resourceID, } ) - if err := validateGetResourceDecision(entitlements, action, resource); err != nil { + if err := validateGetResourceDecision(entitlements, action, resource, namespacedPolicy); err != nil { return nil, err } @@ -75,14 +78,39 @@ func getResourceDecision( slog.Any("action_attribute_values", regResValue.GetActionAttributeValues()), ) + if namespacedPolicy { + // the parsing is validated in the validator, so ignoring error here + parsed, _ := identifier.Parse[*identifier.FullyQualifiedRegisteredResourceValue](registeredResourceValueFQN) + requiredNamespaceFqn = &identifier.FullyQualifiedAttribute{Namespace: parsed.Namespace} + } + resourceAttributeValues = &authz.Resource_AttributeValues{ Fqns: make([]string, 0), } for _, aav := range regResValue.GetActionAttributeValues() { aavAttrValueFQN := aav.GetAttributeValue().GetFqn() + // If namespaced policies are enabled, enforce that the attribute value FQN is in the same namespace as the registered resource value and extract the namespace ID for later checks. + // This is a fail safe, as RR and attr NS match should be enforced on creation and update of registered resources + // This ensures that only attribute values from the correct namespace are considered in the evaluation. + if namespacedPolicy { + parsed, err := identifier.Parse[*identifier.FullyQualifiedAttribute](aavAttrValueFQN) + if err != nil { + return nil, fmt.Errorf("invalid attribute value FQN [%s]: %w", aavAttrValueFQN, ErrInvalidResource) + } + if parsed.Namespace != requiredNamespaceFqn.Namespace { + return nil, fmt.Errorf("attribute value FQN [%s] namespace [%s] does not match RR namespace [%s]: %w", aavAttrValueFQN, parsed.Namespace, requiredNamespaceFqn.FQN(), ErrInvalidResource) + } + } // skip evaluating attribute rules on any action-attribute-values without the requested action - if aav.GetAction().GetName() != action.GetName() { + if !isRequestedActionMatch(ctx, l, action, + func() string { + if requiredNamespaceFqn != nil && requiredNamespaceFqn.Namespace != "" { + return requiredNamespaceFqn.FQN() + } + return "" + }(), aav.GetAction(), + namespacedPolicy) { continue } @@ -111,7 +139,7 @@ func getResourceDecision( return failure, nil } - return evaluateResourceAttributeValues(ctx, l, resourceAttributeValues, resourceID, registeredResourceValueFQN, action, entitlements, accessibleAttributeValues) + return evaluateResourceAttributeValues(ctx, l, resourceAttributeValues, resourceID, registeredResourceValueFQN, action, entitlements, accessibleAttributeValues, namespacedPolicy) } // evaluateResourceAttributeValues evaluates a list of attribute values against the action and entitlements @@ -125,6 +153,7 @@ func evaluateResourceAttributeValues( action *policy.Action, entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, accessibleAttributeValues map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue, + namespacedPolicy bool, ) (*ResourceDecision, error) { // Group value FQNs by parent definition definitionFqnToValueFqns := make(map[string][]string) @@ -167,7 +196,7 @@ func evaluateResourceAttributeValues( return nil, fmt.Errorf("%w: %s", ErrDefinitionNotFound, defFQN) } - dataRuleResult, err := evaluateDefinition(ctx, l, entitlements, action, resourceValueFQNs, definition) + dataRuleResult, err := evaluateDefinition(ctx, l, entitlements, action, resourceValueFQNs, definition, namespacedPolicy) if err != nil { return nil, errors.Join(ErrFailedEvaluation, err) } @@ -197,8 +226,10 @@ func evaluateDefinition( action *policy.Action, resourceValueFQNs []string, attrDefinition *policy.Attribute, + namespacedPolicy bool, ) (*DataRuleResult, error) { var entitlementFailures []EntitlementFailure + namespaceFQN := attrDefinition.GetNamespace().GetFqn() l = l.With("definitionRule", attrDefinition.GetRule().String()) l = l.With("definitionFQN", attrDefinition.GetFqn()) @@ -211,13 +242,13 @@ func evaluateDefinition( switch attrDefinition.GetRule() { case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF: - entitlementFailures = allOfRule(ctx, l, entitlements, action, resourceValueFQNs) + entitlementFailures = allOfRule(ctx, l, entitlements, action, resourceValueFQNs, namespaceFQN, namespacedPolicy) case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF: - entitlementFailures = anyOfRule(ctx, l, entitlements, action, resourceValueFQNs) + entitlementFailures = anyOfRule(ctx, l, entitlements, action, resourceValueFQNs, namespaceFQN, namespacedPolicy) case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY: - entitlementFailures = hierarchyRule(ctx, l, entitlements, action, resourceValueFQNs, attrDefinition) + entitlementFailures = hierarchyRule(ctx, l, entitlements, action, resourceValueFQNs, attrDefinition, namespaceFQN, namespacedPolicy) case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED: return nil, fmt.Errorf("%w: %s, rule: %s", ErrMissingRequiredSpecifiedRule, attrDefinition.GetFqn(), attrDefinition.GetRule().String()) @@ -247,11 +278,13 @@ func evaluateDefinition( // 1. For each resource attribute value FQN, the action is entitled // 2. If any FQN is not entitled, or the FQN is missing the requested action, the rule fails func allOfRule( - _ context.Context, - _ *logger.Logger, + ctx context.Context, + l *logger.Logger, entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, action *policy.Action, resourceValueFQNs []string, + requiredNamespaceFQN string, + namespacedPolicy bool, ) []EntitlementFailure { actionName := action.GetName() failures := make([]EntitlementFailure, 0, len(resourceValueFQNs)) // Pre-allocate for efficiency @@ -263,7 +296,7 @@ func allOfRule( // Check if this FQN has the entitled action if entitledActions, ok := entitlements[valueFQN]; ok { for _, entitledAction := range entitledActions { - if strings.EqualFold(entitledAction.GetName(), actionName) { + if isRequestedActionMatch(ctx, l, action, requiredNamespaceFQN, entitledAction, namespacedPolicy) { hasEntitlement = true break } @@ -287,11 +320,13 @@ func allOfRule( // 2. If none of the FQNs are found the entitlements, the rule fails // 3. If none of the matching FQNs in the entitlements contain the requested action, the rule fails func anyOfRule( - _ context.Context, - _ *logger.Logger, + ctx context.Context, + l *logger.Logger, entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, action *policy.Action, resourceValueFQNs []string, + requiredNamespaceFQN string, + namespacedPolicy bool, ) []EntitlementFailure { // No resources to check if len(resourceValueFQNs) == 0 { @@ -309,7 +344,7 @@ func anyOfRule( entitledActions, ok := entitlements[valueFQN] if ok { for _, entitledAction := range entitledActions { - if strings.EqualFold(entitledAction.GetName(), actionName) { + if isRequestedActionMatch(ctx, l, action, requiredNamespaceFQN, entitledAction, namespacedPolicy) { foundEntitlementForThisFQN = true anyEntitlementFound = true break @@ -344,6 +379,8 @@ func hierarchyRule( action *policy.Action, resourceValueFQNs []string, attrDefinition *policy.Attribute, + requiredNamespaceFQN string, + namespacedPolicy bool, ) []EntitlementFailure { // No resources to check if len(resourceValueFQNs) == 0 { @@ -374,7 +411,7 @@ func hierarchyRule( if idx, exists := valueFQNToIndex[entitlementFQN]; exists && idx <= lowestValueFQNIndex { // Check if the required action is entitled for _, entitledAction := range entitledActions { - if strings.EqualFold(entitledAction.GetName(), actionName) { + if isRequestedActionMatch(ctx, l, action, requiredNamespaceFQN, entitledAction, namespacedPolicy) { l.DebugContext(ctx, "hierarchy rule satisfied", slog.Group("entitled_by_value", slog.String("FQN", entitlementFQN), @@ -397,7 +434,7 @@ func hierarchyRule( foundValue := false if entitledActions, ok := entitlements[valueFQN]; ok { for _, entitledAction := range entitledActions { - if strings.EqualFold(entitledAction.GetName(), actionName) { + if isRequestedActionMatch(ctx, l, action, requiredNamespaceFQN, entitledAction, namespacedPolicy) { foundValue = true break } @@ -414,3 +451,95 @@ func hierarchyRule( return entitlementFailures } + +// This function checks if there are any conflicts between two actions based on their IDs and namespaces, and determines which action to prefer in case of a conflict. +// This is used when merging actions from different sources (e.g., direct entitlements and subject mapping) to ensure deterministic behavior. +// The requestedAction is the action from the access request, and the entitledAction is the action from the entitlements. +// The requiredNamespaceID and namespacedPolicy parameters are used to enforce namespace constraints if strict namespaced policies are enabled. +func isRequestedActionMatch(ctx context.Context, l *logger.Logger, requestedAction *policy.Action, requiredNamespaceFQN string, entitledAction *policy.Action, namespacedPolicy bool) bool { + if requestedAction == nil || entitledAction == nil { + return false + } + + // Action identity precedence for matching: + // 1) request action id (if present) is authoritative, + // 2) otherwise name (case-insensitive), + // 3) optional request namespace (id or fqn) further narrows matches. + // Note: API validation still requires request action name today; this logic + // defines matcher behavior when additional identity fields are present. + if requestedAction.GetId() != "" { + if requestedAction.GetId() != entitledAction.GetId() { + return false + } + } else { + if requestedAction.GetName() == "" || !strings.EqualFold(requestedAction.GetName(), entitledAction.GetName()) { + return false + } + } + + // If the caller explicitly provides a request action namespace, always enforce + // that identity constraint regardless of namespacedPolicy mode. + if requestNamespace := requestedAction.GetNamespace(); requestNamespace != nil && (requestNamespace.GetId() != "" || requestNamespace.GetFqn() != "") { + // the requested action has a namespace, so enforce that the entitled action also has a + // namespace and that they match on id if provided, otherwise fqn (case-insensitive) + entitledNamespace := entitledAction.GetNamespace() + if entitledNamespace == nil { + // the entitled action is missing a namespace while the request action has one, + // so this is a mismatch and we should not consider this a match + l.TraceContext(ctx, "action match request namespace mismatch", + slog.String("requested_action_namespace_id", requestNamespace.GetId()), + ) + return false + } + if requestNamespace.GetId() != "" && entitledNamespace.GetId() != requestNamespace.GetId() { + // the requested action namespace has an id and it does not match the entitled action namespace id, + // so this is a mismatch and we should not consider this a match + l.TraceContext(ctx, "action match request namespace mismatch", + slog.String("requested_action_namespace_id", requestNamespace.GetId()), + slog.String("candidate_namespace_id", entitledNamespace.GetId()), + ) + return false + } + if requestNamespace.GetId() == "" && requestNamespace.GetFqn() != "" && !strings.EqualFold(entitledNamespace.GetFqn(), requestNamespace.GetFqn()) { + // the requested action namespace has an FQN and it does not match the entitled action namespace FQN, + // so this is a mismatch and we should not consider this a match + l.TraceContext(ctx, "action match request namespace mismatch", + slog.String("requested_action_namespace_fqn", requestNamespace.GetFqn()), + slog.String("candidate_namespace_fqn", entitledNamespace.GetFqn()), + ) + return false + } + } + + if !namespacedPolicy { + return true + } + + // Strict namespaced-policy mode requires a resolved target namespace from + // the resource/definition context and a namespaced entitled action. + if requiredNamespaceFQN == "" { + // we are in strict namespaced policy mode but do not have a required namespace from the resource context + // this should not happen, it should be caught upstream + l.TraceContext(ctx, "action match strict namespace mismatch, required_namespace is empty") + return false + } + + entitledNamespace := entitledAction.GetNamespace() + if entitledNamespace == nil || entitledNamespace.GetId() == "" { + // the entitled action is missing a namespace while strict namespaced policy mode requires it + l.TraceContext(ctx, "action match strict namespace mismatch", + slog.String("required_namespace", requiredNamespaceFQN), + ) + return false + } + if entitledNamespace.GetFqn() != requiredNamespaceFQN { + // the entitled action namespace FQN does not match the required namespace FQN from the resource context + l.TraceContext(ctx, "action match strict namespace mismatch", + slog.String("required_namespace", requiredNamespaceFQN), + slog.String("candidate_namespace", entitledNamespace.GetFqn()), + ) + return false + } + + return true +} diff --git a/service/internal/access/v2/evaluate_test.go b/service/internal/access/v2/evaluate_test.go index 9e096afe1a..5131fb4c6a 100644 --- a/service/internal/access/v2/evaluate_test.go +++ b/service/internal/access/v2/evaluate_test.go @@ -1,9 +1,12 @@ package access import ( + "context" "strings" "testing" + "github.com/opentdf/platform/lib/identifier" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" authz "github.com/opentdf/platform/protocol/go/authorization/v2" @@ -17,7 +20,7 @@ import ( // Constants for namespaces and attribute FQNs var ( // Base namespaces - baseNamespace = "https://namespace.com" + baseNamespace = "namespace.com" levelFQN = createAttrFQN(baseNamespace, "level") departmentFQN = createAttrFQN(baseNamespace, "department") projectFQN = createAttrFQN(baseNamespace, "project") @@ -41,8 +44,9 @@ var ( projectFantasicFourFQN = createAttrValueFQN(baseNamespace, "project", "fantasticfour") // Registered resource values - netRegResValFQN = createRegisteredResourceValueFQN("network", "external") - platRegResValFQN = createRegisteredResourceValueFQN("platform", "internal") + netRegResValFQN = createRegisteredResourceValueFQN("", "network", "external") + platRegResValFQN = createRegisteredResourceValueFQN("", "platform", "internal") + ns1NetRegResValFQN = createRegisteredResourceValueFQN("ns1", "network", "external") ) var ( @@ -205,6 +209,33 @@ func (s *EvaluateTestSuite) SetupTest() { }, }, }, + ns1NetRegResValFQN: { + Resource: &policy.RegisteredResource{ + Namespace: &policy.Namespace{ + Id: "ns1", + }, + }, + Id: "ns1-network-registered-res-id", + Value: "external", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Id: "ns1-network-action-attr-val-1", + Action: actionRead, + AttributeValue: &policy.Value{ + Fqn: levelHighestFQN, + Value: "highest", + }, + }, + { + Id: "ns1-network-action-attr-val-2", + Action: actionCreate, + AttributeValue: &policy.Value{ + Fqn: levelMidFQN, + Value: "mid", + }, + }, + }, + }, } } @@ -304,7 +335,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { for _, tc := range tests { s.Run(tc.name, func() { - failures := allOfRule(s.T().Context(), s.logger, tc.entitlements, s.action, tc.resourceValueFQNs) + failures := allOfRule(s.T().Context(), s.logger, tc.entitlements, s.action, tc.resourceValueFQNs, "", false) s.Len(failures, tc.expectedFailures) @@ -438,7 +469,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { for _, tc := range tests { s.Run(tc.name, func() { // Execute - failures := anyOfRule(s.T().Context(), s.logger, tc.entitlements, s.action, tc.resourceValueFQNs) + failures := anyOfRule(s.T().Context(), s.logger, tc.entitlements, s.action, tc.resourceValueFQNs, "", false) // Assert if tc.expectedFailCount == 0 { @@ -618,7 +649,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { for _, tc := range tests { s.Run(tc.name, func() { // Execute - failures := hierarchyRule(s.T().Context(), s.logger, tc.entitlements, s.action, tc.resourceValueFQNs, s.hierarchicalClassAttr) + failures := hierarchyRule(s.T().Context(), s.logger, tc.entitlements, s.action, tc.resourceValueFQNs, s.hierarchicalClassAttr, "", false) // Assert if tc.expectedFailures { @@ -698,7 +729,7 @@ func (s *EvaluateTestSuite) TestEvaluateDefinition() { for _, tc := range tests { s.Run(tc.name, func() { - result, err := evaluateDefinition(s.T().Context(), s.logger, tc.entitlements, s.action, tc.resourceValues, tc.definition) + result, err := evaluateDefinition(s.T().Context(), s.logger, tc.entitlements, s.action, tc.resourceValues, tc.definition, false) if tc.expectError { s.Require().Error(err) @@ -711,6 +742,193 @@ func (s *EvaluateTestSuite) TestEvaluateDefinition() { } } +func (s *EvaluateTestSuite) TestEvaluateDefinition_NamespacedPolicy() { + namespaceA := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://ns-a.example.com"} + namespaceB := &policy.Namespace{Id: "22222222-2222-2222-2222-222222222222", Fqn: "https://ns-b.example.com"} + + allOfDef := &policy.Attribute{ + Id: "all-of-def", + Fqn: projectFQN, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + Namespace: namespaceA, + Values: []*policy.Value{ + {Fqn: projectAvengersFQN}, + {Fqn: projectJusticeLeagueFQN}, + }, + } + + anyOfDef := &policy.Attribute{ + Id: "any-of-def", + Fqn: departmentFQN, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + Namespace: namespaceA, + Values: []*policy.Value{ + {Fqn: deptFinanceFQN}, + {Fqn: deptMarketingFQN}, + }, + } + + hierarchyDef := &policy.Attribute{ + Id: "hierarchy-def", + Fqn: levelFQN, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + Namespace: namespaceA, + Values: []*policy.Value{ + {Fqn: levelHighestFQN}, + {Fqn: levelUpperMidFQN}, + {Fqn: levelMidFQN}, + }, + } + + tests := []struct { + name string + definition *policy.Attribute + resourceFQNs []string + entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + requested *policy.Action + namespaced bool + expectPass bool + expectErr error + errorContains string + }{ + { + name: "all_of strict pass with matching namespace", + definition: allOfDef, + resourceFQNs: []string{ + projectAvengersFQN, + projectJusticeLeagueFQN, + }, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + projectAvengersFQN: {{Name: actions.ActionNameRead, Namespace: namespaceA}}, + projectJusticeLeagueFQN: {{Name: actions.ActionNameRead, Namespace: namespaceA}}, + }, + requested: &policy.Action{Name: actions.ActionNameRead}, + namespaced: true, + expectPass: true, + }, + { + name: "all_of strict fail with wrong namespace", + definition: allOfDef, + resourceFQNs: []string{ + projectAvengersFQN, + projectJusticeLeagueFQN, + }, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + projectAvengersFQN: {{Name: actions.ActionNameRead, Namespace: namespaceB}}, + projectJusticeLeagueFQN: {{Name: actions.ActionNameRead, Namespace: namespaceB}}, + }, + requested: &policy.Action{Name: actions.ActionNameRead}, + namespaced: true, + expectPass: false, + }, + { + name: "any_of strict pass when one namespace-matching action exists", + definition: anyOfDef, + resourceFQNs: []string{ + deptFinanceFQN, + deptMarketingFQN, + }, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + deptFinanceFQN: {{Name: actions.ActionNameRead, Namespace: namespaceB}}, + deptMarketingFQN: {{Name: actions.ActionNameRead, Namespace: namespaceA}}, + }, + requested: &policy.Action{Name: actions.ActionNameRead}, + namespaced: true, + expectPass: true, + }, + { + name: "hierarchy strict pass with higher entitled value in same namespace", + definition: hierarchyDef, + resourceFQNs: []string{ + levelUpperMidFQN, + levelMidFQN, + }, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + levelHighestFQN: {{Name: actions.ActionNameRead, Namespace: namespaceA}}, + }, + requested: &policy.Action{Name: actions.ActionNameRead}, + namespaced: true, + expectPass: true, + }, + { + name: "strict mode fails when definition namespace missing", + definition: &policy.Attribute{Id: "def-no-ns", Fqn: levelFQN, Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, Values: []*policy.Value{{Fqn: levelMidFQN}}}, + resourceFQNs: []string{ + levelMidFQN, + }, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + levelMidFQN: {{Name: actions.ActionNameRead, Namespace: namespaceA}}, + }, + requested: &policy.Action{Name: actions.ActionNameRead}, + namespaced: true, + expectPass: false, + }, + { + name: "request action namespace filter is enforced", + definition: anyOfDef, + resourceFQNs: []string{ + deptFinanceFQN, + }, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + deptFinanceFQN: {{Name: actions.ActionNameRead, Namespace: namespaceA}}, + }, + requested: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceB}, + namespaced: false, + expectPass: false, + }, + { + name: "request action id precedence is enforced", + definition: anyOfDef, + resourceFQNs: []string{ + deptFinanceFQN, + }, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + deptFinanceFQN: {{Id: "entitled-id", Name: actions.ActionNameRead, Namespace: namespaceA}}, + }, + requested: &policy.Action{Id: "requested-id", Name: actions.ActionNameRead}, + namespaced: true, + expectPass: false, + }, + { + name: "unspecified rule returns expected error", + definition: &policy.Attribute{Fqn: levelFQN, Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED, Values: []*policy.Value{{Fqn: levelMidFQN}}}, + resourceFQNs: []string{levelMidFQN}, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + requested: &policy.Action{Name: actions.ActionNameRead}, + namespaced: true, + expectErr: ErrMissingRequiredSpecifiedRule, + errorContains: "cannot be unspecified", + }, + { + name: "unknown rule returns expected error", + definition: &policy.Attribute{Fqn: levelFQN, Rule: policy.AttributeRuleTypeEnum(999)}, + resourceFQNs: []string{levelMidFQN}, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + requested: &policy.Action{Name: actions.ActionNameRead}, + namespaced: true, + expectErr: ErrUnrecognizedRule, + errorContains: "unrecognized", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + result, err := evaluateDefinition(s.T().Context(), s.logger, tc.entitlements, tc.requested, tc.resourceFQNs, tc.definition, tc.namespaced) + + if tc.expectErr != nil { + s.Require().Error(err) + s.Require().ErrorIs(err, tc.expectErr) + s.ErrorContains(err, tc.errorContains) + return + } + + s.Require().NoError(err) + s.Require().NotNil(result) + s.Equal(tc.expectPass, result.Passed) + }) + } +} + // Test cases for evaluateResourceAttributeValues func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() { tests := []struct { @@ -792,6 +1010,7 @@ func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() { s.action, tc.entitlements, s.accessibleAttrValues, + false, ) if tc.expectError { @@ -827,12 +1046,13 @@ func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() { // Test cases for getResourceDecision func (s *EvaluateTestSuite) TestGetResourceDecision() { - nonExistentRegResValueFQN := createRegisteredResourceValueFQN("nonexistent", "value") + nonExistentRegResValueFQN := createRegisteredResourceValueFQN("", "nonexistent", "value") tests := []struct { name string resource *authz.Resource entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + namespaced bool expectError bool expectPass bool }{ @@ -849,6 +1069,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ levelMidFQN: []*policy.Action{actionRead}, }, + namespaced: false, expectError: false, expectPass: true, }, @@ -863,6 +1084,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ levelHighestFQN: []*policy.Action{actionRead}, }, + namespaced: false, expectError: false, expectPass: true, }, @@ -878,6 +1100,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { projectAvengersFQN: []*policy.Action{actionRead}, projectJusticeLeagueFQN: []*policy.Action{actionRead}, }, + namespaced: false, expectError: false, expectPass: true, }, @@ -893,6 +1116,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { // Missing projectJusticeLeagueFQN projectAvengersFQN: []*policy.Action{actionRead}, }, + namespaced: false, expectError: false, expectPass: false, // Missing entitlement for projectJusticeLeagueFQN }, @@ -908,9 +1132,22 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { // Wrong action levelHighestFQN: []*policy.Action{actionCreate}, }, + namespaced: false, expectError: false, expectPass: false, }, + { + name: "registered resource value unnamespaced in strict mode", + resource: &authz.Resource{ + Resource: &authz.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: createRegisteredResourceValueFQN("", "network", "external"), + }, + EphemeralId: "test-reg-res-id-unnamespaced-strict", + }, + entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + namespaced: true, + expectError: true, + }, { name: "nonexistent registered resource value - should DENY", resource: &authz.Resource{ @@ -920,6 +1157,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { EphemeralId: "test-reg-res-id-5", }, entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + namespaced: false, expectError: false, expectPass: false, }, @@ -936,6 +1174,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { EphemeralId: "test-attr-missing-fqns", }, entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + namespaced: false, expectError: false, expectPass: false, }, @@ -952,6 +1191,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { EphemeralId: "test-attr-missing-fqns", }, entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + namespaced: false, expectError: false, expectPass: false, }, @@ -968,6 +1208,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { EphemeralId: "test-attr-missing-fqns", }, entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + namespaced: false, expectError: false, expectPass: false, }, @@ -987,6 +1228,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ levelMidFQN: []*policy.Action{actionRead}, }, + namespaced: false, expectError: false, expectPass: false, }, @@ -994,6 +1236,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { name: "invalid nil resource", resource: nil, entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + namespaced: false, expectError: true, }, { @@ -1007,6 +1250,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ levelHighestFQN: []*policy.Action{actionRead}, }, + namespaced: false, expectError: false, expectPass: true, }, @@ -1022,6 +1266,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { tc.entitlements, s.action, tc.resource, + tc.namespaced, ) if tc.expectError { @@ -1094,6 +1339,7 @@ func (s *EvaluateTestSuite) Test_getResourceDecision_MultiResources_GranularDeni entitlements, s.action, tc.resource, + false, ) s.Require().NoError(err, "Should not error for resource: %s", tc.name) @@ -1102,3 +1348,283 @@ func (s *EvaluateTestSuite) Test_getResourceDecision_MultiResources_GranularDeni }) } } + +func (s *EvaluateTestSuite) Test_getResourceDecision_StrictMode_DeniesOnActionNamespaceMismatch() { + namespaceA := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://ns-a.example.com"} + namespaceB := &policy.Namespace{Id: "22222222-2222-2222-2222-222222222222", Fqn: "https://ns-b.example.com"} + + s.accessibleAttrValues[projectAvengersFQN].Attribute.Namespace = namespaceA + + resource := &authz.Resource{ + Resource: &authz.Resource_AttributeValues_{ + AttributeValues: &authz.Resource_AttributeValues{Fqns: []string{projectAvengersFQN}}, + }, + EphemeralId: "ns-mismatch-resource", + } + + entitlements := subjectmappingbuiltin.AttributeValueFQNsToActions{ + projectAvengersFQN: { + {Name: actions.ActionNameRead, Namespace: namespaceB}, + }, + } + + decision, err := getResourceDecision( + s.T().Context(), + s.logger, + s.accessibleAttrValues, + s.accessibleRegisteredResourceValues, + entitlements, + actionRead, + resource, + true, + ) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.False(decision.Entitled, "desired namespaced-policy behavior: same-name action in wrong namespace should deny") +} + +func (s *EvaluateTestSuite) Test_getResourceDecision_StrictMode_RegisteredResourceFiltersAAVByNamespace() { + namespaceA := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://ns-a.example.com"} + namespaceB := &policy.Namespace{Id: "22222222-2222-2222-2222-222222222222", Fqn: "https://ns-b.example.com"} + namespacedRegResFQN := (&identifier.FullyQualifiedRegisteredResourceValue{ + Namespace: "namespace.com", + Name: "network", + Value: "external", + }).FQN() + + s.accessibleAttrValues[levelHighestFQN].Attribute.Namespace = namespaceA + regResValue := s.accessibleRegisteredResourceValues[netRegResValFQN] + regResValue.Resource = &policy.RegisteredResource{ + Name: "network", + Namespace: namespaceA, + } + regResValue.ActionAttributeValues = []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Id: "network-action-attr-val-wrong-ns", + Action: &policy.Action{ + Name: actions.ActionNameRead, + Namespace: namespaceB, + }, + AttributeValue: &policy.Value{Fqn: levelHighestFQN, Value: "highest"}, + }, + } + s.accessibleRegisteredResourceValues[namespacedRegResFQN] = regResValue + + resource := &authz.Resource{ + Resource: &authz.Resource_RegisteredResourceValueFqn{RegisteredResourceValueFqn: namespacedRegResFQN}, + EphemeralId: "rr-ns-mismatch-resource", + } + + entitlements := subjectmappingbuiltin.AttributeValueFQNsToActions{ + levelHighestFQN: { + {Name: actions.ActionNameRead, Namespace: namespaceA}, + }, + } + + decision, err := getResourceDecision( + s.T().Context(), + s.logger, + s.accessibleAttrValues, + s.accessibleRegisteredResourceValues, + entitlements, + actionRead, + resource, + true, + ) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.False(decision.Entitled, "desired namespaced-policy behavior: RR AAV action namespace must match evaluated namespace") +} + +func (s *EvaluateTestSuite) Test_getResourceDecision_RequestActionIDPrecedence() { + namespaceA := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://namespace.com"} + + s.accessibleAttrValues[projectAvengersFQN].Attribute.Namespace = namespaceA + + resource := &authz.Resource{ + Resource: &authz.Resource_AttributeValues_{ + AttributeValues: &authz.Resource_AttributeValues{Fqns: []string{projectAvengersFQN}}, + }, + EphemeralId: "request-action-id-precedence", + } + + entitlements := subjectmappingbuiltin.AttributeValueFQNsToActions{ + projectAvengersFQN: { + {Id: "entitled-id", Name: actions.ActionNameRead, Namespace: namespaceA}, + }, + } + + decision, err := getResourceDecision( + s.T().Context(), + s.logger, + s.accessibleAttrValues, + s.accessibleRegisteredResourceValues, + entitlements, + &policy.Action{Id: "different-id", Name: actions.ActionNameRead}, + resource, + true, + ) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.False(decision.Entitled, "requested action id should take precedence over name match") +} + +func (s *EvaluateTestSuite) Test_getResourceDecision_StrictMode_ErrorsOnAAVNamespaceMismatch() { + namespaceA := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://namespace.com"} + namespacedRegResFQN := (&identifier.FullyQualifiedRegisteredResourceValue{ + Namespace: "namespace.com", + Name: "network", + Value: "external", + }).FQN() + + knownFQN := levelHighestFQN + unknownFQN := "https://unknown.example.com/attr/missing/value/x" + + s.accessibleAttrValues[knownFQN].Attribute.Namespace = namespaceA + regResValue := s.accessibleRegisteredResourceValues[netRegResValFQN] + regResValue.Resource = &policy.RegisteredResource{ + Name: "network", + Namespace: namespaceA, + } + regResValue.ActionAttributeValues = []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Id: "rr-aav-known", + Action: &policy.Action{ + Name: actions.ActionNameRead, + Namespace: namespaceA, + }, + AttributeValue: &policy.Value{Fqn: knownFQN, Value: "highest"}, + }, + { + Id: "rr-aav-unknown", + Action: &policy.Action{ + Name: actions.ActionNameRead, + Namespace: namespaceA, + }, + AttributeValue: &policy.Value{Fqn: unknownFQN, Value: "x"}, + }, + } + s.accessibleRegisteredResourceValues[namespacedRegResFQN] = regResValue + + resource := &authz.Resource{ + Resource: &authz.Resource_RegisteredResourceValueFqn{RegisteredResourceValueFqn: namespacedRegResFQN}, + EphemeralId: "rr-aav-unresolvable-ns", + } + + entitlements := subjectmappingbuiltin.AttributeValueFQNsToActions{ + knownFQN: { + {Name: actions.ActionNameRead, Namespace: namespaceA}, + }, + } + + decision, err := getResourceDecision( + s.T().Context(), + s.logger, + s.accessibleAttrValues, + s.accessibleRegisteredResourceValues, + entitlements, + actionRead, + resource, + true, + ) + + s.Require().Error(err) + s.Nil(decision) +} + +func Test_isRequestedActionMatch(t *testing.T) { + namespaceA := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://ns-a.example.com"} + namespaceB := &policy.Namespace{Id: "22222222-2222-2222-2222-222222222222", Fqn: "https://ns-b.example.com"} + + tests := []struct { + name string + requestedAction *policy.Action + requiredNamespace string + entitledAction *policy.Action + namespacedPolicy bool + expectedActionMatch bool + }{ + { + name: "nil requested action", + requestedAction: nil, + requiredNamespace: namespaceA.GetFqn(), + entitledAction: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceA}, + namespacedPolicy: true, + expectedActionMatch: false, + }, + { + name: "id precedence denies same-name different-id", + requestedAction: &policy.Action{Id: "requested-id", Name: actions.ActionNameRead}, + requiredNamespace: namespaceA.GetFqn(), + entitledAction: &policy.Action{Id: "entitled-id", Name: actions.ActionNameRead, Namespace: namespaceA}, + namespacedPolicy: true, + expectedActionMatch: false, + }, + { + name: "name is case-insensitive in legacy mode", + requestedAction: &policy.Action{Name: "READ"}, + requiredNamespace: namespaceA.GetFqn(), + entitledAction: &policy.Action{Name: "read"}, + namespacedPolicy: false, + expectedActionMatch: true, + }, + { + name: "request namespace id must match entitled namespace id", + requestedAction: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceA}, + requiredNamespace: namespaceA.GetFqn(), + entitledAction: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceB}, + namespacedPolicy: false, + expectedActionMatch: false, + }, + { + name: "request namespace fqn must match entitled namespace fqn", + requestedAction: &policy.Action{Name: actions.ActionNameRead, Namespace: &policy.Namespace{Fqn: "https://ns-a.example.com"}}, + requiredNamespace: namespaceA.GetFqn(), + entitledAction: &policy.Action{Name: actions.ActionNameRead, Namespace: &policy.Namespace{Id: namespaceA.GetId(), Fqn: "HTTPS://NS-A.EXAMPLE.COM"}}, + namespacedPolicy: false, + expectedActionMatch: true, + }, + { + name: "strict mode requires required namespace id", + requestedAction: &policy.Action{Name: actions.ActionNameRead}, + requiredNamespace: "", + entitledAction: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceA}, + namespacedPolicy: true, + expectedActionMatch: false, + }, + { + name: "strict mode requires entitled action namespace id", + requestedAction: &policy.Action{Name: actions.ActionNameRead}, + requiredNamespace: namespaceA.GetFqn(), + entitledAction: &policy.Action{Name: actions.ActionNameRead}, + namespacedPolicy: true, + expectedActionMatch: false, + }, + { + name: "strict mode allows matching namespace id", + requestedAction: &policy.Action{Name: actions.ActionNameRead}, + requiredNamespace: namespaceA.GetFqn(), + entitledAction: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceA}, + namespacedPolicy: true, + expectedActionMatch: true, + }, + { + name: "strict mode denies mismatched namespace id", + requestedAction: &policy.Action{Name: actions.ActionNameRead}, + requiredNamespace: namespaceA.GetFqn(), + entitledAction: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceB}, + namespacedPolicy: true, + expectedActionMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matched := isRequestedActionMatch(context.Background(), logger.CreateTestLogger(), tt.requestedAction, tt.requiredNamespace, tt.entitledAction, tt.namespacedPolicy) + assert.Equal(t, tt.expectedActionMatch, matched) + }) + } +} diff --git a/service/internal/access/v2/helpers_test.go b/service/internal/access/v2/helpers_test.go index d891c6a8e1..3862c6f8b0 100644 --- a/service/internal/access/v2/helpers_test.go +++ b/service/internal/access/v2/helpers_test.go @@ -391,19 +391,19 @@ func TestPopulateHigherValuesIfHierarchy(t *testing.T) { valueSecret := &policy.Value{ Fqn: exampleSecretFQN, - SubjectMappings: []*policy.SubjectMapping{createSimpleSubjectMapping(exampleSecretFQN, "secret", []*policy.Action{actionRead}, ".test", []string{"value"})}, + SubjectMappings: []*policy.SubjectMapping{createSimpleSubjectMapping(exampleSecretFQN, "secret", []*policy.Action{actionRead}, ".test", []string{"value"}, nil)}, } valueRestricted := &policy.Value{ Fqn: exampleRestrictedFQN, - SubjectMappings: []*policy.SubjectMapping{createSimpleSubjectMapping(exampleSecretFQN, "restricted", []*policy.Action{actionRead}, ".test", []string{"somethingelse"})}, + SubjectMappings: []*policy.SubjectMapping{createSimpleSubjectMapping(exampleRestrictedFQN, "restricted", []*policy.Action{actionRead}, ".test", []string{"somethingelse"}, nil)}, } valueConf := &policy.Value{ Fqn: exampleConfidentialFQN, - SubjectMappings: []*policy.SubjectMapping{createSimpleSubjectMapping(exampleConfidentialFQN, "confidential", []*policy.Action{actionRead}, ".hello", []string{"world"})}, + SubjectMappings: []*policy.SubjectMapping{createSimpleSubjectMapping(exampleConfidentialFQN, "confidential", []*policy.Action{actionRead}, ".hello", []string{"world"}, nil)}, } valuePublic := &policy.Value{ Fqn: examplePublicFQN, - SubjectMappings: []*policy.SubjectMapping{createSimpleSubjectMapping(examplePublicFQN, "public", []*policy.Action{actionRead}, ".goodnight", []string{"moon"})}, + SubjectMappings: []*policy.SubjectMapping{createSimpleSubjectMapping(examplePublicFQN, "public", []*policy.Action{actionRead}, ".goodnight", []string{"moon"}, nil)}, } values := []*policy.Value{valueSecret, valueRestricted, valueConf, valuePublic} diff --git a/service/internal/access/v2/just_in_time_pdp.go b/service/internal/access/v2/just_in_time_pdp.go index 00824ed5f1..d90addfd4a 100644 --- a/service/internal/access/v2/just_in_time_pdp.go +++ b/service/internal/access/v2/just_in_time_pdp.go @@ -50,6 +50,7 @@ func NewJustInTimePDP( sdk *otdfSDK.SDK, store EntitlementPolicyStore, allowDirectEntitlements bool, + namespacedPolicy bool, ) (*JustInTimePDP, error) { var err error @@ -91,7 +92,7 @@ func NewJustInTimePDP( return nil, fmt.Errorf("failed to fetch all obligations: %w", err) } - pdp, err := NewPolicyDecisionPoint(ctx, log, allAttributes, allSubjectMappings, allRegisteredResources, allowDirectEntitlements) + pdp, err := NewPolicyDecisionPoint(ctx, log, allAttributes, allSubjectMappings, allRegisteredResources, allowDirectEntitlements, namespacedPolicy) if err != nil { return nil, fmt.Errorf("failed to create new policy decision point: %w", err) } diff --git a/service/internal/access/v2/pdp.go b/service/internal/access/v2/pdp.go index 06979e01c7..784ea6988e 100644 --- a/service/internal/access/v2/pdp.go +++ b/service/internal/access/v2/pdp.go @@ -7,6 +7,7 @@ import ( "log/slog" "slices" "strconv" + "strings" "github.com/opentdf/platform/lib/identifier" authz "github.com/opentdf/platform/protocol/go/authorization/v2" @@ -61,6 +62,7 @@ type PolicyDecisionPoint struct { allRegisteredResourceValuesByFQN map[string]*policy.RegisteredResourceValue allAttributesByDefinitionFQN map[string]*policy.Attribute allowDirectEntitlements bool + namespacedPolicy bool } var ( @@ -82,6 +84,7 @@ func NewPolicyDecisionPoint( allSubjectMappings []*policy.SubjectMapping, allRegisteredResources []*policy.RegisteredResource, allowDirectEntitlements bool, + namespacedPolicy bool, ) (*PolicyDecisionPoint, error) { var err error @@ -124,6 +127,20 @@ func NewPolicyDecisionPoint( ) continue } + + if namespacedPolicy { + ns := sm.GetNamespace() + if ns == nil || (ns.GetId() == "" && ns.GetFqn() == "") { + l.TraceContext(ctx, + "unnamespaced subject mapping in strict namespaced-policy mode - skipping", + slog.String("reason", "subject_mapping_namespace_missing"), + slog.String("subject_mapping_id", sm.GetId()), + slog.String("mapped_value_fqn", sm.GetAttributeValue().GetFqn()), + ) + continue + } + } + mappedValue := sm.GetAttributeValue() mappedValueFQN := mappedValue.GetFqn() if _, ok := allEntitleableAttributesByValueFQN[mappedValueFQN]; ok { @@ -155,11 +172,22 @@ func NewPolicyDecisionPoint( return nil, fmt.Errorf("invalid registered resource value: %w", err) } + namespaceName := namespaceNameFromPolicyNamespace(rr.GetNamespace()) + fullyQualifiedValue := identifier.FullyQualifiedRegisteredResourceValue{ - Name: rrName, - Value: v.GetValue(), + Namespace: namespaceName, + Name: rrName, + Value: v.GetValue(), } allRegisteredResourceValuesByFQN[fullyQualifiedValue.FQN()] = v + + if !namespacedPolicy { + legacyQualifiedValue := identifier.FullyQualifiedRegisteredResourceValue{ + Name: rrName, + Value: v.GetValue(), + } + allRegisteredResourceValuesByFQN[legacyQualifiedValue.FQN()] = v + } } } @@ -169,10 +197,32 @@ func NewPolicyDecisionPoint( allRegisteredResourceValuesByFQN, allAttributesByDefinitionFQN, allowDirectEntitlements, + namespacedPolicy, } return pdp, nil } +func namespaceNameFromPolicyNamespace(ns *policy.Namespace) string { + if ns == nil { + return "" + } + + if ns.GetName() != "" { + return ns.GetName() + } + + if ns.GetFqn() == "" { + return "" + } + + parsed, err := identifier.Parse[*identifier.FullyQualifiedAttribute](ns.GetFqn()) + if err != nil { + return "" + } + + return parsed.Namespace +} + // GetDecision evaluates the action on the resources for the entity and returns a decision along with entitlements. func (p *PolicyDecisionPoint) GetDecision( ctx context.Context, @@ -189,7 +239,15 @@ func (p *PolicyDecisionPoint) GetDecision( } // Filter all attributes down to only those that relevant to the entitlement decisioning of these specific resources - decisionableAttributes, err := getResourceDecisionableAttributes(ctx, l, p.allRegisteredResourceValuesByFQN, p.allEntitleableAttributesByValueFQN, p.allAttributesByDefinitionFQN /* action, */, resources, p.allowDirectEntitlements) + decisionableAttributes, err := getResourceDecisionableAttributes( + ctx, + l, + p.allRegisteredResourceValuesByFQN, + p.allEntitleableAttributesByValueFQN, + p.allAttributesByDefinitionFQN, /* action, */ + resources, + p.allowDirectEntitlements, + ) if err != nil { if !errors.Is(err, ErrFQNNotFound) { return nil, nil, fmt.Errorf("error getting decisionable attributes: %w", err) @@ -200,7 +258,7 @@ func (p *PolicyDecisionPoint) GetDecision( l.DebugContext(ctx, "filtered to only entitlements relevant to decisioning", slog.Int("decisionable_attribute_values_count", len(decisionableAttributes))) // Resolve them to their entitled FQNs and the actions available on each - entitledFQNsToActions, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(decisionableAttributes, entityRepresentation) + entitledFQNsToActions, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(decisionableAttributes, entityRepresentation, l.Logger) if err != nil { return nil, nil, fmt.Errorf("error evaluating subject mappings for entitlement: %w", err) } @@ -214,10 +272,29 @@ func (p *PolicyDecisionPoint) GetDecision( for _, directEntitlement := range entityRepresentation.GetDirectEntitlements() { fqn := directEntitlement.GetAttributeValueFqn() actionNames := directEntitlement.GetActions() + // In strict namespaced-policy mode, direct-entitlement actions must carry + // the same namespace context as the entitled attribute value so they can + // satisfy namespace-aware action matching during rule evaluation. + var actionNamespace *policy.Namespace + if attrAndValue, ok := decisionableAttributes[fqn]; ok { + actionNamespace = attrAndValue.GetAttribute().GetNamespace() + } else if fallbackAttrAndValue, ok2 := p.allEntitleableAttributesByValueFQN[fqn]; ok2 { + // Fallback for direct entitlements that may not be present in the + // narrowed decisionable set for this specific request. + actionNamespace = fallbackAttrAndValue.GetAttribute().GetNamespace() + } - actions := make([]*policy.Action, len(actionNames)) - for i, name := range actionNames { - actions[i] = &policy.Action{Name: name} + // Merge direct-entitlement actions with subject-mapping actions for the + // same value FQN instead of replacing them. + actions, ok := entitledFQNsToActions[fqn] + if !ok { + actions = make([]*policy.Action, 0, len(actionNames)) + } + for _, name := range actionNames { + actions = append(actions, &policy.Action{ + Name: name, + Namespace: actionNamespace, + }) } entitledFQNsToActions[fqn] = actions @@ -230,7 +307,7 @@ func (p *PolicyDecisionPoint) GetDecision( } for idx, resource := range resources { - resourceDecision, err := getResourceDecision(ctx, l, decisionableAttributes, p.allRegisteredResourceValuesByFQN, entitledFQNsToActions, action, resource) + resourceDecision, err := getResourceDecision(ctx, l, decisionableAttributes, p.allRegisteredResourceValuesByFQN, entitledFQNsToActions, action, resource, p.namespacedPolicy) if err != nil || resourceDecision == nil { return nil, nil, fmt.Errorf("error evaluating a decision on resource [%v]: %w", resource, err) } @@ -262,7 +339,7 @@ func (p *PolicyDecisionPoint) GetDecisionRegisteredResource( l = l.With("action", action.GetName()) l.DebugContext(ctx, "getting decision", slog.Int("resources_count", len(resources))) - if err := validateGetDecisionRegisteredResource(entityRegisteredResourceValueFQN, action, resources); err != nil { + if err := validateGetDecisionRegisteredResource(entityRegisteredResourceValueFQN, action, resources, p.namespacedPolicy); err != nil { return nil, nil, err } @@ -272,7 +349,15 @@ func (p *PolicyDecisionPoint) GetDecisionRegisteredResource( } // Filter all attributes down to only those that relevant to the entitlement decisioning of these specific resources - decisionableAttributes, err := getResourceDecisionableAttributes(ctx, l, p.allRegisteredResourceValuesByFQN, p.allEntitleableAttributesByValueFQN, p.allAttributesByDefinitionFQN /*action, */, resources, p.allowDirectEntitlements) + decisionableAttributes, err := getResourceDecisionableAttributes( + ctx, + l, + p.allRegisteredResourceValuesByFQN, + p.allEntitleableAttributesByValueFQN, + p.allAttributesByDefinitionFQN, /*action, */ + resources, + p.allowDirectEntitlements, + ) if err != nil { return nil, nil, fmt.Errorf("error getting decisionable attributes: %w", err) } @@ -281,20 +366,33 @@ func (p *PolicyDecisionPoint) GetDecisionRegisteredResource( entitledFQNsToActions := make(map[string][]*policy.Action) for _, aav := range entityRegisteredResourceValue.GetActionAttributeValues() { aavAction := aav.GetAction() - if action.GetName() != aavAction.GetName() { - l.DebugContext(ctx, "skipping action not matching Decision Request action", slog.String("action_name", aavAction.GetName())) + attrVal := aav.GetAttributeValue() + attrValFQN := attrVal.GetFqn() + + requiredNamespaceFQN := "" + if attrAndValue, ok2 := decisionableAttributes[attrValFQN]; ok2 { + requiredNamespaceFQN = attrAndValue.GetAttribute().GetNamespace().GetFqn() + } + + if !isRequestedActionMatch(ctx, l, action, requiredNamespaceFQN, aavAction, p.namespacedPolicy) { + l.DebugContext(ctx, "skipping action not matching Decision Request action", + slog.String("action_name", aavAction.GetName()), + slog.String("attribute_value_fqn", attrValFQN), + slog.String("required_namespace", requiredNamespaceFQN), + ) continue } - attrVal := aav.GetAttributeValue() - attrValFQN := attrVal.GetFqn() actionsList, actionsAreOK := entitledFQNsToActions[attrValFQN] if !actionsAreOK { actionsList = make([]*policy.Action, 0) } if !slices.ContainsFunc(actionsList, func(a *policy.Action) bool { - return a.GetName() == aavAction.GetName() + if a.GetId() != "" && aavAction.GetId() != "" { + return a.GetId() == aavAction.GetId() + } + return strings.EqualFold(a.GetName(), aavAction.GetName()) }) { actionsList = append(actionsList, aavAction) } @@ -308,7 +406,7 @@ func (p *PolicyDecisionPoint) GetDecisionRegisteredResource( } for idx, resource := range resources { - resourceDecision, err := getResourceDecision(ctx, l, decisionableAttributes, p.allRegisteredResourceValuesByFQN, entitledFQNsToActions, action, resource) + resourceDecision, err := getResourceDecision(ctx, l, decisionableAttributes, p.allRegisteredResourceValuesByFQN, entitledFQNsToActions, action, resource, p.namespacedPolicy) if err != nil || resourceDecision == nil { return nil, nil, fmt.Errorf("error evaluating a decision on resource [%v]: %w", resource, err) } @@ -359,7 +457,7 @@ func (p *PolicyDecisionPoint) GetEntitlements( } // Resolve them to their entitled FQNs and the actions available on each - entityIDsToFQNsToActions, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions(entitleableAttributes, entityRepresentations) + entityIDsToFQNsToActions, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions(entitleableAttributes, entityRepresentations, l.Logger) if err != nil { return nil, fmt.Errorf("error evaluating subject mappings for entitlement: %w", err) } diff --git a/service/internal/access/v2/pdp_test.go b/service/internal/access/v2/pdp_test.go index 0601d4ed28..f3d98c3003 100644 --- a/service/internal/access/v2/pdp_test.go +++ b/service/internal/access/v2/pdp_test.go @@ -46,10 +46,11 @@ func createAttrValueFQN(namespace, name, value string) string { } // Helper function to create registered resource value FQNs -func createRegisteredResourceValueFQN(name, value string) string { +func createRegisteredResourceValueFQN(ns, name, value string) string { resourceValue := &identifier.FullyQualifiedRegisteredResourceValue{ - Name: name, - Value: value, + Namespace: ns, + Name: name, + Value: value, } return resourceValue.FQN() } @@ -92,20 +93,20 @@ var ( testPlatformHybridFQN = createAttrValueFQN(testSecondaryNamespace, "platform", "hybrid") // Registered resource value FQNs - testNetworkPrivateFQN = createRegisteredResourceValueFQN("network", "private") - testNetworkPublicFQN = createRegisteredResourceValueFQN("network", "public") + testNetworkPrivateFQN = createRegisteredResourceValueFQN("", "network", "private") + testNetworkPublicFQN = createRegisteredResourceValueFQN("", "network", "public") ) // registered resource value FQNs using identifier package var ( // Classification values - testClassSecretRegResFQN = createRegisteredResourceValueFQN("classification", "secret") - testClassConfidentialRegResFQN = createRegisteredResourceValueFQN("classification", "confidential") + testClassSecretRegResFQN = createRegisteredResourceValueFQN("", "classification", "secret") + testClassConfidentialRegResFQN = createRegisteredResourceValueFQN("", "classification", "confidential") // Department values - testDeptEngineeringRegResFQN = createRegisteredResourceValueFQN("department", "engineering") - testDeptFinanceRegResFQN = createRegisteredResourceValueFQN("department", "finance") - testProjectAlphaRegResFQN = createRegisteredResourceValueFQN("project", "alpha") + testDeptEngineeringRegResFQN = createRegisteredResourceValueFQN("", "department", "engineering") + testDeptFinanceRegResFQN = createRegisteredResourceValueFQN("", "department", "finance") + testProjectAlphaRegResFQN = createRegisteredResourceValueFQN("", "project", "alpha") ) // Registered resource value FQNs using identifier package @@ -283,6 +284,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead}, ".properties.clearance", []string{"ts"}, + nil, ) s.fixtures.secretMapping = createSimpleSubjectMapping( @@ -291,6 +293,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead, testActionUpdate}, ".properties.clearance", []string{"secret"}, + nil, ) s.fixtures.confidentialMapping = createSimpleSubjectMapping( @@ -299,6 +302,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead}, ".properties.clearance", []string{"confidential"}, + nil, ) s.fixtures.publicMapping = createSimpleSubjectMapping( @@ -307,6 +311,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead}, ".properties.clearance", []string{"public"}, + nil, ) s.fixtures.engineeringMapping = createSimpleSubjectMapping( @@ -315,6 +320,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead, testActionCreate}, ".properties.department", []string{"engineering"}, + nil, ) s.fixtures.financeMapping = createSimpleSubjectMapping( @@ -323,6 +329,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead, testActionUpdate}, ".properties.department", []string{"finance"}, + nil, ) s.fixtures.rndMapping = createSimpleSubjectMapping( @@ -331,6 +338,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead, testActionUpdate}, ".properties.department", []string{"rnd"}, + nil, ) s.fixtures.usaMapping = createSimpleSubjectMapping( @@ -339,6 +347,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead}, ".properties.country[]", []string{"us"}, + nil, ) s.fixtures.ukMapping = createSimpleSubjectMapping( @@ -347,6 +356,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead, testActionDelete}, ".properties.country[]", []string{"uk"}, + nil, ) s.fixtures.projectAlphaMapping = createSimpleSubjectMapping( @@ -355,6 +365,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead, testActionCreate}, ".properties.project", []string{"alpha"}, + nil, ) s.fixtures.platformCloudMapping = createSimpleSubjectMapping( @@ -363,6 +374,7 @@ func (s *PDPTestSuite) SetupTest() { []*policy.Action{testActionRead, testActionDelete}, ".properties.platform", []string{"cloud"}, + nil, ) // Initialize registered resources @@ -797,12 +809,12 @@ func (s *PDPTestSuite) SetupTest() { s.fixtures.regResValMultiActionMultiAttrVal = regResValMultiActionMultiAttrVal s.fixtures.regResValComprehensiveHierarchyActionAttrVal = regResValComprehensiveHierarchyActionAttrVal - regResValNoActionAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValNoActionAttrVal.GetValue()) - regResValSingleActionAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValSingleActionAttrVal.GetValue()) - regResValDuplicateActionAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValDuplicateActionAttrVal.GetValue()) - regResValMultiActionSingleAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValMultiActionSingleAttrVal.GetValue()) - regResValMultiActionMultiAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValMultiActionMultiAttrVal.GetValue()) - regResValComprehensiveHierarchyActionAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValComprehensiveHierarchyActionAttrVal.GetValue()) + regResValNoActionAttrValFQN = createRegisteredResourceValueFQN("", regRes.GetName(), regResValNoActionAttrVal.GetValue()) + regResValSingleActionAttrValFQN = createRegisteredResourceValueFQN("", regRes.GetName(), regResValSingleActionAttrVal.GetValue()) + regResValDuplicateActionAttrValFQN = createRegisteredResourceValueFQN("", regRes.GetName(), regResValDuplicateActionAttrVal.GetValue()) + regResValMultiActionSingleAttrValFQN = createRegisteredResourceValueFQN("", regRes.GetName(), regResValMultiActionSingleAttrVal.GetValue()) + regResValMultiActionMultiAttrValFQN = createRegisteredResourceValueFQN("", regRes.GetName(), regResValMultiActionMultiAttrVal.GetValue()) + regResValComprehensiveHierarchyActionAttrValFQN = createRegisteredResourceValueFQN("", regRes.GetName(), regResValComprehensiveHierarchyActionAttrVal.GetValue()) } // TestPDPSuite runs the test suite @@ -858,7 +870,7 @@ func (s *PDPTestSuite) TestNewPolicyDecisionPoint() { for _, tc := range tests { s.Run(tc.name, func() { pdp, err := NewPolicyDecisionPoint( - s.T().Context(), s.logger, tc.attributes, tc.subjectMappings, tc.registeredResources, allowDirectEntitlements) + s.T().Context(), s.logger, tc.attributes, tc.subjectMappings, tc.registeredResources, allowDirectEntitlements, false) if tc.expectError { s.Require().Error(err) @@ -871,6 +883,110 @@ func (s *PDPTestSuite) TestNewPolicyDecisionPoint() { } } +func (s *PDPTestSuite) TestNewPolicyDecisionPoint_AllowsAttributeDefinitionsWithoutValues() { + f := s.fixtures + emptyAttrFQN := createAttrFQN(testSecondaryNamespace, "empty") + emptyAttr := &policy.Attribute{ + Fqn: emptyAttrFQN, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + } + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{f.classificationAttr, emptyAttr}, + []*policy.SubjectMapping{f.secretMapping}, + nil, + allowDirectEntitlements, + false, + ) + + s.Require().NoError(err) + s.Require().NotNil(pdp) + + s.Require().Contains(pdp.allAttributesByDefinitionFQN, emptyAttrFQN) + s.Require().NotContains(pdp.allEntitleableAttributesByValueFQN, emptyAttrFQN) +} + +func (s *PDPTestSuite) TestNewPolicyDecisionPoint_IndexesRegisteredResourceValuesByNamespacedAndLegacyFQN() { + tests := []struct { + name string + namespaceName string + namespaceFQN string + expectedNS string + namespacedPolicy bool + expectLegacyFQN bool + }{ + { + name: "uses namespace name when present (legacy indexed when not strict)", + namespaceName: "ns-one.example.com", + namespaceFQN: "https://ns-one.example.com", + expectedNS: "ns-one.example.com", + namespacedPolicy: false, + expectLegacyFQN: true, + }, + { + name: "falls back to namespace fqn when name absent (legacy indexed when not strict)", + namespaceFQN: "https://ns-two.example.com", + expectedNS: "ns-two.example.com", + namespacedPolicy: false, + expectLegacyFQN: true, + }, + { + name: "strict mode skips legacy FQN", + namespaceName: "ns-one.example.com", + namespaceFQN: "https://ns-one.example.com", + expectedNS: "ns-one.example.com", + namespacedPolicy: true, + expectLegacyFQN: false, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + regResVal := &policy.RegisteredResourceValue{Value: "prod-config"} + regRes := &policy.RegisteredResource{ + Name: "app-config", + Namespace: &policy.Namespace{ + Name: tc.namespaceName, + Fqn: tc.namespaceFQN, + }, + Values: []*policy.RegisteredResourceValue{regResVal}, + } + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{}, + []*policy.SubjectMapping{}, + []*policy.RegisteredResource{regRes}, + allowDirectEntitlements, + tc.namespacedPolicy, + ) + s.Require().NoError(err) + + namespacedFQN := (&identifier.FullyQualifiedRegisteredResourceValue{ + Namespace: tc.expectedNS, + Name: regRes.GetName(), + Value: regResVal.GetValue(), + }).FQN() + legacyFQN := (&identifier.FullyQualifiedRegisteredResourceValue{ + Name: regRes.GetName(), + Value: regResVal.GetValue(), + }).FQN() + + s.Require().Contains(pdp.allRegisteredResourceValuesByFQN, namespacedFQN) + s.Same(regResVal, pdp.allRegisteredResourceValuesByFQN[namespacedFQN]) + if tc.expectLegacyFQN { + s.Require().Contains(pdp.allRegisteredResourceValuesByFQN, legacyFQN) + s.Same(regResVal, pdp.allRegisteredResourceValuesByFQN[legacyFQN]) + } else { + s.Require().NotContains(pdp.allRegisteredResourceValuesByFQN, legacyFQN) + } + }) + } +} + // Test_GetDecision_MultipleResources tests the GetDecision method with some generalized scenarios for multiple resources func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { f := s.fixtures @@ -883,6 +999,7 @@ func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { []*policy.SubjectMapping{f.secretMapping, f.topSecretMapping, f.confidentialMapping, f.publicMapping, f.rndMapping, f.engineeringMapping, f.financeMapping}, []*policy.RegisteredResource{f.classificationRegRes, f.deptRegRes}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -1071,8 +1188,8 @@ func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { "department": "rnd", }) - rndDeptRegResFQN := createRegisteredResourceValueFQN(f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) - topsecretClassRegResFQN := createRegisteredResourceValueFQN(f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) + rndDeptRegResFQN := createRegisteredResourceValueFQN("", f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) + topsecretClassRegResFQN := createRegisteredResourceValueFQN("", f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) resources := []*authz.Resource{ { @@ -1118,8 +1235,8 @@ func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { "department": "rnd", }) - rndDeptRegResFQN := createRegisteredResourceValueFQN(f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) - topsecretClassRegResFQN := createRegisteredResourceValueFQN(f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) + rndDeptRegResFQN := createRegisteredResourceValueFQN("", f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) + topsecretClassRegResFQN := createRegisteredResourceValueFQN("", f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) // Upper case both registered resource value FQNs for assurance FQNs will be case-normalized resources := []*authz.Resource{ @@ -1166,8 +1283,8 @@ func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { "department": "finance", // Not rnd }) - rndDeptRegResFQN := createRegisteredResourceValueFQN(f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) - topsecretClassRegResFQN := createRegisteredResourceValueFQN(f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) + rndDeptRegResFQN := createRegisteredResourceValueFQN("", f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) + topsecretClassRegResFQN := createRegisteredResourceValueFQN("", f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) resources := []*authz.Resource{ { @@ -1213,8 +1330,8 @@ func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { "department": "rnd", // subject mapping permits read/update }) - rndDeptRegResFQN := createRegisteredResourceValueFQN(f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) - topsecretClassRegResFQN := createRegisteredResourceValueFQN(f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) + rndDeptRegResFQN := createRegisteredResourceValueFQN("", f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) + topsecretClassRegResFQN := createRegisteredResourceValueFQN("", f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) resources := []*authz.Resource{ { @@ -1259,8 +1376,8 @@ func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { "department": "rnd", // subject mapping permits read/update }) - rndDeptRegResFQN := createRegisteredResourceValueFQN(f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) - topsecretClassRegResFQN := createRegisteredResourceValueFQN(f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) + rndDeptRegResFQN := createRegisteredResourceValueFQN("", f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) + topsecretClassRegResFQN := createRegisteredResourceValueFQN("", f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) resources := []*authz.Resource{ { @@ -1307,8 +1424,8 @@ func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { "department": "rnd", }) - rndDeptRegResFQN := createRegisteredResourceValueFQN(f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) - topsecretClassRegResFQN := createRegisteredResourceValueFQN(f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) + rndDeptRegResFQN := createRegisteredResourceValueFQN("", f.deptRegRes.GetName(), f.deptRegRes.GetValues()[0].GetValue()) + topsecretClassRegResFQN := createRegisteredResourceValueFQN("", f.classificationRegRes.GetName(), f.classificationRegRes.GetValues()[0].GetValue()) resources := []*authz.Resource{ { @@ -1363,6 +1480,7 @@ func (s *PDPTestSuite) Test_GetDecision_ReturnsDecisionRelatedEntitlements() { []*policy.SubjectMapping{f.topSecretMapping, f.engineeringMapping}, []*policy.RegisteredResource{}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -1425,6 +1543,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { []*policy.Action{testActionRead, testActionPrint}, ".properties.clearance", []string{"confidential"}, + nil, ) readConfidentialRegRes := &policy.RegisteredResource{ @@ -1455,6 +1574,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { }, ".properties.clearance", []string{"public"}, + nil, ) // Create a view mapping for Project Alpha with view being a parent action of read and list @@ -1464,6 +1584,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { []*policy.Action{testActionView, actionCreate, actionRead}, ".properties.project", []string{"alpha"}, + nil, ) // Create a PDP with relevant attributes and mappings @@ -1480,6 +1601,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { readConfidentialRegRes, f.countryRegRes, }, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -1599,6 +1721,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { []*policy.Action{testActionRead}, // Only read is allowed ".properties.clearance", []string{"restricted"}, + nil, ) restrictedRegRes := &policy.RegisteredResource{ Name: "confidential-restricted", @@ -1625,6 +1748,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { []*policy.SubjectMapping{allActionsPublicMapping, restrictedMapping}, []*policy.RegisteredResource{f.classificationRegRes, restrictedRegRes}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(classificationPDP) @@ -1661,7 +1785,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { "clearance": "confidential", }) - readConfidentialRegResFQN := createRegisteredResourceValueFQN(readConfidentialRegRes.GetName(), readConfidentialRegRes.GetValues()[0].GetValue()) + readConfidentialRegResFQN := createRegisteredResourceValueFQN("", readConfidentialRegRes.GetName(), readConfidentialRegRes.GetValues()[0].GetValue()) resources := []*authz.Resource{ { @@ -1696,7 +1820,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { "country": []any{"uk"}, }) - readCountryUKRegResFQN := createRegisteredResourceValueFQN(f.countryRegRes.GetName(), f.countryRegRes.GetValues()[1].GetValue()) + readCountryUKRegResFQN := createRegisteredResourceValueFQN("", f.countryRegRes.GetName(), f.countryRegRes.GetValues()[1].GetValue()) resources := []*authz.Resource{ { @@ -1743,6 +1867,7 @@ func (s *PDPTestSuite) Test_GetDecision_CombinedAttributeRules_SingleResource() }, []*policy.RegisteredResource{}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -2065,6 +2190,7 @@ func (s *PDPTestSuite) Test_GetDecision_AcrossNamespaces() { []*policy.Action{testActionRead, testActionUpdate}, ".properties.project", []string{"beta"}, + nil, ) gammaMapping := createSimpleSubjectMapping( @@ -2073,6 +2199,7 @@ func (s *PDPTestSuite) Test_GetDecision_AcrossNamespaces() { []*policy.Action{testActionRead, testActionCreate, testActionDelete}, ".properties.project", []string{"gamma"}, + nil, ) onPremMapping := createSimpleSubjectMapping( @@ -2081,6 +2208,7 @@ func (s *PDPTestSuite) Test_GetDecision_AcrossNamespaces() { []*policy.Action{testActionRead, testActionUpdate}, ".properties.platform", []string{"onprem"}, + nil, ) hybridMapping := createSimpleSubjectMapping( @@ -2089,6 +2217,7 @@ func (s *PDPTestSuite) Test_GetDecision_AcrossNamespaces() { []*policy.Action{testActionRead, testActionCreate, testActionUpdate, testActionDelete}, ".properties.platform", []string{"hybrid"}, + nil, ) // Create a PDP with attributes and mappings from all namespaces @@ -2104,6 +2233,7 @@ func (s *PDPTestSuite) Test_GetDecision_AcrossNamespaces() { }, []*policy.RegisteredResource{}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -2384,6 +2514,336 @@ func (s *PDPTestSuite) Test_GetDecision_AcrossNamespaces() { }) } +// Legacy behavior regression: without strict namespaced-policy enforcement, +// action matching remains name-based and cross-namespace action names still match. +func (s *PDPTestSuite) Test_GetDecision_LegacyBehavior_AllowsCrossNamespaceActionNameMatch() { + f := s.fixtures + + baseNS := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://" + testBaseNamespace} + secondaryNS := &policy.Namespace{Id: "22222222-2222-2222-2222-222222222222", Fqn: "https://" + testSecondaryNamespace} + + f.classificationAttr.Namespace = baseNS + f.projectAttr.Namespace = secondaryNS + + secretMapping := createSimpleSubjectMapping( + testClassSecretFQN, + "secret", + []*policy.Action{{Name: actions.ActionNameRead, Namespace: baseNS}}, + ".properties.clearance", + []string{"secret"}, + baseNS, + ) + + projectAlphaWrongNamespace := createSimpleSubjectMapping( + testProjectAlphaFQN, + "alpha", + []*policy.Action{{Name: actions.ActionNameRead, Namespace: baseNS}}, + ".properties.project", + []string{"alpha"}, + secondaryNS, + ) + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{f.classificationAttr, f.projectAttr}, + []*policy.SubjectMapping{secretMapping, projectAlphaWrongNamespace}, + []*policy.RegisteredResource{}, + allowDirectEntitlements, + false, + ) + s.Require().NoError(err) + s.Require().NotNil(pdp) + + entity := s.createEntityWithProps("legacy-cross-ns-action-match", map[string]interface{}{ + "clearance": "secret", + "project": "alpha", + }) + + resource := createAttributeValueResource("mixed-namespaces-legacy", testClassSecretFQN, testProjectAlphaFQN) + + decision, _, err := pdp.GetDecision(s.T().Context(), entity, testActionRead, []*authz.Resource{resource}) + s.Require().NoError(err) + s.Require().NotNil(decision) + + s.True(decision.AllPermitted, "legacy behavior should remain name-based until strict namespaced-policy mode is enabled") +} + +// Desired strict-mode behavior (NamespacedPolicy=true): +// if the same action name is only entitled in a different namespace than the evaluated value, +// decisioning must fail closed. +func (s *PDPTestSuite) Test_GetDecision_StrictMode_DeniesCrossNamespaceActionMatch() { + f := s.fixtures + + baseNS := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://" + testBaseNamespace} + secondaryNS := &policy.Namespace{Id: "22222222-2222-2222-2222-222222222222", Fqn: "https://" + testSecondaryNamespace} + + f.classificationAttr.Namespace = baseNS + f.projectAttr.Namespace = secondaryNS + + secretMapping := createSimpleSubjectMapping( + testClassSecretFQN, + "secret", + []*policy.Action{{Name: actions.ActionNameRead, Namespace: baseNS}}, + ".properties.clearance", + []string{"secret"}, + baseNS, + ) + + projectAlphaWrongNamespace := createSimpleSubjectMapping( + testProjectAlphaFQN, + "alpha", + []*policy.Action{{Name: actions.ActionNameRead, Namespace: baseNS}}, + ".properties.project", + []string{"alpha"}, + secondaryNS, + ) + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{f.classificationAttr, f.projectAttr}, + []*policy.SubjectMapping{secretMapping, projectAlphaWrongNamespace}, + []*policy.RegisteredResource{}, + allowDirectEntitlements, + true, + ) + s.Require().NoError(err) + s.Require().NotNil(pdp) + + entity := s.createEntityWithProps("cross-ns-action-mismatch", map[string]interface{}{ + "clearance": "secret", + "project": "alpha", + }) + + resource := createAttributeValueResource("mixed-namespaces", testClassSecretFQN, testProjectAlphaFQN) + + decision, _, err := pdp.GetDecision(s.T().Context(), entity, testActionRead, []*authz.Resource{resource}) + s.Require().NoError(err) + s.Require().NotNil(decision) + + s.False(decision.AllPermitted, "desired strict namespaced behavior: action should not match across namespaces") +} + +func (s *PDPTestSuite) Test_GetDecision_StrictMode_SkipsUnnamespacedSubjectMappings() { + f := s.fixtures + + baseNS := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://" + testBaseNamespace} + f.classificationAttr.Namespace = baseNS + + unnamespacedMapping := createSimpleSubjectMapping( + testClassSecretFQN, + "secret", + []*policy.Action{{Name: actions.ActionNameRead, Namespace: baseNS}}, + ".properties.clearance", + []string{"secret"}, + nil, + ) + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{f.classificationAttr}, + []*policy.SubjectMapping{unnamespacedMapping}, + []*policy.RegisteredResource{}, + allowDirectEntitlements, + true, + ) + s.Require().NoError(err) + s.Require().NotNil(pdp) + + entity := s.createEntityWithProps("strict-skip-unnamespaced-sm", map[string]interface{}{ + "clearance": "secret", + }) + + resource := createAttributeValueResource("strict-skip-unnamespaced-sm-resource", testClassSecretFQN) + + decision, _, err := pdp.GetDecision(s.T().Context(), entity, testActionRead, []*authz.Resource{resource}) + s.Require().NoError(err) + s.Require().NotNil(decision) + s.False(decision.AllPermitted, "strict namespaced-policy mode should skip unnamespaced subject mappings") +} + +func (s *PDPTestSuite) Test_GetDecision_LegacyBehavior_UsesUnnamespacedSubjectMappings() { + f := s.fixtures + + baseNS := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://" + testBaseNamespace} + f.classificationAttr.Namespace = baseNS + + unnamespacedMapping := createSimpleSubjectMapping( + testClassSecretFQN, + "secret", + []*policy.Action{{Name: actions.ActionNameRead, Namespace: baseNS}}, + ".properties.clearance", + []string{"secret"}, + nil, + ) + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{f.classificationAttr}, + []*policy.SubjectMapping{unnamespacedMapping}, + []*policy.RegisteredResource{}, + allowDirectEntitlements, + false, + ) + s.Require().NoError(err) + s.Require().NotNil(pdp) + + entity := s.createEntityWithProps("legacy-uses-unnamespaced-sm", map[string]interface{}{ + "clearance": "secret", + }) + + resource := createAttributeValueResource("legacy-uses-unnamespaced-sm-resource", testClassSecretFQN) + + decision, _, err := pdp.GetDecision(s.T().Context(), entity, testActionRead, []*authz.Resource{resource}) + s.Require().NoError(err) + s.Require().NotNil(decision) + s.True(decision.AllPermitted, "legacy mode should continue evaluating unnamespaced subject mappings") +} + +func (s *PDPTestSuite) Test_GetDecision_StrictMode_UsesOnlyNamespacedSubjectMappings() { + f := s.fixtures + + baseNS := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://" + testBaseNamespace} + f.classificationAttr.Namespace = baseNS + + unnamespacedMapping := createSimpleSubjectMapping( + testClassSecretFQN, + "secret", + []*policy.Action{{Name: actions.ActionNameRead, Namespace: baseNS}}, + ".properties.clearance", + []string{"secret"}, + nil, + ) + + namespacedMapping := createSimpleSubjectMapping( + testClassSecretFQN, + "secret", + []*policy.Action{{Name: actions.ActionNameCreate, Namespace: baseNS}}, + ".properties.clearance", + []string{"secret"}, + baseNS, + ) + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{f.classificationAttr}, + []*policy.SubjectMapping{unnamespacedMapping, namespacedMapping}, + []*policy.RegisteredResource{}, + allowDirectEntitlements, + true, + ) + s.Require().NoError(err) + s.Require().NotNil(pdp) + + entity := s.createEntityWithProps("strict-namespaced-only-sm", map[string]interface{}{ + "clearance": "secret", + }) + + resource := createAttributeValueResource("strict-namespaced-only-sm-resource", testClassSecretFQN) + + readDecision, _, err := pdp.GetDecision(s.T().Context(), entity, testActionRead, []*authz.Resource{resource}) + s.Require().NoError(err) + s.Require().NotNil(readDecision) + s.False(readDecision.AllPermitted, "strict mode should ignore unnamespaced read mapping") + + createDecision, _, err := pdp.GetDecision(s.T().Context(), entity, testActionCreate, []*authz.Resource{resource}) + s.Require().NoError(err) + s.Require().NotNil(createDecision) + s.True(createDecision.AllPermitted, "strict mode should keep namespaced mappings") +} + +func (s *PDPTestSuite) Test_GetDecision_StrictMode_RequestActionIdentityMatrix() { + f := s.fixtures + + namespaceA := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://" + testBaseNamespace} + namespaceB := &policy.Namespace{Id: "22222222-2222-2222-2222-222222222222", Fqn: "https://" + testSecondaryNamespace} + + f.classificationAttr.Namespace = namespaceA + + const ( + idNamespaceARead = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + idNamespaceBRead = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + ) + + mapping := createSimpleSubjectMapping( + testClassSecretFQN, + "secret", + []*policy.Action{ + {Id: idNamespaceARead, Name: actions.ActionNameRead, Namespace: namespaceA}, + }, + ".properties.clearance", + []string{"secret"}, + namespaceA, + ) + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{f.classificationAttr}, + []*policy.SubjectMapping{mapping}, + []*policy.RegisteredResource{}, + allowDirectEntitlements, + true, + ) + s.Require().NoError(err) + s.Require().NotNil(pdp) + + entity := s.createEntityWithProps("strict-action-identity-matrix", map[string]interface{}{ + "clearance": "secret", + }) + resource := createAttributeValueResource("strict-action-identity-matrix-resource", testClassSecretFQN) + + tests := []struct { + name string + action *policy.Action + permitted bool + }{ + { + name: "id match in required namespace permits", + action: &policy.Action{Id: idNamespaceARead, Name: actions.ActionNameRead}, + permitted: true, + }, + { + name: "non-existent action id denies", + action: &policy.Action{Id: idNamespaceBRead, Name: actions.ActionNameRead}, + permitted: false, + }, + { + name: "id match plus wrong request namespace denies", + action: &policy.Action{Id: idNamespaceARead, Name: actions.ActionNameRead, Namespace: namespaceB}, + permitted: false, + }, + { + name: "name plus matching namespace permits", + action: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceA}, + permitted: true, + }, + { + name: "name plus wrong namespace denies", + action: &policy.Action{Name: actions.ActionNameRead, Namespace: namespaceB}, + permitted: false, + }, + { + name: "name only resolves contextually and permits", + action: &policy.Action{Name: actions.ActionNameRead}, + permitted: true, + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + decision, _, decisionErr := pdp.GetDecision(s.T().Context(), entity, tt.action, []*authz.Resource{resource}) + s.Require().NoError(decisionErr) + s.Require().NotNil(decision) + s.Equal(tt.permitted, decision.AllPermitted) + }) + } +} + func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_MultipleResources() { f := s.fixtures @@ -2458,12 +2918,13 @@ func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_MultipleResources() { []*policy.SubjectMapping{f.secretMapping, f.topSecretMapping, f.confidentialMapping, f.publicMapping, f.engineeringMapping, f.financeMapping}, []*policy.RegisteredResource{f.networkRegRes, regResS3BucketEntity}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) s.Run("Multiple resources and entitled actions/attributes - full access", func() { - entityRegResFQN := createRegisteredResourceValueFQN(regResS3BucketEntity.GetName(), "ts-engineering") + entityRegResFQN := createRegisteredResourceValueFQN("", regResS3BucketEntity.GetName(), "ts-engineering") resources := createResourcePerFqn(testClassSecretFQN, testDeptEngineeringFQN, testNetworkPrivateFQN, testNetworkPublicFQN) decision, _, err := pdp.GetDecisionRegisteredResource(s.T().Context(), entityRegResFQN, testActionRead, resources) @@ -2488,7 +2949,7 @@ func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_MultipleResources() { }) s.Run("Multiple resources and entitled actions/attributes of varied casing - full access", func() { - entityRegResFQN := createRegisteredResourceValueFQN(regResS3BucketEntity.GetName(), "ts-engineering") + entityRegResFQN := createRegisteredResourceValueFQN("", regResS3BucketEntity.GetName(), "ts-engineering") secretFQN := strings.ToUpper(testClassSecretFQN) networkPrivateFQN := strings.ToUpper(testNetworkPrivateFQN) @@ -2516,7 +2977,7 @@ func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_MultipleResources() { }) s.Run("Multiple resources and unentitled attributes - full denial", func() { - entityRegResFQN := createRegisteredResourceValueFQN(regResS3BucketEntity.GetName(), "confidential-finance") + entityRegResFQN := createRegisteredResourceValueFQN("", regResS3BucketEntity.GetName(), "confidential-finance") resources := createResourcePerFqn(testClassSecretFQN, testDeptEngineeringFQN, testNetworkPrivateFQN, testNetworkPublicFQN) @@ -2547,7 +3008,7 @@ func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_MultipleResources() { }) s.Run("Multiple resources and unentitled actions - full denial", func() { - entityRegResFQN := createRegisteredResourceValueFQN(regResS3BucketEntity.GetName(), "ts-engineering") + entityRegResFQN := createRegisteredResourceValueFQN("", regResS3BucketEntity.GetName(), "ts-engineering") resources := createResourcePerFqn(testDeptEngineeringFQN, testClassSecretFQN, testNetworkPrivateFQN, testNetworkPublicFQN) @@ -2569,7 +3030,7 @@ func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_MultipleResources() { }) s.Run("Multiple resources - partial access", func() { - entityRegResFQN := createRegisteredResourceValueFQN(regResS3BucketEntity.GetName(), "secret-engineering") + entityRegResFQN := createRegisteredResourceValueFQN("", regResS3BucketEntity.GetName(), "secret-engineering") resources := createResourcePerFqn(testClassSecretFQN, testDeptFinanceFQN, testNetworkPrivateFQN, testNetworkPublicFQN) @@ -2632,6 +3093,7 @@ func (s *PDPTestSuite) Test_GetEntitlements() { }, []*policy.RegisteredResource{}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -2888,6 +3350,7 @@ func (s *PDPTestSuite) Test_GetEntitlements_AdvancedHierarchy() { []*policy.Action{testActionRead}, ".properties.levels[]", []string{"top"}, + nil, ) upperMiddleMapping := createSimpleSubjectMapping( upperMiddleValueFQN, @@ -2895,6 +3358,7 @@ func (s *PDPTestSuite) Test_GetEntitlements_AdvancedHierarchy() { []*policy.Action{testActionCreate}, ".properties.levels[]", []string{"upper-middle"}, + nil, ) middleMapping := createSimpleSubjectMapping( middleValueFQN, @@ -2902,6 +3366,7 @@ func (s *PDPTestSuite) Test_GetEntitlements_AdvancedHierarchy() { []*policy.Action{testActionUpdate, {Name: actionNameTransmit}}, ".properties.levels[]", []string{"middle"}, + nil, ) lowerMiddleMapping := createSimpleSubjectMapping( lowerMiddleValueFQN, @@ -2909,6 +3374,7 @@ func (s *PDPTestSuite) Test_GetEntitlements_AdvancedHierarchy() { []*policy.Action{testActionDelete}, ".properties.levels[]", []string{"lower-middle"}, + nil, ) bottomMapping := createSimpleSubjectMapping( bottomValueFQN, @@ -2916,6 +3382,7 @@ func (s *PDPTestSuite) Test_GetEntitlements_AdvancedHierarchy() { []*policy.Action{{Name: customActionGather}}, ".properties.levels[]", []string{"bottom"}, + nil, ) // Create a PDP with the hierarchy attribute and mappings @@ -2932,6 +3399,7 @@ func (s *PDPTestSuite) Test_GetEntitlements_AdvancedHierarchy() { }, []*policy.RegisteredResource{}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -3011,6 +3479,7 @@ func (s *PDPTestSuite) Test_GetEntitlementsRegisteredResource() { []*policy.SubjectMapping{}, []*policy.RegisteredResource{f.regRes}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -3028,7 +3497,7 @@ func (s *PDPTestSuite) Test_GetEntitlementsRegisteredResource() { }) s.Run("Valid but non-existent registered resource value FQN", func() { - validButNonexistentFQN := createRegisteredResourceValueFQN("test-res-not-exist", "test-value-not-exist") + validButNonexistentFQN := createRegisteredResourceValueFQN("", "test-res-not-exist", "test-value-not-exist") entitlements, err := pdp.GetEntitlementsRegisteredResource( s.T().Context(), validButNonexistentFQN, @@ -3249,8 +3718,8 @@ func createSimpleSubjectConditionSet(selector string, values []string) *policy.S } // createSimpleSubjectMapping creates a complete subject mapping with a simple condition -func createSimpleSubjectMapping(attrValueFQN string, attrValue string, actions []*policy.Action, selector string, values []string) *policy.SubjectMapping { - return &policy.SubjectMapping{ +func createSimpleSubjectMapping(attrValueFQN string, attrValue string, actions []*policy.Action, selector string, values []string, namespace *policy.Namespace) *policy.SubjectMapping { + mapping := &policy.SubjectMapping{ AttributeValue: &policy.Value{ Fqn: attrValueFQN, Value: attrValue, @@ -3258,6 +3727,11 @@ func createSimpleSubjectMapping(attrValueFQN string, attrValue string, actions [ SubjectConditionSet: createSimpleSubjectConditionSet(selector, values), Actions: actions, } + if namespace != nil { + mapping.Namespace = namespace + mapping.SubjectConditionSet.Namespace = namespace + } + return mapping } // Helper function to test decision results @@ -3287,6 +3761,7 @@ func (s *PDPTestSuite) Test_GetDecision_NonExistentAttributeFQN() { []*policy.SubjectMapping{f.secretMapping, f.topSecretMapping}, []*policy.RegisteredResource{}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -3390,6 +3865,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialFQNsInResource() { []*policy.SubjectMapping{f.secretMapping, f.topSecretMapping, f.confidentialMapping}, []*policy.RegisteredResource{}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -3438,6 +3914,7 @@ func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_NonExistentFQN() { []*policy.SubjectMapping{f.secretMapping}, []*policy.RegisteredResource{f.classificationRegRes}, // Only classification registered resource allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -3447,7 +3924,7 @@ func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_NonExistentFQN() { "clearance": "secret", }) - nonExistentRegResFQN := createRegisteredResourceValueFQN("special-system", "classified") + nonExistentRegResFQN := createRegisteredResourceValueFQN("", "special-system", "classified") resources := []*authz.Resource{ { Resource: &authz.Resource_RegisteredResourceValueFqn{ @@ -3476,7 +3953,7 @@ func (s *PDPTestSuite) Test_GetDecisionRegisteredResource_NonExistentFQN() { "clearance": "secret", }) - nonExistentRegResFQN := createRegisteredResourceValueFQN("secret-system", "classified") + nonExistentRegResFQN := createRegisteredResourceValueFQN("", "secret-system", "classified") resources := []*authz.Resource{ { Resource: &authz.Resource_RegisteredResourceValueFqn{ @@ -3523,6 +4000,7 @@ func (s *PDPTestSuite) Test_GetDecision_NoPolicies() { []*policy.SubjectMapping{}, []*policy.RegisteredResource{}, allowDirectEntitlements, + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -3606,6 +4084,7 @@ func (s *PDPTestSuite) Test_GetDecision_DirectEntitlements() { []*policy.SubjectMapping{}, []*policy.RegisteredResource{}, true, // Allow direct entitlements + false, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -3701,6 +4180,73 @@ func (s *PDPTestSuite) Test_GetDecision_DirectEntitlements() { }) } +func (s *PDPTestSuite) Test_GetDecision_DirectEntitlements_StrictNamespacedPolicy() { + ctx := s.T().Context() + + namespace1 := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://demo.com"} + namespace2 := &policy.Namespace{Id: "22222222-2222-2222-2222-222222222222", Fqn: "https://demo-two.com"} + + attr1 := &policy.Attribute{ + Fqn: "https://demo.com/attr/adhoc", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + Namespace: namespace1, + Values: []*policy.Value{ + {Fqn: "https://demo.com/attr/adhoc/value/direct_entitlement_1", Value: "direct_entitlement_1"}, + }, + } + attr2 := &policy.Attribute{ + Fqn: "https://demo-two.com/attr/adhoc_2", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + Namespace: namespace2, + Values: []*policy.Value{ + {Fqn: "https://demo-two.com/attr/adhoc_2/value/direct_entitlement_2", Value: "direct_entitlement_2"}, + }, + } + + attr1ValueFQN := attr1.GetValues()[0].GetFqn() + attr2ValueFQN := attr2.GetValues()[0].GetFqn() + + pdp, err := NewPolicyDecisionPoint( + ctx, + s.logger, + []*policy.Attribute{attr1, attr2}, + []*policy.SubjectMapping{}, + []*policy.RegisteredResource{}, + true, + true, + ) + s.Require().NoError(err) + + entityRep := &entityresolutionV2.EntityRepresentation{ + DirectEntitlements: []*entityresolutionV2.DirectEntitlement{ + {AttributeValueFqn: attr1ValueFQN, Actions: []string{actions.ActionNameCreate}}, + {AttributeValueFqn: attr2ValueFQN, Actions: []string{actions.ActionNameCreate}}, + }, + } + + s.Run("permits when direct entitlement action resolves to required namespace", func() { + decision, _, decisionErr := pdp.GetDecision(ctx, entityRep, testActionCreate, []*authz.Resource{ + createAttributeValueResource(attr1ValueFQN, attr1ValueFQN), + createAttributeValueResource(attr2ValueFQN, attr2ValueFQN), + }) + s.Require().NoError(decisionErr) + s.Require().NotNil(decision) + s.True(decision.AllPermitted) + }) + + s.Run("denies when explicit request namespace mismatches", func() { + decision, _, decisionErr := pdp.GetDecision(ctx, entityRep, &policy.Action{ + Name: actions.ActionNameCreate, + Namespace: namespace1, + }, []*authz.Resource{ + createAttributeValueResource(attr2ValueFQN, attr2ValueFQN), + }) + s.Require().NoError(decisionErr) + s.Require().NotNil(decision) + s.False(decision.AllPermitted) + }) +} + // Helper functions for all tests // assertDecisionResult is a helper function to assert that a decision result for a given FQN matches the expected pass/fail state diff --git a/service/internal/access/v2/validators.go b/service/internal/access/v2/validators.go index b10515b7e2..fff3d6e451 100644 --- a/service/internal/access/v2/validators.go +++ b/service/internal/access/v2/validators.go @@ -45,7 +45,7 @@ func validateGetDecision(entityRepresentation *entityresolutionV2.EntityRepresen // - registeredResourceValueFQN: must be a valid registered resource value FQN // - action: must not be nil // - resources: must not be nil and must contain at least one resource -func validateGetDecisionRegisteredResource(registeredResourceValueFQN string, action *policy.Action, resources []*authzV2.Resource) error { +func validateGetDecisionRegisteredResource(registeredResourceValueFQN string, action *policy.Action, resources []*authzV2.Resource, namespacedPolicy bool) error { if _, err := identifier.Parse[*identifier.FullyQualifiedRegisteredResourceValue](registeredResourceValueFQN); err != nil { return err } @@ -60,6 +60,15 @@ func validateGetDecisionRegisteredResource(registeredResourceValueFQN string, ac return fmt.Errorf("resource is nil: %w", ErrInvalidResource) } } + if namespacedPolicy { + parsed, err := identifier.Parse[*identifier.FullyQualifiedRegisteredResourceValue](registeredResourceValueFQN) + if err != nil { + return fmt.Errorf("invalid registered resource value FQN [%s]: %w", registeredResourceValueFQN, ErrInvalidResource) + } + if parsed.Namespace == "" { + return fmt.Errorf("registered resource value FQN must be namespaced in strict mode [%s]: %w", registeredResourceValueFQN, ErrInvalidResource) + } + } return nil } @@ -93,8 +102,10 @@ func validateSubjectMapping(subjectMapping *policy.SubjectMapping) error { // // - must not be nil // - must have a non-empty FQN -// - must have non-empty values -// - must have non-empty values FQNs +// - if values are present, they must have non-empty values FQNs +// +// Attribute definitions may legitimately exist before any values have been created +// for them, so an empty values list is allowed here. func validateAttribute(attribute *policy.Attribute) error { if attribute == nil { return fmt.Errorf("attribute is nil: %w", ErrInvalidAttributeDefinition) @@ -102,9 +113,6 @@ func validateAttribute(attribute *policy.Attribute) error { if attribute.GetFqn() == "" { return fmt.Errorf("attribute FQN is empty: %w", ErrInvalidAttributeDefinition) } - if len(attribute.GetValues()) == 0 { - return fmt.Errorf("attribute values are empty: %w", ErrInvalidAttributeDefinition) - } for _, value := range attribute.GetValues() { if value == nil { return fmt.Errorf("attribute value is nil: %w", ErrInvalidAttributeDefinition) @@ -176,6 +184,7 @@ func validateGetResourceDecision( entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, action *policy.Action, resource *authzV2.Resource, + namespacedPolicy bool, ) error { if entitlements == nil { return fmt.Errorf("entitled FQNs to actions are nil: %w", ErrInvalidEntitledFQNsToActions) @@ -186,5 +195,18 @@ func validateGetResourceDecision( if resource.GetResource() == nil { return fmt.Errorf("resource is nil: %w", ErrInvalidResource) } + if namespacedPolicy { //nolint:nestif // validation reads clearer inline + if _, ok := resource.GetResource().(*authzV2.Resource_RegisteredResourceValueFqn); ok { + registeredResourceValueFQN := strings.ToLower(resource.GetRegisteredResourceValueFqn()) + // If namespaced policies are enabled, enforce that the registered resource value FQN is namespaced. + parsed, err := identifier.Parse[*identifier.FullyQualifiedRegisteredResourceValue](registeredResourceValueFQN) + if err != nil { + return fmt.Errorf("invalid registered resource value FQN [%s]: %w", registeredResourceValueFQN, ErrInvalidResource) + } + if parsed.Namespace == "" { + return fmt.Errorf("registered resource value FQN must be namespaced in strict mode [%s]: %w", registeredResourceValueFQN, ErrInvalidResource) + } + } + } return nil } diff --git a/service/internal/access/v2/validators_test.go b/service/internal/access/v2/validators_test.go index 67c6d7557c..ea56660f9b 100644 --- a/service/internal/access/v2/validators_test.go +++ b/service/internal/access/v2/validators_test.go @@ -217,22 +217,22 @@ func TestValidateAttribute(t *testing.T) { wantErr: ErrInvalidAttributeDefinition, }, { - name: "Empty attribute values", + name: "Empty attribute values are allowed", attribute: &policy.Attribute{ Fqn: "https://example.org/attr/name", Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, Values: []*policy.Value{}, }, - wantErr: ErrInvalidAttributeDefinition, + wantErr: nil, }, { - name: "Nil attribute values", + name: "Nil attribute values are allowed", attribute: &policy.Attribute{ Fqn: "https://example.org/attr/name", Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, Values: nil, }, - wantErr: ErrInvalidAttributeDefinition, + wantErr: nil, }, { name: "Nil value in attribute values", @@ -421,60 +421,81 @@ func TestValidateGetResourceDecision(t *testing.T) { }, } + validRRResource := &authzV2.Resource{ + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: "https://reg_res/resource1/value/value1", + }, + } + tests := []struct { - name string - entitlements map[string][]*policy.Action - action *policy.Action - resource *authzV2.Resource - wantErr error + name string + entitlements map[string][]*policy.Action + action *policy.Action + resource *authzV2.Resource + namespacedPolicy bool + wantErr error }{ { - name: "Valid inputs", - entitlements: validEntitledFQNsToActions, - action: validAction, - resource: validResource, - wantErr: nil, + name: "Valid inputs", + entitlements: validEntitledFQNsToActions, + action: validAction, + resource: validResource, + namespacedPolicy: false, + wantErr: nil, }, { - name: "Nil entitlements", - entitlements: nil, - action: validAction, - resource: validResource, - wantErr: ErrInvalidEntitledFQNsToActions, + name: "Nil entitlements", + entitlements: nil, + action: validAction, + resource: validResource, + namespacedPolicy: false, + wantErr: ErrInvalidEntitledFQNsToActions, }, { - name: "Nil action", - entitlements: validEntitledFQNsToActions, - action: nil, - resource: validResource, - wantErr: ErrInvalidAction, + name: "Nil action", + entitlements: validEntitledFQNsToActions, + action: nil, + resource: validResource, + namespacedPolicy: false, + wantErr: ErrInvalidAction, }, { - name: "Nil resource", - entitlements: validEntitledFQNsToActions, - action: validAction, - resource: nil, - wantErr: ErrInvalidResource, + name: "Nil resource", + entitlements: validEntitledFQNsToActions, + action: validAction, + resource: nil, + namespacedPolicy: false, + wantErr: ErrInvalidResource, }, { - name: "Empty action", - entitlements: validEntitledFQNsToActions, - action: &policy.Action{}, - resource: validResource, - wantErr: ErrInvalidAction, + name: "Empty action", + entitlements: validEntitledFQNsToActions, + action: &policy.Action{}, + resource: validResource, + namespacedPolicy: false, + wantErr: ErrInvalidAction, }, { - name: "Empty resource", - entitlements: validEntitledFQNsToActions, - action: validAction, - resource: &authzV2.Resource{}, - wantErr: ErrInvalidResource, + name: "Empty resource", + entitlements: validEntitledFQNsToActions, + action: validAction, + resource: &authzV2.Resource{}, + namespacedPolicy: false, + wantErr: ErrInvalidResource, + }, + { + name: "Unnamespaced RR resource", + entitlements: validEntitledFQNsToActions, + action: validAction, + resource: validRRResource, + namespacedPolicy: true, + wantErr: ErrInvalidResource, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateGetResourceDecision(tt.entitlements, tt.action, tt.resource) + err := validateGetResourceDecision(tt.entitlements, tt.action, tt.resource, tt.namespacedPolicy) if tt.wantErr != nil { require.ErrorIs(t, err, tt.wantErr) } else { @@ -486,6 +507,7 @@ func TestValidateGetResourceDecision(t *testing.T) { func TestValidateGetDecisionRegisteredResource(t *testing.T) { validRegisteredResourceValueFQN := "https://reg_res/resource1/value/value1" + validNamespacedRegisteredResourceValueFQN := "https://namespace.com/reg_res/resource1/value/value1" validAction := &policy.Action{ Name: "read", @@ -510,6 +532,7 @@ func TestValidateGetDecisionRegisteredResource(t *testing.T) { registeredResourceValueFQN string action *policy.Action resources []*authzV2.Resource + namespacedPolicy bool wantErr error }{ { @@ -517,6 +540,7 @@ func TestValidateGetDecisionRegisteredResource(t *testing.T) { registeredResourceValueFQN: validRegisteredResourceValueFQN, action: validAction, resources: validResources, + namespacedPolicy: false, wantErr: nil, }, { @@ -524,6 +548,7 @@ func TestValidateGetDecisionRegisteredResource(t *testing.T) { registeredResourceValueFQN: "invalid-fqn", action: validAction, resources: validResources, + namespacedPolicy: false, wantErr: identifier.ErrInvalidFQNFormat, }, { @@ -531,6 +556,7 @@ func TestValidateGetDecisionRegisteredResource(t *testing.T) { registeredResourceValueFQN: validRegisteredResourceValueFQN, action: emptyNameAction, resources: validResources, + namespacedPolicy: false, wantErr: ErrInvalidAction, }, { @@ -538,6 +564,7 @@ func TestValidateGetDecisionRegisteredResource(t *testing.T) { registeredResourceValueFQN: validRegisteredResourceValueFQN, action: validAction, resources: []*authzV2.Resource{}, + namespacedPolicy: false, wantErr: ErrInvalidResource, }, { @@ -545,13 +572,30 @@ func TestValidateGetDecisionRegisteredResource(t *testing.T) { registeredResourceValueFQN: validRegisteredResourceValueFQN, action: validAction, resources: []*authzV2.Resource{nil}, + namespacedPolicy: false, + wantErr: ErrInvalidResource, + }, + { + name: "Unnamespaced RR resource in strict mode", + registeredResourceValueFQN: validRegisteredResourceValueFQN, + action: validAction, + resources: validResources, + namespacedPolicy: true, wantErr: ErrInvalidResource, }, + { + name: "Namespaced RR resource in strict mode pass", + registeredResourceValueFQN: validNamespacedRegisteredResourceValueFQN, + action: validAction, + resources: validResources, + namespacedPolicy: true, + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateGetDecisionRegisteredResource(tt.registeredResourceValueFQN, tt.action, tt.resources) + err := validateGetDecisionRegisteredResource(tt.registeredResourceValueFQN, tt.action, tt.resources, tt.namespacedPolicy) if tt.wantErr != nil { require.ErrorIs(t, err, tt.wantErr) } else { diff --git a/service/internal/auth/README.md b/service/internal/auth/README.md new file mode 100644 index 0000000000..5c839509f9 --- /dev/null +++ b/service/internal/auth/README.md @@ -0,0 +1,78 @@ +# Auth Package + +This package handles authentication (authn) and authorization (authz) for the OpenTDF platform. + +## Package Structure + +``` +auth/ +├── authn.go # Authentication middleware and token validation +├── casbin.go # V1 Casbin enforcer (legacy, path-based authz) +├── config.go # Configuration types +├── discovery.go # OIDC discovery +└── authz/ # V2 authorization system + ├── authorizer.go # Authorizer interface and factory + ├── resolver.go # AuthzResolver for fine-grained resource authorization + └── casbin/ # V2 Casbin implementation with multi-claim support +``` + +## Security Guidelines + +### Never Log Sensitive Authentication Data + +**DO NOT log the following:** + +1. **JWT Tokens** - Never log full tokens, even at DEBUG level + - Tokens can be replayed if logs are compromised + - Tokens may contain PII in claims + - Large tokens can be used for DoS attacks (disk/memory exhaustion) + - Unsanitized token content can enable log injection attacks + +2. **Credentials** - Never log passwords, API keys, or secrets + +3. **Full UserInfo responses** - May contain PII + +**Safe to log:** +- Claim names (e.g., which claim was missing) +- Extracted role/group names (after validation) +- Subject identifiers (if not sensitive in your context) +- Error types and messages (without embedding tokens) + +### Example: What NOT to do + +```go +// BAD - logs full token (security risk) +e.logger.Debug("processing token", slog.Any("token", token)) + +// BAD - token in error message +e.logger.Error("auth failed", slog.String("token", tokenString)) +``` + +### Example: Safe logging + +```go +// GOOD - no sensitive data +e.logger.Debug("extracting roles from token") + +// GOOD - only logs claim name, not value +e.logger.Warn("claim not found", slog.String("claim", claimName)) + +// GOOD - logs extracted, bounded data +e.logger.Debug("roles extracted", slog.Int("count", len(roles))) +``` + +### Log Injection Prevention + +Even when logging "safe" data extracted from tokens, be aware that: +- Claims can contain newlines (fake log entries) +- Claims can contain ANSI escape codes +- Claims can be arbitrarily large + +Consider truncating or sanitizing any user-controlled data before logging. + +## V1 vs V2 Authorization + +- **V1** (`casbin.go`): Legacy path-based authorization using `(subject, resource, action)` +- **V2** (`authz/casbin/`): RPC + dimensions authorization using `(subject, rpc, dimensions)` with support for fine-grained resource authorization via `AuthzResolver` + +V1 is being maintained for backward compatibility. New features should use V2. diff --git a/service/internal/auth/authn.go b/service/internal/auth/authn.go index e4cd0bdf47..be226592c6 100644 --- a/service/internal/auth/authn.go +++ b/service/internal/auth/authn.go @@ -11,7 +11,6 @@ import ( "log/slog" "net/http" "net/url" - "regexp" "slices" "strings" "time" @@ -23,11 +22,13 @@ import ( "github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/opentdf/platform/service/internal/auth/authz" + _ "github.com/opentdf/platform/service/internal/auth/authz/casbin" // Register casbin authorizer "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/logger/audit" - "google.golang.org/grpc/metadata" - ctxAuth "github.com/opentdf/platform/service/pkg/auth" + platformauthz "github.com/opentdf/platform/service/pkg/authz" + "google.golang.org/grpc/metadata" ) var ( @@ -39,9 +40,6 @@ var ( // KAS Public Key Endpoints "/kas.AccessService/PublicKey", "/kas.AccessService/LegacyPublicKey", - "/kas.AccessService/Info", - "/kas/kas_public_key", - "/kas/v2/kas_public_key", // HealthZ "/healthz", "/grpc.health.v1.Health/Check", @@ -70,10 +68,15 @@ var ( canonicalIPCHeaderClientID = http.CanonicalHeaderKey("x-ipc-auth-client-id") canonicalIPCHeaderAccessToken = http.CanonicalHeaderKey("x-ipc-access-token") + + // errNoResourceContext indicates no resolver is registered or resource authorization is not supported. + // This is not an error condition - it means resource-level authorization is not applicable. + errNoResourceContext = errors.New("no resource context") ) const ( refreshInterval = 15 * time.Minute + dpopJWTType = "dpop+jwt" ActionRead = "read" ActionWrite = "write" ActionDelete = "delete" @@ -84,12 +87,16 @@ const ( // Authentication holds a jwks cache and information about the openid configuration type Authentication struct { enforceDPoP bool - // keySet holds a cached key set - cachedKeySet jwk.Set + // tokenVerifier validates access tokens against the configured IdP. + tokenVerifier *TokenVerifier // openidConfigurations holds the openid configuration for the issuer oidcConfiguration AuthNConfig - // Casbin enforcer + // Casbin enforcer for v1 authorization (implements authz.V1Enforcer) enforcer *Enforcer + // authorizer is the pluggable authorization engine (v1, v2, etc.) + authorizer authz.Authorizer + // authzResolverRegistry holds per-method resolvers for extracting authorization dimensions + authzResolverRegistry *authz.ResolverRegistry // Public Routes HTTP & gRPC publicRoutes []string // IPC Reauthorization Routes @@ -101,66 +108,78 @@ type Authentication struct { _testCheckTokenFunc func(ctx context.Context, authHeader []string, dpopInfo receiverInfo, dpopHeader []string) (jwt.Token, context.Context, error) } +// AuthenticatorOption is a functional option for configuring Authentication. +type AuthenticatorOption func(*Authentication) + +// WithAuthzResolverRegistry sets the authorization resolver registry. +// When set, the interceptors will call resolvers to extract authorization dimensions. +func WithAuthzResolverRegistry(registry *authz.ResolverRegistry) AuthenticatorOption { + return func(a *Authentication) { + a.authzResolverRegistry = registry + } +} + // Creates new authN which is used to verify tokens for a set of given issuers -func NewAuthenticator(ctx context.Context, cfg Config, logger *logger.Logger, wellknownRegistration func(namespace string, config any) error) (*Authentication, error) { +func NewAuthenticator(ctx context.Context, cfg Config, logger *logger.Logger, wellknownRegistration func(namespace string, config any) error, opts ...AuthenticatorOption) (*Authentication, error) { a := &Authentication{ enforceDPoP: cfg.EnforceDPoP, logger: logger, } - // validate the configuration - if err := cfg.validateAuthNConfig(a.logger); err != nil { - return nil, err + // Apply options + for _, opt := range opts { + opt(a) } - cache := jwk.NewCache(ctx) - - // Build new cache - // Discover OIDC Configuration - oidcConfig, err := DiscoverOIDCConfiguration(ctx, cfg.Issuer, a.logger) + tokenVerifier, oidcConfig, err := newTokenVerifier(ctx, cfg.AuthNConfig, a.logger) if err != nil { return nil, err } + a.tokenVerifier = tokenVerifier - // If the issuer is different from the one in the configuration, update the configuration - // This could happen if we are hitting an internal endpoint. Example we might point to https://keycloak.opentdf.svc/realms/opentdf - // but the external facing issuer is https://keycloak.opentdf.local/realms/opentdf - if oidcConfig.Issuer != cfg.Issuer { - cfg.Issuer = oidcConfig.Issuer - } - - cacheInterval, err := time.ParseDuration(cfg.CacheRefresh) + roleProvider, err := resolveRoleProvider(ctx, cfg, logger) if err != nil { - logger.ErrorContext(ctx, - "invalid cache_refresh_interval", - slog.String("cache_refresh_interval", cfg.CacheRefresh), - slog.Any("err", err), - ) - cacheInterval = refreshInterval - } - - // Register the jwks_uri with the cache - if err := cache.Register(oidcConfig.JwksURI, jwk.WithMinRefreshInterval(cacheInterval)); err != nil { return nil, err } - casbinConfig := CasbinConfig{ PolicyConfig: cfg.Policy, + RoleProvider: roleProvider, } logger.Info("initializing casbin enforcer") if a.enforcer, err = NewCasbinEnforcer(casbinConfig, a.logger); err != nil { return nil, fmt.Errorf("failed to initialize casbin enforcer: %w", err) } - // Need to refresh the cache to verify jwks is available - _, err = cache.Refresh(ctx, oidcConfig.JwksURI) - if err != nil { - return nil, err + // Initialize the pluggable authorizer based on engine and version + authzCfg := authz.Config{ + Engine: cfg.Policy.Engine, + Version: cfg.Policy.Version, + PolicyConfig: authz.PolicyConfig{ + Engine: cfg.Policy.Engine, + Version: cfg.Policy.Version, + UserNameClaim: cfg.Policy.UserNameClaim, + GroupsClaim: cfg.Policy.GroupsClaim, + ClientIDClaim: cfg.Policy.ClientIDClaim, + Csv: cfg.Policy.Csv, + Extension: cfg.Policy.Extension, + Model: cfg.Policy.Model, + RoleMap: cfg.Policy.RoleMap, + Adapter: cfg.Policy.Adapter, + }, + Logger: logger, + // Pass the v1 enforcer to break circular dependency + // The casbin authorizer will use this for v1 mode + Options: []authz.Option{authz.WithV1Enforcer(a.enforcer)}, + } + logger.Info( + "initializing authorizer", + slog.String("engine", authzCfg.Engine), + slog.String("version", authzCfg.Version), + ) + if a.authorizer, err = authz.New(authzCfg); err != nil { + return nil, fmt.Errorf("failed to initialize authorizer: %w", err) } - // Set the cache - a.cachedKeySet = jwk.NewCachedSet(cache, oidcConfig.JwksURI) - // Combine public routes a.publicRoutes = append(a.publicRoutes, cfg.PublicRoutes...) a.publicRoutes = append(a.publicRoutes, allowedPublicEndpoints[:]...) @@ -168,10 +187,10 @@ func NewAuthenticator(ctx context.Context, cfg Config, logger *logger.Logger, we // Combine IPC reauthorization routes a.ipcReauthRoutes = append(ipcReauthRoutes[:], cfg.IPCReauthRoutes...) - a.oidcConfiguration = cfg.AuthNConfig + a.oidcConfiguration = tokenVerifier.oidcConfiguration // Try an register oidc issuer to wellknown service but don't return an error if it fails - if err := wellknownRegistration("platform_issuer", cfg.Issuer); err != nil { + if err := wellknownRegistration("platform_issuer", a.oidcConfiguration.Issuer); err != nil { logger.Warn("failed to register platform issuer", slog.Any("error", err)) } @@ -242,7 +261,8 @@ func (a Authentication) MuxHandler(handler http.Handler) http.Handler { m: []string{r.Method}, }, dp) if err != nil { - log.WarnContext(ctx, + log.WarnContext( + ctx, "unauthenticated", slog.Any("error", err), slog.Any("dpop", dp), @@ -277,24 +297,28 @@ func (a Authentication) MuxHandler(handler http.Handler) http.Handler { default: action = ActionUnsafe } - if allow, err := a.enforcer.Enforce(accessTok, r.URL.Path, action); err != nil { - if err.Error() == "permission denied" { + roleReq := platformauthz.RoleRequest{ + Issuer: a.oidcConfiguration.Issuer, + Resource: r.URL.Path, + Action: action, + } + if allowed, metadata, err := a.enforcer.Enforce(ctx, accessTok, roleReq); err != nil { + if errors.Is(err, ErrPermissionDenied) { log.WarnContext( ctx, "permission denied", - slog.String("azp", accessTok.Subject()), - slog.Any("error", err), + permissionDeniedLogAttrs(accessTok, metadata, err)..., ) http.Error(w, "permission denied", http.StatusForbidden) return } http.Error(w, "internal server error", http.StatusInternalServerError) return - } else if !allow { + } else if !allowed { log.WarnContext( ctx, "permission denied", - slog.String("azp", accessTok.Subject()), + permissionDeniedLogAttrs(accessTok, metadata, nil)..., ) http.Error(w, "permission denied", http.StatusForbidden) return @@ -324,8 +348,6 @@ func (a Authentication) ConnectUnaryServerInterceptor() connect.UnaryInterceptor m: []string{http.MethodPost}, } - ri.u = append(ri.u, a.lookupGatewayPaths(ctx, req.Spec().Procedure, req.Header())...) - // Interceptor Logic // Allow health checks and other public routes to pass through if slices.ContainsFunc(a.publicRoutes, a.isPublicRoute(req.Spec().Procedure)) { //nolint:contextcheck // There is no way to pass a context here @@ -337,9 +359,8 @@ func (a Authentication) ConnectUnaryServerInterceptor() connect.UnaryInterceptor return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("missing authorization header")) } - // parse the rpc method + // parse the rpc method to extract action p := strings.Split(req.Spec().Procedure, "/") - resource := p[1] + "/" + p[2] action := getAction(p[2]) token, ctxWithJWK, err := a.checkToken( @@ -366,29 +387,62 @@ func (a Authentication) ConnectUnaryServerInterceptor() connect.UnaryInterceptor ctxWithJWK = ctxAuth.EnrichIncomingContextMetadataWithAuthn(ctxWithJWK, log, clientID) } - // Check if the token is allowed to access the resource - if allowed, err := a.enforcer.Enforce(token, resource, action); err != nil { - if err.Error() == "permission denied" { - log.WarnContext( - ctxWithJWK, - "permission denied", - slog.String("azp", token.Subject()), - slog.Any("error", err), - ) - return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied")) - } - return nil, err - } else if !allowed { - log.WarnContext(ctxWithJWK, "permission denied", slog.String("azp", token.Subject())) + // Perform authorization check + result := a.authorize(ctxWithJWK, log, token, req, action) + if result.err != nil { + return nil, connect.NewError(result.errCode, result.err) + } + + decision := result.decision + if !decision.Allowed { + log.WarnContext( + ctxWithJWK, "permission denied", + slog.String("azp", token.Subject()), + slog.String("mode", string(decision.Mode)), + slog.String("reason", decision.Reason), + ) return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied")) } - return next(ctxWithJWK, req) + log.DebugContext( + ctxWithJWK, "authorization granted", + slog.String("mode", string(decision.Mode)), + slog.String("reason", decision.Reason), + ) + + // Inject ResolverContext into handler context for cache reuse + // (avoids duplicate DB queries when resolver already fetched the data) + handlerCtx := ctxWithJWK + if result.resourceContext != nil { + handlerCtx = authz.ContextWithResolverContext(handlerCtx, result.resourceContext) + } + + return next(handlerCtx, req) }) } return connect.UnaryInterceptorFunc(interceptor) } +func permissionDeniedLogAttrs(token jwt.Token, casbinAuthz map[string]any, err error) []any { + attrs := []any{slog.String("azp", token.Subject())} + + configuredGroupsClaim, _ := casbinAuthz[casbinAuthzConfiguredGroupsClaimKey].(string) + subjectGroups, hasSubjectGroups := casbinAuthz[casbinAuthzSubjectGroupsKey] + if configuredGroupsClaim != "" || hasSubjectGroups { + attrs = append(attrs, slog.Group( + "casbin_authz", + slog.String("configured_groups_claim", configuredGroupsClaim), + slog.Any("subject_groups", subjectGroups), + )) + } + + if err != nil { + attrs = append(attrs, slog.Any("error", err)) + } + + return attrs +} + // IPCMetadataClientInterceptor transfers gRPC outgoing metadata to Connect request headers for IPC calls func IPCMetadataClientInterceptor(log *logger.Logger) connect.UnaryInterceptorFunc { return connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { @@ -457,6 +511,103 @@ func (a Authentication) IPCUnaryServerInterceptor() connect.UnaryInterceptorFunc return connect.UnaryInterceptorFunc(interceptor) } +// authzResult holds the result of an authorization check +type authzResult struct { + decision *authz.Decision + resourceContext *authz.ResolverContext // Cached resolver data for handler reuse + err error + errCode connect.Code +} + +// resolveResourceContext attempts to resolve authorization dimensions using a registered resolver. +// Returns errNoResourceContext if no resolver is registered or if resolvers are not supported. +func (a *Authentication) resolveResourceContext( + ctx context.Context, + log *logger.Logger, + req connect.AnyRequest, +) (*authz.ResolverContext, error) { + // Skip if resolver registry not available or authorizer doesn't support resource authorization + if a.authzResolverRegistry == nil || a.authorizer == nil || !a.authorizer.SupportsResourceAuthorization() { + return nil, errNoResourceContext + } + + resolver, ok := a.authzResolverRegistry.Get(req.Spec().Procedure) + if !ok { + return nil, errNoResourceContext + } + + resolvedCtx, err := resolver(ctx, req) + if err != nil { + log.WarnContext( + ctx, "authz resolver failed", + slog.String("procedure", req.Spec().Procedure), + slog.Any("error", err), + ) + return nil, err + } + + return &resolvedCtx, nil +} + +// authorize performs the full authorization check for a request. +// It builds the authorization request, resolves resource context if applicable, +// and returns the authorization decision. +func (a *Authentication) authorize( + ctx context.Context, + log *logger.Logger, + token jwt.Token, + req connect.AnyRequest, + action string, +) authzResult { + // Defensive check: authorizer must be initialized + if a.authorizer == nil { + log.ErrorContext(ctx, "authorizer not initialized") + return authzResult{ + err: errors.New("authorization system not configured"), + errCode: connect.CodeInternal, + } + } + + // Build authorization request + authzReq := &authz.Request{ + Token: token, + RPC: req.Spec().Procedure, + Action: action, + } + + // Try to resolve resource context for fine-grained authorization + resourceCtx, resolveErr := a.resolveResourceContext(ctx, log, req) + if resolveErr != nil && !errors.Is(resolveErr, errNoResourceContext) { + return authzResult{ + err: errors.New("authorization context resolution failed"), + errCode: connect.CodePermissionDenied, + } + } + // Only set resource context if we actually resolved one (not errNoResourceContext) + if resolveErr == nil { + authzReq.ResourceContext = resourceCtx + } + + // Perform authorization check + decision, authzErr := a.authorizer.Authorize(ctx, authzReq) + if authzErr != nil { + log.ErrorContext( + ctx, "authorization error", + slog.Any("error", authzErr), + slog.String("procedure", req.Spec().Procedure), + ) + return authzResult{ + err: errors.New("authorization system error"), + errCode: connect.CodeInternal, + } + } + + return authzResult{ + decision: decision, + resourceContext: resourceCtx, // Pass resolved data to handler for cache reuse + } +} + // getAction returns the action based on the rpc name func getAction(method string) string { switch { @@ -492,16 +643,12 @@ func (a *Authentication) checkToken(ctx context.Context, authHeader []string, dp return nil, nil, errors.New("not of type bearer or dpop") } - // Now we verify the token signature - accessToken, err := jwt.Parse([]byte(tokenRaw), - jwt.WithKeySet(a.cachedKeySet), - jwt.WithValidate(true), - jwt.WithIssuer(a.oidcConfiguration.Issuer), - jwt.WithAudience(a.oidcConfiguration.Audience), - jwt.WithAcceptableSkew(a.oidcConfiguration.TokenSkew), - ) + if a.tokenVerifier == nil { + return nil, nil, errors.New("access token verifier is not configured") + } + + accessToken, err := a.tokenVerifier.VerifyAccessToken(ctx, tokenRaw) if err != nil { - a.logger.Warn("failed to validate auth token", slog.Any("err", err)) return nil, nil, err } @@ -564,7 +711,7 @@ func (a Authentication) validateDPoP(accessToken jwt.Token, acessTokenRaw string } sig := dpop.Signatures()[0] protectedHeaders := sig.ProtectedHeaders() - if protectedHeaders.Type() != "dpop+jwt" { + if protectedHeaders.Type() != dpopJWTType { return nil, fmt.Errorf("invalid typ on DPoP JWT: %v", protectedHeaders.Type()) } @@ -655,14 +802,16 @@ func (a Authentication) isPublicRoute(path string) func(string) bool { return func(route string) bool { matched, err := doublestar.Match(route, path) if err != nil { - a.logger.Warn("error matching route", + a.logger.Warn( + "error matching route", slog.String("route", route), slog.String("path", path), slog.Any("error", err), ) return false } - a.logger.Trace("matching route", + a.logger.Trace( + "matching route", slog.String("route", route), slog.String("path", path), slog.Bool("matched", matched), @@ -671,78 +820,6 @@ func (a Authentication) isPublicRoute(path string) func(string) bool { } } -func (a Authentication) lookupOrigins(header http.Header) []string { - result := make([]string, 0) - for _, m := range []string{"Grpcgateway-Origin", "Grpcgateway-Referer", "Origin"} { - origins := header.Values(m) - if len(origins) == 0 { - continue - } - for _, o := range origins { - if strings.HasSuffix(o, ":443") { - o = "https://" + strings.TrimPrefix(strings.TrimSuffix(o, ":443"), "https://") - } else { - o = strings.TrimSuffix(o, ":80") - } - result = append(result, o) - } - } - return result -} - -var goodPaths = regexp.MustCompile(`^[\w/-]{1,128}$`) - -func (a Authentication) lookupGatewayPaths(ctx context.Context, procedure string, header http.Header) []string { - origins := a.lookupOrigins(header) - if len(origins) == 0 { - return nil - } - - var paths []string - switch procedure { - case "/kas.AccessService/Rewrap": - paths = append(paths, "/kas/v2/rewrap") - default: - patterns := header["Pattern"] - if len(patterns) == 0 { - a.logger.InfoContext(ctx, - "underspecified grpc gateway path; no pattern header", - slog.Any("origin", origins), - slog.String("procedure", procedure), - ) - paths = allowedPublicEndpoints[:] - } else { - a.logger.InfoContext(ctx, - "underspecified grpc gateway path; patterns found", - slog.Any("origin", origins), - slog.String("procedure", procedure), - slog.Any("patterns", patterns), - ) - } - for _, pattern := range patterns { - if matched := goodPaths.MatchString(pattern); matched { - paths = append(paths, pattern) - } - } - if len(paths) != len(patterns) { - a.logger.WarnContext(ctx, - "invalid grpc gateway path; ignoring one or more invalid patterns", - slog.Any("origin", origins), - slog.String("procedure", procedure), - slog.Any("patterns", patterns), - ) - } - } - - u := make([]string, 0, len(origins)*len(paths)) - for _, o := range origins { - for _, p := range paths { - u = append(u, normalizeURL(o, &url.URL{Path: p})) - } - } - return u -} - func (a Authentication) ipcReauthCheck(ctx context.Context, path string, header http.Header) (context.Context, error) { for _, route := range a.ipcReauthRoutes { reqPath := path @@ -753,12 +830,9 @@ func (a Authentication) ipcReauthCheck(ctx context.Context, path string, header return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("missing authorization header")) } - u := []string{path} - u = append(u, a.lookupGatewayPaths(ctx, path, header)...) - // Validate the token and create a JWT token token, ctxWithJWK, err := a.checkToken(ctx, authHeader, receiverInfo{ - u: u, + u: []string{path}, m: []string{http.MethodPost}, }, header["Dpop"]) if err != nil { diff --git a/service/internal/auth/authn_test.go b/service/internal/auth/authn_test.go index fcc0b6b1e2..5b9eaec933 100644 --- a/service/internal/auth/authn_test.go +++ b/service/internal/auth/authn_test.go @@ -215,6 +215,57 @@ func TestNormalizeUrl(t *testing.T) { } } +func TestPermissionDeniedLogAttrs(t *testing.T) { + tok := jwt.New() + require.NoError(t, tok.Set(jwt.SubjectKey, "client-subject")) + + attrs := permissionDeniedLogAttrs(tok, map[string]any{ + casbinAuthzConfiguredGroupsClaimKey: "custom.groups", + casbinAuthzSubjectGroupsKey: []string{"opentdf-standard"}, + }, ErrPermissionDenied) + + require.Len(t, attrs, 3) + assert.Equal(t, slog.String("azp", "client-subject"), attrs[0]) + + casbinAuthzAttr, ok := attrs[1].(slog.Attr) + require.True(t, ok) + assert.Equal(t, "casbin_authz", casbinAuthzAttr.Key) + + casbinAuthzAttrs := casbinAuthzAttr.Value.Group() + require.Len(t, casbinAuthzAttrs, 2) + assert.Equal(t, slog.String("configured_groups_claim", "custom.groups"), casbinAuthzAttrs[0]) + assert.Equal(t, "subject_groups", casbinAuthzAttrs[1].Key) + assert.Equal(t, []string{"opentdf-standard"}, casbinAuthzAttrs[1].Value.Any()) + + errorAttr, ok := attrs[2].(slog.Attr) + require.True(t, ok) + assert.Equal(t, "error", errorAttr.Key) + loggedErr, ok := errorAttr.Value.Any().(error) + require.True(t, ok) + if !errors.Is(loggedErr, ErrPermissionDenied) { + t.Fatalf("expected error to wrap %v", ErrPermissionDenied) + } +} + +func TestPermissionDeniedLogAttrsWithoutSubjectInfo(t *testing.T) { + tok := jwt.New() + require.NoError(t, tok.Set(jwt.SubjectKey, "client-subject")) + + attrs := permissionDeniedLogAttrs(tok, nil, ErrPermissionDenied) + + require.Len(t, attrs, 2) + assert.Equal(t, slog.String("azp", "client-subject"), attrs[0]) + + errorAttr, ok := attrs[1].(slog.Attr) + require.True(t, ok) + assert.Equal(t, "error", errorAttr.Key) + loggedErr, ok := errorAttr.Value.Any().(error) + require.True(t, ok) + if !errors.Is(loggedErr, ErrPermissionDenied) { + t.Fatalf("expected error to wrap %v", ErrPermissionDenied) + } +} + func (s *AuthSuite) Test_IPCUnaryServerInterceptor() { // Mock the checkToken method to return a valid token and context mockToken := jwt.New() @@ -601,89 +652,6 @@ func (s *AuthSuite) TestDPoPEndToEnd_GRPC() { s.Equal(dpopJWK.N(), dpopJWKFromRequest.N()) } -func (s *AuthSuite) TestDPoPEndToEnd_HTTP() { - dpopKeyRaw, err := rsa.GenerateKey(rand.Reader, 2048) - s.Require().NoError(err) - dpopKey, err := jwk.FromRaw(dpopKeyRaw) - s.Require().NoError(err) - s.Require().NoError(dpopKey.Set(jwk.AlgorithmKey, jwa.RS256)) - - tok := jwt.New() - s.Require().NoError(tok.Set(jwt.ExpirationKey, time.Now().Add(time.Hour))) - s.Require().NoError(tok.Set("iss", s.server.URL)) - s.Require().NoError(tok.Set("aud", "test")) - s.Require().NoError(tok.Set("cid", "client2")) - s.Require().NoError(tok.Set("realm_access", map[string][]string{"roles": {"opentdf-standard"}})) - thumbprint, err := dpopKey.Thumbprint(crypto.SHA256) - s.Require().NoError(err) - cnf := map[string]string{"jkt": base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(thumbprint)} - s.Require().NoError(tok.Set("cnf", cnf)) - signedTok, err := jwt.Sign(tok, jwt.WithKey(jwa.RS256, s.key)) - s.Require().NoError(err) - - jwkChan := make(chan jwk.Key, 1) - timeout := make(chan string, 1) - clientIDChan := make(chan string, 1) - go func() { - time.Sleep(5 * time.Second) - timeout <- "" - }() - server := httptest.NewServer(s.auth.MuxHandler(http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { - jwkChan <- ctxAuth.GetJWKFromContext(req.Context(), logger.CreateTestLogger()) - inbound := true - cid, _ := ctxAuth.GetClientIDFromContext(req.Context(), inbound) - clientIDChan <- cid - }))) - defer server.Close() - - req, err := http.NewRequest(http.MethodGet, server.URL+"/attributes", nil) - - addingInterceptor := sdkauth.NewTokenAddingInterceptorWithClient(&FakeTokenSource{ - key: dpopKey, - accessToken: string(signedTok), - }, httputil.SafeHTTPClientWithTLSConfig(&tls.Config{ - MinVersion: tls.VersionTLS12, - })) - s.Require().NoError(err) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedTok)) - dpopTok, err := addingInterceptor.GetDPoPToken(server.URL+"/attributes", "GET", string(signedTok)) - s.Require().NoError(err) - req.Header.Set("DPoP", dpopTok) - - client := httputil.SafeHTTPClient() // use safe client to help validate the client - _, err = client.Do(req) - s.Require().NoError(err) - var dpopKeyFromRequest jwk.Key - select { - case k := <-jwkChan: - dpopKeyFromRequest = k - case <-timeout: - s.Require().FailNow("timed out waiting for call to complete") - } - var clientID string - select { - case cid := <-clientIDChan: - clientID = cid - case <-timeout: - s.Require().FailNow("timed out waiting for call to complete") - } - - s.Equal("client2", clientID) - - s.NotNil(dpopKeyFromRequest) - dpopJWKFromRequest, ok := dpopKeyFromRequest.(jwk.RSAPublicKey) - s.True(ok) - s.Require().NoError(err) - dpopPublic, err := dpopKey.PublicKey() - s.Require().NoError(err) - dpopJWK, ok := dpopPublic.(jwk.RSAPublicKey) - s.True(ok) - - s.Equal(dpopJWK.Algorithm(), dpopJWKFromRequest.Algorithm()) - s.Equal(dpopJWK.E(), dpopJWKFromRequest.E()) - s.Equal(dpopJWK.N(), dpopJWKFromRequest.N()) -} - func makeDPoPToken(t *testing.T, tc dpopTestCase) string { jtiBytes := make([]byte, sdkauth.JTILength) _, err := rand.Read(jtiBytes) @@ -747,9 +715,10 @@ func (s *AuthSuite) Test_Allowing_Auth_With_No_DPoP() { } config := Config{} config.AuthNConfig = authnConfig - auth, err := NewAuthenticator(context.Background(), config, &logger.Logger{ - Logger: slog.New(slog.Default().Handler()), - }, + auth, err := NewAuthenticator( + context.Background(), config, &logger.Logger{ + Logger: slog.New(slog.Default().Handler()), + }, func(_ string, _ any) error { return nil }, ) @@ -815,69 +784,6 @@ func (s *AuthSuite) Test_GetAction() { } } -func (s *AuthSuite) Test_LookupGatewayPaths() { - tests := []struct { - name string - path string - header http.Header - expected []string - }{ - { - name: "Valid Rewrap Path", - path: "/kas.AccessService/Rewrap", - header: http.Header{ - "Grpcgateway-Origin": []string{s.server.URL}, - }, - expected: []string{s.server.URL + "/kas/v2/rewrap"}, - }, - { - name: "Multiple Origins", - path: "/kas.AccessService/Rewrap", - header: http.Header{ - "Grpcgateway-Origin": []string{s.server.URL, "https://origin.1.com"}, - "Origin": []string{"https://origin.com"}, - }, - expected: []string{ - s.server.URL + "/kas/v2/rewrap", - "https://origin.1.com/kas/v2/rewrap", "https://origin.com/kas/v2/rewrap", - }, - }, - { - name: "Unknown Path with Pattern", - path: "/unknown/path", - header: http.Header{ - "Grpcgateway-Origin": []string{"https://origin.com"}, - "Pattern": []string{"some-pattern"}, - }, - expected: []string{"https://origin.com/some-pattern"}, - }, - { - name: "Unknown Path without Pattern", - path: "/unknown/path", - header: http.Header{ - "Grpcgateway-Origin": []string{"https://origin.com"}, - }, - expected: []string{"https://origin.com/wellknownconfiguration.WellKnownService/GetWellKnownConfiguration", "https://origin.com/.well-known/opentdf-configuration", "https://origin.com/kas.AccessService/PublicKey", "https://origin.com/kas.AccessService/LegacyPublicKey", "https://origin.com/kas.AccessService/Info", "https://origin.com/kas/kas_public_key", "https://origin.com/kas/v2/kas_public_key", "https://origin.com/healthz", "https://origin.com/grpc.health.v1.Health/Check"}, - }, - { - name: "Bad Path", - path: "/unkown.App", - header: http.Header{ - "Origin": []string{"https://origin.com"}, - "Pattern": []string{"/?this. is=bad"}, - }, - expected: []string{}, - }, - } - - for _, tt := range tests { - s.Run(tt.name, func() { - result := s.auth.lookupGatewayPaths(context.Background(), tt.path, tt.header) - s.Equal(tt.expected, result) - }) - } -} - func Test_GetClientIDFromToken(t *testing.T) { tests := []struct { name string diff --git a/service/internal/auth/authz/adapter_config.go b/service/internal/auth/authz/adapter_config.go new file mode 100644 index 0000000000..220ef43ed3 --- /dev/null +++ b/service/internal/auth/authz/adapter_config.go @@ -0,0 +1,202 @@ +package authz + +import "github.com/casbin/casbin/v2/persist" + +// EngineType identifies the authorization engine implementation. +type EngineType string + +const ( + // EngineCasbin uses Casbin for policy enforcement. + EngineCasbin EngineType = "casbin" + // // EngineCedar uses AWS Cedar for policy enforcement (future). + // EngineCedar EngineType = "cedar" + // // EngineOPA uses Open Policy Agent for policy enforcement (future). + // EngineOPA EngineType = "opa" +) + +// BaseAdapterConfig contains configuration common to all authorization adapters. +// This provides a consistent interface for subject extraction across engines. +type BaseAdapterConfig struct { + // UserNameClaim is the JWT claim containing the username. + UserNameClaim string + + // GroupsClaim is the JWT claim containing roles/groups (dot notation supported). + // Example: "realm_access.roles" for Keycloak. + GroupsClaim string + + // ClientIDClaim is the JWT claim containing the client ID. + ClientIDClaim string + + // Logger for authorization decisions (type: *logger.Logger) + Logger any +} + +// CasbinV1Config configures the legacy path-based Casbin authorizer. +// This model uses (subject, resource, action) tuples for authorization. +// +// Example policy: +// +// p, role:admin, *, *, allow +// p, role:standard, /attributes*, read, allow +type CasbinV1Config struct { + BaseAdapterConfig + + // Csv is the policy CSV content (overrides builtin if set). + Csv string + + // Extension appends additional rules to the policy. + Extension string + + // Model is the Casbin model configuration. + // If empty, uses the default RBAC model. + Model string + + // RoleMap maps external IdP roles to internal platform roles. + // + // Deprecated: Use Casbin grouping statements instead. + RoleMap map[string]string + + // Adapter is a custom policy adapter (e.g., SQL). + // If nil, uses string adapter with Csv content. + Adapter persist.Adapter + + // Enforcer is an existing v1 enforcer to delegate to. + // If provided, other policy fields are ignored. + Enforcer V1Enforcer +} + +// CasbinV2Config configures the RPC + dimensions Casbin authorizer. +// This model uses (subject, rpc, dimensions) tuples for authorization. +// +// Example policy: +// +// p, role:admin, *, *, allow +// p, role:standard, /policy.attributes.AttributesService/*, read, allow +// p, role:ns-admin, /policy.attributes.AttributesService/*, *, ns:my-namespace, allow +type CasbinV2Config struct { + BaseAdapterConfig + + // Csv is the policy CSV content (overrides builtin if set). + Csv string + + // Extension appends additional rules to the policy. + Extension string + + // Model is the Casbin model configuration. + // If empty, uses the v2 model with dimension support. + Model string + + // RoleMap maps external IdP roles to internal platform roles. + // + // Deprecated: Use Casbin grouping statements instead. + RoleMap map[string]string + + // Adapter is a custom policy adapter (e.g., SQL). + // If nil, uses string adapter with Csv content. + Adapter persist.Adapter +} + +// CedarConfig configures the AWS Cedar authorization engine (future). +// Cedar provides a policy language with strong typing and formal verification. +type CedarConfig struct { + BaseAdapterConfig + + // SchemaPath is the path to the Cedar schema file. + SchemaPath string + + // PoliciesPath is the path to Cedar policy files. + PoliciesPath string + + // EntitiesPath is the path to Cedar entities file. + EntitiesPath string +} + +// OPAConfig configures the Open Policy Agent authorization engine (future). +// OPA provides a general-purpose policy engine with Rego query language. +type OPAConfig struct { + BaseAdapterConfig + + // BundlePath is the path to the OPA bundle. + BundlePath string + + // Query is the Rego query for authorization decisions. + Query string +} + +// AdapterConfigFromExternal maps external configuration to the appropriate +// internal adapter configuration. This provides a clean boundary between +// customer-facing config (stable) and internal adapter config (can evolve). +// +// The external PolicyConfig is what customers configure in YAML/JSON. +// The internal adapter configs are what the authorization engines consume. +// +// Engine selection: +// - "casbin" (default): Returns CasbinV1Config or CasbinV2Config based on Version +// - "cedar": Returns CedarConfig (future) +// - "opa": Returns OPAConfig (future) +func AdapterConfigFromExternal(cfg Config) any { + base := BaseAdapterConfig{ + UserNameClaim: cfg.UserNameClaim, + GroupsClaim: cfg.GroupsClaim, + ClientIDClaim: cfg.ClientIDClaim, + Logger: cfg.Logger, + } + + opts := applyOptions(cfg.Options...) + + // Default engine to casbin for backwards compatibility + engine := cfg.Engine + if engine == "" { + engine = string(EngineCasbin) + } + + switch engine { + case string(EngineCasbin): + return casbinConfigFromExternal(cfg, base, opts) + // Future engines: + // case string(EngineCedar): + // return cedarConfigFromExternal(cfg, base) + // case string(EngineOPA): + // return opaConfigFromExternal(cfg, base) + default: + // Unknown engine defaults to casbin v1 for backwards compatibility + return casbinConfigFromExternal(cfg, base, opts) + } +} + +// casbinConfigFromExternal creates the appropriate Casbin config based on version. +func casbinConfigFromExternal(cfg Config, base BaseAdapterConfig, opts *optionConfig) any { + switch cfg.Version { + case "v2": + return CasbinV2Config{ + BaseAdapterConfig: base, + Csv: cfg.Csv, + Extension: cfg.Extension, + Model: cfg.Model, + RoleMap: cfg.RoleMap, + Adapter: adapterFromAny(cfg.Adapter), + } + default: // v1 or empty + return CasbinV1Config{ + BaseAdapterConfig: base, + Csv: cfg.Csv, + Extension: cfg.Extension, + Model: cfg.Model, + RoleMap: cfg.RoleMap, + Adapter: adapterFromAny(cfg.Adapter), + Enforcer: opts.V1Enforcer, + } + } +} + +// adapterFromAny converts an any type to persist.Adapter. +// Returns nil if the value is nil or not an Adapter. +func adapterFromAny(v any) persist.Adapter { + if v == nil { + return nil + } + if adapter, ok := v.(persist.Adapter); ok { + return adapter + } + return nil +} diff --git a/service/internal/auth/authz/adapter_config_test.go b/service/internal/auth/authz/adapter_config_test.go new file mode 100644 index 0000000000..26694beae5 --- /dev/null +++ b/service/internal/auth/authz/adapter_config_test.go @@ -0,0 +1,235 @@ +package authz + +import ( + "testing" + + "github.com/casbin/casbin/v2/model" + "github.com/casbin/casbin/v2/persist" + "github.com/stretchr/testify/assert" +) + +func TestEngineTypeConstants(t *testing.T) { + assert.Equal(t, EngineCasbin, EngineType("casbin")) +} + +func TestBaseAdapterConfig(t *testing.T) { + cfg := BaseAdapterConfig{ + UserNameClaim: "preferred_username", + GroupsClaim: "realm_access.roles", + ClientIDClaim: "azp", + } + + assert.Equal(t, "preferred_username", cfg.UserNameClaim) + assert.Equal(t, "realm_access.roles", cfg.GroupsClaim) + assert.Equal(t, "azp", cfg.ClientIDClaim) + assert.Nil(t, cfg.Logger) +} + +func TestAdapterConfigFromExternal_CasbinV1(t *testing.T) { + cfg := Config{ + Engine: "casbin", + Version: "v1", + PolicyConfig: PolicyConfig{ + UserNameClaim: "sub", + GroupsClaim: "roles", + ClientIDClaim: "client_id", + Csv: "p, role:admin, *, *, allow", + Extension: "p, role:test, /test, read, allow", + Model: "custom-model", + RoleMap: map[string]string{"ext-admin": "admin"}, + }, + } + + result := AdapterConfigFromExternal(cfg) + + v1Config, ok := result.(CasbinV1Config) + assert.True(t, ok, "Expected CasbinV1Config") + assert.Equal(t, "sub", v1Config.UserNameClaim) + assert.Equal(t, "roles", v1Config.GroupsClaim) + assert.Equal(t, "client_id", v1Config.ClientIDClaim) + assert.Equal(t, "p, role:admin, *, *, allow", v1Config.Csv) + assert.Equal(t, "p, role:test, /test, read, allow", v1Config.Extension) + assert.Equal(t, "custom-model", v1Config.Model) + assert.Equal(t, map[string]string{"ext-admin": "admin"}, v1Config.RoleMap) + assert.Nil(t, v1Config.Adapter) + assert.Nil(t, v1Config.Enforcer) +} + +func TestAdapterConfigFromExternal_CasbinV2(t *testing.T) { + cfg := Config{ + Engine: "casbin", + Version: "v2", + PolicyConfig: PolicyConfig{ + UserNameClaim: "sub", + GroupsClaim: "roles", + ClientIDClaim: "client_id", + Csv: "p, role:admin, *, *, allow", + Extension: "p, role:test, /test, read, allow", + Model: "custom-v2-model", + RoleMap: map[string]string{"ext-admin": "admin"}, + }, + } + + result := AdapterConfigFromExternal(cfg) + + v2Config, ok := result.(CasbinV2Config) + assert.True(t, ok, "Expected CasbinV2Config") + assert.Equal(t, "sub", v2Config.UserNameClaim) + assert.Equal(t, "roles", v2Config.GroupsClaim) + assert.Equal(t, "client_id", v2Config.ClientIDClaim) + assert.Equal(t, "p, role:admin, *, *, allow", v2Config.Csv) + assert.Equal(t, "p, role:test, /test, read, allow", v2Config.Extension) + assert.Equal(t, "custom-v2-model", v2Config.Model) + assert.Equal(t, map[string]string{"ext-admin": "admin"}, v2Config.RoleMap) + assert.Nil(t, v2Config.Adapter) +} + +func TestAdapterConfigFromExternal_DefaultEngine(t *testing.T) { + // Empty engine should default to casbin + cfg := Config{ + Engine: "", + Version: "v1", + } + + result := AdapterConfigFromExternal(cfg) + + _, ok := result.(CasbinV1Config) + assert.True(t, ok, "Expected CasbinV1Config with empty engine") +} + +func TestAdapterConfigFromExternal_DefaultVersion(t *testing.T) { + // Empty version should default to v1 + cfg := Config{ + Engine: "casbin", + Version: "", + } + + result := AdapterConfigFromExternal(cfg) + + _, ok := result.(CasbinV1Config) + assert.True(t, ok, "Expected CasbinV1Config with empty version") +} + +func TestAdapterConfigFromExternal_UnknownEngine(t *testing.T) { + // Unknown engine should fall back to casbin v1 + cfg := Config{ + Engine: "unknown-engine", + Version: "v1", + } + + result := AdapterConfigFromExternal(cfg) + + _, ok := result.(CasbinV1Config) + assert.True(t, ok, "Expected CasbinV1Config for unknown engine") +} + +func TestAdapterConfigFromExternal_WithV1Enforcer(t *testing.T) { + mockEnforcer := &mockV1Enforcer{} + + cfg := Config{ + Engine: "casbin", + Version: "v1", + Options: []Option{WithV1Enforcer(mockEnforcer)}, + } + + result := AdapterConfigFromExternal(cfg) + + v1Config, ok := result.(CasbinV1Config) + assert.True(t, ok) + assert.Equal(t, mockEnforcer, v1Config.Enforcer) +} + +func TestAdapterConfigFromExternal_WithAdapter(t *testing.T) { + mockAdpt := &mockAdapter{} + + cfg := Config{ + Engine: "casbin", + Version: "v2", + PolicyConfig: PolicyConfig{ + Adapter: mockAdpt, + }, + } + + result := AdapterConfigFromExternal(cfg) + + v2Config, ok := result.(CasbinV2Config) + assert.True(t, ok) + assert.Equal(t, mockAdpt, v2Config.Adapter) +} + +func TestAdapterFromAny_Nil(t *testing.T) { + result := adapterFromAny(nil) + assert.Nil(t, result) +} + +func TestAdapterFromAny_ValidAdapter(t *testing.T) { + mockAdpt := &mockAdapter{} + result := adapterFromAny(mockAdpt) + assert.Equal(t, mockAdpt, result) +} + +func TestAdapterFromAny_InvalidType(t *testing.T) { + result := adapterFromAny("not an adapter") + assert.Nil(t, result) +} + +func TestCasbinV1Config_Struct(t *testing.T) { + cfg := CasbinV1Config{ + BaseAdapterConfig: BaseAdapterConfig{ + UserNameClaim: "sub", + }, + Csv: "p, role:admin, *, *, allow", + } + + // Verify all fields are accessible and have expected values + assert.Equal(t, "sub", cfg.UserNameClaim) + assert.Equal(t, "p, role:admin, *, *, allow", cfg.Csv) + assert.Empty(t, cfg.Extension) + assert.Empty(t, cfg.Model) + assert.Nil(t, cfg.RoleMap) + assert.Nil(t, cfg.Adapter) + assert.Nil(t, cfg.Enforcer) +} + +func TestCasbinV2Config_Struct(t *testing.T) { + cfg := CasbinV2Config{ + BaseAdapterConfig: BaseAdapterConfig{ + UserNameClaim: "sub", + }, + Csv: "p, role:admin, *, *, allow", + } + + // Verify all fields are accessible and have expected values + assert.Equal(t, "sub", cfg.UserNameClaim) + assert.Equal(t, "p, role:admin, *, *, allow", cfg.Csv) + assert.Empty(t, cfg.Extension) + assert.Empty(t, cfg.Model) + assert.Nil(t, cfg.RoleMap) + assert.Nil(t, cfg.Adapter) +} + +// mockAdapter implements persist.Adapter for testing +type mockAdapter struct{} + +func (m *mockAdapter) LoadPolicy(_ model.Model) error { + return nil +} + +func (m *mockAdapter) SavePolicy(_ model.Model) error { + return nil +} + +func (m *mockAdapter) AddPolicy(_, _ string, _ []string) error { + return nil +} + +func (m *mockAdapter) RemovePolicy(_, _ string, _ []string) error { + return nil +} + +func (m *mockAdapter) RemoveFilteredPolicy(_, _ string, _ int, _ ...string) error { + return nil +} + +// Verify at compile time that mockAdapter implements persist.Adapter +var _ persist.Adapter = (*mockAdapter)(nil) diff --git a/service/internal/auth/authz/authorizer.go b/service/internal/auth/authz/authorizer.go new file mode 100644 index 0000000000..1e80b9375e --- /dev/null +++ b/service/internal/auth/authz/authorizer.go @@ -0,0 +1,211 @@ +// Package authz provides the authorization interface and types for the OpenTDF platform. +// It defines the contract between the authentication middleware and authorization engines. +package authz + +import ( + "context" + "fmt" + "sync" + + "github.com/lestrrat-go/jwx/v2/jwt" + platformauthz "github.com/opentdf/platform/service/pkg/authz" +) + +// Mode indicates which authorization strategy was used for a decision. +type Mode string + +const ( + // ModeV1 indicates legacy path-based authorization (v1 model). + ModeV1 Mode = "v1" + // ModeV2 indicates RPC + dimensions authorization (v2 model). + ModeV2 Mode = "v2" +) + +// Request encapsulates all information needed for an authorization decision. +// This is the contract between the interceptor and any authorization engine. +type Request struct { + // Subject information extracted from JWT + Token jwt.Token + UserInfo []byte // Optional userInfo from IdP + + // RPC method path (e.g., "/policy.attributes.AttributesService/UpdateAttribute") + // Used as the primary resource identifier in v2 model. + RPC string + + // Action derived from RPC method (read, write, delete, unsafe). + // Used in v1 model; informational in v2 model. + Action string + + // ResourceContext contains resolved authorization dimensions (namespace, attribute, etc.). + // If non-nil, indicates resource-level authorization should be attempted. + // Populated by ResolverRegistry when a resolver is registered for the RPC. + ResourceContext *ResolverContext +} + +// Decision represents the result of an authorization check. +type Decision struct { + // Allowed indicates whether the request is permitted. + Allowed bool + + // Reason provides a human-readable explanation for audit logging. + Reason string + + // Mode indicates which authorization model was used. + Mode Mode + + // MatchedPolicy optionally contains the policy rule that matched (for debugging). + MatchedPolicy string +} + +// Authorizer is the interface for pluggable authorization engines. +// Implementations must be thread-safe. +// +// The OpenTDF platform supports multiple authorization versions: +// - v1: Legacy path-based authorization using (subject, resource, action) tuple +// - v2: RPC + dimensions authorization using (subject, rpc, dimensions) tuple +// +// When implementing a new authorization engine (e.g., OPA, Cedar), implement this interface +// and register it via the Factory. +type Authorizer interface { + // Authorize performs an authorization check. + // + // The implementation should: + // 1. Extract subjects (roles/username) from the token + // 2. Apply the appropriate authorization model based on configuration + // 3. For v2: Use ResourceContext dimensions if available + // 4. Return an error only for system failures, not for denied access + // + // Thread-safety: This method may be called concurrently from multiple goroutines. + Authorize(ctx context.Context, req *Request) (*Decision, error) + + // Version returns the authorization model version this authorizer implements. + // Returns "v1" for legacy path-based, "v2" for RPC+dimensions, etc. + Version() string + + // SupportsResourceAuthorization returns true if this authorizer + // supports resource-level authorization with dimensions. + // If false, ResourceContext will always be ignored. + SupportsResourceAuthorization() bool +} + +// Factory creates Authorizer instances based on configuration. +// This allows the platform to instantiate different authorization engines +// (Casbin, OPA, Cedar) based on configuration. +type Factory func(cfg Config) (Authorizer, error) + +// Config provides configuration for authorization engine initialization. +type Config struct { + // Engine specifies which authorization engine to use ("casbin", "cedar", "opa"). + // Defaults to "casbin" if empty. + Engine string + + // Version specifies which authorization model to use ("v1", "v2", etc.) + // This is engine-specific. For Casbin: "v1" (path-based) or "v2" (RPC+dimensions). + Version string + + // Policy configuration (claims, CSV, adapter, etc.) + PolicyConfig + + // Logger for authorization decisions + Logger any + + // Options for engine-specific configuration + Options []Option +} + +// Option is a functional option for authorizer configuration. +type Option func(*optionConfig) + +// optionConfig holds optional configuration for authorizers. +type optionConfig struct { + // V1Enforcer is the legacy casbin enforcer for v1 authorization. + // When provided, the casbin authorizer will delegate v1 auth to this enforcer + // instead of creating its own. + V1Enforcer V1Enforcer +} + +// V1Enforcer is the interface for the legacy v1 casbin enforcer. +// This allows the casbin authorizer to delegate v1 authorization +// to the existing enforcer without circular dependencies. +type V1Enforcer interface { + // Enforce checks if the given token is allowed to perform the requested action. + Enforce(ctx context.Context, token jwt.Token, req platformauthz.RoleRequest) (bool, map[string]any, error) + + // BuildSubjectFromTokenAndUserInfo extracts subjects (roles/username) from token and userInfo. + BuildSubjectFromTokenAndUserInfo(token jwt.Token, userInfo []byte) []string +} + +// WithV1Enforcer sets the v1 enforcer for backwards compatibility. +// This option is used when initializing a casbin authorizer that needs +// to support both v1 and v2 authorization modes. +func WithV1Enforcer(enforcer V1Enforcer) Option { + return func(cfg *optionConfig) { + cfg.V1Enforcer = enforcer + } +} + +// applyOptions applies the given options and returns the resulting config. +func applyOptions(opts ...Option) *optionConfig { + cfg := &optionConfig{} + for _, opt := range opts { + opt(cfg) + } + return cfg +} + +// factories is a registry of authorization engine factories. +var ( + factories = make(map[string]Factory) + factoriesMu sync.RWMutex +) + +// RegisterFactory registers an authorization engine factory. +// This is called during init() by each authorizer implementation. +func RegisterFactory(name string, factory Factory) { + factoriesMu.Lock() + defer factoriesMu.Unlock() + if _, exists := factories[name]; exists { + panic(fmt.Sprintf("authorizer %q already registered", name)) + } + factories[name] = factory +} + +// GetFactory returns the factory for the given name, if registered. +func GetFactory(name string) (Factory, bool) { + factoriesMu.RLock() + defer factoriesMu.RUnlock() + factory, exists := factories[name] + return factory, exists +} + +// DefaultEngine is the default authorization engine when none is specified. +const DefaultEngine = "casbin" + +// New creates an Authorizer based on configuration. +// The engine is selected based on cfg.Engine: +// - "casbin" (default): Casbin policy engine +// - "cedar": AWS Cedar policy engine (future) +// - "opa": Open Policy Agent engine (future) +// +// For Casbin, the version determines the authorization model: +// - "v1" (default): Legacy path-based model (subject, resource, action) +// - "v2": RPC+dimensions model (subject, rpc, dimensions) +func New(cfg Config) (Authorizer, error) { + // Default engine to casbin for backwards compatibility + engine := cfg.Engine + if engine == "" { + engine = DefaultEngine + } + + // Default version to v1 for backwards compatibility + if cfg.Version == "" { + cfg.Version = "v1" + } + + factory, exists := GetFactory(engine) + if !exists { + return nil, fmt.Errorf("authorization engine %q not registered", engine) + } + + return factory(cfg) +} diff --git a/service/internal/auth/authz/authorizer_test.go b/service/internal/auth/authz/authorizer_test.go new file mode 100644 index 0000000000..bdce75c350 --- /dev/null +++ b/service/internal/auth/authz/authorizer_test.go @@ -0,0 +1,205 @@ +package authz + +import ( + "context" + "testing" + + "github.com/lestrrat-go/jwx/v2/jwt" + platformauthz "github.com/opentdf/platform/service/pkg/authz" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestModeConstants(t *testing.T) { + // Verify mode constants have expected values + assert.Equal(t, ModeV1, Mode("v1")) + assert.Equal(t, ModeV2, Mode("v2")) +} + +func TestDefaultEngine(t *testing.T) { + assert.Equal(t, "casbin", DefaultEngine) +} + +func TestRequestStruct(t *testing.T) { + // Test that Request struct can be constructed with all fields + dims := make(map[string]string) + dims["namespace"] = "test" + resource := ResolverResource(dims) + + req := Request{ + RPC: "/test.Service/Method", + Action: "read", + ResourceContext: &ResolverContext{ + Resources: []*ResolverResource{&resource}, + }, + } + + assert.Equal(t, "/test.Service/Method", req.RPC) + assert.Equal(t, "read", req.Action) + assert.NotNil(t, req.ResourceContext) + assert.Len(t, req.ResourceContext.Resources, 1) +} + +func TestDecisionStruct(t *testing.T) { + // Test that Decision struct can be constructed with all fields + decision := Decision{ + Allowed: true, + Reason: "test reason", + Mode: ModeV2, + MatchedPolicy: "role:admin", + } + + assert.True(t, decision.Allowed) + assert.Equal(t, "test reason", decision.Reason) + assert.Equal(t, ModeV2, decision.Mode) + assert.Equal(t, "role:admin", decision.MatchedPolicy) +} + +func TestRegisterAndGetFactory(t *testing.T) { + // Use a unique factory name to avoid conflicts with other tests + testFactoryName := "test-factory-register" + + // Test factory that returns a mock authorizer + testFactory := func(cfg Config) (Authorizer, error) { + return &mockAuthorizer{version: cfg.Version}, nil + } + + // Register the factory + RegisterFactory(testFactoryName, testFactory) + + // Get the factory back + factory, exists := GetFactory(testFactoryName) + require.True(t, exists) + require.NotNil(t, factory) + + // Test that the factory works + auth, err := factory(Config{Version: "v1"}) + require.NoError(t, err) + assert.Equal(t, "v1", auth.Version()) +} + +func TestGetFactory_NotFound(t *testing.T) { + factory, exists := GetFactory("non-existent-factory") + assert.False(t, exists) + assert.Nil(t, factory) +} + +func TestRegisterFactory_Panic_OnDuplicate(t *testing.T) { + // Use a unique factory name + testFactoryName := "test-factory-duplicate" + + testFactory := func(_ Config) (Authorizer, error) { + return &mockAuthorizer{}, nil + } + + // First registration should succeed + RegisterFactory(testFactoryName, testFactory) + + // Second registration with same name should panic + assert.Panics(t, func() { + RegisterFactory(testFactoryName, testFactory) + }) +} + +func TestNew_UnregisteredEngine(t *testing.T) { + cfg := Config{ + Engine: "unregistered-engine", + Version: "v1", + } + + auth, err := New(cfg) + require.Error(t, err) + assert.Nil(t, auth) + assert.Contains(t, err.Error(), "not registered") +} + +func TestNew_DefaultValues(t *testing.T) { + // Register a test factory for this test + testFactoryName := "test-factory-defaults" + var receivedCfg Config + + testFactory := func(cfg Config) (Authorizer, error) { + receivedCfg = cfg + return &mockAuthorizer{version: cfg.Version}, nil + } + + RegisterFactory(testFactoryName, testFactory) + + // Call New with minimal config (but specify engine so we use our test factory) + cfg := Config{ + Engine: testFactoryName, + } + + auth, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, auth) + + // Verify defaults were applied + assert.Equal(t, "v1", receivedCfg.Version, "Version should default to v1") +} + +func TestWithV1Enforcer(t *testing.T) { + mockEnforcer := &mockV1Enforcer{} + + opt := WithV1Enforcer(mockEnforcer) + cfg := applyOptions(opt) + + assert.Equal(t, mockEnforcer, cfg.V1Enforcer) +} + +func TestApplyOptions_Empty(t *testing.T) { + cfg := applyOptions() + assert.Nil(t, cfg.V1Enforcer) +} + +func TestApplyOptions_Multiple(t *testing.T) { + mockEnforcer := &mockV1Enforcer{} + + cfg := applyOptions( + WithV1Enforcer(mockEnforcer), + ) + + assert.Equal(t, mockEnforcer, cfg.V1Enforcer) +} + +// mockAuthorizer implements Authorizer for testing +type mockAuthorizer struct { + version string + supportsResourceAuth bool + authorizeFunc func(ctx context.Context, req *Request) (*Decision, error) +} + +func (m *mockAuthorizer) Authorize(ctx context.Context, req *Request) (*Decision, error) { + if m.authorizeFunc != nil { + return m.authorizeFunc(ctx, req) + } + return &Decision{Allowed: true, Mode: ModeV1}, nil +} + +func (m *mockAuthorizer) Version() string { + if m.version == "" { + return "v1" + } + return m.version +} + +func (m *mockAuthorizer) SupportsResourceAuthorization() bool { + return m.supportsResourceAuth +} + +// mockV1Enforcer implements V1Enforcer for testing +type mockV1Enforcer struct { + enforceResult bool + subjects []string +} + +func (m *mockV1Enforcer) Enforce(_ context.Context, _ jwt.Token, _ platformauthz.RoleRequest) (bool, map[string]any, error) { + return m.enforceResult, nil, nil +} + +func (m *mockV1Enforcer) BuildSubjectFromTokenAndUserInfo(_ jwt.Token, _ []byte) []string { + if m.subjects != nil { + return m.subjects + } + return []string{"role:test"} +} diff --git a/service/internal/auth/authz/casbin/casbin.go b/service/internal/auth/authz/casbin/casbin.go new file mode 100644 index 0000000000..cec581a173 --- /dev/null +++ b/service/internal/auth/authz/casbin/casbin.go @@ -0,0 +1,653 @@ +// Package casbin provides a Casbin-based authorization implementation. +package casbin + +import ( + "context" + _ "embed" + "encoding/json" + "errors" + "fmt" + "log/slog" + "sort" + "strings" + + "github.com/casbin/casbin/v2" + casbinModel "github.com/casbin/casbin/v2/model" + "github.com/casbin/casbin/v2/persist" + stringadapter "github.com/casbin/casbin/v2/persist/string-adapter" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/opentdf/platform/service/internal/auth/authz" + "github.com/opentdf/platform/service/logger" + platformauthz "github.com/opentdf/platform/service/pkg/authz" + "github.com/opentdf/platform/service/pkg/util" +) + +//go:embed model.conf +var modelV2 string + +//go:embed policy.csv +var builtinPolicyV2 string + +const ( + // rolePrefix is the prefix for role subjects in casbin policies. + rolePrefix = "role:" + // disallowedDimensionKeyChars are separators used in dimension serialization. + disallowedDimensionKeyChars = "=&" + // defaultRole is the role assigned when no roles are found. + defaultRole = "unknown" + // defaultSubjectsCapacity is the default capacity for subjects/roles slices. + defaultSubjectsCapacity = 4 + // dimensionMatchArgCount is the expected argument count for dimensionMatch function. + dimensionMatchArgCount = 2 + // kvPairParts is the expected number of parts when splitting key=value pairs. + kvPairParts = 2 +) + +func init() { + // Register the Casbin authorizer factory + authz.RegisterFactory("casbin", NewAuthorizer) +} + +// Authorizer implements authz.Authorizer using Casbin. +// It supports both v1 (path-based) and v2 (RPC+dimensions) authorization models. +type Authorizer struct { + // version indicates which model is active ("v1" or "v2") + version string + + // v1Enforcer handles legacy path-based authorization + // Used when version == "v1" + v1Enforcer authz.V1Enforcer + + // v2Enforcer handles RPC+dimensions authorization + // Used when version == "v2" + v2Enforcer *casbin.Enforcer + + logger *logger.Logger + + // baseConfig holds common configuration extracted from adapter config + baseConfig authz.BaseAdapterConfig + + // groupClaimSelectors are precomputed selectors for extracting roles from JWT claims + // Used in v2-only mode when v1Enforcer is nil + groupClaimSelectors [][]string +} + +// NewAuthorizer creates a new Casbin Authorizer based on configuration. +// It maps the external Config to the appropriate internal adapter config +// (CasbinV1Config or CasbinV2Config) for cleaner separation of concerns. +func NewAuthorizer(cfg authz.Config) (authz.Authorizer, error) { + log, ok := cfg.Logger.(*logger.Logger) + if !ok || log == nil { + return nil, errors.New("logger is required for CasbinAuthorizer") + } + + // Map external config to internal adapter config + adapterCfg := authz.AdapterConfigFromExternal(cfg) + + switch typedCfg := adapterCfg.(type) { + case authz.CasbinV1Config: + return newCasbinV1Authorizer(typedCfg, log) + case authz.CasbinV2Config: + return newCasbinV2Authorizer(typedCfg, log) + default: + return nil, fmt.Errorf("unsupported adapter config type: %T", adapterCfg) + } +} + +// newCasbinV1Authorizer creates a v1 (path-based) Casbin authorizer. +func newCasbinV1Authorizer(cfg authz.CasbinV1Config, log *logger.Logger) (*Authorizer, error) { + if cfg.Enforcer == nil { + return nil, errors.New("v1 enforcer is required for v1 authorization mode (use authz.WithV1Enforcer)") + } + + authorizer := &Authorizer{ + version: "v1", + logger: log, + baseConfig: cfg.BaseAdapterConfig, + v1Enforcer: cfg.Enforcer, + } + + log.Info( + "casbin authorizer initialized", + slog.String("version", authorizer.version), + slog.Bool("supportsResourceAuth", authorizer.SupportsResourceAuthorization()), + ) + + return authorizer, nil +} + +// newCasbinV2Authorizer creates a v2 (RPC+dimensions) Casbin authorizer. +func newCasbinV2Authorizer(cfg authz.CasbinV2Config, log *logger.Logger) (*Authorizer, error) { + enforcer, err := createV2EnforcerFromConfig(cfg, log) + if err != nil { + return nil, fmt.Errorf("failed to create v2 casbin enforcer: %w", err) + } + + // Precompute group claim selector for v2 role extraction + // GroupsClaim is a dot-notation path like "realm_access.roles" + var groupClaimSelectors [][]string + if cfg.GroupsClaim != "" { + groupClaimSelectors = [][]string{strings.Split(cfg.GroupsClaim, ".")} + } + + authorizer := &Authorizer{ + version: "v2", + logger: log, + baseConfig: cfg.BaseAdapterConfig, + v2Enforcer: enforcer, + groupClaimSelectors: groupClaimSelectors, + } + + log.Info( + "casbin authorizer initialized", + slog.String("version", authorizer.version), + slog.Bool("supportsResourceAuth", authorizer.SupportsResourceAuthorization()), + ) + + return authorizer, nil +} + +// createV2EnforcerFromConfig creates a Casbin enforcer for the v2 model +// using the internal CasbinV2Config type. +func createV2EnforcerFromConfig(cfg authz.CasbinV2Config, log *logger.Logger) (*casbin.Enforcer, error) { + // Use embedded v2 model or custom model from config + modelStr := modelV2 + if cfg.Model != "" { + modelStr = cfg.Model + } + + m, err := casbinModel.NewModelFromString(modelStr) + if err != nil { + return nil, fmt.Errorf("failed to create v2 casbin model: %w", err) + } + + // Build policy adapter + var adapter persist.Adapter + if cfg.Adapter != nil { + adapter = cfg.Adapter + } else { + // Build CSV policy for v2 + csvPolicy := buildV2PolicyFromConfig(cfg) + adapter = stringadapter.NewAdapter(csvPolicy) + log.Debug("v2 policy loaded", slog.String("policy", csvPolicy)) + } + + e, err := casbin.NewEnforcer(m, adapter) + if err != nil { + return nil, fmt.Errorf("failed to create v2 casbin enforcer: %w", err) + } + + // Load policy from adapter + if err := e.LoadPolicy(); err != nil { + return nil, fmt.Errorf("failed to load v2 casbin policy: %w", err) + } + + // Register custom dimension matching function + e.AddFunction("dimensionMatch", dimensionMatchFunc) + + return e, nil +} + +// buildV2PolicyFromConfig constructs the CSV policy for v2 model +// using the internal CasbinV2Config type. +func buildV2PolicyFromConfig(cfg authz.CasbinV2Config) string { + var policies []string + + if cfg.Csv != "" { + // Custom policy overrides default + policies = append(policies, cfg.Csv) + } else { + // Use embedded default v2 policy + policies = append(policies, builtinPolicyV2) + } + + // Add extension policy + if cfg.Extension != "" { + policies = append(policies, cfg.Extension) + } + + return strings.Join(policies, "\n") +} + +// Authorize implements authz.Authorizer.Authorize. +func (a *Authorizer) Authorize(ctx context.Context, req *authz.Request) (*authz.Decision, error) { + switch a.version { + case "v1": + return a.authorizeV1(ctx, req) + case "v2": + return a.authorizeV2(ctx, req) + default: + return nil, fmt.Errorf("unsupported authorization version: %s", a.version) + } +} + +// Version implements authz.Authorizer.Version. +func (a *Authorizer) Version() string { + return a.version +} + +// SupportsResourceAuthorization implements authz.Authorizer.SupportsResourceAuthorization. +func (a *Authorizer) SupportsResourceAuthorization() bool { + return a.version == "v2" +} + +// authorizeV1 performs legacy path-based authorization. +// +// Path handling heuristic for v1 policy compatibility: +// The v1 Casbin policy file (casbin_policy.csv) uses two different path formats: +// - gRPC paths WITHOUT leading slash: kas.AccessService/Rewrap, policy.*, authorization.AuthorizationService/GetDecisions +// - HTTP paths WITH leading slash: /kas/v2/rewrap, /attributes*, /namespaces* +// +// ConnectRPC always provides paths with a leading slash (e.g., /kas.AccessService/Rewrap). +// We distinguish gRPC from HTTP paths using a simple heuristic: gRPC service names contain "." +// (e.g., "kas.AccessService"), while HTTP paths do not (e.g., "/kas/v2/rewrap"). +// +// This preserves full backwards compatibility with the existing v1 policy format. +func (a *Authorizer) authorizeV1(ctx context.Context, req *authz.Request) (*authz.Decision, error) { + resource := req.RPC + if strings.Contains(req.RPC, ".") { + // gRPC-style path (contains '.'): strip leading slash for v1 policy compatibility + // Example: /kas.AccessService/Rewrap -> kas.AccessService/Rewrap + resource = strings.TrimPrefix(req.RPC, "/") + } + // HTTP paths (no '.') keep their leading slash + // Example: /kas/v2/rewrap -> /kas/v2/rewrap + + allowed, _, err := a.v1Enforcer.Enforce(ctx, req.Token, platformauthz.RoleRequest{ + Resource: resource, + Action: req.Action, + }) + if err != nil { + if !allowed { + return &authz.Decision{ + Allowed: false, + Reason: fmt.Sprintf("v1: denied %s %s", req.Action, resource), + Mode: authz.ModeV1, + }, nil + } + return nil, fmt.Errorf("v1 authorization system error: %w", err) + } + + return &authz.Decision{ + Allowed: allowed, + Reason: fmt.Sprintf("v1: %s %s", req.Action, resource), + Mode: authz.ModeV1, + }, nil +} + +// authorizeV2 performs RPC+dimensions authorization. +func (a *Authorizer) authorizeV2(_ context.Context, req *authz.Request) (*authz.Decision, error) { + subjects := a.extractSubjects(req) + + // If no subjects found, use default role + if len(subjects) == 0 { + subjects = append(subjects, rolePrefix+defaultRole) + } + + // Serialize dimensions to canonical string + dims, err := serializeDimensions(req.ResourceContext) + if err != nil { + return nil, fmt.Errorf("v2 authorization invalid resource dimensions: %w", err) + } + + a.logger.Debug( + "v2 authorization check", + slog.Any("subjects", subjects), + slog.String("rpc", req.RPC), + slog.String("dims", dims), + ) + + // Check each subject (role or username) + // Track if any enforcement succeeded without error to distinguish + // "all denied" from "all errored" (system failure) + var ( + anyCheckedSuccessfully bool + lastErr error + ) + + for _, subject := range subjects { + allowed, err := a.v2Enforcer.Enforce(subject, req.RPC, dims) + if err != nil { + a.logger.Error( + "v2 enforcement error", + slog.String("subject", subject), + slog.String("rpc", req.RPC), + slog.String("dims", dims), + slog.Any("error", err), + ) + lastErr = err + continue + } + + anyCheckedSuccessfully = true + + if allowed { + a.logger.Debug( + "v2 authorization allowed", + slog.String("subject", subject), + slog.String("rpc", req.RPC), + slog.String("dims", dims), + ) + return &authz.Decision{ + Allowed: true, + Reason: fmt.Sprintf("v2: %s on %s with dims=%s", subject, req.RPC, dims), + Mode: authz.ModeV2, + MatchedPolicy: subject, + }, nil + } + } + + // If ALL subjects failed with errors (none checked successfully), + // return a system error instead of a denial + if !anyCheckedSuccessfully && lastErr != nil { + return nil, fmt.Errorf("v2 authorization system error: %w", lastErr) + } + + a.logger.Debug( + "v2 authorization denied", + slog.Any("subjects", subjects), + slog.String("rpc", req.RPC), + slog.String("dims", dims), + ) + + return &authz.Decision{ + Allowed: false, + Reason: fmt.Sprintf("v2: denied %s with dims=%s", req.RPC, dims), + Mode: authz.ModeV2, + }, nil +} + +// extractSubjects extracts roles/username from JWT token and userInfo. +func (a *Authorizer) extractSubjects(req *authz.Request) []string { + if a.v1Enforcer != nil { // ? What's the point of this + // Reuse v1 subject extraction logic + return a.v1Enforcer.BuildSubjectFromTokenAndUserInfo(req.Token, req.UserInfo) + } + + // For v2-only mode, implement subject extraction + subjects := make([]string, 0, defaultSubjectsCapacity) + + // Extract roles from token claims + if req.Token != nil { + roles := a.extractRolesFromToken(req.Token) + for _, role := range roles { + if role != "" { + subjects = append(subjects, rolePrefix+role) + } + } + + // Extract username claim + if username := a.extractUsernameFromToken(req.Token); username != "" { + subjects = append(subjects, username) + } + } + + // Extract roles from userInfo + if req.UserInfo != nil { + roles := a.extractRolesFromUserInfo(req.UserInfo) + for _, role := range roles { + if role != "" { + subjects = append(subjects, rolePrefix+role) + } + } + } + + return subjects +} + +// extractUsernameFromToken extracts and validates username subject from token. +func (a *Authorizer) extractUsernameFromToken(token jwt.Token) string { + if token == nil || a.baseConfig.UserNameClaim == "" { + return "" + } + + claim, found := token.Get(a.baseConfig.UserNameClaim) + if !found { + return "" + } + + username, ok := claim.(string) + if !ok || username == "" { + return "" + } + + if strings.HasPrefix(username, rolePrefix) { + a.logger.Warn( + "ignoring username subject with reserved role prefix", + slog.String("claim", a.baseConfig.UserNameClaim), + slog.String("prefix", rolePrefix), + ) + return "" + } + + return username +} + +// extractRolesFromToken extracts roles from a jwt.Token based on the configured claim path. +func (a *Authorizer) extractRolesFromToken(token jwt.Token) []string { + roles := make([]string, 0, defaultSubjectsCapacity) + for _, selectors := range a.groupClaimSelectors { + if len(selectors) == 0 { + continue + } + claim, exists := token.Get(selectors[0]) + if !exists { + continue + } + if len(selectors) > 1 { + claimMap, ok := claim.(map[string]any) + if !ok { + continue + } + claim = util.Dotnotation(claimMap, strings.Join(selectors[1:], ".")) + if claim == nil { + continue + } + } + // Extract roles from the claim value + switch v := claim.(type) { + case string: + roles = append(roles, v) + case []any: + for _, rr := range v { + if r, ok := rr.(string); ok { + roles = append(roles, r) + } + } + case []string: + roles = append(roles, v...) + } + } + return roles +} + +// extractRolesFromUserInfo extracts roles from a userInfo JSON ([]byte) based on the configured claim path. +func (a *Authorizer) extractRolesFromUserInfo(userInfo []byte) []string { + roles := make([]string, 0, defaultSubjectsCapacity) + if len(userInfo) == 0 { + return roles + } + var userInfoMap map[string]any + if err := json.Unmarshal(userInfo, &userInfoMap); err != nil { + return roles + } + for _, selectors := range a.groupClaimSelectors { + if len(selectors) == 0 { + continue + } + claim := util.Dotnotation(userInfoMap, strings.Join(selectors, ".")) + if claim == nil { + continue + } + switch v := claim.(type) { + case string: + roles = append(roles, v) + case []any: + for _, rr := range v { + if r, ok := rr.(string); ok { + roles = append(roles, r) + } + } + case []string: + roles = append(roles, v...) + } + } + return roles +} + +// serializeDimensions converts ResolverContext to canonical dimension string. +// Format: key1=value1&key2=value2 (keys sorted alphabetically) +// Returns "*" if no dimensions are present. +func serializeDimensions(ctx *authz.ResolverContext) (string, error) { + if ctx == nil || len(ctx.Resources) == 0 { + return "*", nil + } + + // Collect all dimensions from all resources + allDims := make(map[string]string) + for _, resource := range ctx.Resources { + if resource == nil { + continue + } + for k, v := range *resource { + if !isValidDimensionKey(k) { + return "", fmt.Errorf("invalid dimension key %q: keys must not contain any of %q", k, disallowedDimensionKeyChars) + } + if existing, exists := allDims[k]; exists && existing != v { + return "", fmt.Errorf("conflicting values for dimension key %q: %q != %q", k, existing, v) + } + allDims[k] = v + } + } + + if len(allDims) == 0 { + return "*", nil + } + + // Sort keys for canonical ordering + keys := make([]string, 0, len(allDims)) + for k := range allDims { + keys = append(keys, k) + } + sort.Strings(keys) + + // Build canonical string + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, fmt.Sprintf("%s=%s", k, allDims[k])) + } + + return strings.Join(parts, "&"), nil +} + +// isValidDimensionKey reports whether a dimension key can be safely serialized. +func isValidDimensionKey(key string) bool { + if key == "" { + return false + } + + return !strings.ContainsAny(key, disallowedDimensionKeyChars) +} + +// dimensionMatchFunc is the Casbin custom function for dimension matching. +// It compares request dimensions against policy dimensions. +// +// Policy format: "namespace=hr&attribute=*" (AND logic, * is wildcard) +// Request format: "namespace=hr&attribute=classification" +func dimensionMatchFunc(args ...any) (any, error) { + if len(args) != dimensionMatchArgCount { + return false, fmt.Errorf("dimensionMatch requires %d arguments, got %d", dimensionMatchArgCount, len(args)) + } + + reqDims, ok := args[0].(string) + if !ok { + return false, fmt.Errorf("request dimensions must be string, got %T", args[0]) + } + + policyDims, ok := args[1].(string) + if !ok { + return false, fmt.Errorf("policy dimensions must be string, got %T", args[1]) + } + + return dimensionMatch(reqDims, policyDims), nil +} + +// dimensionMatch compares request dimensions against policy dimensions. +// Returns true if the request satisfies the policy. +// +// Rules: +// - Policy "*" matches any request dimensions +// - Each policy dimension must be satisfied by the request +// - Policy value "*" matches any value for that dimension +// - Request must have all dimensions specified in policy +func dimensionMatch(reqDims, policyDims string) bool { + // Wildcard policy matches everything + if policyDims == "*" { + return true + } + + // Parse request dimensions into map + reqMap, ok := parseDimensions(reqDims) + if !ok { + return false + } + + // Empty policy with non-wildcard request: check if request also empty + if policyDims == "" { + return len(reqMap) == 0 + } + + // Each policy dimension must be satisfied + for _, pair := range strings.Split(policyDims, "&") { + pair = strings.TrimSpace(pair) + if pair == "" { + continue + } + + kv := strings.SplitN(pair, "=", kvPairParts) + if len(kv) != kvPairParts { + return false + } + key, policyVal := kv[0], kv[1] + if !isValidDimensionKey(key) { + return false + } + + reqVal, exists := reqMap[key] + if !exists { + // Policy requires a dimension that request doesn't have + return false + } + + // Wildcard matches any value + if policyVal != "*" && policyVal != reqVal { + return false + } + } + + return true +} + +// parseDimensions parses a dimension string into a map. +func parseDimensions(dims string) (map[string]string, bool) { + result := make(map[string]string) + if dims == "*" || dims == "" { + return result, true + } + + for _, pair := range strings.Split(dims, "&") { + if pair == "" { + continue + } + + kv := strings.SplitN(pair, "=", kvPairParts) + if len(kv) != kvPairParts { + return nil, false + } + if !isValidDimensionKey(kv[0]) { + return nil, false + } + result[kv[0]] = kv[1] + } + return result, true +} diff --git a/service/internal/auth/authz/casbin/casbin_test.go b/service/internal/auth/authz/casbin/casbin_test.go new file mode 100644 index 0000000000..d55a64666e --- /dev/null +++ b/service/internal/auth/authz/casbin/casbin_test.go @@ -0,0 +1,902 @@ +package casbin + +import ( + "context" + "testing" + + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/opentdf/platform/service/internal/auth/authz" + "github.com/opentdf/platform/service/logger" + platformauthz "github.com/opentdf/platform/service/pkg/authz" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// mockV1Enforcer implements authz.V1Enforcer for testing +type mockV1Enforcer struct { + enforceFunc func(ctx context.Context, token jwt.Token, req platformauthz.RoleRequest) (bool, map[string]any, error) + extractFunc func(token jwt.Token, userInfo []byte) []string +} + +func (m *mockV1Enforcer) Enforce(ctx context.Context, token jwt.Token, req platformauthz.RoleRequest) (bool, map[string]any, error) { + if m.enforceFunc != nil { + return m.enforceFunc(ctx, token, req) + } + return false, nil, nil +} + +func (m *mockV1Enforcer) BuildSubjectFromTokenAndUserInfo(token jwt.Token, userInfo []byte) []string { + if m.extractFunc != nil { + return m.extractFunc(token, userInfo) + } + return nil +} + +type CasbinAuthorizerSuite struct { + suite.Suite + logger *logger.Logger +} + +func TestCasbinAuthorizerSuite(t *testing.T) { + suite.Run(t, new(CasbinAuthorizerSuite)) +} + +func (s *CasbinAuthorizerSuite) SetupTest() { + s.logger = logger.CreateTestLogger() +} + +func (s *CasbinAuthorizerSuite) TestNewCasbinAuthorizer_V1() { + mockEnforcer := &mockV1Enforcer{} + + cfg := authz.Config{ + Version: "v1", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + Options: []authz.Option{authz.WithV1Enforcer(mockEnforcer)}, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + s.Require().NotNil(authorizer) + + s.Equal("v1", authorizer.Version()) + s.False(authorizer.SupportsResourceAuthorization()) +} + +func (s *CasbinAuthorizerSuite) TestNewCasbinAuthorizer_V2() { + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + s.Require().NotNil(authorizer) + + s.Equal("v2", authorizer.Version()) + s.True(authorizer.SupportsResourceAuthorization()) +} + +func (s *CasbinAuthorizerSuite) TestNewCasbinAuthorizer_UnknownVersionFallsBackToV1() { + // Unknown versions default to v1, which requires a v1 enforcer. + // This maintains backwards compatibility while providing a clear error. + cfg := authz.Config{ + Version: "v99", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().Error(err) + s.Nil(authorizer) + s.Contains(err.Error(), "v1 enforcer is required") +} + +func (s *CasbinAuthorizerSuite) TestNewCasbinAuthorizer_NilLogger() { + cfg := authz.Config{ + Version: "v1", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: nil, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().Error(err) + s.Nil(authorizer) + s.Contains(err.Error(), "logger is required") +} + +func (s *CasbinAuthorizerSuite) TestNewCasbinAuthorizer_V1_NoEnforcerError() { + cfg := authz.Config{ + Version: "v1", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + // No V1Enforcer option provided + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().Error(err) + s.Nil(authorizer) + s.Contains(err.Error(), "v1 enforcer is required") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV2_AdminWildcard() { + // Policy: admin can do anything + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + Csv: "p, role:admin, *, *, allow", + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + // Create token with admin role + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"admin"}, + }, + }) + + req := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: "write", + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr", "attribute": "classification"}, + }, + }, + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.True(decision.Allowed) + s.Equal(authz.ModeV2, decision.Mode) +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV2_DefaultPolicyIncludesDefaultRoleGroupings() { + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"opentdf-admin"}, + }, + }) + req := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: "write", + } + + decision, err := authorizer.Authorize(s.T().Context(), req) + s.Require().NoError(err) + s.Require().NotNil(decision) + s.True(decision.Allowed, "default v2 policy should map opentdf-admin to role:admin") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV2_NamespaceScopedAccess() { + // Policy: hr-admin can only access HR namespace + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + Csv: `p, role:hr-admin, /policy.attributes.AttributesService/*, namespace=hr, allow +p, role:finance-admin, /policy.attributes.AttributesService/*, namespace=finance, allow`, + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + // Create token with hr-admin role + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"hr-admin"}, + }, + }) + + // Should allow access to HR namespace + hrReq := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: "write", + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr"}, + }, + }, + } + + decision, err := authorizer.Authorize(context.Background(), hrReq) + s.Require().NoError(err) + s.True(decision.Allowed, "hr-admin should be allowed to access HR namespace") + + // Should deny access to Finance namespace + financeReq := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: "write", + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "finance"}, + }, + }, + } + + decision, err = authorizer.Authorize(context.Background(), financeReq) + s.Require().NoError(err) + s.False(decision.Allowed, "hr-admin should NOT be allowed to access Finance namespace") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV2_MultipleDimensions() { + // Policy: requires both namespace and attribute dimensions + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + Csv: `p, role:classification-owner, /policy.attributes.AttributesService/Update*, namespace=hr&attribute=classification, allow`, + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"classification-owner"}, + }, + }) + + // Should allow with both dimensions matching + req := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: "write", + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr", "attribute": "classification"}, + }, + }, + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.True(decision.Allowed, "should be allowed with matching namespace and attribute") + + // Should deny with wrong attribute + wrongAttrReq := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: "write", + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr", "attribute": "department"}, + }, + }, + } + + decision, err = authorizer.Authorize(context.Background(), wrongAttrReq) + s.Require().NoError(err) + s.False(decision.Allowed, "should NOT be allowed with wrong attribute") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV2_WildcardDimension() { + // Policy: wildcard for attribute dimension + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + Csv: `p, role:hr-viewer, /policy.attributes.AttributesService/Get*, namespace=hr&attribute=*, allow`, + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"hr-viewer"}, + }, + }) + + // Should allow any attribute in HR namespace + req := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/GetAttribute", + Action: "read", + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr", "attribute": "any-attribute"}, + }, + }, + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.True(decision.Allowed, "should be allowed with wildcard attribute") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV2_NoDimensions() { + // Policy with wildcard dimensions + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + Csv: `p, role:standard, /policy.attributes.AttributesService/Get*, *, allow`, + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"standard"}, + }, + }) + + // Should allow with no resource context (nil) + req := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/GetAttribute", + Action: "read", + ResourceContext: nil, + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.True(decision.Allowed, "should be allowed with nil resource context when policy has wildcard") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV2_UsernameWithRolePrefixIsIgnored() { + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + UserNameClaim: "preferred_username", + Csv: `p, role:admin, /policy.attributes.AttributesService/Get*, *, allow`, + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + token := createTestToken(s.T(), map[string]interface{}{ + "preferred_username": "role:admin", + }) + + req := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/GetAttribute", + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.False(decision.Allowed, "username with reserved role prefix must not match role subjects") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV2_KASRESTfulPathsAllowed() { + // v2 uses leading slashes for ALL paths (both gRPC and HTTP) + // This test ensures KAS RESTful paths work in v2 authorization + cfg := authz.Config{ + Version: "v2", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + Csv: `p, role:standard, /kas.AccessService/*, *, allow +p, role:standard, /kas/v2/rewrap, *, allow +p, role:unknown, /kas.AccessService/Rewrap, *, allow +p, role:unknown, /kas/v2/rewrap, *, allow`, + }, + Logger: s.logger, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + standardToken := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"standard"}, + }, + }) + + unknownToken := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"unknown"}, + }, + }) + + tests := []struct { + name string + token jwt.Token + rpc string + action string + allowed bool + }{ + // gRPC paths - standard role (v2 keeps leading slash) + {"standard gRPC rewrap read", standardToken, "/kas.AccessService/Rewrap", "read", true}, + {"standard gRPC rewrap write", standardToken, "/kas.AccessService/Rewrap", "write", true}, + // HTTP paths - standard role + {"standard HTTP rewrap read", standardToken, "/kas/v2/rewrap", "read", true}, + {"standard HTTP rewrap write", standardToken, "/kas/v2/rewrap", "write", true}, + // gRPC paths - unknown role + {"unknown gRPC rewrap", unknownToken, "/kas.AccessService/Rewrap", "read", true}, + // HTTP paths - unknown role + {"unknown HTTP rewrap", unknownToken, "/kas/v2/rewrap", "write", true}, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + req := &authz.Request{ + Token: tc.token, + RPC: tc.rpc, + Action: tc.action, + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.Equal(tc.allowed, decision.Allowed, "expected allowed=%v for %s", tc.allowed, tc.name) + s.Equal(authz.ModeV2, decision.Mode) + }) + } +} + +// Test dimension matching logic +func TestDimensionMatch(t *testing.T) { + tests := []struct { + name string + reqDims string + policyDims string + expected bool + }{ + { + name: "wildcard policy matches anything", + reqDims: "namespace=hr&attribute=classification", + policyDims: "*", + expected: true, + }, + { + name: "exact match", + reqDims: "namespace=hr", + policyDims: "namespace=hr", + expected: true, + }, + { + name: "exact match multiple dimensions", + reqDims: "attribute=classification&namespace=hr", + policyDims: "namespace=hr&attribute=classification", + expected: true, + }, + { + name: "wildcard value matches any", + reqDims: "namespace=hr&attribute=classification", + policyDims: "namespace=hr&attribute=*", + expected: true, + }, + { + name: "request has extra dimensions - still matches", + reqDims: "attribute=classification&namespace=hr&value=secret", + policyDims: "namespace=hr", + expected: true, + }, + { + name: "policy requires dimension not in request", + reqDims: "namespace=hr", + policyDims: "namespace=hr&attribute=classification", + expected: false, + }, + { + name: "value mismatch", + reqDims: "namespace=finance", + policyDims: "namespace=hr", + expected: false, + }, + { + name: "empty request matches wildcard", + reqDims: "*", + policyDims: "*", + expected: true, + }, + { + name: "empty policy matches empty request", + reqDims: "", + policyDims: "", + expected: true, + }, + { + name: "malformed request dimensions fail", + reqDims: "name&space=hr", + policyDims: "space=hr", + expected: false, + }, + { + name: "invalid policy key with separator fails", + reqDims: "namespace=hr", + policyDims: "name&space=hr", + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := dimensionMatch(tc.reqDims, tc.policyDims) + assert.Equal(t, tc.expected, result) + }) + } +} + +// Test dimension serialization +func TestSerializeDimensions(t *testing.T) { + tests := []struct { + name string + ctx *authz.ResolverContext + expected string + expectErr bool + }{ + { + name: "nil context", + ctx: nil, + expected: "*", + }, + { + name: "empty context", + ctx: &authz.ResolverContext{}, + expected: "*", + }, + { + name: "single dimension", + ctx: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr"}, + }, + }, + expected: "namespace=hr", + }, + { + name: "multiple dimensions sorted alphabetically", + ctx: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr", "attribute": "classification"}, + }, + }, + expected: "attribute=classification&namespace=hr", + }, + { + name: "multiple resources merged", + ctx: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr"}, + {"attribute": "classification"}, + }, + }, + expected: "attribute=classification&namespace=hr", + }, + { + name: "conflicting duplicate dimension key fails", + ctx: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"namespace": "hr"}, + {"namespace": "finance"}, + }, + }, + expectErr: true, + }, + { + name: "invalid key with separator fails", + ctx: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{ + {"name=space": "hr"}, + }, + }, + expectErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := serializeDimensions(tc.ctx) + if tc.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expected, result) + }) + } +} + +// Test NewAuthorizer factory function via authz.New +func TestNewAuthorizer(t *testing.T) { + log := logger.CreateTestLogger() + mockEnforcer := &mockV1Enforcer{} + + tests := []struct { + name string + version string + expectVersion string + expectError bool + withEnforcer bool + }{ + { + name: "empty version defaults to v1", + version: "", + expectVersion: "v1", + expectError: false, + withEnforcer: true, + }, + { + name: "explicit v1", + version: "v1", + expectVersion: "v1", + expectError: false, + withEnforcer: true, + }, + { + name: "explicit v2", + version: "v2", + expectVersion: "v2", + expectError: false, + withEnforcer: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := authz.Config{ + Version: tc.version, + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: log, + } + if tc.withEnforcer { + cfg.Options = []authz.Option{authz.WithV1Enforcer(mockEnforcer)} + } + + authorizer, err := authz.New(cfg) + if tc.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.NotNil(t, authorizer) + assert.Equal(t, tc.expectVersion, authorizer.Version()) + }) + } +} + +// ============================================================================= +// V1 Path Handling Tests - Ensuring backwards compatibility +// ============================================================================= +// The v1 policy file uses two different path formats: +// - gRPC paths WITHOUT leading slash: kas.AccessService/Rewrap +// - HTTP paths WITH leading slash: /kas/v2/rewrap +// +// The authorizeV1 function must handle paths from ConnectRPC (which always +// have leading slashes) and translate them correctly for v1 policy matching. +// ============================================================================= + +func (s *CasbinAuthorizerSuite) TestAuthorizeV1_GRPCPathStripsLeadingSlash() { + // v1 policy: gRPC paths have NO leading slash + // Create a mock enforcer that validates the resource path + var receivedResource string + mockEnforcer := &mockV1Enforcer{ + enforceFunc: func(_ context.Context, _ jwt.Token, req platformauthz.RoleRequest) (bool, map[string]any, error) { + receivedResource = req.Resource + // Allow if resource matches expected stripped path + return req.Resource == "kas.AccessService/Rewrap", nil, nil + }, + } + + cfg := authz.Config{ + Version: "v1", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + Options: []authz.Option{authz.WithV1Enforcer(mockEnforcer)}, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"standard"}, + }, + }) + + // ConnectRPC passes paths WITH leading slash, even for gRPC + // The authorizer must strip it to match v1 policy + req := &authz.Request{ + Token: token, + RPC: "/kas.AccessService/Rewrap", // ConnectRPC format + Action: "read", + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.True(decision.Allowed, "gRPC path should be allowed after stripping leading slash") + s.Equal(authz.ModeV1, decision.Mode) + s.Equal("kas.AccessService/Rewrap", receivedResource, "resource should have leading slash stripped") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV1_HTTPPathKeepsLeadingSlash() { + // v1 policy: HTTP paths KEEP their leading slash + var receivedResource string + mockEnforcer := &mockV1Enforcer{ + enforceFunc: func(_ context.Context, _ jwt.Token, req platformauthz.RoleRequest) (bool, map[string]any, error) { + receivedResource = req.Resource + // Allow if resource matches expected path with leading slash + return req.Resource == "/kas/v2/rewrap", nil, nil + }, + } + + cfg := authz.Config{ + Version: "v1", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + Options: []authz.Option{authz.WithV1Enforcer(mockEnforcer)}, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + testToken := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"standard"}, + }, + }) + + // HTTP paths should keep their leading slash for v1 policy matching + req := &authz.Request{ + Token: testToken, + RPC: "/kas/v2/rewrap", + Action: "write", + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.True(decision.Allowed, "HTTP path should be allowed with leading slash intact") + s.Equal(authz.ModeV1, decision.Mode) + s.Equal("/kas/v2/rewrap", receivedResource, "HTTP path should keep leading slash") +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV1_PolicyServiceGRPCPath() { + // Test policy.* wildcard matching with gRPC path + var receivedResource string + mockEnforcer := &mockV1Enforcer{ + enforceFunc: func(_ context.Context, _ jwt.Token, req platformauthz.RoleRequest) (bool, map[string]any, error) { + receivedResource = req.Resource + // Allow if resource starts with policy. (gRPC style, no leading slash) + return len(req.Resource) > 7 && req.Resource[:7] == "policy.", nil, nil + }, + } + + cfg := authz.Config{ + Version: "v1", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + Options: []authz.Option{authz.WithV1Enforcer(mockEnforcer)}, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"standard"}, + }, + }) + + // gRPC path from ConnectRPC + req := &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/GetAttribute", + Action: "read", + } + + decision, err := authorizer.Authorize(context.Background(), req) + s.Require().NoError(err) + s.True(decision.Allowed, "policy.* wildcard should match gRPC path after stripping leading slash") + s.Equal(authz.ModeV1, decision.Mode) + s.Equal("policy.attributes.AttributesService/GetAttribute", receivedResource) +} + +func (s *CasbinAuthorizerSuite) TestAuthorizeV1_PathHandlingHeuristic() { + // Test the specific heuristic: paths with "." are gRPC, others are HTTP + var receivedResources []string + mockEnforcer := &mockV1Enforcer{ + enforceFunc: func(_ context.Context, _ jwt.Token, req platformauthz.RoleRequest) (bool, map[string]any, error) { + receivedResources = append(receivedResources, req.Resource) + return true, nil, nil + }, + } + + cfg := authz.Config{ + Version: "v1", + PolicyConfig: authz.PolicyConfig{ + GroupsClaim: "realm_access.roles", + }, + Logger: s.logger, + Options: []authz.Option{authz.WithV1Enforcer(mockEnforcer)}, + } + + authorizer, err := NewAuthorizer(cfg) + s.Require().NoError(err) + + token := createTestToken(s.T(), map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": []interface{}{"test"}, + }, + }) + + // gRPC path (contains ".") - leading slash should be stripped + grpcReq := &authz.Request{ + Token: token, + RPC: "/some.Service/Method", + Action: "read", + } + + decision, err := authorizer.Authorize(context.Background(), grpcReq) + s.Require().NoError(err) + s.True(decision.Allowed, "gRPC path should be allowed") + s.Equal("some.Service/Method", receivedResources[0], "gRPC path should have leading slash stripped") + + // HTTP path (no ".") - leading slash should be kept + httpReq := &authz.Request{ + Token: token, + RPC: "/http/path", + Action: "read", + } + + decision, err = authorizer.Authorize(context.Background(), httpReq) + s.Require().NoError(err) + s.True(decision.Allowed, "HTTP path should be allowed") + s.Equal("/http/path", receivedResources[1], "HTTP path should keep leading slash") +} + +// Helper function to create test JWT tokens +func createTestToken(t *testing.T, claims map[string]interface{}) jwt.Token { + t.Helper() + + token := jwt.New() + for k, v := range claims { + if err := token.Set(k, v); err != nil { + t.Fatalf("failed to set claim %s: %v", k, err) + } + } + return token +} diff --git a/service/internal/auth/authz/casbin/model.conf b/service/internal/auth/authz/casbin/model.conf new file mode 100644 index 0000000000..1696a53594 --- /dev/null +++ b/service/internal/auth/authz/casbin/model.conf @@ -0,0 +1,56 @@ +# Casbin Model v2 - RPC + Dimensions Authorization +# +# This model replaces the legacy path+action model with a cleaner RPC+dimensions approach: +# - sub: subject (roles extracted from JWT, or username) +# - rpc: full gRPC method path (e.g., /policy.attributes.AttributesService/UpdateAttribute) +# - dims: resolved authorization dimensions (e.g., namespace=hr&attribute=classification) +# +# The 'action' field from v1 is removed as the RPC method itself implies the operation. +# This simplifies policy when the gRPC Gateway is removed. +# +# ================================ +# Dimension Serialization Format +# ================================ +# +# Request dimensions are serialized as: key1=value1&key2=value2 +# - Keys are sorted alphabetically for canonical ordering +# - '&' is the delimiter between key-value pairs (AND semantics) +# - '=' separates key from value +# - Empty dimensions serialize to "*" +# +# Policy dimension patterns: +# - "*" : Wildcard, matches any dimensions (including empty) +# - "namespace=hr" : Match single dimension exactly +# - "namespace=*" : Match any value for 'namespace' key +# - "namespace=hr&attr=x" : Match multiple dimensions (AND logic) +# +# Matching rules (dimensionMatch function): +# - Policy "*" matches everything +# - Each policy dimension must be satisfied by the request +# - Policy can omit dimensions (partial match OK) +# - Request can have extra dimensions not in policy (OK) +# - Use multiple policy lines for OR logic +# +# Example policies: +# p, role:admin, *, *, allow # Admin can do anything +# p, role:hr-admin, /policy.attributes.AttributesService/*, namespace=hr, allow # HR admin on HR namespace +# p, role:viewer, /policy.*/Get*, *, allow # Viewer can read any policy service + +[request_definition] +r = sub, rpc, dims + +[policy_definition] +p = sub, rpc, dims, eft + +[role_definition] +g = _, _ + +[policy_effect] +# Allow if any policy explicitly allows AND no policy explicitly denies +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + +[matchers] +# g(r.sub, p.sub): role/group membership check +# keyMatch(r.rpc, p.rpc): RPC path matching with wildcards +# dimensionMatch(r.dims, p.dims): custom function for dimension matching +m = g(r.sub, p.sub) && keyMatch(r.rpc, p.rpc) && dimensionMatch(r.dims, p.dims) diff --git a/service/internal/auth/authz/casbin/policy.csv b/service/internal/auth/authz/casbin/policy.csv new file mode 100644 index 0000000000..ce62189253 --- /dev/null +++ b/service/internal/auth/authz/casbin/policy.csv @@ -0,0 +1,56 @@ +# Casbin Policy v2 - RPC + Dimensions Authorization +# +# Format: p, subject, rpc, dimensions, effect +# - subject: role:rolename or username +# - rpc: gRPC method path or HTTP path (supports * wildcard) +# - dimensions: namespace=value&attribute=value (supports * wildcard) +# - effect: allow or deny +# +# Note: HTTP routes are prefixed with / and use path patterns +# gRPC routes follow package.Service/Method format + +# ============================================================================ +# Admin Role - Full Access +# ============================================================================ +p, role:admin, *, *, allow +g, role:opentdf-admin, role:admin + +# ============================================================================ +# Standard Role - Authenticated Users +# ============================================================================ +g, role:opentdf-standard, role:standard + +# Discovery and health endpoints +p, role:standard, /wellknownconfiguration.WellKnownService/*, *, allow +p, role:standard, /grpc.health.v1.Health/*, *, allow + +# KAS (Key Access Service) - required for TDF operations +p, role:standard, /kas.AccessService/*, *, allow + +# Policy services (read access via gRPC) +p, role:standard, /policy.attributes.AttributesService/Get*, *, allow +p, role:standard, /policy.attributes.AttributesService/List*, *, allow +p, role:standard, /policy.namespaces.NamespaceService/Get*, *, allow +p, role:standard, /policy.namespaces.NamespaceService/List*, *, allow +p, role:standard, /policy.subjectmapping.SubjectMappingService/Get*, *, allow +p, role:standard, /policy.subjectmapping.SubjectMappingService/List*, *, allow +p, role:standard, /policy.subjectmapping.SubjectMappingService/Match*, *, allow +p, role:standard, /policy.resourcemapping.ResourceMappingService/Get*, *, allow +p, role:standard, /policy.resourcemapping.ResourceMappingService/List*, *, allow +p, role:standard, /policy.kasregistry.KeyAccessServerRegistryService/Get*, *, allow +p, role:standard, /policy.kasregistry.KeyAccessServerRegistryService/List*, *, allow + +# Authorization service +p, role:standard, /authorization.AuthorizationService/*, *, allow +p, role:standard, /authorization.v2.AuthorizationService/*, *, allow + +# Entity resolution service +p, role:standard, /entityresolution.EntityResolutionService/*, *, allow +p, role:standard, /entityresolution.v2.EntityResolutionService/*, *, allow + +# ============================================================================ +# Unknown Role - Unauthenticated/Public Access +# ============================================================================ +# KAS rewrap is allowed for unknown roles (authentication enforced by KAS itself) +p, role:unknown, /kas.AccessService/Rewrap, *, allow +p, role:unknown, /kas/v2/rewrap, *, allow diff --git a/service/internal/auth/authz/policy.go b/service/internal/auth/authz/policy.go new file mode 100644 index 0000000000..4974a263b9 --- /dev/null +++ b/service/internal/auth/authz/policy.go @@ -0,0 +1,45 @@ +package authz + +// PolicyConfig contains the policy configuration for authorization. +// This mirrors auth.PolicyConfig to avoid circular imports while maintaining +// the same field structure for consistent configuration. +type PolicyConfig struct { + // Engine specifies the authorization engine to use. + // - "casbin" (default): Casbin policy engine + // - "cedar": AWS Cedar policy engine (future) + // - "opa": Open Policy Agent engine (future) + Engine string + + // Version specifies the engine-specific authorization model version. + // For Casbin: + // - "v1" (default): Legacy path-based authorization (subject, resource, action) + // - "v2": RPC + dimensions authorization (subject, rpc, dimensions) + Version string + + // Username claim to use for user information + UserNameClaim string + + // Claim to use for group/role information (dot notation supported, e.g., "realm_access.roles") + GroupsClaim string + + // Claim to use to reference idP clientID + ClientIDClaim string + + // Override the builtin policy with a custom policy (CSV format) + Csv string + + // Extend the builtin policy with a custom policy + Extension string + + // Casbin model configuration (for custom models) + Model string + + // RoleMap maps IdP roles to internal platform roles. + // + // Deprecated: Use Casbin grouping statements g, , + RoleMap map[string]string + + // Adapter is an optional custom policy adapter (e.g., SQL) + // If nil, the default CSV string adapter is used. + Adapter any +} diff --git a/service/internal/auth/authz/resolver.go b/service/internal/auth/authz/resolver.go new file mode 100644 index 0000000000..e952a4d5ea --- /dev/null +++ b/service/internal/auth/authz/resolver.go @@ -0,0 +1,194 @@ +package authz + +import ( + "context" + "fmt" + "slices" + "sync" + + "connectrpc.com/connect" + "google.golang.org/grpc" +) + +// ResolverResource represents a single resource's authorization dimensions. +// Each key-value pair is a dimension (e.g., "namespace" -> "hr"). +type ResolverResource map[string]string + +// ResolverContext holds the resolved authorization context for a request. +// Multiple resources are supported for operations like "move from A to B" +// where authorization is required for both source and destination. +type ResolverContext struct { + Resources []*ResolverResource + + // ResolvedData stores data fetched during resolution (e.g., attributes, namespaces) + // to avoid duplicate DB queries in handlers. Keys are service-defined strings. + // Handlers can retrieve this data via GetResolvedDataFromContext(). + ResolvedData map[string]any +} + +// ResolverFunc is the function signature for service-provided resolvers. +// Services implement this to extract authorization dimensions from requests. +// +// Parameters: +// - ctx: Request context (includes auth info, can be used for DB calls) +// - req: The connect request (use Deserialize helper to get typed proto) +// +// Returns: +// - ResolverContext with populated dimensions +// - Error if resolution fails (results in 403) +// +// Service maintainers are responsible for: +// 1. Deserializing the request using the provided helper +// 2. Extracting relevant fields +// 3. Performing any required DB lookups +// 4. Populating dimensions in ResolverContext +type ResolverFunc func(ctx context.Context, req connect.AnyRequest) (ResolverContext, error) + +// ResolverRegistry holds resolver functions keyed by service method. +// This is the global registry used by the interceptor. +// It is thread-safe for concurrent read/write access. +type ResolverRegistry struct { + mu sync.RWMutex + resolvers map[string]ResolverFunc // full method path -> resolver +} + +// NewResolverRegistry creates a new resolver registry. +func NewResolverRegistry() *ResolverRegistry { + return &ResolverRegistry{ + resolvers: make(map[string]ResolverFunc), + } +} + +// Get returns the resolver for a method, if registered. +func (r *ResolverRegistry) Get(method string) (ResolverFunc, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + resolver, ok := r.resolvers[method] + return resolver, ok +} + +// ScopedForService creates a namespace-scoped registry that only allows +// registering resolvers for the given service's methods. +// This prevents services from registering resolvers for other services. +// Panics if serviceDesc is nil. +func (r *ResolverRegistry) ScopedForService(serviceDesc *grpc.ServiceDesc) *ScopedResolverRegistry { + if serviceDesc == nil { + panic("serviceDesc cannot be nil") + } + return &ScopedResolverRegistry{ + parent: r, + serviceDesc: serviceDesc, + } +} + +// register is internal - adds a resolver for a specific full method path. +// External callers should use ScopedResolverRegistry. +func (r *ResolverRegistry) register(fullMethodPath string, resolver ResolverFunc) { + r.mu.Lock() + defer r.mu.Unlock() + r.resolvers[fullMethodPath] = resolver +} + +// ScopedResolverRegistry is a namespace-scoped view of the registry. +// It only allows registering resolvers for the service it was created for. +type ScopedResolverRegistry struct { + parent *ResolverRegistry + serviceDesc *grpc.ServiceDesc +} + +// Register adds a resolver for a method in this service. +// Only the method name is required (e.g., "UpdateAttribute"), not the full path. +// The full path is derived from the ServiceDesc. +// +// Returns an error if the method doesn't exist in the ServiceDesc. +func (s *ScopedResolverRegistry) Register(methodName string, resolver ResolverFunc) error { + // Validate method exists in ServiceDesc + methodExists := slices.ContainsFunc(s.serviceDesc.Methods, func(m grpc.MethodDesc) bool { + return m.MethodName == methodName + }) + if !methodExists { + return fmt.Errorf("method %q not found in service %q", methodName, s.serviceDesc.ServiceName) + } + + // Build full method path: // + fullPath := "/" + s.serviceDesc.ServiceName + "/" + methodName + s.parent.register(fullPath, resolver) + return nil +} + +// MustRegister is like Register but panics on error. +// Use during service initialization where errors should be fatal. +func (s *ScopedResolverRegistry) MustRegister(methodName string, resolver ResolverFunc) { + if err := s.Register(methodName, resolver); err != nil { + panic(err) + } +} + +// ServiceName returns the service name this registry is scoped to. +func (s *ScopedResolverRegistry) ServiceName() string { + return s.serviceDesc.ServiceName +} + +// NewResolverContext creates a new empty resolver context. +func NewResolverContext() ResolverContext { + return ResolverContext{} +} + +// NewResource creates and adds a new resource to the context. +func (a *ResolverContext) NewResource() *ResolverResource { + resource := make(ResolverResource) + a.Resources = append(a.Resources, &resource) + return &resource +} + +// AddDimension adds a dimension to the resource. +func (a *ResolverResource) AddDimension(dimension, value string) { + (*a)[dimension] = value +} + +// SetResolvedData stores data in the resolver context cache. +// Use this to cache fetched resources (e.g., attributes) for handler reuse. +// The key should be a descriptive string (e.g., "attribute", "namespace"). +func (a *ResolverContext) SetResolvedData(key string, value any) { + if a.ResolvedData == nil { + a.ResolvedData = make(map[string]any) + } + a.ResolvedData[key] = value +} + +// GetResolvedData retrieves cached data by key. +// Returns nil if key not found. Caller should type-assert the result. +func (a *ResolverContext) GetResolvedData(key string) any { + if a.ResolvedData == nil { + return nil + } + return a.ResolvedData[key] +} + +// resolverContextKey is the context key for storing ResolverContext. +type resolverContextKey struct{} + +// ContextWithResolverContext returns a new context with the ResolverContext attached. +// This is called by the auth interceptor after resolution to make cached data +// available to handlers. +func ContextWithResolverContext(ctx context.Context, rc *ResolverContext) context.Context { + return context.WithValue(ctx, resolverContextKey{}, rc) +} + +// ResolverContextFromContext retrieves the ResolverContext from the context. +// Returns nil if not present (e.g., no resolver registered for the method). +func ResolverContextFromContext(ctx context.Context) *ResolverContext { + rc, _ := ctx.Value(resolverContextKey{}).(*ResolverContext) + return rc +} + +// GetResolvedDataFromContext is a convenience function to retrieve cached data +// from the ResolverContext in the given context. +// Returns nil if no ResolverContext or key not found. +func GetResolvedDataFromContext(ctx context.Context, key string) any { + rc := ResolverContextFromContext(ctx) + if rc == nil { + return nil + } + return rc.GetResolvedData(key) +} diff --git a/service/internal/auth/authz/resolver_test.go b/service/internal/auth/authz/resolver_test.go new file mode 100644 index 0000000000..f6a1154fe6 --- /dev/null +++ b/service/internal/auth/authz/resolver_test.go @@ -0,0 +1,450 @@ +package authz + +import ( + "context" + "sync" + "testing" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" +) + +// Test suite for Resolver functionality +type ResolverSuite struct { + suite.Suite +} + +func TestResolverSuite(t *testing.T) { + suite.Run(t, new(ResolverSuite)) +} + +// --- ResolverRegistry Tests --- + +func (s *ResolverSuite) TestNewResolverRegistry() { + registry := NewResolverRegistry() + + s.NotNil(registry) + s.NotNil(registry.resolvers) + s.Empty(registry.resolvers) +} + +func (s *ResolverSuite) TestRegistry_Get_NotFound() { + registry := NewResolverRegistry() + + resolver, ok := registry.Get("/service.Method") + s.False(ok) + s.Nil(resolver) +} + +func (s *ResolverSuite) TestRegistry_RegisterAndGet() { + registry := NewResolverRegistry() + called := false + testResolver := func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + called = true + return NewResolverContext(), nil + } + + // Use internal register method (normally called via scoped registry) + registry.register("/test.Service/TestMethod", testResolver) + + resolver, ok := registry.Get("/test.Service/TestMethod") + s.True(ok) + s.NotNil(resolver) + + // Verify the resolver is the same by calling it + _, _ = resolver(context.Background(), nil) + s.True(called) +} + +func (s *ResolverSuite) TestRegistry_ThreadSafety() { + registry := NewResolverRegistry() + const numGoroutines = 100 + const numOperations = 100 + + var wg sync.WaitGroup + wg.Add(numGoroutines * 2) // readers and writers + + // Writers + for i := range numGoroutines { + go func(id int) { + defer wg.Done() + for j := range numOperations { + methodPath := "/test.Service/Method" + string(rune('A'+id%26)) + string(rune('0'+j%10)) + registry.register(methodPath, func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + return NewResolverContext(), nil + }) + } + }(i) + } + + // Readers + for range numGoroutines { + go func() { + defer wg.Done() + for range numOperations { + registry.Get("/test.Service/MethodA0") + } + }() + } + + // Should complete without race conditions + wg.Wait() +} + +// --- ScopedResolverRegistry Tests --- + +func (s *ResolverSuite) TestScopedForService_NilServiceDesc_Panics() { + registry := NewResolverRegistry() + + s.Panics(func() { + registry.ScopedForService(nil) + }) +} + +func (s *ResolverSuite) TestScopedForService_ValidServiceDesc() { + registry := NewResolverRegistry() + serviceDesc := &grpc.ServiceDesc{ + ServiceName: "test.TestService", + Methods: []grpc.MethodDesc{ + {MethodName: "GetThing"}, + {MethodName: "CreateThing"}, + }, + } + + scoped := registry.ScopedForService(serviceDesc) + + s.NotNil(scoped) + s.Equal("test.TestService", scoped.ServiceName()) + s.Same(registry, scoped.parent) +} + +func (s *ResolverSuite) TestScoped_Register_ValidMethod() { + registry := NewResolverRegistry() + serviceDesc := &grpc.ServiceDesc{ + ServiceName: "policy.AttributesService", + Methods: []grpc.MethodDesc{ + {MethodName: "CreateAttribute"}, + {MethodName: "GetAttribute"}, + }, + } + scoped := registry.ScopedForService(serviceDesc) + + testResolver := func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + return NewResolverContext(), nil + } + + err := scoped.Register("CreateAttribute", testResolver) + + s.Require().NoError(err) + + // Verify it was registered with full path + resolver, ok := registry.Get("/policy.AttributesService/CreateAttribute") + s.True(ok) + s.NotNil(resolver) +} + +func (s *ResolverSuite) TestScoped_Register_InvalidMethod() { + registry := NewResolverRegistry() + serviceDesc := &grpc.ServiceDesc{ + ServiceName: "policy.AttributesService", + Methods: []grpc.MethodDesc{ + {MethodName: "CreateAttribute"}, + }, + } + scoped := registry.ScopedForService(serviceDesc) + + testResolver := func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + return NewResolverContext(), nil + } + + err := scoped.Register("NonExistentMethod", testResolver) + + s.Require().Error(err) + s.Contains(err.Error(), "method \"NonExistentMethod\" not found in service \"policy.AttributesService\"") + + // Verify nothing was registered + _, ok := registry.Get("/policy.AttributesService/NonExistentMethod") + s.False(ok) +} + +func (s *ResolverSuite) TestScoped_MustRegister_ValidMethod() { + registry := NewResolverRegistry() + serviceDesc := &grpc.ServiceDesc{ + ServiceName: "policy.AttributesService", + Methods: []grpc.MethodDesc{ + {MethodName: "GetAttribute"}, + }, + } + scoped := registry.ScopedForService(serviceDesc) + + testResolver := func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + return NewResolverContext(), nil + } + + // Should not panic + s.NotPanics(func() { + scoped.MustRegister("GetAttribute", testResolver) + }) + + // Verify registration + resolver, ok := registry.Get("/policy.AttributesService/GetAttribute") + s.True(ok) + s.NotNil(resolver) +} + +func (s *ResolverSuite) TestScoped_MustRegister_InvalidMethod_Panics() { + registry := NewResolverRegistry() + serviceDesc := &grpc.ServiceDesc{ + ServiceName: "policy.AttributesService", + Methods: []grpc.MethodDesc{ + {MethodName: "GetAttribute"}, + }, + } + scoped := registry.ScopedForService(serviceDesc) + + testResolver := func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + return NewResolverContext(), nil + } + + s.Panics(func() { + scoped.MustRegister("InvalidMethod", testResolver) + }) +} + +func (s *ResolverSuite) TestScoped_MultipleServicesIsolation() { + registry := NewResolverRegistry() + + serviceA := &grpc.ServiceDesc{ + ServiceName: "serviceA.ServiceA", + Methods: []grpc.MethodDesc{ + {MethodName: "MethodA"}, + }, + } + serviceB := &grpc.ServiceDesc{ + ServiceName: "serviceB.ServiceB", + Methods: []grpc.MethodDesc{ + {MethodName: "MethodB"}, + }, + } + + scopedA := registry.ScopedForService(serviceA) + scopedB := registry.ScopedForService(serviceB) + + resolverA := func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + ctx := NewResolverContext() + res := ctx.NewResource() + res.AddDimension("service", "A") + return ctx, nil + } + resolverB := func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + ctx := NewResolverContext() + res := ctx.NewResource() + res.AddDimension("service", "B") + return ctx, nil + } + + // Service A can only register for its own methods + err := scopedA.Register("MethodA", resolverA) + s.Require().NoError(err) + + err = scopedA.Register("MethodB", resolverA) // Should fail - MethodB not in ServiceA + s.Require().Error(err) + + // Service B can only register for its own methods + err = scopedB.Register("MethodB", resolverB) + s.Require().NoError(err) + + err = scopedB.Register("MethodA", resolverB) // Should fail - MethodA not in ServiceB + s.Require().Error(err) + + // Both registrations should exist in global registry with correct paths + rA, okA := registry.Get("/serviceA.ServiceA/MethodA") + s.True(okA) + s.NotNil(rA) + + rB, okB := registry.Get("/serviceB.ServiceB/MethodB") + s.True(okB) + s.NotNil(rB) + + // Verify they're distinct resolvers + ctxA, _ := rA(context.Background(), nil) + ctxB, _ := rB(context.Background(), nil) + + s.Equal("A", (*ctxA.Resources[0])["service"]) + s.Equal("B", (*ctxB.Resources[0])["service"]) +} + +// --- ResolverContext Tests --- + +func (s *ResolverSuite) TestNewResolverContext() { + ctx := NewResolverContext() + + s.NotNil(ctx) + s.Nil(ctx.Resources) // Should be nil initially, not empty slice +} + +func (s *ResolverSuite) TestResolverContext_NewResource() { + ctx := NewResolverContext() + + res1 := ctx.NewResource() + s.NotNil(res1) + s.Len(ctx.Resources, 1) + + res2 := ctx.NewResource() + s.NotNil(res2) + s.Len(ctx.Resources, 2) + + // Verify they're different resources + s.NotSame(res1, res2) +} + +func (s *ResolverSuite) TestResolverContext_MultipleResources() { + ctx := NewResolverContext() + + // Simulate "move from namespace A to namespace B" scenario + source := ctx.NewResource() + source.AddDimension("namespace", "ns-source") + source.AddDimension("operation", "read") + + destination := ctx.NewResource() + destination.AddDimension("namespace", "ns-destination") + destination.AddDimension("operation", "write") + + s.Len(ctx.Resources, 2) + s.Equal("ns-source", (*ctx.Resources[0])["namespace"]) + s.Equal("ns-destination", (*ctx.Resources[1])["namespace"]) +} + +// --- ResolverResource Tests --- + +func (s *ResolverSuite) TestResolverResource_AddDimension() { + ctx := NewResolverContext() + res := ctx.NewResource() + + res.AddDimension("namespace", "hr") + res.AddDimension("action", "create") + res.AddDimension("resource_type", "attribute") + + s.Equal("hr", (*res)["namespace"]) + s.Equal("create", (*res)["action"]) + s.Equal("attribute", (*res)["resource_type"]) +} + +func (s *ResolverSuite) TestResolverResource_OverwriteDimension() { + ctx := NewResolverContext() + res := ctx.NewResource() + + res.AddDimension("namespace", "original") + res.AddDimension("namespace", "updated") + + s.Equal("updated", (*res)["namespace"]) +} + +func (s *ResolverSuite) TestResolverResource_EmptyValues() { + ctx := NewResolverContext() + res := ctx.NewResource() + + res.AddDimension("", "empty-key") + res.AddDimension("empty-value", "") + + s.Equal("empty-key", (*res)[""]) + s.Empty((*res)["empty-value"]) +} + +// --- Integration Tests --- + +func (s *ResolverSuite) TestFullWorkflow_ServiceRegistration() { + // Simulates how a service would use the registry during initialization + registry := NewResolverRegistry() + + // Service descriptor (normally from proto-generated code) + serviceDesc := &grpc.ServiceDesc{ + ServiceName: "policy.attributes.AttributesService", + Methods: []grpc.MethodDesc{ + {MethodName: "CreateAttribute"}, + {MethodName: "GetAttribute"}, + {MethodName: "UpdateAttribute"}, + {MethodName: "DeleteAttribute"}, + {MethodName: "ListAttributes"}, + }, + } + + // Platform creates scoped registry for service + scopedRegistry := registry.ScopedForService(serviceDesc) + + // Service registers resolvers during initialization (like in RegisterFunc) + scopedRegistry.MustRegister("CreateAttribute", func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + ctx := NewResolverContext() + res := ctx.NewResource() + res.AddDimension("namespace", "test-ns") + res.AddDimension("action", "create") + return ctx, nil + }) + + scopedRegistry.MustRegister("GetAttribute", func(_ context.Context, _ connect.AnyRequest) (ResolverContext, error) { + ctx := NewResolverContext() + res := ctx.NewResource() + res.AddDimension("namespace", "test-ns") + res.AddDimension("action", "read") + return ctx, nil + }) + + // Interceptor looks up resolvers by method path + createResolver, ok := registry.Get("/policy.attributes.AttributesService/CreateAttribute") + s.True(ok) + + getResolver, ok := registry.Get("/policy.attributes.AttributesService/GetAttribute") + s.True(ok) + + // Methods without resolvers return false + _, ok = registry.Get("/policy.attributes.AttributesService/ListAttributes") + s.False(ok) + + // Verify resolver execution + createCtx, err := createResolver(context.Background(), nil) + s.Require().NoError(err) + s.Len(createCtx.Resources, 1) + s.Equal("create", (*createCtx.Resources[0])["action"]) + + getCtx, err := getResolver(context.Background(), nil) + s.Require().NoError(err) + s.Len(getCtx.Resources, 1) + s.Equal("read", (*getCtx.Resources[0])["action"]) +} + +// --- Additional Test Functions (non-suite) --- + +func TestResolverRegistry_Basic(t *testing.T) { + registry := NewResolverRegistry() + require.NotNil(t, registry) + assert.Empty(t, registry.resolvers) +} + +func TestScopedRegistry_ServiceName(t *testing.T) { + registry := NewResolverRegistry() + serviceDesc := &grpc.ServiceDesc{ + ServiceName: "my.custom.Service", + Methods: []grpc.MethodDesc{{MethodName: "DoSomething"}}, + } + + scoped := registry.ScopedForService(serviceDesc) + + assert.Equal(t, "my.custom.Service", scoped.ServiceName()) +} + +func TestResolverContext_ResourceIndependence(t *testing.T) { + ctx := NewResolverContext() + + res1 := ctx.NewResource() + res1.AddDimension("key", "value1") + + res2 := ctx.NewResource() + res2.AddDimension("key", "value2") + + // Modifying res1 shouldn't affect res2 + assert.Equal(t, "value1", (*res1)["key"]) + assert.Equal(t, "value2", (*res2)["key"]) +} diff --git a/service/internal/auth/casbin.go b/service/internal/auth/casbin.go index ac9a40f598..5d8a96b6ad 100644 --- a/service/internal/auth/casbin.go +++ b/service/internal/auth/casbin.go @@ -1,6 +1,7 @@ package auth import ( + "context" "errors" "fmt" "log/slog" @@ -11,13 +12,20 @@ import ( stringadapter "github.com/casbin/casbin/v2/persist/string-adapter" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/authz" _ "embed" ) var ( - rolePrefix = "role:" - defaultRole = "unknown" + rolePrefix = "role:" + defaultRole = "unknown" + ErrPermissionDenied = errors.New("permission denied") +) + +const ( + casbinAuthzConfiguredGroupsClaimKey = "configured_groups_claim" + casbinAuthzSubjectGroupsKey = "subject_groups" ) //go:embed casbin_policy.csv @@ -26,6 +34,7 @@ var builtinPolicy string //go:embed casbin_model.conf var defaultModel string +// Enforcer is the Casbin enforcer with platform-specific configuration type Enforcer struct { *casbin.Enforcer Config CasbinConfig @@ -34,15 +43,17 @@ type Enforcer struct { isDefaultPolicy bool isDefaultModel bool + roleProvider authz.RoleProvider } type casbinSubject []string type CasbinConfig struct { PolicyConfig + RoleProvider authz.RoleProvider } -// newCasbinEnforcer creates a new casbin enforcer +// NewCasbinEnforcer creates a new casbin enforcer func NewCasbinEnforcer(c CasbinConfig, logger *logger.Logger) (*Enforcer, error) { // Set Casbin config defaults if not provided isDefaultModel := false @@ -94,7 +105,8 @@ func NewCasbinEnforcer(c CasbinConfig, logger *logger.Logger) (*Enforcer, error) c.Adapter = stringadapter.NewAdapter(c.Csv) } - logger.Debug("creating casbin enforcer", + logger.Debug( + "creating casbin enforcer", slog.Any("config", c), slog.Bool("isDefaultModel", isDefaultModel), slog.Bool("isBuiltinPolicy", isDefaultPolicy), @@ -112,6 +124,11 @@ func NewCasbinEnforcer(c CasbinConfig, logger *logger.Logger) (*Enforcer, error) return nil, fmt.Errorf("failed to create casbin enforcer: %w", err) } + roleProvider := c.RoleProvider + if roleProvider == nil { + roleProvider = newJWTClaimsRoleProvider(c.GroupsClaim, logger) + } + return &Enforcer{ Enforcer: e, Config: c, @@ -119,55 +136,82 @@ func NewCasbinEnforcer(c CasbinConfig, logger *logger.Logger) (*Enforcer, error) isDefaultPolicy: isDefaultPolicy, isDefaultModel: isDefaultModel, logger: logger, + roleProvider: roleProvider, }, nil } // casbinEnforce is a helper function to enforce the policy with casbin // TODO implement a common type so this can be used for both http and grpc -func (e *Enforcer) Enforce(token jwt.Token, resource, action string) (bool, error) { +func (e *Enforcer) Enforce(ctx context.Context, token jwt.Token, req authz.RoleRequest) (bool, map[string]any, error) { // extract the role claim from the token - s := e.buildSubjectFromToken(token) + s, subjectGroups, err := e.buildSubjectFromToken(ctx, token, req) + metadata := map[string]any{ + casbinAuthzConfiguredGroupsClaimKey: e.Config.GroupsClaim, + casbinAuthzSubjectGroupsKey: subjectGroups, + } + if err != nil { + e.logger.Warn("role provider error", slog.Any("error", err)) + return false, metadata, ErrPermissionDenied + } s = append(s, rolePrefix+defaultRole) + resource := req.Resource + action := req.Action for _, info := range s { allowed, err := e.Enforcer.Enforce(info, resource, action) if err != nil { - e.logger.Error("enforce by role error", + e.logger.Error( + "enforce by role error", slog.String("subject_info", info), slog.String("action", action), slog.String("resource", resource), - slog.Any("error", err), + slog.String("error", err.Error()), ) } if allowed { - e.logger.Debug("allowed by policy", + e.logger.Debug( + "allowed by policy", slog.String("subject_info", info), slog.String("action", action), slog.String("resource", resource), ) - return true, nil + return true, metadata, nil } } - e.logger.Debug("permission denied by policy", + e.logger.Debug( + "permission denied by policy", slog.Any("subject_info", s), slog.String("action", action), slog.String("resource", resource), ) - return false, errors.New("permission denied") + return false, metadata, ErrPermissionDenied } -func (e *Enforcer) buildSubjectFromToken(t jwt.Token) casbinSubject { +func (e *Enforcer) BuildSubjectFromTokenAndUserInfo(token jwt.Token, _ []byte) []string { + subjects, _, err := e.buildSubjectFromToken(context.Background(), token, authz.RoleRequest{}) + if err != nil { + e.logger.Warn("failed to extract subjects", slog.Any("error", err)) + return nil + } + return subjects +} + +func (e *Enforcer) buildSubjectFromToken(ctx context.Context, t jwt.Token, req authz.RoleRequest) (casbinSubject, []string, error) { var subject string info := casbinSubject{} e.logger.Debug("building subject from token") - roles := e.extractRolesFromToken(t) + roles, err := e.roleProvider.Roles(ctx, t, req) + if err != nil { + return nil, nil, err + } if claim, found := t.Get(e.Config.UserNameClaim); found { sub, ok := claim.(string) subject = sub if !ok { - e.logger.Warn("username claim not of type string", + e.logger.Warn( + "username claim not of type string", slog.String("claim", e.Config.UserNameClaim), slog.Any("claims", claim), ) @@ -176,66 +220,5 @@ func (e *Enforcer) buildSubjectFromToken(t jwt.Token) casbinSubject { } info = append(info, roles...) info = append(info, subject) - return info -} - -func (e *Enforcer) extractRolesFromToken(t jwt.Token) []string { - e.logger.Debug("extracting roles from token") - roles := []string{} - - roleClaim := e.Config.GroupsClaim - // roleMap := e.Config.RoleMap - - selectors := strings.Split(roleClaim, ".") - claim, exists := t.Get(selectors[0]) - if !exists { - e.logger.Warn("claim not found", - slog.String("claim", roleClaim), - slog.Any("claims", claim), - ) - return nil - } - e.logger.Debug("root claim found", - slog.String("claim", roleClaim), - slog.Any("claims", claim), - ) - // use dotnotation if the claim is nested - if len(selectors) > 1 { - claimMap, ok := claim.(map[string]interface{}) - if !ok { - e.logger.Warn("claim is not of type map[string]interface{}", - slog.String("claim", roleClaim), - slog.Any("claims", claim), - ) - return nil - } - claim = dotNotation(claimMap, strings.Join(selectors[1:], ".")) - if claim == nil { - e.logger.Warn("claim not found", - slog.String("claim", roleClaim), - slog.Any("claims", claim), - ) - return nil - } - } - - // check the type of the role claim - switch v := claim.(type) { - case string: - roles = append(roles, v) - case []interface{}: - for _, rr := range v { - if r, ok := rr.(string); ok { - roles = append(roles, r) - } - } - default: - e.logger.Warn("could not get claim type", - slog.String("selector", roleClaim), - slog.Any("claims", claim), - ) - return nil - } - - return roles + return info, append([]string(nil), roles...), nil } diff --git a/service/internal/auth/casbin_policy.csv b/service/internal/auth/casbin_policy.csv index 820f8293ea..9ee527d8f4 100644 --- a/service/internal/auth/casbin_policy.csv +++ b/service/internal/auth/casbin_policy.csv @@ -22,26 +22,15 @@ p, role:admin, *, *, allow p, role:standard, policy.*, read, allow p, role:standard, kasregistry.*, read, allow p, role:standard, kas.AccessService/Rewrap, *, allow +p, role:standard, /kas/v2/rewrap, write, allow p, role:standard, authorization.AuthorizationService/GetDecisions, read, allow p, role:standard, authorization.AuthorizationService/GetDecisionsByToken, read, allow p, role:standard, authorization.v2.AuthorizationService/GetDecision, read, allow p, role:standard, authorization.v2.AuthorizationService/GetDecisionMultiResource, read, allow p, role:standard, authorization.v2.AuthorizationService/GetDecisionBulk, read, allow -## HTTP routes -p, role:standard, /attributes*, read, allow -p, role:standard, /namespaces*, read, allow -p, role:standard, /subject-mappings*, read, allow -p, role:standard, /resource-mappings*, read, allow -p, role:standard, /key-access-servers*, read, allow -p, role:standard, /kas/v2/rewrap, write, allow -p, role:standard, /v1/authorization, write, allow -p, role:standard, /v1/token/authorization, write, allow - # Public routes ## gRPC routes ## for ERS, right now we don't care about requester role, just that a valid jwt is provided when the OPA engine calls (enforced in the ERS itself, not casbin) p, role:unknown, kas.AccessService/Rewrap, *, allow -## HTTP routes -## for ERS, right now we don't care about requester role, just that a valid jwt is provided when the OPA engine calls (enforced in the ERS itself, not casbin) -p, role:unknown, /kas/v2/rewrap, *, allow \ No newline at end of file +p, role:unknown, /kas/v2/rewrap, *, allow diff --git a/service/internal/auth/casbin_test.go b/service/internal/auth/casbin_test.go index a67f45fb0b..1e573f2af7 100644 --- a/service/internal/auth/casbin_test.go +++ b/service/internal/auth/casbin_test.go @@ -1,6 +1,7 @@ package auth import ( + "context" "fmt" "log/slog" "strings" @@ -10,6 +11,7 @@ import ( "github.com/creasty/defaults" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/authz" "github.com/stretchr/testify/suite" ) @@ -80,7 +82,7 @@ func (s *AuthnCasbinSuite) Test_NewEnforcerWithCustomModel() { }) s.Require().NoError(err) - allowed, err := enforcer.Enforce(tok, "", "") + allowed, err := s.enforce(enforcer, tok, "", "") s.Require().NoError(err) s.True(allowed) } @@ -121,18 +123,6 @@ func (s *AuthnCasbinSuite) Test_Enforcement() { resource: "policy.attributes.DoSomething", action: "write", }, - { - allowed: true, - roles: admin, - resource: "/attributes/do/something", - action: "read", - }, - { - allowed: true, - roles: admin, - resource: "/attributes/do/something", - action: "write", - }, { allowed: true, roles: admin, @@ -153,30 +143,12 @@ func (s *AuthnCasbinSuite) Test_Enforcement() { resource: "policy.attributes.DoSomething", action: "write", }, - { - allowed: true, - roles: standard, - resource: "/attributes", - action: "read", - }, - { - allowed: false, - roles: standard, - resource: "/attributes", - action: "write", - }, { allowed: false, roles: standard, resource: "non-existent", action: "read", }, - { - allowed: true, - roles: standard, - resource: "/kas/v2/rewrap", - action: "write", - }, { allowed: true, roles: standard, @@ -203,18 +175,6 @@ func (s *AuthnCasbinSuite) Test_Enforcement() { resource: "policy.attributes.DoSomething", action: "write", }, - { - allowed: false, - roles: unknown, - resource: "/attributes", - action: "read", - }, - { - allowed: false, - roles: unknown, - resource: "/attributes", - action: "write", - }, { allowed: false, roles: unknown, @@ -244,13 +204,14 @@ func (s *AuthnCasbinSuite) Test_Enforcement() { enforcer, err := NewCasbinEnforcer(CasbinConfig{PolicyConfig: policyCfg}, logger.CreateTestLogger()) s.Require().NoError(err, name) tok := s.newTokWithDefaultClaim(test.roles[0], test.roles[1], "", "") - allowed, err := enforcer.Enforce(tok, test.resource, test.action) + allowed, err := s.enforce(enforcer, tok, test.resource, test.action) if !test.allowed { s.Require().Error(err, name) + s.False(allowed, name) } else { s.Require().NoError(err, name) + s.True(allowed, name) } - s.Equal(test.allowed, allowed, name) slog.Info("running test w/ custom claim", slog.String("name", name)) @@ -261,13 +222,14 @@ func (s *AuthnCasbinSuite) Test_Enforcement() { }, logger.CreateTestLogger()) s.Require().NoError(err, name) _, tok = s.newTokenWithCustomClaim(test.roles[0], test.roles[1]) - allowed, err = enforcer.Enforce(tok, test.resource, test.action) + allowed, err = s.enforce(enforcer, tok, test.resource, test.action) if !test.allowed { s.Require().Error(err, name) + s.False(allowed, name) } else { s.Require().NoError(err, name) + s.True(allowed, name) } - s.Equal(test.allowed, allowed, name) slog.Info("running test w/ custom rolemap", slog.String("name", name)) @@ -282,13 +244,14 @@ func (s *AuthnCasbinSuite) Test_Enforcement() { }, logger.CreateTestLogger()) s.Require().NoError(err, name) _, tok = s.newTokenWithCustomRoleMap(test.roles[0], test.roles[1]) - allowed, err = enforcer.Enforce(tok, test.resource, test.action) + allowed, err = s.enforce(enforcer, tok, test.resource, test.action) if !test.allowed { s.Require().Error(err, name) + s.False(allowed, name) } else { s.Require().NoError(err, name) + s.True(allowed, name) } - s.Equal(test.allowed, allowed) slog.Info("running test w/ client_id", slog.String("name", name)) roleMap := make(map[string]string) @@ -306,14 +269,15 @@ func (s *AuthnCasbinSuite) Test_Enforcement() { PolicyConfig: policyCfg, }, logger.CreateTestLogger()) s.Require().NoError(err, name) - _, tok = s.newTokenWithCilentID() - allowed, err = enforcer.Enforce(tok, test.resource, test.action) + _, tok = s.newTokenWithClientID() + allowed, err = s.enforce(enforcer, tok, test.resource, test.action) if !test.allowed { s.Require().Error(err, name) + s.False(allowed, name) } else { s.Require().NoError(err, name) + s.True(allowed, name) } - s.Equal(test.allowed, allowed, name) } } @@ -332,92 +296,95 @@ func (s *AuthnCasbinSuite) Test_ExtendDefaultPolicies() { s.Require().NoError(err) // other roles denied new policy: admin tok := s.newTokWithDefaultClaim(true, false, "", "") - allowed, err := enforcer.Enforce(tok, "new.service.DoSomething", "read") + allowed, err := s.enforce(enforcer, tok, "new.service.DoSomething", "read") s.Require().NoError(err) s.True(allowed) - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "write") + allowed, err = s.enforce(enforcer, tok, "new.service.DoSomething", "write") s.Require().NoError(err) s.True(allowed) // other roles denied new policy: standard tok = s.newTokWithDefaultClaim(false, true, "", "") - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "read") + allowed, err = s.enforce(enforcer, tok, "new.service.DoSomething", "read") s.Require().NoError(err) s.True(allowed) - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "write") + allowed, err = s.enforce(enforcer, tok, "new.service.DoSomething", "write") s.Require().Error(err) s.False(allowed) } func (s *AuthnCasbinSuite) Test_ExtendDefaultPolicies_MalformedErrors() { - policyCfg := PolicyConfig{} - err := defaults.Set(&policyCfg) - s.Require().NoError(err) - - enforcer, err := NewCasbinEnforcer(CasbinConfig{PolicyConfig: policyCfg}, logger.CreateTestLogger()) - s.Require().NoError(err) - tok := s.newTokWithDefaultClaim(true, false, "", "") - allowed, err := enforcer.Enforce(tok, "policy.attributes.DoSomething", "read") - s.Require().NoError(err) - s.True(allowed) - - // missing 'p' - policyCfg.Extension = strings.Join([]string{ - "g, opentdf-admin, role:admin", - "g, opentdf-standard, role:standard", - "role:admin, new.service.DoSomething, *", - }, "\n") - enforcer, err = NewCasbinEnforcer(CasbinConfig{ - PolicyConfig: policyCfg, - }, logger.CreateTestLogger()) - s.Require().NoError(err) - tok = s.newTokWithDefaultClaim(true, false, "", "") - allowed, err = enforcer.Enforce(tok, "policy.attributes.DoSomething", "read") - s.Require().NoError(err) - s.True(allowed) - - // missing effect - policyCfg.Extension = strings.Join([]string{ - "g, opentdf-admin, role:admin", - "g, opentdf-standard, role:standard", - "p, role:admin, new.service.DoSomething, *", - }, "\n") - enforcer, err = NewCasbinEnforcer(CasbinConfig{ - PolicyConfig: policyCfg, - }, logger.CreateTestLogger()) - s.Require().NoError(err) - tok = s.newTokWithDefaultClaim(true, false, "", "") - allowed, err = enforcer.Enforce(tok, "policy.attributes.DoSomething", "read") - s.Require().NoError(err) - s.True(allowed) - - // empty - policyCfg.Extension = strings.Join([]string{ - "", - }, "\n") - enforcer, err = NewCasbinEnforcer(CasbinConfig{ - PolicyConfig: policyCfg, - }, logger.CreateTestLogger()) - s.Require().NoError(err) - tok = s.newTokWithDefaultClaim(true, false, "", "") - allowed, err = enforcer.Enforce(tok, "policy.attributes.DoSomething", "read") - s.Require().NoError(err) - s.True(allowed) + testCases := []struct { + name string + extension string + expectErr bool + allowed bool // expected result from enforce + }{ + { + name: "admin no extension, empty resource or action", + extension: "", + expectErr: false, + allowed: true, + }, + { + name: "missing 'p' in policy line", + extension: strings.Join([]string{ + "g, opentdf-admin, role:admin", + "g, opentdf-standard, role:standard", + "role:admin, new.service.DoSomething, *", + }, "\n"), + expectErr: false, // v1 casbin doesn't validate CSV format + allowed: true, // admin still has access via default policy + valid group mapping + }, + { + name: "missing effect", + extension: strings.Join([]string{ + "g, opentdf-admin, role:admin", + "g, opentdf-standard, role:standard", + "p, role:admin, new.service.DoSomething, *", + }, "\n"), + expectErr: false, // v1 casbin doesn't validate CSV format + allowed: true, // admin still has access via default policy + valid group mapping + }, + { + name: "missing role prefix", + extension: strings.Join([]string{ + "g, opentdf-admin, admin", + "g, opentdf-standard, standard", + "p, admin, new.service.DoSomething, *", + }, "\n"), + expectErr: false, // v1 casbin doesn't validate CSV format + allowed: false, // role mapping without prefix won't match + }, + } - // missing role prefix - policyCfg.Extension = strings.Join([]string{ - "g, opentdf-admin, role:admin", - "g, opentdf-standard, role:standard", - "p, admin, new.service.DoSomething, *", - }, "\n") - enforcer, err = NewCasbinEnforcer(CasbinConfig{ - PolicyConfig: policyCfg, - }, logger.CreateTestLogger()) - s.Require().NoError(err) - tok = s.newTokWithDefaultClaim(true, false, "", "") - allowed, err = enforcer.Enforce(tok, "policy.attributes.DoSomething", "read") - s.Require().NoError(err) - s.True(allowed) + for _, tc := range testCases { + s.Run(tc.name, func() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + policyCfg.Extension = tc.extension + enforcer, err := NewCasbinEnforcer(CasbinConfig{PolicyConfig: policyCfg}, logger.CreateTestLogger()) + if tc.expectErr { + s.Require().Error(err) + s.Nil(enforcer) + return + } + + s.Require().NoError(err) + s.NotNil(enforcer) + + tok := s.newTokWithDefaultClaim(true, false, "", "") + allowed, err := s.enforce(enforcer, tok, "policy.attributes.DoSomething", "read") + if tc.allowed { + s.Require().NoError(err) + s.True(allowed) + } else { + s.Require().Error(err) + s.False(allowed) + } + }) + } } func (s *AuthnCasbinSuite) Test_SetBuiltinPolicy() { @@ -433,53 +400,128 @@ func (s *AuthnCasbinSuite) Test_SetBuiltinPolicy() { "g, opentdf-standard, role:standard", }, "\n") - enforcer, err := NewCasbinEnforcer(CasbinConfig{PolicyConfig: policyCfg}, logger.CreateTestLogger()) - s.Require().NoError(err) - - // unauthorized role - tok := s.newTokWithDefaultClaim(false, false, "", "") - allowed, err := enforcer.Enforce(tok, "new.hello.World", "read") - s.Require().Error(err) - s.False(allowed) - allowed, err = enforcer.Enforce(tok, "new.hello.World", "write") - s.Require().Error(err) - s.False(allowed) - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "read") - s.Require().Error(err) - s.False(allowed) - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "write") - s.Require().Error(err) - s.False(allowed) + testCases := []struct { + name string + admin bool + standard bool + resource string + action string + allowed bool + }{ + { + name: "unauthorized role cannot read new.hello.World", + admin: false, + standard: false, + resource: "new.hello.World", + action: "read", + allowed: false, + }, + { + name: "unauthorized role cannot write new.hello.World", + admin: false, + standard: false, + resource: "new.hello.World", + action: "write", + allowed: false, + }, + { + name: "unauthorized role cannot read new.service.DoSomething", + admin: false, + standard: false, + resource: "new.service.DoSomething", + action: "read", + allowed: false, + }, + { + name: "unauthorized role cannot write new.service.DoSomething", + admin: false, + standard: false, + resource: "new.service.DoSomething", + action: "write", + allowed: false, + }, + { + name: "admin can read new.hello.World", + admin: true, + standard: false, + resource: "new.hello.World", + action: "read", + allowed: true, + }, + { + name: "admin can write new.hello.World", + admin: true, + standard: false, + resource: "new.hello.World", + action: "write", + allowed: true, + }, + { + name: "admin cannot read new.service.DoSomething", + admin: true, + standard: false, + resource: "new.service.DoSomething", + action: "read", + allowed: false, + }, + { + name: "admin cannot write new.service.DoSomething", + admin: true, + standard: false, + resource: "new.service.DoSomething", + action: "write", + allowed: false, + }, + { + name: "standard can read new.hello.World", + admin: false, + standard: true, + resource: "new.hello.World", + action: "read", + allowed: true, + }, + { + name: "standard cannot write new.hello.World", + admin: false, + standard: true, + resource: "new.hello.World", + action: "write", + allowed: false, + }, + { + name: "standard cannot read new.service.DoSomething", + admin: false, + standard: true, + resource: "new.service.DoSomething", + action: "read", + allowed: false, + }, + { + name: "standard cannot write new.service.DoSomething", + admin: false, + standard: true, + resource: "new.service.DoSomething", + action: "write", + allowed: false, + }, + } - // other roles denied new policy: admin - tok = s.newTokWithDefaultClaim(true, false, "", "") - allowed, err = enforcer.Enforce(tok, "new.hello.World", "read") - s.Require().NoError(err) - s.True(allowed) - allowed, err = enforcer.Enforce(tok, "new.hello.World", "write") + enforcer, err := NewCasbinEnforcer(CasbinConfig{PolicyConfig: policyCfg}, logger.CreateTestLogger()) s.Require().NoError(err) - s.True(allowed) - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "read") - s.Require().Error(err) - s.False(allowed) - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "write") - s.Require().Error(err) - s.False(allowed) - // other roles denied new policy: standard - tok = s.newTokWithDefaultClaim(false, true, "", "") - allowed, err = enforcer.Enforce(tok, "new.hello.World", "read") - s.Require().NoError(err) - s.True(allowed) - allowed, err = enforcer.Enforce(tok, "new.hello.World", "write") - s.Require().Error(err) - s.False(allowed) - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "read") - s.Require().Error(err) - s.False(allowed) - allowed, err = enforcer.Enforce(tok, "new.service.DoSomething", "write") - s.Require().Error(err) - s.False(allowed) + for _, tc := range testCases { + s.Run(tc.name, func() { + tok := s.newTokWithDefaultClaim(tc.admin, tc.standard, "", "") + allowed, err := s.enforce(enforcer, tok, tc.resource, tc.action) + if tc.allowed { + s.Require().NoError(err, tc.name) + s.True(allowed, tc.name) + } else { + s.Require().Error(err, tc.name) + s.False(allowed, tc.name) + } + }) + } } func (s *AuthnCasbinSuite) Test_Username_Policy() { @@ -495,36 +537,127 @@ func (s *AuthnCasbinSuite) Test_Username_Policy() { s.Require().NoError(err) tok := s.newTokWithDefaultClaim(true, false, "preferred_username", "") - allowed, err := enforcer.Enforce(tok, "new.service.DoSomething", "read") + allowed, err := s.enforce(enforcer, tok, "new.service.DoSomething", "read") s.Require().NoError(err) s.True(allowed) - allowed, err = enforcer.Enforce(tok, "policy.attributes.List", "read") + allowed, err = s.enforce(enforcer, tok, "policy.attributes.List", "read") s.Require().Error(err) s.False(allowed) } -func (s *AuthnCasbinSuite) Test_Override_Of_Username_Claim() { +type staticProvider struct { + roles []string + err error +} + +func (p staticProvider) Roles(_ context.Context, _ jwt.Token, _ authz.RoleRequest) ([]string, error) { + return p.roles, p.err +} + +func (s *AuthnCasbinSuite) Test_ExternalRoleProvider() { policyCfg := PolicyConfig{} err := defaults.Set(&policyCfg) s.Require().NoError(err) - policyCfg.UserNameClaim = "username" policyCfg.Extension = strings.Join([]string{ - "p, casbin-user, new.service.*, read, allow", + "p, role:admin, policy.attributes.*, read, allow", }, "\n") - enforcer, err := NewCasbinEnforcer(CasbinConfig{PolicyConfig: policyCfg}, logger.CreateTestLogger()) + enforcer, err := NewCasbinEnforcer(CasbinConfig{ + PolicyConfig: policyCfg, + RoleProvider: staticProvider{roles: []string{"role:admin"}}, + }, logger.CreateTestLogger()) s.Require().NoError(err) - tok := s.newTokWithDefaultClaim(true, false, "username", "") - allowed, err := enforcer.Enforce(tok, "new.service.DoSomething", "read") + tok := jwt.New() + allowed, err := s.enforce(enforcer, tok, "policy.attributes.List", "read") s.Require().NoError(err) s.True(allowed) +} - allowed, err = enforcer.Enforce(tok, "policy.attributes.List", "read") - s.Require().Error(err) - s.False(allowed) +func (s *AuthnCasbinSuite) Test_Override_Of_Username_Claim() { + tests := []struct { + name string + usernameClaim string + resource string + action string + shouldAllow bool + setClaim bool // whether to set the username claim in the token + }{ + { + name: "Allow with correct username claim (override)", + usernameClaim: "username", + resource: "new.service.DoSomething", + action: "read", + shouldAllow: true, + setClaim: true, + }, + { + name: "Deny with incorrect resource (override)", + usernameClaim: "username", + resource: "policy.attributes.List", + action: "read", + shouldAllow: false, + setClaim: true, + }, + { + name: "Allow with correct username claim (default)", + usernameClaim: "preferred_username", + resource: "new.service.DoSomething", + action: "read", + shouldAllow: true, + setClaim: true, + }, + { + name: "Deny with incorrect resource (default)", + usernameClaim: "preferred_username", + resource: "policy.attributes.List", + action: "read", + shouldAllow: false, + setClaim: true, + }, + { + name: "Deny when username claim not set in token", + usernameClaim: "username", + resource: "new.service.DoSomething", + action: "read", + shouldAllow: false, + setClaim: false, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err, tc.name) + + policyCfg.UserNameClaim = tc.usernameClaim + policyCfg.Extension = strings.Join([]string{ + "p, casbin-user, new.service.*, read, allow", + }, "\n") + + enforcer, err := NewCasbinEnforcer(CasbinConfig{PolicyConfig: policyCfg}, logger.CreateTestLogger()) + s.Require().NoError(err, tc.name) + + var tok jwt.Token + if tc.setClaim { + tok = s.newTokWithDefaultClaim(true, false, tc.usernameClaim, "") + } else { + tok = s.newTokWithDefaultClaim(true, false, "", "") + } + + allowed, err := s.enforce(enforcer, tok, tc.resource, tc.action) + if tc.shouldAllow { + s.Require().NoError(err, tc.name) + s.True(allowed, tc.name) + } else { + s.Require().Error(err, tc.name) + s.False(allowed, tc.name) + } + }) + } } func (s *AuthnCasbinSuite) Test_Override_Of_Groups_Claim() { @@ -538,15 +671,32 @@ func (s *AuthnCasbinSuite) Test_Override_Of_Groups_Claim() { s.Require().NoError(err) tok := s.newTokWithDefaultClaim(false, true, "", "groups") - allowed, err := enforcer.Enforce(tok, "new.service.DoSomething", "read") + allowed, err := s.enforce(enforcer, tok, "new.service.DoSomething", "read") s.Require().Error(err) s.False(allowed) - allowed, err = enforcer.Enforce(tok, "policy.attributes.List", "read") + allowed, err = s.enforce(enforcer, tok, "policy.attributes.List", "read") s.Require().NoError(err) s.True(allowed) } +// Test_Casbin_Claims_Matrix was removed as it tested multi-claim and userInfo +// features that are now v2-only. V1 casbin uses single GroupsClaim string and +// ignores the userInfo parameter. These features are tested in +// service/internal/auth/authz/casbin/casbin_test.go for v2. + +func (s *AuthnCasbinSuite) enforce(enforcer *Enforcer, tok jwt.Token, resource, action string) (bool, error) { + allowed, _, err := enforcer.Enforce( + context.Background(), + tok, + authz.RoleRequest{ + Resource: resource, + Action: action, + }, + ) + return allowed, err +} + func (s *AuthnCasbinSuite) buildTokenRoles(admin bool, standard bool, roleMaps []string) []interface{} { adminRole := "opentdf-admin" if len(roleMaps) > 0 { @@ -557,21 +707,19 @@ func (s *AuthnCasbinSuite) buildTokenRoles(admin bool, standard bool, roleMaps [ standardRole = roleMaps[1] } - i := 0 - roles := make([]interface{}, 2) + roles := make([]interface{}, 0, 2) if admin { - roles[i] = adminRole - i++ + roles = append(roles, adminRole) } if standard { - roles[i] = standardRole + roles = append(roles, standardRole) } return roles } -func (s *AuthnCasbinSuite) newTokWithDefaultClaim(admin bool, standard bool, usernameClaimName, groupClaimName string) jwt.Token { +func (s *AuthnCasbinSuite) newTokWithDefaultClaim(admin bool, standard bool, usernameClaimName string, groupClaimName string) jwt.Token { tok := jwt.New() if groupClaimName == "" { @@ -610,7 +758,7 @@ func (s *AuthnCasbinSuite) newTokenWithCustomRoleMap(admin bool, standard bool) return "", tok } -func (s *AuthnCasbinSuite) newTokenWithCilentID() (string, jwt.Token) { +func (s *AuthnCasbinSuite) newTokenWithClientID() (string, jwt.Token) { tok := jwt.New() if err := tok.Set("client_id", "test"); err != nil { s.T().Fatal(err) diff --git a/service/internal/auth/config.go b/service/internal/auth/config.go index 5e48877cf1..161587050c 100644 --- a/service/internal/auth/config.go +++ b/service/internal/auth/config.go @@ -6,6 +6,7 @@ import ( "github.com/casbin/casbin/v2/persist" "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/authz" ) // AuthConfig pulls AuthN and AuthZ together @@ -15,6 +16,10 @@ type Config struct { // Used for re-authentication of IPC connections IPCReauthRoutes []string `mapstructure:"-" json:"-"` AuthNConfig `mapstructure:",squash"` + + // Programmatic role provider overrides (not loaded from config) + RoleProvider authz.RoleProvider `mapstructure:"-" json:"-"` + RoleProviderFactories map[string]authz.RoleProviderFactory `mapstructure:"-" json:"-"` } // AuthNConfig is the configuration need for the platform to validate tokens @@ -30,13 +35,26 @@ type AuthNConfig struct { //nolint:revive // AuthNConfig is a valid name type PolicyConfig struct { Builtin string `mapstructure:"-" json:"-"` + // Engine specifies the authorization engine to use. + // - "casbin" (default): Casbin policy engine + // - "cedar": AWS Cedar policy engine (future) + // - "opa": Open Policy Agent engine (future) + Engine string `mapstructure:"engine" json:"engine" default:"casbin"` + // Version specifies the engine-specific authorization model version. + // For Casbin: + // - "v1" (default): Legacy path-based authorization (subject, resource, action) + // - "v2": RPC + dimensions authorization (subject, rpc, dimensions) + // v2 enables fine-grained resource-level authorization using AuthzResolvers. + Version string `mapstructure:"version" json:"version" default:"v1"` // Username claim to use for user information UserNameClaim string `mapstructure:"username_claim" json:"username_claim" default:"preferred_username"` // Claim to use for group/role information GroupsClaim string `mapstructure:"groups_claim" json:"groups_claim" default:"realm_access.roles"` + // Role provider configuration (resolved via StartOptions) + RolesProvider RolesProviderConfig `mapstructure:"roles_provider" json:"roles_provider"` // Claim to use to reference idP clientID ClientIDClaim string `mapstructure:"client_id_claim" json:"client_id_claim" default:"azp"` - // Deprecated: Use GroupClain instead + // Deprecated: Use GroupsClaim instead RoleClaim string `mapstructure:"claim" json:"claim" default:"realm_access.roles"` // Deprecated: Use Casbin grouping statements g, , RoleMap map[string]string `mapstructure:"map" json:"map"` @@ -49,6 +67,11 @@ type PolicyConfig struct { Adapter persist.Adapter `mapstructure:"-" json:"-"` } +type RolesProviderConfig struct { + Name string `mapstructure:"name" json:"name"` + Config map[string]any `mapstructure:"config" json:"config"` +} + func (c AuthNConfig) validateAuthNConfig(logger *logger.Logger) error { if c.Issuer == "" { return errors.New("config Auth.Issuer is required") diff --git a/service/internal/auth/discovery.go b/service/internal/auth/discovery.go index a397e5303c..b314fe24c5 100644 --- a/service/internal/auth/discovery.go +++ b/service/internal/auth/discovery.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "net/http" + "net/url" "github.com/opentdf/platform/service/logger" ) @@ -32,8 +33,11 @@ type OIDCConfiguration struct { // DiscoverOPENIDConfiguration discovers the openid configuration for the issuer provided func DiscoverOIDCConfiguration(ctx context.Context, issuer string, logger *logger.Logger) (*OIDCConfiguration, error) { logger.DebugContext(ctx, "discovering openid configuration", slog.String("issuer", issuer)) - url := fmt.Sprintf("%s%s", issuer, DiscoveryPath) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + discoveryURL, err := url.JoinPath(issuer, DiscoveryPath) + if err != nil { + return nil, fmt.Errorf("invalid issuer URL %q: %w", issuer, err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, discoveryURL, nil) if err != nil { return nil, err } @@ -44,7 +48,7 @@ func DiscoverOIDCConfiguration(ctx context.Context, issuer string, logger *logge } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to discover idp at %s: %s", url, resp.Status) + return nil, fmt.Errorf("failed to discover idp at %s: %s", discoveryURL, resp.Status) } defer resp.Body.Close() diff --git a/service/internal/auth/dotnotation_test.go b/service/internal/auth/dotnotation_test.go deleted file mode 100644 index a40ca21eb0..0000000000 --- a/service/internal/auth/dotnotation_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package auth - -import ( - "testing" -) - -func TestDotNotation(t *testing.T) { - tests := []struct { - name string - input map[string]any - key string - expected any - }{ - {name: "valid key", input: map[string]any{"a": map[string]any{"b": 1}}, key: "a.b", expected: 1}, - {name: "non-existent key", input: map[string]any{"a": map[string]any{"b": 1}}, key: "a.c", expected: nil}, - {name: "nested map", input: map[string]any{"a": map[string]any{"b": map[string]any{"c": 2}}}, key: "a.b.c", expected: 2}, - {name: "invalid key type", input: map[string]any{"a": 1}, key: "a.b", expected: nil}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := dotNotation(tt.input, tt.key) - if result != tt.expected { - t.Errorf("expected %v, got %v", tt.expected, result) - } - }) - } -} diff --git a/service/internal/auth/interceptor_authz_test.go b/service/internal/auth/interceptor_authz_test.go new file mode 100644 index 0000000000..3fc62f31ec --- /dev/null +++ b/service/internal/auth/interceptor_authz_test.go @@ -0,0 +1,721 @@ +package auth + +import ( + "context" + "strings" + "testing" + + "connectrpc.com/connect" + "github.com/creasty/defaults" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/service/internal/auth/authz" + _ "github.com/opentdf/platform/service/internal/auth/authz/casbin" // Register casbin authorizer + "github.com/opentdf/platform/service/logger" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" +) + +// InterceptorAuthzSuite tests the authorization flow through the interceptor +// with both Casbin v1 and v2 modes. +// These tests verify the core authorization decisions that the gRPC/HTTP +// interceptors rely on for permit/deny decisions. +type InterceptorAuthzSuite struct { + suite.Suite + logger *logger.Logger +} + +func TestInterceptorAuthzSuite(t *testing.T) { + suite.Run(t, new(InterceptorAuthzSuite)) +} + +func (s *InterceptorAuthzSuite) SetupTest() { + s.logger = logger.CreateTestLogger() +} + +// ============================================================================= +// V1 Mode Tests - Path-based authorization (used by ConnectUnaryServerInterceptor) +// ============================================================================= + +func (s *InterceptorAuthzSuite) TestV1_AdminCanAccessAll() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + authorizer := s.createV1Authorizer(policyCfg) + token := s.newTokenWithRoles("opentdf-admin") + + tests := []struct { + name string + rpc string + action string + expected bool + }{ + {"admin read policy", "/policy.attributes.AttributesService/GetAttribute", ActionRead, true}, + {"admin write policy", "/policy.attributes.AttributesService/CreateAttribute", ActionWrite, true}, + {"admin delete policy", "/policy.attributes.AttributesService/DeleteAttribute", ActionDelete, true}, + {"admin read kas", "/kas.AccessService/Rewrap", ActionRead, true}, + {"admin non-existent", "/non.existent.Service/Method", ActionRead, true}, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + req := &authz.Request{ + Token: token, + RPC: tc.rpc, + Action: tc.action, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.Equal(tc.expected, decision.Allowed, "expected allowed=%v for %s", tc.expected, tc.name) + s.Equal(authz.ModeV1, decision.Mode) + }) + } +} + +func (s *InterceptorAuthzSuite) TestV1_StandardUserPermissions() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + authorizer := s.createV1Authorizer(policyCfg) + token := s.newTokenWithRoles("opentdf-standard") + + tests := []struct { + name string + rpc string + action string + expected bool + }{ + // Standard user can read policy resources + {"standard read policy", "/policy.attributes.AttributesService/GetAttribute", ActionRead, true}, + {"standard list policy", "/policy.attributes.AttributesService/ListAttributes", ActionRead, true}, + // Standard user cannot write to policy resources + {"standard write policy denied", "/policy.attributes.AttributesService/CreateAttribute", ActionWrite, false}, + {"standard delete policy denied", "/policy.attributes.AttributesService/DeleteAttribute", ActionDelete, false}, + // Standard user can access KAS rewrap (HTTP path) + {"standard kas rewrap http", "/kas/v2/rewrap", ActionWrite, true}, + // Standard user cannot access non-existent resources + {"standard non-existent denied", "/non.existent.Service/Method", ActionRead, false}, + // Standard user can access authorization service + {"standard authz decisions", "/authorization.AuthorizationService/GetDecisions", ActionRead, true}, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + req := &authz.Request{ + Token: token, + RPC: tc.rpc, + Action: tc.action, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.Equal(tc.expected, decision.Allowed, "expected allowed=%v for %s", tc.expected, tc.name) + s.Equal(authz.ModeV1, decision.Mode) + }) + } +} + +func (s *InterceptorAuthzSuite) TestV1_UnknownRoleDenied() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + authorizer := s.createV1Authorizer(policyCfg) + token := s.newTokenWithRoles("unknown-role") + + // Note: KAS rewrap is NOT in this list because the default v1 policy + // explicitly allows unknown roles to access it (it's a public route for ERS). + // The policy has: "p, role:unknown, kas.AccessService/Rewrap, *, allow" + tests := []struct { + name string + rpc string + }{ + {"policy read", "/policy.attributes.AttributesService/GetAttribute"}, + {"policy write", "/policy.attributes.AttributesService/CreateAttribute"}, + {"non-existent", "/some.Service/Method"}, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + req := &authz.Request{ + Token: token, + RPC: tc.rpc, + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.False(decision.Allowed, "unknown role should be denied for %s", tc.rpc) + s.Equal(authz.ModeV1, decision.Mode) + }) + } +} + +func (s *InterceptorAuthzSuite) TestV1_UnknownRolePublicRoutes() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + authorizer := s.createV1Authorizer(policyCfg) + token := s.newTokenWithRoles("unknown-role") + + // The default v1 policy explicitly allows unknown roles to access certain + // public routes, primarily for ERS (Entity Resolution Service) functionality. + // This tests that behavior is maintained. + tests := []struct { + name string + rpc string + }{ + {"kas rewrap gRPC", "/kas.AccessService/Rewrap"}, + {"kas rewrap HTTP", "/kas/v2/rewrap"}, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + req := &authz.Request{ + Token: token, + RPC: tc.rpc, + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.True(decision.Allowed, "unknown role should be ALLOWED for public route %s", tc.rpc) + s.Equal(authz.ModeV1, decision.Mode) + }) + } +} + +func (s *InterceptorAuthzSuite) TestV1_CustomRoleMapping() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + // Map external roles to internal roles + policyCfg.RoleMap = map[string]string{ + "admin": "external-admin", + "standard": "external-standard", + } + + authorizer := s.createV1Authorizer(policyCfg) + + // Token with mapped admin role + adminToken := s.newTokenWithRoles("external-admin") + req := &authz.Request{ + Token: adminToken, + RPC: "/policy.attributes.AttributesService/CreateAttribute", + Action: ActionWrite, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.True(decision.Allowed, "mapped admin role should be allowed") + + // Token with mapped standard role + standardToken := s.newTokenWithRoles("external-standard") + req = &authz.Request{ + Token: standardToken, + RPC: "/policy.attributes.AttributesService/CreateAttribute", + Action: ActionWrite, + } + decision, err = authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.False(decision.Allowed, "mapped standard role should be denied for write") +} + +func (s *InterceptorAuthzSuite) TestV1_ExtendedPolicy() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + // Extend the default policy with a new rule + policyCfg.Extension = strings.Join([]string{ + "p, role:custom-role, custom.service.*, read, allow", + "g, custom-user, role:custom-role", + }, "\n") + + authorizer := s.createV1Authorizer(policyCfg) + token := s.newTokenWithRoles("custom-user") + + // Custom role can access custom service + req := &authz.Request{ + Token: token, + RPC: "/custom.service.CustomService/GetCustom", + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.True(decision.Allowed, "custom role should be allowed for custom service") + + // Custom role cannot access other services + req = &authz.Request{ + Token: token, + RPC: "/policy.attributes.AttributesService/GetAttribute", + Action: ActionRead, + } + decision, err = authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.False(decision.Allowed, "custom role should be denied for policy service") +} + +// ============================================================================= +// V2 Mode Tests - RPC + Dimensions authorization +// ============================================================================= + +func (s *InterceptorAuthzSuite) TestV2_AdminWildcardAccess() { + csvPolicy := "p, role:admin, *, *, allow" + authorizer := s.createV2Authorizer(csvPolicy) + token := s.newTokenWithRoles("admin") + + tests := []struct { + name string + rpc string + }{ + {"policy service", "/policy.attributes.AttributesService/GetAttribute"}, + {"kas service", "/kas.AccessService/Rewrap"}, + {"any service", "/any.Service/AnyMethod"}, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + req := &authz.Request{ + Token: token, + RPC: tc.rpc, + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.True(decision.Allowed, "admin should have wildcard access to %s", tc.rpc) + s.Equal(authz.ModeV2, decision.Mode) + }) + } +} + +func (s *InterceptorAuthzSuite) TestV2_ServiceScopedAccess() { + csvPolicy := `p, role:policy-reader, /policy.*, *, allow +p, role:kas-user, /kas.*, *, allow` + + authorizer := s.createV2Authorizer(csvPolicy) + + // Policy reader token + policyToken := s.newTokenWithRoles("policy-reader") + policyReq := &authz.Request{ + Token: policyToken, + RPC: "/policy.attributes.AttributesService/GetAttribute", + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), policyReq) + + s.Require().NoError(err) + s.True(decision.Allowed, "policy-reader should access policy service") + + // Policy reader cannot access KAS + kasReq := &authz.Request{ + Token: policyToken, + RPC: "/kas.AccessService/Rewrap", + Action: ActionRead, + } + decision, err = authorizer.Authorize(context.Background(), kasReq) + + s.Require().NoError(err) + s.False(decision.Allowed, "policy-reader should not access kas service") + + // KAS user token + kasToken := s.newTokenWithRoles("kas-user") + kasReq = &authz.Request{ + Token: kasToken, + RPC: "/kas.AccessService/Rewrap", + Action: ActionRead, + } + decision, err = authorizer.Authorize(context.Background(), kasReq) + + s.Require().NoError(err) + s.True(decision.Allowed, "kas-user should access kas service") +} + +func (s *InterceptorAuthzSuite) TestV2_UnknownRoleDenied() { + csvPolicy := `p, role:known-role, /some.Service/*, *, allow` + authorizer := s.createV2Authorizer(csvPolicy) + + // Token with unknown role + token := s.newTokenWithRoles("unknown-role") + req := &authz.Request{ + Token: token, + RPC: "/some.Service/Method", + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.Require().NotNil(decision) + s.False(decision.Allowed, "unknown role should be denied") + s.Equal(authz.ModeV2, decision.Mode) +} + +func (s *InterceptorAuthzSuite) TestV2_MultipleRoles() { + // Policy where different roles have access to different services + csvPolicy := `p, role:role-a, /service.A/*, *, allow +p, role:role-b, /service.B/*, *, allow` + + authorizer := s.createV2Authorizer(csvPolicy) + + // Token with multiple roles + token := s.newTokenWithRoles("role-a", "role-b") + + // Should access service A + reqA := &authz.Request{ + Token: token, + RPC: "/service.A/Method", + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), reqA) + s.Require().NoError(err) + s.True(decision.Allowed, "token with role-a should access service A") + + // Should access service B + reqB := &authz.Request{ + Token: token, + RPC: "/service.B/Method", + Action: ActionRead, + } + decision, err = authorizer.Authorize(context.Background(), reqB) + s.Require().NoError(err) + s.True(decision.Allowed, "token with role-b should access service B") + + // Should not access service C + reqC := &authz.Request{ + Token: token, + RPC: "/service.C/Method", + Action: ActionRead, + } + decision, err = authorizer.Authorize(context.Background(), reqC) + s.Require().NoError(err) + s.False(decision.Allowed, "token should not access service C") +} + +func (s *InterceptorAuthzSuite) TestV2_ResourceContextDimensions() { + // Policy with dimension constraints + csvPolicy := `p, role:hr-admin, /policy.attributes.AttributesService/*, namespace=hr, allow +p, role:finance-admin, /policy.attributes.AttributesService/*, namespace=finance, allow` + + authorizer := s.createV2Authorizer(csvPolicy) + + // HR admin with HR namespace dimension + hrToken := s.newTokenWithRoles("hr-admin") + hrResource := authz.ResolverResource(map[string]string{"namespace": "hr"}) + hrReq := &authz.Request{ + Token: hrToken, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: ActionWrite, + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{&hrResource}, + }, + } + decision, err := authorizer.Authorize(context.Background(), hrReq) + + s.Require().NoError(err) + s.True(decision.Allowed, "hr-admin should be allowed with namespace=hr dimension") + + // HR admin with finance namespace dimension should be denied + financeResource := authz.ResolverResource(map[string]string{"namespace": "finance"}) + financeReq := &authz.Request{ + Token: hrToken, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: ActionWrite, + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{&financeResource}, + }, + } + decision, err = authorizer.Authorize(context.Background(), financeReq) + + s.Require().NoError(err) + s.False(decision.Allowed, "hr-admin should be denied for namespace=finance dimension") + + // Finance admin with finance namespace should be allowed + financeToken := s.newTokenWithRoles("finance-admin") + financeReq = &authz.Request{ + Token: financeToken, + RPC: "/policy.attributes.AttributesService/UpdateAttribute", + Action: ActionWrite, + ResourceContext: &authz.ResolverContext{ + Resources: []*authz.ResolverResource{&financeResource}, + }, + } + decision, err = authorizer.Authorize(context.Background(), financeReq) + + s.Require().NoError(err) + s.True(decision.Allowed, "finance-admin should be allowed with namespace=finance dimension") +} + +func (s *InterceptorAuthzSuite) TestAuthorizeV2_InvokesRegisteredResolver() { + csvPolicy := "p, role:hr-admin, /policy.attributes.AttributesService/*, namespace=hr, allow" + registry := authz.NewResolverRegistry() + scopedRegistry := registry.ScopedForService(&grpc.ServiceDesc{ + ServiceName: "policy.attributes.AttributesService", + Methods: []grpc.MethodDesc{ + {MethodName: "UpdateAttribute"}, + }, + }) + + resolverCalled := false + scopedRegistry.MustRegister("UpdateAttribute", func(_ context.Context, _ connect.AnyRequest) (authz.ResolverContext, error) { + resolverCalled = true + resolverCtx := authz.NewResolverContext() + res := resolverCtx.NewResource() + res.AddDimension("namespace", "hr") + resolverCtx.SetResolvedData("attribute", "resolved") + return resolverCtx, nil + }) + + authn := &Authentication{ + logger: s.logger, + authorizer: s.createV2Authorizer(csvPolicy), + authzResolverRegistry: registry, + } + + req := &authzTestRequest{ + Request: connect.NewRequest(&attributes.GetAttributeRequest{}), + procedure: "/policy.attributes.AttributesService/UpdateAttribute", + } + + result := authn.authorize(s.T().Context(), s.logger, s.newTokenWithRoles("hr-admin"), req, ActionWrite) + + s.Require().NoError(result.err) + s.Require().NotNil(result.decision) + s.True(result.decision.Allowed) + s.True(resolverCalled, "registered resolver should be invoked") + s.Require().NotNil(result.resourceContext) + s.Equal("resolved", result.resourceContext.GetResolvedData("attribute")) +} + +func (s *InterceptorAuthzSuite) TestV2_EmptyToken() { + csvPolicy := "p, role:admin, *, *, allow" + authorizer := s.createV2Authorizer(csvPolicy) + + // Empty token (no roles) + token := jwt.New() + req := &authz.Request{ + Token: token, + RPC: "/some.Service/Method", + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.Require().NotNil(decision) + // Should be denied because no matching role (defaults to unknown) + s.False(decision.Allowed, "empty token should be denied") +} + +type authzTestRequest struct { + *connect.Request[attributes.GetAttributeRequest] + procedure string +} + +func (r *authzTestRequest) Spec() connect.Spec { + return connect.Spec{Procedure: r.procedure} +} + +// ============================================================================= +// Action Mapping Tests (used by getAction in the interceptor) +// ============================================================================= + +func (s *InterceptorAuthzSuite) TestGetAction() { + tests := []struct { + method string + expected string + }{ + {"GetAttribute", ActionRead}, + {"ListAttributes", ActionRead}, + {"CreateAttribute", ActionWrite}, + {"UpdateAttribute", ActionWrite}, + {"AssignKeyAccess", ActionWrite}, + {"DeleteAttribute", ActionDelete}, + {"RemoveKeyAccess", ActionDelete}, + {"DeactivateEntity", ActionDelete}, + {"UnsafeOperation", ActionUnsafe}, + {"SomeOtherMethod", ActionOther}, + } + + for _, tc := range tests { + s.Run(tc.method, func() { + action := getAction(tc.method) + s.Equal(tc.expected, action) + }) + } +} + +// ============================================================================= +// Version and Mode Tests +// ============================================================================= + +func (s *InterceptorAuthzSuite) TestV1_ReturnsCorrectMode() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + authorizer := s.createV1Authorizer(policyCfg) + s.Equal("v1", authorizer.Version()) + s.False(authorizer.SupportsResourceAuthorization()) +} + +func (s *InterceptorAuthzSuite) TestV2_ReturnsCorrectMode() { + csvPolicy := "p, role:admin, *, *, allow" + authorizer := s.createV2Authorizer(csvPolicy) + s.Equal("v2", authorizer.Version()) + s.True(authorizer.SupportsResourceAuthorization()) +} + +// ============================================================================= +// Path Handling Tests (v1 strips gRPC leading slash, keeps HTTP leading slash) +// ============================================================================= + +func (s *InterceptorAuthzSuite) TestV1_GRPCPathCompatibility() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + authorizer := s.createV1Authorizer(policyCfg) + adminToken := s.newTokenWithRoles("opentdf-admin") + + // gRPC paths with leading slash (as provided by ConnectRPC) + grpcPaths := []string{ + "/policy.attributes.AttributesService/GetAttribute", + "/kas.AccessService/Rewrap", + "/authorization.AuthorizationService/GetDecisions", + } + + for _, path := range grpcPaths { + s.Run(path, func() { + req := &authz.Request{ + Token: adminToken, + RPC: path, + Action: ActionRead, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.True(decision.Allowed, "admin should access gRPC path: %s", path) + s.Equal(authz.ModeV1, decision.Mode) + }) + } +} + +func (s *InterceptorAuthzSuite) TestV1_HTTPPathCompatibility() { + policyCfg := PolicyConfig{} + err := defaults.Set(&policyCfg) + s.Require().NoError(err) + + authorizer := s.createV1Authorizer(policyCfg) + standardToken := s.newTokenWithRoles("opentdf-standard") + + // HTTP paths with leading slash + httpPaths := []string{ + "/kas/v2/rewrap", + } + + for _, path := range httpPaths { + s.Run(path, func() { + req := &authz.Request{ + Token: standardToken, + RPC: path, + Action: ActionWrite, + } + decision, err := authorizer.Authorize(context.Background(), req) + + s.Require().NoError(err) + s.True(decision.Allowed, "standard should access HTTP path: %s", path) + s.Equal(authz.ModeV1, decision.Mode) + }) + } +} + +// ============================================================================= +// Helper Methods (must be placed after all exported Test methods per lint rules) +// ============================================================================= + +// newTestToken creates a test JWT token with the given claims +func (s *InterceptorAuthzSuite) newTestToken(claims map[string]interface{}) jwt.Token { + tok := jwt.New() + for k, v := range claims { + err := tok.Set(k, v) + s.Require().NoError(err) + } + return tok +} + +// newTokenWithRoles creates a token with specified roles +func (s *InterceptorAuthzSuite) newTokenWithRoles(roles ...string) jwt.Token { + roleInterfaces := make([]interface{}, len(roles)) + for i, r := range roles { + roleInterfaces[i] = r + } + return s.newTestToken(map[string]interface{}{ + "realm_access": map[string]interface{}{ + "roles": roleInterfaces, + }, + }) +} + +// createV1Authorizer creates a v1 Casbin authorizer using the same path as the interceptor +func (s *InterceptorAuthzSuite) createV1Authorizer(policyCfg PolicyConfig) authz.Authorizer { + // Create the v1 Casbin enforcer (same as authn.go) + enforcer, err := NewCasbinEnforcer(CasbinConfig{PolicyConfig: policyCfg}, s.logger) + s.Require().NoError(err) + + // Create authz config matching authn.go initialization + authzPolicyCfg := authz.PolicyConfig{ + Engine: policyCfg.Engine, + Version: "v1", + UserNameClaim: policyCfg.UserNameClaim, + GroupsClaim: policyCfg.GroupsClaim, + ClientIDClaim: policyCfg.ClientIDClaim, + Csv: policyCfg.Csv, + Extension: policyCfg.Extension, + Model: policyCfg.Model, + RoleMap: policyCfg.RoleMap, + } + authzCfg := authz.Config{ + Engine: "casbin", + Version: "v1", + PolicyConfig: authzPolicyCfg, + Logger: s.logger, + Options: []authz.Option{authz.WithV1Enforcer(enforcer)}, + } + + authorizer, err := authz.New(authzCfg) + s.Require().NoError(err) + return authorizer +} + +// createV2Authorizer creates a v2 Casbin authorizer +func (s *InterceptorAuthzSuite) createV2Authorizer(csvPolicy string) authz.Authorizer { + authzPolicyCfg := authz.PolicyConfig{ + Engine: "casbin", + Version: "v2", + GroupsClaim: "realm_access.roles", + Csv: csvPolicy, + } + authzCfg := authz.Config{ + Engine: "casbin", + Version: "v2", + PolicyConfig: authzPolicyCfg, + Logger: s.logger, + } + + authorizer, err := authz.New(authzCfg) + s.Require().NoError(err) + return authorizer +} diff --git a/service/internal/auth/role_provider.go b/service/internal/auth/role_provider.go new file mode 100644 index 0000000000..c00811b23b --- /dev/null +++ b/service/internal/auth/role_provider.go @@ -0,0 +1,118 @@ +package auth + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/authz" +) + +type jwtClaimsRoleProvider struct { + groupsClaim string + logger *logger.Logger +} + +func newJWTClaimsRoleProvider(groupsClaim string, logger *logger.Logger) authz.RoleProvider { + return &jwtClaimsRoleProvider{ + groupsClaim: groupsClaim, + logger: logger, + } +} + +func (p *jwtClaimsRoleProvider) Roles(_ context.Context, token jwt.Token, _ authz.RoleRequest) ([]string, error) { + p.logger.Debug("extracting roles from token") + if p.groupsClaim == "" { + p.logger.Warn("groups claim not configured") + return nil, nil + } + + selectors := strings.Split(p.groupsClaim, ".") + claim, exists := token.Get(selectors[0]) + if !exists { + p.logger.Warn("claim not found", + slog.String("claim", p.groupsClaim), + slog.Any("claims", claim), + ) + return nil, nil + } + p.logger.Debug("root claim found", + slog.String("claim", p.groupsClaim), + slog.Any("claims", claim), + ) + + if len(selectors) > 1 { + claimMap, ok := claim.(map[string]interface{}) + if !ok { + p.logger.Warn("claim is not of type map[string]interface{}", + slog.String("claim", p.groupsClaim), + slog.Any("claims", claim), + ) + return nil, nil + } + claim = dotNotation(claimMap, strings.Join(selectors[1:], ".")) + if claim == nil { + p.logger.Warn("claim not found", + slog.String("claim", p.groupsClaim), + slog.Any("claims", claim), + ) + return nil, nil + } + } + + roles := []string{} + switch v := claim.(type) { + case string: + roles = append(roles, v) + case []interface{}: + for _, rr := range v { + if r, ok := rr.(string); ok { + roles = append(roles, r) + } + } + default: + p.logger.Warn("could not get claim type", + slog.String("selector", p.groupsClaim), + slog.Any("claims", claim), + ) + return nil, nil + } + + return roles, nil +} + +func resolveRoleProvider(ctx context.Context, cfg Config, logger *logger.Logger) (authz.RoleProvider, error) { + if cfg.Policy.RolesProvider.Name != "" { + if cfg.RoleProvider != nil && cfg.RoleProviderFactories != nil { + logger.Warn( + "role provider configured in start options is ignored because roles_provider is set", + slog.String("roles_provider", cfg.Policy.RolesProvider.Name), + ) + } + if cfg.RoleProviderFactories == nil { + return nil, fmt.Errorf("no role provider factories are registered, cannot create provider %q", cfg.Policy.RolesProvider.Name) + } + factory, ok := cfg.RoleProviderFactories[cfg.Policy.RolesProvider.Name] + if !ok { + return nil, fmt.Errorf("role provider factory not registered: %s", cfg.Policy.RolesProvider.Name) + } + providerCfg := authz.ProviderConfig{ + Config: cfg.Policy.RolesProvider.Config, + UsernameClaim: cfg.Policy.UserNameClaim, + GroupsClaim: cfg.Policy.GroupsClaim, + ClientIDClaim: cfg.Policy.ClientIDClaim, + } + provider, err := factory(ctx, providerCfg) + if err != nil { + return nil, fmt.Errorf("role provider factory failed: %w", err) + } + return provider, nil + } + if cfg.RoleProvider != nil { + return cfg.RoleProvider, nil + } + return newJWTClaimsRoleProvider(cfg.Policy.GroupsClaim, logger), nil +} diff --git a/service/internal/auth/role_provider_test.go b/service/internal/auth/role_provider_test.go new file mode 100644 index 0000000000..7c0114f7d3 --- /dev/null +++ b/service/internal/auth/role_provider_test.go @@ -0,0 +1,56 @@ +package auth + +import ( + "context" + "testing" + + "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/authz" + "github.com/stretchr/testify/require" +) + +func TestResolveRoleProviderDefault(t *testing.T) { + logger := logger.CreateTestLogger() + cfg := Config{} + provider, err := resolveRoleProvider(context.Background(), cfg, logger) + require.NoError(t, err) + require.NotNil(t, provider) + require.IsType(t, &jwtClaimsRoleProvider{}, provider) +} + +func TestResolveRoleProviderNamed(t *testing.T) { + logger := logger.CreateTestLogger() + cfg := Config{ + AuthNConfig: AuthNConfig{ + Policy: PolicyConfig{ + RolesProvider: RolesProviderConfig{ + Name: "mock", + }, + }, + }, + RoleProviderFactories: map[string]authz.RoleProviderFactory{ + "mock": func(_ context.Context, _ authz.ProviderConfig) (authz.RoleProvider, error) { + return staticProvider{roles: []string{"role:admin"}}, nil + }, + }, + } + provider, err := resolveRoleProvider(context.Background(), cfg, logger) + require.NoError(t, err) + require.NotNil(t, provider) +} + +func TestResolveRoleProviderMissingName(t *testing.T) { + logger := logger.CreateTestLogger() + cfg := Config{ + AuthNConfig: AuthNConfig{ + Policy: PolicyConfig{ + RolesProvider: RolesProviderConfig{ + Name: "missing", + }, + }, + }, + } + provider, err := resolveRoleProvider(context.Background(), cfg, logger) + require.Error(t, err) + require.Nil(t, provider) +} diff --git a/service/internal/auth/token_verifier.go b/service/internal/auth/token_verifier.go new file mode 100644 index 0000000000..918e4a1ad4 --- /dev/null +++ b/service/internal/auth/token_verifier.go @@ -0,0 +1,105 @@ +package auth + +import ( + "context" + "errors" + "log/slog" + "time" + + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" + + "github.com/opentdf/platform/service/logger" +) + +var errNilTokenVerifier = errors.New("access token verifier is not configured") + +// AccessTokenVerifier validates raw access tokens. +type AccessTokenVerifier interface { + VerifyAccessToken(ctx context.Context, tokenRaw string) (jwt.Token, error) +} + +// TokenVerifier validates access tokens against the platform's configured IdP. +type TokenVerifier struct { + cachedKeySet jwk.Set + oidcConfiguration AuthNConfig + log *logger.Logger +} + +func newTokenVerifier(ctx context.Context, cfg AuthNConfig, log *logger.Logger) (*TokenVerifier, *OIDCConfiguration, error) { + if err := cfg.validateAuthNConfig(log); err != nil { + return nil, nil, err + } + + cache := jwk.NewCache(ctx) + + oidcConfig, err := DiscoverOIDCConfiguration(ctx, cfg.Issuer, log) + if err != nil { + return nil, nil, err + } + + if oidcConfig.Issuer != cfg.Issuer { + cfg.Issuer = oidcConfig.Issuer + } + + cacheInterval, err := time.ParseDuration(cfg.CacheRefresh) + if err != nil { + log.ErrorContext(ctx, + "invalid cache_refresh_interval", + slog.String("cache_refresh_interval", cfg.CacheRefresh), + slog.Any("err", err), + ) + cacheInterval = refreshInterval + } + + if err := cache.Register(oidcConfig.JwksURI, jwk.WithMinRefreshInterval(cacheInterval)); err != nil { + return nil, nil, err + } + + if _, err := cache.Refresh(ctx, oidcConfig.JwksURI); err != nil { + return nil, nil, err + } + + return &TokenVerifier{ + cachedKeySet: jwk.NewCachedSet(cache, oidcConfig.JwksURI), + oidcConfiguration: cfg, + log: log, + }, oidcConfig, nil +} + +// NewTokenVerifier creates a reusable verifier backed by the IdP JWKS endpoint. +func NewTokenVerifier(ctx context.Context, cfg AuthNConfig, log *logger.Logger) (*TokenVerifier, error) { + verifier, _, err := newTokenVerifier(ctx, cfg, log) + return verifier, err +} + +// AccessTokenVerifier returns the authenticator's shared access-token verifier. +func (a *Authentication) AccessTokenVerifier() AccessTokenVerifier { + if a == nil || a.tokenVerifier == nil { + return nil + } + + return a.tokenVerifier +} + +// VerifyAccessToken validates the provided raw JWT and returns the parsed token on success. +func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, tokenRaw string) (jwt.Token, error) { + if v == nil { + return nil, errNilTokenVerifier + } + + token, err := jwt.Parse([]byte(tokenRaw), + jwt.WithKeySet(v.cachedKeySet, jws.WithInferAlgorithmFromKey(true)), + jwt.WithValidate(true), + jwt.WithIssuer(v.oidcConfiguration.Issuer), + jwt.WithAudience(v.oidcConfiguration.Audience), + jwt.WithAcceptableSkew(v.oidcConfiguration.TokenSkew), + ) + if err != nil { + v.log.WarnContext(ctx, "failed to validate auth token", slog.Any("err", err)) + return nil, err + } + + return token, nil +} diff --git a/service/internal/auth/token_verifier_test.go b/service/internal/auth/token_verifier_test.go new file mode 100644 index 0000000000..3bc66388d2 --- /dev/null +++ b/service/internal/auth/token_verifier_test.go @@ -0,0 +1,200 @@ +package auth + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/opentdf/platform/service/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tokenVerifierFixture struct { + server *httptest.Server + privateKey *rsa.PrivateKey + keyID string +} + +func newTokenVerifierFixture(t *testing.T) *tokenVerifierFixture { + privateKey, publicKeyJWK := newTokenVerifierKeyPair(t) + require.NoError(t, publicKeyJWK.Set(jwk.AlgorithmKey, jwa.RS256)) + + return newTokenVerifierFixtureWithPublicKey(t, privateKey, publicKeyJWK) +} + +func newTokenVerifierFixtureWithoutPublicKeyAlgorithm(t *testing.T) *tokenVerifierFixture { + privateKey, publicKeyJWK := newTokenVerifierKeyPair(t) + require.NoError(t, publicKeyJWK.Remove(jwk.AlgorithmKey)) + _, hasAlgorithm := publicKeyJWK.Get(jwk.AlgorithmKey) + require.False(t, hasAlgorithm) + + return newTokenVerifierFixtureWithPublicKey(t, privateKey, publicKeyJWK) +} + +func newTokenVerifierKeyPair(t *testing.T) (*rsa.PrivateKey, jwk.Key) { + t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + publicKeyJWK, err := jwk.FromRaw(privateKey.PublicKey) + require.NoError(t, err) + require.NoError(t, publicKeyJWK.Set(jws.KeyIDKey, "test-key")) + + return privateKey, publicKeyJWK +} + +func newTokenVerifierFixtureWithPublicKey(t *testing.T, privateKey *rsa.PrivateKey, publicKeyJWK jwk.Key) *tokenVerifierFixture { + t.Helper() + + keySet := jwk.NewSet() + require.NoError(t, keySet.AddKey(publicKeyJWK)) + + fixture := &tokenVerifierFixture{ + privateKey: privateKey, + keyID: "test-key", + } + + fixture.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case DiscoveryPath, "/alias" + DiscoveryPath: + if err := json.NewEncoder(w).Encode(map[string]string{ + "issuer": fixture.server.URL, + "jwks_uri": fixture.server.URL + "/jwks", + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + case "/jwks": + if err := json.NewEncoder(w).Encode(keySet); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + default: + http.NotFound(w, r) + } + })) + + t.Cleanup(fixture.server.Close) + return fixture +} + +func (f *tokenVerifierFixture) signToken(t *testing.T, issuer, audience string, signer *rsa.PrivateKey) string { + t.Helper() + + token := jwt.New() + now := time.Now() + + require.NoError(t, token.Set(jwt.SubjectKey, "user-123")) + require.NoError(t, token.Set(jwt.IssuedAtKey, now)) + require.NoError(t, token.Set(jwt.ExpirationKey, now.Add(time.Hour))) + require.NoError(t, token.Set(jwt.IssuerKey, issuer)) + require.NoError(t, token.Set(jwt.AudienceKey, audience)) + + key, err := jwk.FromRaw(signer) + require.NoError(t, err) + + keyID := f.keyID + if signer != f.privateKey { + keyID = "other-key" + } + + require.NoError(t, key.Set(jws.KeyIDKey, keyID)) + require.NoError(t, key.Set(jwk.AlgorithmKey, jwa.RS256)) + + signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, key)) + require.NoError(t, err) + + return string(signedToken) +} + +func TestNewTokenVerifier_UsesDiscoveredIssuer(t *testing.T) { + fixture := newTokenVerifierFixture(t) + + verifier, err := NewTokenVerifier(t.Context(), AuthNConfig{ + Issuer: fixture.server.URL + "/alias", + Audience: "test-audience", + CacheRefresh: "15m", + TokenSkew: time.Minute, + }, logger.CreateTestLogger()) + require.NoError(t, err) + + assert.Equal(t, fixture.server.URL, verifier.oidcConfiguration.Issuer) + + token := fixture.signToken(t, fixture.server.URL, "test-audience", fixture.privateKey) + verifiedToken, err := verifier.VerifyAccessToken(t.Context(), token) + require.NoError(t, err) + assert.Equal(t, "user-123", verifiedToken.Subject()) +} + +func TestTokenVerifier_VerifyAccessToken(t *testing.T) { + fixture := newTokenVerifierFixture(t) + + verifier, err := NewTokenVerifier(t.Context(), AuthNConfig{ + Issuer: fixture.server.URL, + Audience: "test-audience", + CacheRefresh: "15m", + TokenSkew: time.Minute, + }, logger.CreateTestLogger()) + require.NoError(t, err) + + t.Run("valid token", func(t *testing.T) { + token := fixture.signToken(t, fixture.server.URL, "test-audience", fixture.privateKey) + + verifiedToken, err := verifier.VerifyAccessToken(t.Context(), token) + require.NoError(t, err) + assert.Equal(t, "user-123", verifiedToken.Subject()) + }) + + t.Run("invalid audience", func(t *testing.T) { + token := fixture.signToken(t, fixture.server.URL, "wrong-audience", fixture.privateKey) + + _, err := verifier.VerifyAccessToken(t.Context(), token) + require.Error(t, err) + assert.ErrorContains(t, err, "\"aud\"") + }) + + t.Run("invalid signature", func(t *testing.T) { + otherKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + token := fixture.signToken(t, fixture.server.URL, "test-audience", otherKey) + + _, err = verifier.VerifyAccessToken(t.Context(), token) + require.Error(t, err) + }) + + t.Run("valid token with JWKS key missing alg", func(t *testing.T) { + missingAlgFixture := newTokenVerifierFixtureWithoutPublicKeyAlgorithm(t) + + missingAlgVerifier, err := NewTokenVerifier(t.Context(), AuthNConfig{ + Issuer: missingAlgFixture.server.URL, + Audience: "test-audience", + CacheRefresh: "15m", + TokenSkew: time.Minute, + }, logger.CreateTestLogger()) + require.NoError(t, err) + + token := missingAlgFixture.signToken(t, missingAlgFixture.server.URL, "test-audience", missingAlgFixture.privateKey) + + verifiedToken, err := missingAlgVerifier.VerifyAccessToken(t.Context(), token) + require.NoError(t, err) + assert.Equal(t, "user-123", verifiedToken.Subject()) + }) +} + +func TestTokenVerifier_NilHandling(t *testing.T) { + authn := &Authentication{} + assert.Nil(t, authn.AccessTokenVerifier()) + + var verifier *TokenVerifier + _, err := verifier.VerifyAccessToken(t.Context(), "token") + require.ErrorIs(t, err, errNilTokenVerifier) +} diff --git a/service/internal/fixtures/fixtures.go b/service/internal/fixtures/fixtures.go index 0a01710aee..21908f12e7 100644 --- a/service/internal/fixtures/fixtures.go +++ b/service/internal/fixtures/fixtures.go @@ -790,7 +790,7 @@ func (f *Fixtures) provision(ctx context.Context, t string, c []string, v [][]an // Migration adds standard actions [create, read, update, delete] to the database func (f *Fixtures) loadMigratedStandardActions(ctx context.Context) { actions := make(map[string]string) - rows, err := f.db.Client.Query(ctx, "SELECT id, name FROM actions WHERE is_standard = TRUE", nil) + rows, err := f.db.Client.Query(ctx, "SELECT id, name FROM actions WHERE is_standard = TRUE AND namespace_id IS NULL", nil) if err != nil { slog.Error("could not get standard actions", slog.Any("error", err)) panic("could not get standard actions") diff --git a/service/internal/security/basic_manager.go b/service/internal/security/basic_manager.go index 29a93ae6e2..a33ce486f7 100644 --- a/service/internal/security/basic_manager.go +++ b/service/internal/security/basic_manager.go @@ -26,6 +26,20 @@ const ( ristrettoCacheTTL = 30 ) +// BasicManagerSupportedAlgorithms is the canonical set of algorithms the +// BasicManager knows how to serve when a key has been provisioned. Keep in +// sync with the switch in Decrypt. +var BasicManagerSupportedAlgorithms = []ocrypto.KeyType{ + ocrypto.RSA2048Key, + ocrypto.RSA4096Key, + ocrypto.EC256Key, + ocrypto.EC384Key, + ocrypto.EC521Key, + ocrypto.HybridXWingKey, + ocrypto.HybridSecp256r1MLKEM768Key, + ocrypto.HybridSecp384r1MLKEM1024Key, +} + type BasicManager struct { l *logger.Logger rootKey []byte @@ -97,6 +111,57 @@ func (b *BasicManager) Decrypt(ctx context.Context, keyDetails trust.KeyDetails, return nil, fmt.Errorf("failed to create protected key: %w", err) } return protectedKey, nil + case ocrypto.HybridXWingKey: + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for X-Wing decryption") + } + xwingPrivKey, err := ocrypto.XWingPrivateKeyFromPem(privKey) + if err != nil { + return nil, fmt.Errorf("failed to create X-Wing private key from PEM: %w", err) + } + plaintext, err := ocrypto.XWingUnwrapDEK(xwingPrivKey, ciphertext) + if err != nil { + return nil, fmt.Errorf("failed to decrypt with X-Wing: %w", err) + } + protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) + if err != nil { + return nil, fmt.Errorf("failed to create protected key: %w", err) + } + return protectedKey, nil + case ocrypto.HybridSecp256r1MLKEM768Key: + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for hybrid decryption") + } + privKeyBytes, err := ocrypto.P256MLKEM768PrivateKeyFromPem(privKey) + if err != nil { + return nil, fmt.Errorf("failed to parse P256-MLKEM768 private key from PEM: %w", err) + } + plaintext, err := ocrypto.P256MLKEM768UnwrapDEK(privKeyBytes, ciphertext) + if err != nil { + return nil, fmt.Errorf("failed to decrypt with P256-MLKEM768: %w", err) + } + protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) + if err != nil { + return nil, fmt.Errorf("failed to create protected key: %w", err) + } + return protectedKey, nil + case ocrypto.HybridSecp384r1MLKEM1024Key: + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for hybrid decryption") + } + privKeyBytes, err := ocrypto.P384MLKEM1024PrivateKeyFromPem(privKey) + if err != nil { + return nil, fmt.Errorf("failed to parse P384-MLKEM1024 private key from PEM: %w", err) + } + plaintext, err := ocrypto.P384MLKEM1024UnwrapDEK(privKeyBytes, ciphertext) + if err != nil { + return nil, fmt.Errorf("failed to decrypt with P384-MLKEM1024: %w", err) + } + protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) + if err != nil { + return nil, fmt.Errorf("failed to create protected key: %w", err) + } + return protectedKey, nil } return nil, fmt.Errorf("unsupported algorithm: %s", keyDetails.Algorithm()) @@ -134,7 +199,7 @@ func (b *BasicManager) DeriveKey(ctx context.Context, keyDetails trust.KeyDetail return nil, fmt.Errorf("failed to compute ECDH key: %w", err) } - key, err := ocrypto.CalculateHKDF(NanoVersionSalt(), symmetricKey) + key, err := ocrypto.CalculateHKDF(TDFSalt(), symmetricKey) if err != nil { return nil, fmt.Errorf("failed to calculate HKDF: %w", err) } @@ -151,6 +216,7 @@ type OCEncapsulator struct { func (e *OCEncapsulator) Encapsulate(dek ocrypto.ProtectedKey) ([]byte, error) { // Delegate to the ProtectedKey to avoid exposing raw key material + //nolint:staticcheck // Export remains required until ProtectedKey deprecation is removed upstream. return dek.Export(e) } @@ -159,7 +225,7 @@ func (e *OCEncapsulator) PublicKeyAsPEM() (string, error) { } func (b *BasicManager) GenerateECSessionKey(_ context.Context, ephemeralPublicKey string) (ocrypto.Encapsulator, error) { - pke, err := ocrypto.FromPublicPEMWithSalt(ephemeralPublicKey, NanoVersionSalt(), nil) + pke, err := ocrypto.FromPublicPEMWithSalt(ephemeralPublicKey, TDFSalt(), nil) if err != nil { return nil, fmt.Errorf("failed to create public key encryptor: %w", err) } @@ -182,7 +248,8 @@ func (b *BasicManager) unwrap(ctx context.Context, kid string, wrappedKey string if privKeyBytes, ok := privKey.([]byte); ok { return privKeyBytes, nil } - b.l.ErrorContext(ctx, + b.l.ErrorContext( + ctx, "private key in cache is not of type []byte", slog.String("kid", kid), slog.Any("type", fmt.Sprintf("%T", privKey)), @@ -202,17 +269,24 @@ func (b *BasicManager) unwrap(ctx context.Context, kid string, wrappedKey string gcm, err := ocrypto.NewAESGcm(b.rootKey) if err != nil { + if errors.Is(err, ocrypto.ErrInvalidKeyData) { + return nil, fmt.Errorf("basic key manager is not configured: %w", err) + } return nil, fmt.Errorf("failed to create AES-GCM instance: %w", err) } privKey, err := gcm.Decrypt(wk) if err != nil { + if errors.Is(err, ocrypto.ErrInvalidCiphertext) { + return nil, fmt.Errorf("wrapped key data is corrupted or invalid format: %w", err) + } return nil, fmt.Errorf("failed to decrypt wrapped key: %w", err) } if cacheEnabled { if err := b.cache.Set(ctx, kid, privKey, nil); err != nil { - b.l.ErrorContext(ctx, + b.l.ErrorContext( + ctx, "failed to cache private key", slog.String("kid", kid), slog.Any("error", err), diff --git a/service/internal/security/basic_manager_test.go b/service/internal/security/basic_manager_test.go index d5915d1cdf..ad237c9662 100644 --- a/service/internal/security/basic_manager_test.go +++ b/service/internal/security/basic_manager_test.go @@ -118,6 +118,7 @@ type noOpEncapsulator struct{} func (n *noOpEncapsulator) Encapsulate(pk ocrypto.ProtectedKey) ([]byte, error) { // Delegate to ProtectedKey to avoid accessing raw key directly + //nolint:staticcheck // Export is used in tests until ProtectedKey deprecation is removed upstream. return pk.Export(n) } @@ -156,6 +157,49 @@ func generateECKeyAndPEM(curve ocrypto.ECCMode) (ocrypto.ECKeyPair, error) { return ocrypto.NewECKeyPair(curve) } +func compressEphemeralPublicKey(t *testing.T, der []byte) []byte { + t.Helper() + + pub, err := x509.ParsePKIXPublicKey(der) + require.NoError(t, err) + + switch pub := pub.(type) { + case *ecdsa.PublicKey: + ecdhPub, err := pub.ECDH() + require.NoError(t, err) + return compressUncompressedPoint(t, ecdhPub.Bytes()) + case *ecdh.PublicKey: + return compressUncompressedPoint(t, pub.Bytes()) + default: + t.Fatalf("unsupported public key type: %T", pub) + } + + return nil +} + +func compressUncompressedPoint(t *testing.T, uncompressed []byte) []byte { + t.Helper() + + require.NotEmpty(t, uncompressed, "unexpected uncompressed key format") + require.Equal(t, byte(4), uncompressed[0], "unexpected uncompressed key format") + require.Equal(t, 0, (len(uncompressed)-1)%2, "invalid uncompressed key length") + + coordSize := (len(uncompressed) - 1) / 2 + x := uncompressed[1 : 1+coordSize] + y := uncompressed[1+coordSize:] + require.Len(t, y, coordSize, "invalid coordinate sizes") + + prefix := byte(2) + if y[coordSize-1]&1 == 1 { + prefix = 3 + } + + compressed := make([]byte, 1+coordSize) + compressed[0] = prefix + copy(compressed[1:], x) + return compressed +} + // Helper to create a test cache func newTestCache(t *testing.T, log *logger.Logger) *cache.Cache { t.Helper() @@ -309,7 +353,7 @@ func TestBasicManager_Decrypt(t *testing.T) { mockDetails.On("Algorithm").Return(mockDetails.MAlgorithm) mockDetails.On("ExportPrivateKey").Return(&trust.PrivateKey{WrappingKeyID: trust.KeyIdentifier(mockDetails.MPrivateKey.GetKeyId()), WrappedKey: mockDetails.MPrivateKey.GetWrappedKey()}, nil) - rsaEncryptor, err := ocrypto.NewAsymEncryption(rsaPubKey) + rsaEncryptor, err := ocrypto.FromPublicPEM(rsaPubKey) require.NoError(t, err) ciphertext, err := rsaEncryptor.Encrypt(samplePayload) require.NoError(t, err) @@ -320,6 +364,7 @@ func TestBasicManager_Decrypt(t *testing.T) { // Use noOpEncapsulator to get raw key data for testing noOpEnc := &noOpEncapsulator{} + //nolint:staticcheck // Export is used in tests until ProtectedKey deprecation is removed upstream. decryptedPayload, err := protectedKey.Export(noOpEnc) require.NoError(t, err) assert.Equal(t, samplePayload, decryptedPayload) @@ -348,11 +393,68 @@ func TestBasicManager_Decrypt(t *testing.T) { // Use noOpEncapsulator to get raw key data for testing noOpEnc := &noOpEncapsulator{} + //nolint:staticcheck // Export is used in tests until ProtectedKey deprecation is removed upstream. decryptedPayload, err := protectedKey.Export(noOpEnc) require.NoError(t, err) assert.Equal(t, samplePayload, decryptedPayload) }) + for _, tc := range []struct { + name string + mode ocrypto.ECCMode + algorithm string + kid string + }{ + { + name: "P384", + mode: ocrypto.ECCModeSecp384r1, + algorithm: AlgorithmECP384R1, + kid: "ec-kid-decrypt-p384", + }, + { + name: "P521", + mode: ocrypto.ECCModeSecp521r1, + algorithm: AlgorithmECP521R1, + kid: "ec-kid-decrypt-p521", + }, + } { + t.Run("successful EC decryption with compressed ephemeral key "+tc.name, func(t *testing.T) { + curveKey, err := generateECKeyAndPEM(tc.mode) + require.NoError(t, err) + curvePrivKey, err := curveKey.PrivateKeyInPemFormat() + require.NoError(t, err) + curvePubKey, err := curveKey.PublicKeyInPemFormat() + require.NoError(t, err) + + wrappedCurvePrivKeyStr, err := wrapKeyWithAESGCM([]byte(curvePrivKey), rootKey) + require.NoError(t, err) + + mockDetails := new(MockKeyDetails) + mockDetails.MID = tc.kid + mockDetails.MAlgorithm = tc.algorithm + mockDetails.MPrivateKey = &policy.PrivateKeyCtx{WrappedKey: wrappedCurvePrivKeyStr} + + mockDetails.On("ID").Return(trust.KeyIdentifier(mockDetails.MID)) + mockDetails.On("Algorithm").Return(mockDetails.MAlgorithm) + mockDetails.On("ExportPrivateKey").Return(&trust.PrivateKey{WrappingKeyID: trust.KeyIdentifier(mockDetails.MPrivateKey.GetKeyId()), WrappedKey: mockDetails.MPrivateKey.GetWrappedKey()}, nil) + + ecEncryptor, err := ocrypto.FromPublicPEM(curvePubKey) + require.NoError(t, err) + ciphertext, err := ecEncryptor.Encrypt(samplePayload) + require.NoError(t, err) + ephemeralPublicKey := compressEphemeralPublicKey(t, ecEncryptor.EphemeralKey()) + + protectedKey, err := bm.Decrypt(t.Context(), mockDetails, ciphertext, ephemeralPublicKey) + require.NoError(t, err) + require.NotNil(t, protectedKey) + + noOpEnc := &noOpEncapsulator{} + decryptedPayload, err := noOpEnc.Encapsulate(protectedKey) + require.NoError(t, err) + assert.Equal(t, samplePayload, decryptedPayload) + }) + } + t.Run("fail ExportPrivateKey", func(t *testing.T) { mockDetails := new(MockKeyDetails) mockDetails.On("ID").Return(trust.KeyIdentifier("fail-export")) @@ -466,11 +568,12 @@ func TestBasicManager_DeriveKey(t *testing.T) { expectedSharedSecret, err := ocrypto.ComputeECDHKeyFromECDHKeys(clientECDHPublicKey, ecdhPrivKey) require.NoError(t, err) - expectedDerivedKey, err := ocrypto.CalculateHKDF(NanoVersionSalt(), expectedSharedSecret) + expectedDerivedKey, err := ocrypto.CalculateHKDF(TDFSalt(), expectedSharedSecret) require.NoError(t, err) // Use noOpEncapsulator to get raw key data for testing noOpEnc := &noOpEncapsulator{} + //nolint:staticcheck // Export is used in tests until ProtectedKey deprecation is removed upstream. actualDerivedKey, err := protectedKey.Export(noOpEnc) require.NoError(t, err) assert.Equal(t, expectedDerivedKey, actualDerivedKey) diff --git a/service/internal/security/crypto_provider.go b/service/internal/security/crypto_provider.go index 24f0f3c2ea..dcbe1ae5a1 100644 --- a/service/internal/security/crypto_provider.go +++ b/service/internal/security/crypto_provider.go @@ -11,4 +11,11 @@ const ( // Used for encryption with RSA of the KAO AlgorithmRSA2048 = "rsa:2048" AlgorithmRSA4096 = "rsa:4096" + + // Used for hybrid X-Wing wrapping of the KAO + AlgorithmHPQTXWing = "hpqt:xwing" + + // Used for hybrid NIST EC + ML-KEM wrapping of the KAO + AlgorithmHPQTSecp256r1MLKEM768 = "hpqt:secp256r1-mlkem768" + AlgorithmHPQTSecp384r1MLKEM1024 = "hpqt:secp384r1-mlkem1024" ) diff --git a/service/internal/security/in_process_provider.go b/service/internal/security/in_process_provider.go index 45a615ad18..7d4b624e22 100644 --- a/service/internal/security/in_process_provider.go +++ b/service/internal/security/in_process_provider.go @@ -4,6 +4,8 @@ import ( "context" "crypto" "crypto/elliptic" + "crypto/x509" + "encoding/pem" "errors" "fmt" "log/slog" @@ -15,6 +17,18 @@ import ( const inProcessSystemName = "opentdf.io/in-process" +// InProcessSupportedAlgorithms is the canonical set of algorithms the +// InProcessProvider knows how to serve when a corresponding key is loaded. +// Keep in sync with the switch in Decrypt. +var InProcessSupportedAlgorithms = []ocrypto.KeyType{ + ocrypto.RSA2048Key, + ocrypto.RSA4096Key, + ocrypto.EC256Key, + ocrypto.HybridXWingKey, + ocrypto.HybridSecp256r1MLKEM768Key, + ocrypto.HybridSecp384r1MLKEM1024Key, +} + func convertPEMToJWK(_ string) (string, error) { // Implement the conversion logic here or use an external library if available. // For now, return a placeholder error to indicate the function is not implemented. @@ -63,7 +77,7 @@ func (k *KeyDetailsAdapter) ExportPublicKey(_ context.Context, format trust.KeyT switch format { case trust.KeyTypeJWK: // For JWK format (currently only supported for RSA) - if k.algorithm == AlgorithmRSA2048 { + if k.algorithm == AlgorithmRSA2048 || k.algorithm == AlgorithmRSA4096 { return k.cryptoProvider.RSAPublicKeyAsJSON(kid) } // For EC keys, we return the public key in PEM format @@ -82,6 +96,12 @@ func (k *KeyDetailsAdapter) ExportPublicKey(_ context.Context, format trust.KeyT if rsaKey, err := k.cryptoProvider.RSAPublicKey(kid); err == nil { return rsaKey, nil } + if hybridKey, err := k.cryptoProvider.HybridPublicKey(kid); err == nil { + return hybridKey, nil + } + if xwingKey, err := k.cryptoProvider.XWingPublicKey(kid); err == nil { + return xwingKey, nil + } return k.cryptoProvider.ECPublicKey(kid) default: return "", ErrCertNotFound @@ -168,31 +188,17 @@ func (a *InProcessProvider) FindKeyByAlgorithm(_ context.Context, algorithm stri // FindKeyByID finds a key by ID func (a *InProcessProvider) FindKeyByID(_ context.Context, id trust.KeyIdentifier) (trust.KeyDetails, error) { - // Try to determine the algorithm by checking if the key works with known algorithms - for _, alg := range []string{AlgorithmECP256R1, AlgorithmRSA2048} { - // This is a hack since the original provider doesn't have a way to check if a key exists - switch alg { - case AlgorithmECP256R1: - if _, err := a.cryptoProvider.ECPublicKey(string(id)); err == nil { - return &KeyDetailsAdapter{ - id: id, - algorithm: ocrypto.KeyType(alg), - legacy: a.legacyKeys[string(id)], - cryptoProvider: a.cryptoProvider, - }, nil - } - case AlgorithmRSA2048: - if _, err := a.cryptoProvider.RSAPublicKey(string(id)); err == nil { - return &KeyDetailsAdapter{ - id: id, - algorithm: ocrypto.KeyType(alg), - legacy: a.legacyKeys[string(id)], - cryptoProvider: a.cryptoProvider, - }, nil - } - } + keyType, err := a.determineKeyType(string(id)) + if err != nil { + return nil, ErrCertNotFound } - return nil, ErrCertNotFound + + return &KeyDetailsAdapter{ + id: id, + algorithm: ocrypto.KeyType(keyType), + legacy: a.legacyKeys[string(id)], + cryptoProvider: a.cryptoProvider, + }, nil } // ListKeys lists all available keys @@ -205,24 +211,25 @@ func (a *InProcessProvider) ListKeysWith(ctx context.Context, opts trust.ListKey var keys []trust.KeyDetails // Try to find keys for known algorithms - for _, alg := range []string{AlgorithmRSA2048, AlgorithmECP256R1} { - if kids, err := a.cryptoProvider.ListKIDsByAlgorithm(alg); err == nil && len(kids) > 0 { + for _, alg := range InProcessSupportedAlgorithms { + if kids, err := a.cryptoProvider.ListKIDsByAlgorithm(string(alg)); err == nil && len(kids) > 0 { for _, kid := range kids { if opts.LegacyOnly && !a.legacyKeys[kid] { continue // Skip non-legacy keys if LegacyOnly is true } keys = append(keys, &KeyDetailsAdapter{ id: trust.KeyIdentifier(kid), - algorithm: ocrypto.KeyType(alg), + algorithm: alg, cryptoProvider: a.cryptoProvider, legacy: a.legacyKeys[kid], }) } } else if err != nil { if a.logger != nil { - a.logger.WarnContext(ctx, + a.logger.WarnContext( + ctx, "failed to list keys by algorithm", - slog.String("algorithm", alg), + slog.Any("algorithm", alg), slog.Any("error", err), ) } @@ -240,7 +247,7 @@ func (a *InProcessProvider) Decrypt(ctx context.Context, keyDetails trust.KeyDet var err error // Try to determine the key type - keyType, err := a.determineKeyType(ctx, kid) + keyType, err := a.determineKeyType(kid) if err != nil { return nil, err } @@ -248,6 +255,8 @@ func (a *InProcessProvider) Decrypt(ctx context.Context, keyDetails trust.KeyDet var rawKey []byte switch keyType { case AlgorithmRSA2048: + fallthrough + case AlgorithmRSA4096: if len(ephemeralPublicKey) > 0 { return nil, errors.New("ephemeral public key should not be provided for RSA decryption") } @@ -259,6 +268,12 @@ func (a *InProcessProvider) Decrypt(ctx context.Context, keyDetails trust.KeyDet } protectedKey, err = a.cryptoProvider.ECDecrypt(ctx, kid, ephemeralPublicKey, ciphertext) + case AlgorithmHPQTXWing, AlgorithmHPQTSecp256r1MLKEM768, AlgorithmHPQTSecp384r1MLKEM1024: + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for hybrid decryption") + } + return a.cryptoProvider.Decrypt(ctx, trust.KeyIdentifier(kid), ciphertext, nil) + default: return nil, errors.New("unsupported key algorithm") } @@ -277,22 +292,51 @@ func (a *InProcessProvider) Decrypt(ctx context.Context, keyDetails trust.KeyDet return protectedKey, nil } -// DeriveKey generates a symmetric key for NanoTDF +// DeriveKey computes an ECDH shared secret and derives an AES key via HKDF. func (a *InProcessProvider) DeriveKey(_ context.Context, keyDetails trust.KeyDetails, ephemeralPublicKeyBytes []byte, curve elliptic.Curve) (ocrypto.ProtectedKey, error) { - k, err := a.cryptoProvider.GenerateNanoTDFSymmetricKey(string(keyDetails.ID()), ephemeralPublicKeyBytes, curve) + kid := string(keyDetails.ID()) + k, ok := a.cryptoProvider.keysByID[kid] + if !ok { + return nil, ErrKeyPairInfoNotFound + } + ec, ok := k.(StandardECCrypto) + if !ok { + return nil, ErrKeyPairInfoMalformed + } + + ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(curve, ephemeralPublicKeyBytes) if err != nil { return nil, err } - protectedKey, err := ocrypto.NewAESProtectedKey(k) + + derBytes, err := x509.MarshalPKIXPublicKey(ephemeralECDSAPublicKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal ECDSA public key: %w", err) + } + ephemeralECDSAPublicKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: derBytes, + }) + + symmetricKey, err := ocrypto.ComputeECDHKey([]byte(ec.ecPrivateKeyPem), ephemeralECDSAPublicKeyPEM) + if err != nil { + return nil, fmt.Errorf("ocrypto.ComputeECDHKey failed: %w", err) + } + + key, err := ocrypto.CalculateHKDF(TDFSalt(), symmetricKey) + if err != nil { + return nil, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err) + } + protectedKey, err := ocrypto.NewAESProtectedKey(key) if err != nil { return nil, fmt.Errorf("failed to create protected key: %w", err) } return protectedKey, nil } -// GenerateECSessionKey generates a session key for NanoTDF -func (a *InProcessProvider) GenerateECSessionKey(_ context.Context, ephemeralPublicKey string) (trust.Encapsulator, error) { - pke, err := ocrypto.FromPublicPEMWithSalt(ephemeralPublicKey, NanoVersionSalt(), nil) +// GenerateECSessionKey generates a session key for ECDH-based response encryption. +func (a *InProcessProvider) GenerateECSessionKey(_ context.Context, ephemeralPublicKey string) (ocrypto.Encapsulator, error) { + pke, err := ocrypto.FromPublicPEMWithSalt(ephemeralPublicKey, TDFSalt(), nil) if err != nil { return nil, fmt.Errorf("session key generation failed to create public key encryptor: %w", err) } @@ -304,17 +348,22 @@ func (a *InProcessProvider) Close() { a.cryptoProvider.Close() } -// determineKeyType tries to determine the algorithm of a key based on its ID -// This is a helper method for the Decrypt method -func (a *InProcessProvider) determineKeyType(_ context.Context, kid string) (string, error) { - // First try RSA - if _, err := a.cryptoProvider.RSAPublicKey(kid); err == nil { - return AlgorithmRSA2048, nil +// determineKeyType returns the configured algorithm for a loaded key. +func (a *InProcessProvider) determineKeyType(kid string) (string, error) { + key, ok := a.cryptoProvider.keysByID[kid] + if !ok { + return "", errors.New("could not determine key type") } - // Then try EC - if _, err := a.cryptoProvider.ECPublicKey(kid); err == nil { - return AlgorithmECP256R1, nil + switch key := key.(type) { + case StandardRSACrypto: + return key.Algorithm, nil + case StandardECCrypto: + return key.Algorithm, nil + case StandardXWingCrypto: + return key.Algorithm, nil + case StandardHybridCrypto: + return key.Algorithm, nil } return "", errors.New("could not determine key type") diff --git a/service/internal/security/in_process_provider_test.go b/service/internal/security/in_process_provider_test.go new file mode 100644 index 0000000000..9870ad53bb --- /dev/null +++ b/service/internal/security/in_process_provider_test.go @@ -0,0 +1,231 @@ +package security + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/json" + "encoding/pem" + "log/slog" + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/service/trust" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKeyDetailsAdapter(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, true, true) + + rsaAdapter := &KeyDetailsAdapter{ + id: trust.KeyIdentifier(material.rsaKid), + algorithm: ocrypto.KeyType(AlgorithmRSA2048), + cryptoProvider: cryptoProvider, + } + + ecAdapter := &KeyDetailsAdapter{ + id: trust.KeyIdentifier(material.ecKid), + algorithm: ocrypto.KeyType(AlgorithmECP256R1), + cryptoProvider: cryptoProvider, + } + + assert.Equal(t, inProcessSystemName, rsaAdapter.System()) + assert.Equal(t, trust.KeyIdentifier(material.rsaKid), rsaAdapter.ID()) + assert.Equal(t, ocrypto.KeyType(AlgorithmRSA2048), rsaAdapter.Algorithm()) + assert.False(t, rsaAdapter.IsLegacy()) + + _, err := rsaAdapter.ExportPrivateKey(t.Context()) + require.Error(t, err) + + jwk, err := rsaAdapter.ExportPublicKey(t.Context(), trust.KeyTypeJWK) + require.NoError(t, err) + assert.True(t, json.Valid([]byte(jwk))) + + pemKey, err := rsaAdapter.ExportPublicKey(t.Context(), trust.KeyTypePKCS8) + require.NoError(t, err) + assert.Contains(t, pemKey, "PUBLIC KEY") + + _, err = rsaAdapter.ExportCertificate(t.Context()) + require.Error(t, err) + + _, err = ecAdapter.ExportPublicKey(t.Context(), trust.KeyTypeJWK) + require.Error(t, err) + + ecPublic, err := ecAdapter.ExportPublicKey(t.Context(), trust.KeyTypePKCS8) + require.NoError(t, err) + assert.Contains(t, ecPublic, "PUBLIC KEY") + + cert, err := ecAdapter.ExportCertificate(t.Context()) + require.NoError(t, err) + assert.Equal(t, material.ecPublicPEM, cert) + + cfg := ecAdapter.ProviderConfig() + require.NotNil(t, cfg) + assert.Equal(t, inProcessSystemName, cfg.GetManager()) + assert.Equal(t, "static", cfg.GetName()) +} + +func TestInProcessProviderMetadata(t *testing.T) { + cryptoProvider, _ := newStandardCryptoForTest(t, true, false) + providerIface := NewSecurityProviderAdapter(cryptoProvider, nil, nil) + provider, ok := providerIface.(*InProcessProvider) + require.True(t, ok) + + assert.Equal(t, inProcessSystemName, provider.Name()) + assert.Equal(t, inProcessSystemName, provider.String()) + assert.Equal(t, slog.KindString, provider.LogValue().Kind()) + assert.Equal(t, inProcessSystemName, provider.LogValue().String()) + + logger := slog.New(slog.DiscardHandler) + assert.Same(t, provider, provider.WithLogger(logger)) + assert.Same(t, logger, provider.logger) +} + +func TestInProcessProviderKeyLookup(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, true, true) + providerIface := NewSecurityProviderAdapter( + cryptoProvider, + []string{material.rsaKid}, + []string{material.ecKid}, + ) + provider, ok := providerIface.(*InProcessProvider) + require.True(t, ok) + + defaultKey, err := provider.FindKeyByAlgorithm(t.Context(), AlgorithmRSA2048, false) + require.NoError(t, err) + assert.Equal(t, trust.KeyIdentifier(material.rsaKid), defaultKey.ID()) + + legacyKey, err := provider.FindKeyByAlgorithm(t.Context(), AlgorithmECP256R1, true) + require.NoError(t, err) + assert.Equal(t, trust.KeyIdentifier(material.ecKid), legacyKey.ID()) + + byID, err := provider.FindKeyByID(t.Context(), trust.KeyIdentifier(material.rsaKid)) + require.NoError(t, err) + assert.Equal(t, ocrypto.KeyType(AlgorithmRSA2048), byID.Algorithm()) + + _, err = provider.FindKeyByID(t.Context(), trust.KeyIdentifier("missing")) + require.ErrorIs(t, err, ErrCertNotFound) + + keys, err := provider.ListKeys(t.Context()) + require.NoError(t, err) + assert.Len(t, keys, 2) + + legacyOnly, err := provider.ListKeysWith(t.Context(), trust.ListKeyOptions{LegacyOnly: true}) + require.NoError(t, err) + require.Len(t, legacyOnly, 1) + assert.Equal(t, trust.KeyIdentifier(material.ecKid), legacyOnly[0].ID()) +} + +func TestInProcessProviderDecrypt(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, true, true) + providerIface := NewSecurityProviderAdapter( + cryptoProvider, + []string{material.rsaKid}, + []string{material.ecKid}, + ) + provider, ok := providerIface.(*InProcessProvider) + require.True(t, ok) + + rsaDetails, err := provider.FindKeyByID(t.Context(), trust.KeyIdentifier(material.rsaKid)) + require.NoError(t, err) + + rsaKey, ok := cryptoProvider.keysByID[material.rsaKid].(StandardRSACrypto) + require.True(t, ok) + rawRSA := make([]byte, 32) + _, err = rand.Read(rawRSA) + require.NoError(t, err) + cipherRSA, err := rsaKey.asymEncryption.Encrypt(rawRSA) + require.NoError(t, err) + + protected, err := provider.Decrypt(t.Context(), rsaDetails, cipherRSA, nil) + require.NoError(t, err) + assert.Equal(t, rawRSA, exportProtectedKey(t, protected)) + + _, err = provider.Decrypt(t.Context(), rsaDetails, cipherRSA, []byte("bad")) + require.Error(t, err) + + ecDetails, err := provider.FindKeyByID(t.Context(), trust.KeyIdentifier(material.ecKid)) + require.NoError(t, err) + + encryptor, err := ocrypto.FromPublicPEMWithSalt(material.ecPublicPEM, TDFSalt(), nil) + require.NoError(t, err) + rawEC := make([]byte, 32) + _, err = rand.Read(rawEC) + require.NoError(t, err) + cipherEC, err := encryptor.Encrypt(rawEC) + require.NoError(t, err) + + protected, err = provider.Decrypt(t.Context(), ecDetails, cipherEC, encryptor.EphemeralKey()) + require.NoError(t, err) + assert.Equal(t, rawEC, exportProtectedKey(t, protected)) + + _, err = provider.Decrypt(t.Context(), ecDetails, cipherEC, nil) + require.Error(t, err) +} + +func TestInProcessProviderDeriveKey(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, false, true) + providerIface := NewSecurityProviderAdapter(cryptoProvider, nil, []string{material.ecKid}) + provider, ok := providerIface.(*InProcessProvider) + require.True(t, ok) + + ecDetails, err := provider.FindKeyByID(t.Context(), trust.KeyIdentifier(material.ecKid)) + require.NoError(t, err) + + ephemeralKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + compressed := elliptic.MarshalCompressed(elliptic.P256(), ephemeralKey.X, ephemeralKey.Y) + + protected, err := provider.DeriveKey(t.Context(), ecDetails, compressed, elliptic.P256()) + require.NoError(t, err) + + publicDER, err := x509.MarshalPKIXPublicKey(&ephemeralKey.PublicKey) + require.NoError(t, err) + publicPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicDER}) + + symmetricKey, err := ocrypto.ComputeECDHKey([]byte(material.ecPrivatePEM), publicPEM) + require.NoError(t, err) + expected, err := ocrypto.CalculateHKDF(TDFSalt(), symmetricKey) + require.NoError(t, err) + + assert.Equal(t, expected, exportProtectedKey(t, protected)) +} + +func TestInProcessProviderGenerateECSessionKey(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, false, true) + providerIface := NewSecurityProviderAdapter(cryptoProvider, nil, nil) + provider, ok := providerIface.(*InProcessProvider) + require.True(t, ok) + + encapsulator, err := provider.GenerateECSessionKey(t.Context(), material.ecPublicPEM) + require.NoError(t, err) + + pemKey, err := encapsulator.PublicKeyAsPEM() + require.NoError(t, err) + assert.Contains(t, pemKey, "PUBLIC KEY") + + encrypted, err := encapsulator.Encrypt([]byte("data")) + require.NoError(t, err) + assert.NotEmpty(t, encrypted) + assert.NotEmpty(t, encapsulator.EphemeralKey()) +} + +func TestInProcessProviderDetermineKeyType(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, true, true) + providerIface := NewSecurityProviderAdapter(cryptoProvider, nil, nil) + provider, ok := providerIface.(*InProcessProvider) + require.True(t, ok) + + keyType, err := provider.determineKeyType(material.rsaKid) + require.NoError(t, err) + assert.Equal(t, AlgorithmRSA2048, keyType) + + keyType, err = provider.determineKeyType(material.ecKid) + require.NoError(t, err) + assert.Equal(t, AlgorithmECP256R1, keyType) + + _, err = provider.determineKeyType("missing") + require.Error(t, err) +} diff --git a/service/internal/security/standard_crypto.go b/service/internal/security/standard_crypto.go index ff4ec1c197..59953a8523 100644 --- a/service/internal/security/standard_crypto.go +++ b/service/internal/security/standard_crypto.go @@ -4,7 +4,6 @@ import ( "context" "crypto" "crypto/ecdh" - "crypto/elliptic" "crypto/sha256" "crypto/x509" "encoding/json" @@ -19,10 +18,6 @@ import ( "github.com/opentdf/platform/service/trust" ) -const ( - kNanoTDFMagicStringAndVersion = "L1L" -) - type StandardConfig struct { Keys []KeyPairInfo `mapstructure:"keys" json:"keys"` // Deprecated @@ -72,6 +67,18 @@ type StandardECCrypto struct { sk *ecdh.PrivateKey } +type StandardXWingCrypto struct { + KeyPairInfo + xwingPrivateKeyPem string + xwingPublicKeyPem string +} + +type StandardHybridCrypto struct { + KeyPairInfo + hybridPrivateKeyPem string + hybridPublicKeyPem string +} + // List of keys by identifier type keylist map[string]any @@ -155,19 +162,35 @@ func loadKey(k KeyPairInfo) (any, error) { ecPrivateKeyPem: string(privatePEM), ecCertificatePEM: string(certPEM), }, nil + case AlgorithmHPQTXWing: + return StandardXWingCrypto{ + KeyPairInfo: k, + xwingPrivateKeyPem: string(privatePEM), + xwingPublicKeyPem: string(certPEM), + }, nil + case AlgorithmHPQTSecp256r1MLKEM768, AlgorithmHPQTSecp384r1MLKEM1024: + return StandardHybridCrypto{ + KeyPairInfo: k, + hybridPrivateKeyPem: string(privatePEM), + hybridPublicKeyPem: string(certPEM), + }, nil case AlgorithmRSA2048, AlgorithmRSA4096: asymDecryption, err := ocrypto.NewAsymDecryption(string(privatePEM)) if err != nil { return nil, fmt.Errorf("ocrypto.NewAsymDecryption failed: %w", err) } - asymEncryption, err := ocrypto.NewAsymEncryption(string(certPEM)) + publicKeyEncryptor, err := ocrypto.FromPublicPEM(string(certPEM)) if err != nil { - return nil, fmt.Errorf("ocrypto.NewAsymEncryption failed: %w", err) + return nil, fmt.Errorf("ocrypto.FromPublicPEM failed: %w", err) + } + asymEncryption, ok := publicKeyEncryptor.(*ocrypto.AsymEncryption) + if !ok { + return nil, fmt.Errorf("unexpected public key encryptor type: %T", publicKeyEncryptor) } return StandardRSACrypto{ KeyPairInfo: k, asymDecryption: asymDecryption, - asymEncryption: asymEncryption, + asymEncryption: *asymEncryption, }, nil default: return nil, errors.New("unsupported algorithm [" + k.Algorithm + "]") @@ -201,9 +224,13 @@ func loadDeprecatedKeys(rsaKeys map[string]StandardKeyInfo, ecKeys map[string]St return nil, fmt.Errorf("failed to rsa public key file: %w", err) } - asymEncryption, err := ocrypto.NewAsymEncryption(string(publicPemData)) + publicKeyEncryptor, err := ocrypto.FromPublicPEM(string(publicPemData)) if err != nil { - return nil, fmt.Errorf("ocrypto.NewAsymEncryption failed: %w", err) + return nil, fmt.Errorf("ocrypto.FromPublicPEM failed: %w", err) + } + asymEncryption, ok := publicKeyEncryptor.(*ocrypto.AsymEncryption) + if !ok { + return nil, fmt.Errorf("unexpected public key encryptor type: %T", publicKeyEncryptor) } k := StandardRSACrypto{ @@ -214,7 +241,7 @@ func loadDeprecatedKeys(rsaKeys map[string]StandardKeyInfo, ecKeys map[string]St Certificate: kasInfo.PublicKeyPath, }, asymDecryption: asymDecryption, - asymEncryption: asymEncryption, + asymEncryption: *asymEncryption, } keysByAlg[AlgorithmRSA2048][id] = k keysByID[id] = k @@ -326,6 +353,42 @@ func (s StandardCrypto) ECPublicKey(kid string) (string, error) { return string(pemBytes), nil } +func (s StandardCrypto) XWingPublicKey(kid string) (string, error) { + k, ok := s.keysByID[kid] + if !ok { + return "", fmt.Errorf("no xwing key with id [%s]: %w", kid, ErrCertNotFound) + } + xw, ok := k.(StandardXWingCrypto) + if !ok { + return "", fmt.Errorf("key with id [%s] is not an X-Wing key: %w", kid, ErrCertNotFound) + } + if xw.xwingPublicKeyPem == "" { + return "", fmt.Errorf("no X-Wing public key with id [%s]: %w", kid, ErrCertNotFound) + } + return xw.xwingPublicKeyPem, nil +} + +func (s StandardCrypto) HybridPublicKey(kid string) (string, error) { + k, ok := s.keysByID[kid] + if !ok { + return "", fmt.Errorf("no hybrid key with id [%s]: %w", kid, ErrCertNotFound) + } + switch h := k.(type) { + case StandardXWingCrypto: + if h.xwingPublicKeyPem == "" { + return "", fmt.Errorf("no hybrid public key with id [%s]: %w", kid, ErrCertNotFound) + } + return h.xwingPublicKeyPem, nil + case StandardHybridCrypto: + if h.hybridPublicKeyPem == "" { + return "", fmt.Errorf("no hybrid public key with id [%s]: %w", kid, ErrCertNotFound) + } + return h.hybridPublicKeyPem, nil + default: + return "", fmt.Errorf("key with id [%s] is not a hybrid key: %w", kid, ErrCertNotFound) + } +} + func (s StandardCrypto) RSADecrypt(_ crypto.Hash, kid string, _ string, ciphertext []byte) ([]byte, error) { k, ok := s.keysByID[kid] if !ok { @@ -370,49 +433,6 @@ func (s StandardCrypto) RSAPublicKeyAsJSON(kid string) (string, error) { return string(jsonPublicKey), nil } -func (s StandardCrypto) GenerateNanoTDFSymmetricKey(kasKID string, ephemeralPublicKeyBytes []byte, curve elliptic.Curve) ([]byte, error) { - k, ok := s.keysByID[kasKID] - if !ok { - return nil, ErrKeyPairInfoNotFound - } - ec, ok := k.(StandardECCrypto) - if !ok { - return nil, ErrKeyPairInfoMalformed - } - privateKeyPEM := []byte(ec.ecPrivateKeyPem) - - return DeriveNanoTDFSymmetricKey(curve, ephemeralPublicKeyBytes, privateKeyPEM) -} - -func DeriveNanoTDFSymmetricKey(curve elliptic.Curve, clientEphemera []byte, privateKeyPEM []byte) ([]byte, error) { - ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(curve, clientEphemera) - if err != nil { - return nil, err - } - - derBytes, err := x509.MarshalPKIXPublicKey(ephemeralECDSAPublicKey) - if err != nil { - return nil, fmt.Errorf("failed to marshal ECDSA public key: %w", err) - } - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: derBytes, - } - ephemeralECDSAPublicKeyPEM := pem.EncodeToMemory(pemBlock) - - symmetricKey, err := ocrypto.ComputeECDHKey(privateKeyPEM, ephemeralECDSAPublicKeyPEM) - if err != nil { - return nil, fmt.Errorf("ocrypto.ComputeECDHKey failed: %w", err) - } - - key, err := ocrypto.CalculateHKDF(NanoVersionSalt(), symmetricKey) - if err != nil { - return nil, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err) - } - - return key, nil -} - func (s StandardCrypto) Close() { } @@ -423,12 +443,6 @@ func TDFSalt() []byte { return salt } -func NanoVersionSalt() []byte { - digest := sha256.New() - digest.Write([]byte(kNanoTDFMagicStringAndVersion)) - return digest.Sum(nil) -} - // ECDecrypt uses hybrid ECIES to decrypt the data. func (s *StandardCrypto) ECDecrypt(ctx context.Context, keyID string, ephemeralPublicKey, ciphertext []byte) (ocrypto.ProtectedKey, error) { unwrappedKey, err := s.Decrypt(ctx, trust.KeyIdentifier(keyID), ciphertext, ephemeralPublicKey) @@ -485,6 +499,49 @@ func (s *StandardCrypto) Decrypt(_ context.Context, keyID trust.KeyIdentifier, c return nil, fmt.Errorf("error decrypting data: %w", err) } + case StandardXWingCrypto: + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for X-Wing decryption") + } + + privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(key.xwingPrivateKeyPem)) + if err != nil { + return nil, fmt.Errorf("failed to parse X-Wing private key: %w", err) + } + + rawKey, err = ocrypto.XWingUnwrapDEK(privateKey, ciphertext) + if err != nil { + return nil, fmt.Errorf("failed to decrypt with X-Wing: %w", err) + } + + case StandardHybridCrypto: + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for hybrid decryption") + } + + switch key.Algorithm { + case AlgorithmHPQTSecp256r1MLKEM768: + privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(key.hybridPrivateKeyPem)) + if err != nil { + return nil, fmt.Errorf("failed to parse P256-MLKEM768 private key: %w", err) + } + rawKey, err = ocrypto.P256MLKEM768UnwrapDEK(privateKey, ciphertext) + if err != nil { + return nil, fmt.Errorf("failed to decrypt with P256-MLKEM768: %w", err) + } + case AlgorithmHPQTSecp384r1MLKEM1024: + privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(key.hybridPrivateKeyPem)) + if err != nil { + return nil, fmt.Errorf("failed to parse P384-MLKEM1024 private key: %w", err) + } + rawKey, err = ocrypto.P384MLKEM1024UnwrapDEK(privateKey, ciphertext) + if err != nil { + return nil, fmt.Errorf("failed to decrypt with P384-MLKEM1024: %w", err) + } + default: + return nil, fmt.Errorf("unsupported hybrid algorithm [%s]", key.Algorithm) + } + default: return nil, fmt.Errorf("unsupported key type for key ID [%s]", kid) } diff --git a/service/internal/security/standard_crypto_test.go b/service/internal/security/standard_crypto_test.go new file mode 100644 index 0000000000..8f92b857ac --- /dev/null +++ b/service/internal/security/standard_crypto_test.go @@ -0,0 +1,197 @@ +package security + +import ( + "crypto/rand" + "encoding/json" + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/service/trust" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStandardCryptoKeyLookup(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, true, true) + + kids, err := cryptoProvider.ListKIDsByAlgorithm(AlgorithmRSA2048) + require.NoError(t, err) + require.Len(t, kids, 1) + assert.Equal(t, material.rsaKid, kids[0]) + + _, err = cryptoProvider.ListKIDsByAlgorithm("nope") + require.ErrorIs(t, err, ErrCertNotFound) + + found := cryptoProvider.FindKID(AlgorithmRSA2048) + assert.Equal(t, material.rsaKid, found) + assert.Empty(t, cryptoProvider.FindKID("missing")) +} + +func TestStandardCryptoPublicKeys(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, true, true) + + rsaPEM, err := cryptoProvider.RSAPublicKey(material.rsaKid) + require.NoError(t, err) + assert.Contains(t, rsaPEM, "PUBLIC KEY") + + rsaJSON, err := cryptoProvider.RSAPublicKeyAsJSON(material.rsaKid) + require.NoError(t, err) + assert.True(t, json.Valid([]byte(rsaJSON))) + + ecCert, err := cryptoProvider.ECCertificate(material.ecKid) + require.NoError(t, err) + assert.Equal(t, material.ecPublicPEM, ecCert) + + ecPEM, err := cryptoProvider.ECPublicKey(material.ecKid) + require.NoError(t, err) + assert.Contains(t, ecPEM, "PUBLIC KEY") +} + +func TestStandardCryptoDecrypt(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, true, true) + + t.Run("rsa decrypt", func(t *testing.T) { + key, ok := cryptoProvider.keysByID[material.rsaKid].(StandardRSACrypto) + require.True(t, ok) + rawKey := make([]byte, 32) + _, err := rand.Read(rawKey) + require.NoError(t, err) + ciphertext, err := key.asymEncryption.Encrypt(rawKey) + require.NoError(t, err) + + protected, err := cryptoProvider.Decrypt(t.Context(), trust.KeyIdentifier(material.rsaKid), ciphertext, nil) + require.NoError(t, err) + assert.Equal(t, rawKey, exportProtectedKey(t, protected)) + }) + + t.Run("ec decrypt", func(t *testing.T) { + rawKey := make([]byte, 32) + _, err := rand.Read(rawKey) + require.NoError(t, err) + encryptor, err := ocrypto.FromPublicPEMWithSalt(material.ecPublicPEM, TDFSalt(), nil) + require.NoError(t, err) + + ciphertext, err := encryptor.Encrypt(rawKey) + require.NoError(t, err) + + protected, err := cryptoProvider.Decrypt(t.Context(), trust.KeyIdentifier(material.ecKid), ciphertext, encryptor.EphemeralKey()) + require.NoError(t, err) + assert.Equal(t, rawKey, exportProtectedKey(t, protected)) + }) + + t.Run("missing key", func(t *testing.T) { + _, err := cryptoProvider.Decrypt(t.Context(), trust.KeyIdentifier("missing"), nil, nil) + require.Error(t, err) + }) + + t.Run("rsa with ephemeral key", func(t *testing.T) { + key, ok := cryptoProvider.keysByID[material.rsaKid].(StandardRSACrypto) + require.True(t, ok) + rawKey := []byte("rsa-secret") + ciphertext, err := key.asymEncryption.Encrypt(rawKey) + require.NoError(t, err) + + _, err = cryptoProvider.Decrypt(t.Context(), trust.KeyIdentifier(material.rsaKid), ciphertext, []byte("nope")) + require.Error(t, err) + }) + + t.Run("ec without ephemeral key", func(t *testing.T) { + rawKey := []byte("ec-secret") + encryptor, err := ocrypto.FromPublicPEMWithSalt(material.ecPublicPEM, TDFSalt(), nil) + require.NoError(t, err) + + ciphertext, err := encryptor.Encrypt(rawKey) + require.NoError(t, err) + + _, err = cryptoProvider.Decrypt(t.Context(), trust.KeyIdentifier(material.ecKid), ciphertext, nil) + require.Error(t, err) + }) +} + +func TestStandardCryptoLoadErrors(t *testing.T) { + dir := t.TempDir() + privatePath := writeTempFile(t, dir, "key.pem", "test") + + t.Run("duplicate kid", func(t *testing.T) { + cfg := StandardConfig{ + Keys: []KeyPairInfo{ + {Algorithm: AlgorithmRSA2048, KID: "dup", Private: privatePath, Certificate: privatePath}, + {Algorithm: AlgorithmRSA2048, KID: "dup", Private: privatePath, Certificate: privatePath}, + }, + } + _, err := NewStandardCrypto(cfg) + require.Error(t, err) + }) + + t.Run("unsupported algorithm", func(t *testing.T) { + cfg := StandardConfig{ + Keys: []KeyPairInfo{ + {Algorithm: "unsupported", KID: "kid", Private: privatePath, Certificate: privatePath}, + }, + } + _, err := NewStandardCrypto(cfg) + require.Error(t, err) + }) + + t.Run("mixed new and deprecated config", func(t *testing.T) { + cfg := StandardConfig{ + Keys: []KeyPairInfo{{Algorithm: AlgorithmRSA2048, KID: "kid", Private: privatePath, Certificate: privatePath}}, + RSAKeys: map[string]StandardKeyInfo{"legacy": {PrivateKeyPath: privatePath, PublicKeyPath: privatePath}}, + } + _, err := NewStandardCrypto(cfg) + require.Error(t, err) + }) +} + +func TestStandardCryptoDeprecatedKeys(t *testing.T) { + dir := t.TempDir() + + rsaPair, err := generateRSAKeyAndPEM() + require.NoError(t, err) + rsaPrivatePEM, err := rsaPair.PrivateKeyInPemFormat() + require.NoError(t, err) + rsaPublicPEM, err := rsaPair.PublicKeyInPemFormat() + require.NoError(t, err) + + ecPair, err := generateECKeyAndPEM(ocrypto.ECCModeSecp256r1) + require.NoError(t, err) + ecPrivatePEM, err := ecPair.PrivateKeyInPemFormat() + require.NoError(t, err) + ecPublicPEM, err := ecPair.PublicKeyInPemFormat() + require.NoError(t, err) + + rsaPrivatePath := writeTempFile(t, dir, "rsa-private.pem", rsaPrivatePEM) + rsaPublicPath := writeTempFile(t, dir, "rsa-public.pem", rsaPublicPEM) + ecPrivatePath := writeTempFile(t, dir, "ec-private.pem", ecPrivatePEM) + ecPublicPath := writeTempFile(t, dir, "ec-public.pem", ecPublicPEM) + + cfg := StandardConfig{ + RSAKeys: map[string]StandardKeyInfo{ + "rsa-legacy": {PrivateKeyPath: rsaPrivatePath, PublicKeyPath: rsaPublicPath}, + }, + ECKeys: map[string]StandardKeyInfo{ + "ec-legacy": {PrivateKeyPath: ecPrivatePath, PublicKeyPath: ecPublicPath}, + }, + } + + cryptoProvider, err := NewStandardCrypto(cfg) + require.NoError(t, err) + + rsaKids, err := cryptoProvider.ListKIDsByAlgorithm(AlgorithmRSA2048) + require.NoError(t, err) + assert.Equal(t, []string{"rsa-legacy"}, rsaKids) + + ecKids, err := cryptoProvider.ListKIDsByAlgorithm(AlgorithmECP256R1) + require.NoError(t, err) + assert.Equal(t, []string{"ec-legacy"}, ecKids) +} + +func TestStandardCryptoRSAPublicKeyErrors(t *testing.T) { + cryptoProvider, material := newStandardCryptoForTest(t, true, true) + + _, err := cryptoProvider.RSAPublicKey("missing") + require.ErrorIs(t, err, ErrCertNotFound) + + _, err = cryptoProvider.RSAPublicKey(material.ecKid) + require.ErrorIs(t, err, ErrCertNotFound) +} diff --git a/service/internal/security/standard_only_test.go b/service/internal/security/standard_only_test.go new file mode 100644 index 0000000000..ab98a06599 --- /dev/null +++ b/service/internal/security/standard_only_test.go @@ -0,0 +1,28 @@ +package security + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCryptoProvider(t *testing.T) { + t.Run("hsm removed", func(t *testing.T) { + provider, err := NewCryptoProvider(Config{Type: "hsm"}) + require.ErrorIs(t, err, ErrHSMNotFound) + assert.Nil(t, provider) + }) + + t.Run("standard", func(t *testing.T) { + provider, err := NewCryptoProvider(Config{Type: "standard"}) + require.NoError(t, err) + require.NotNil(t, provider) + }) + + t.Run("unknown type falls back to standard", func(t *testing.T) { + provider, err := NewCryptoProvider(Config{Type: "unknown"}) + require.NoError(t, err) + require.NotNil(t, provider) + }) +} diff --git a/service/internal/security/test_helpers_test.go b/service/internal/security/test_helpers_test.go new file mode 100644 index 0000000000..f3736529b7 --- /dev/null +++ b/service/internal/security/test_helpers_test.go @@ -0,0 +1,91 @@ +package security + +import ( + "os" + "path/filepath" + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/stretchr/testify/require" +) + +type testKeyMaterial struct { + rsaKid string + rsaPrivatePEM string + rsaPublicPEM string + + ecKid string + ecPrivatePEM string + ecPublicPEM string +} + +func writeTempFile(t *testing.T, dir, name, contents string) string { + t.Helper() + path := filepath.Join(dir, name) + require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) + return path +} + +func newStandardCryptoForTest(t *testing.T, includeRSA, includeEC bool) (*StandardCrypto, testKeyMaterial) { + t.Helper() + + dir := t.TempDir() + var keys []KeyPairInfo + var material testKeyMaterial + + if includeRSA { + rsaPair, err := generateRSAKeyAndPEM() + require.NoError(t, err) + rsaPrivatePEM, err := rsaPair.PrivateKeyInPemFormat() + require.NoError(t, err) + rsaPublicPEM, err := rsaPair.PublicKeyInPemFormat() + require.NoError(t, err) + + material.rsaKid = "rsa-test-key" + material.rsaPrivatePEM = rsaPrivatePEM + material.rsaPublicPEM = rsaPublicPEM + + privatePath := writeTempFile(t, dir, "rsa-private.pem", rsaPrivatePEM) + publicPath := writeTempFile(t, dir, "rsa-public.pem", rsaPublicPEM) + keys = append(keys, KeyPairInfo{ + Algorithm: AlgorithmRSA2048, + KID: material.rsaKid, + Private: privatePath, + Certificate: publicPath, + }) + } + + if includeEC { + ecPair, err := generateECKeyAndPEM(ocrypto.ECCModeSecp256r1) + require.NoError(t, err) + ecPrivatePEM, err := ecPair.PrivateKeyInPemFormat() + require.NoError(t, err) + ecPublicPEM, err := ecPair.PublicKeyInPemFormat() + require.NoError(t, err) + + material.ecKid = "ec-test-key" + material.ecPrivatePEM = ecPrivatePEM + material.ecPublicPEM = ecPublicPEM + + privatePath := writeTempFile(t, dir, "ec-private.pem", ecPrivatePEM) + publicPath := writeTempFile(t, dir, "ec-public.pem", ecPublicPEM) + keys = append(keys, KeyPairInfo{ + Algorithm: AlgorithmECP256R1, + KID: material.ecKid, + Private: privatePath, + Certificate: publicPath, + }) + } + + crypto, err := NewStandardCrypto(StandardConfig{Keys: keys}) + require.NoError(t, err) + + return crypto, material +} + +func exportProtectedKey(t *testing.T, key ocrypto.ProtectedKey) []byte { + t.Helper() + raw, err := (&noOpEncapsulator{}).Encapsulate(key) + require.NoError(t, err) + return raw +} diff --git a/service/internal/server/server.go b/service/internal/server/server.go index 5888d821b0..20c92c4e17 100644 --- a/service/internal/server/server.go +++ b/service/internal/server/server.go @@ -18,22 +18,19 @@ import ( "connectrpc.com/grpcreflect" "connectrpc.com/validate" "github.com/go-chi/cors" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/opentdf/platform/sdk" sdkAudit "github.com/opentdf/platform/sdk/audit" "github.com/opentdf/platform/service/internal/auth" + "github.com/opentdf/platform/service/internal/auth/authz" "github.com/opentdf/platform/service/internal/security" "github.com/opentdf/platform/service/internal/server/memhttp" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/logger/audit" - ctxAuth "github.com/opentdf/platform/service/pkg/auth" "github.com/opentdf/platform/service/pkg/cache" "github.com/opentdf/platform/service/tracing" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" ) const ( @@ -60,6 +57,10 @@ type Config struct { TLS TLSConfig `mapstructure:"tls" json:"tls"` CORS CORSConfig `mapstructure:"cors" json:"cors"` WellKnownConfigRegister func(namespace string, config any) error `mapstructure:"-" json:"-"` + + // Programmatic interceptors injected at startup (not loaded from config) + ExtraConnectInterceptors []connect.Interceptor `mapstructure:"-" json:"-"` + ExtraIPCInterceptors []connect.Interceptor `mapstructure:"-" json:"-"` // Port to listen on Port int `mapstructure:"port" json:"port" default:"8080"` Host string `mapstructure:"host,omitempty" json:"host"` @@ -70,22 +71,25 @@ type Config struct { EnablePprof bool `mapstructure:"enable_pprof" json:"enable_pprof" default:"false"` // Trace is for configuring open telemetry based tracing. Trace tracing.Config `mapstructure:"trace" json:"trace"` + + // AuthzResolverRegistry contains service-registered resolvers used by the auth interceptor. + AuthzResolverRegistry *authz.ResolverRegistry `mapstructure:"-" json:"-"` } func (c Config) LogValue() slog.Value { group := []slog.Attr{ - slog.Any("auth", c.Auth), + slog.Any("auth_config", c.Auth), slog.Any("grpc", c.GRPC), slog.Any("tls", c.TLS), slog.Any("cors", c.CORS), slog.Int("port", c.Port), slog.String("host", c.Host), - slog.Bool("enablePprof", c.EnablePprof), + slog.Bool("enable_pprof", c.EnablePprof), } // CryptoProvider is deprecated in favor of the trust package. if !c.CryptoProvider.IsEmpty() { - group = append(group, slog.Any("cryptoProvider", c.CryptoProvider)) + group = append(group, slog.Any("crypto_provider", c.CryptoProvider)) } return slog.GroupValue(group...) @@ -222,7 +226,7 @@ type ConnectRPC struct { type OpenTDFServer struct { AuthN *auth.Authentication - GRPCGatewayMux *runtime.ServeMux + HTTPMux *http.ServeMux HTTPServer *http.Server ConnectRPCInProcess *inProcessServer ConnectRPC *ConnectRPC @@ -265,6 +269,7 @@ func NewOpenTDFServer(config Config, logger *logger.Logger, cacheManager *cache. config.Auth, logger, config.WellKnownConfigRegister, + auth.WithAuthzResolverRegistry(config.AuthzResolverRegistry), ) if err != nil { return nil, fmt.Errorf("failed to create authentication interceptor: %w", err) @@ -274,54 +279,37 @@ func NewOpenTDFServer(config Config, logger *logger.Logger, cacheManager *cache. logger.Warn("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP set `enforceDPoP = false`") } - connectRPCIpc, err := newConnectRPCIPC(config, authN, logger) + var ipcAuthInt connect.Interceptor + var connectAuthInt connect.Interceptor + if config.Auth.Enabled && authN != nil { + ipcAuthInt = authN.IPCUnaryServerInterceptor() + connectAuthInt = authN.ConnectUnaryServerInterceptor() + } + + connectRPCIpc, err := newConnectRPC(config, ipcAuthInt, config.ExtraIPCInterceptors, logger) if err != nil { return nil, fmt.Errorf("failed to create connect rpc ipc server: %w", err) } - connectRPC, err := newConnectRPC(config, authN, logger) + connectRPC, err := newConnectRPC(config, connectAuthInt, config.ExtraConnectInterceptors, logger) if err != nil { return nil, fmt.Errorf("failed to create connect rpc server: %w", err) } - // GRPC Gateway Mux - grpcGatewayMux := runtime.NewServeMux( - runtime.WithIncomingHeaderMatcher( - func(key string) (string, bool) { - if k, ok := runtime.DefaultHeaderMatcher(key); ok { - return k, true - } - if textproto.CanonicalMIMEHeaderKey(key) == "Dpop" { - return "Dpop", true - } - return "", false - }, - ), - runtime.WithMetadata(func(ctx context.Context, _ *http.Request) metadata.MD { - md := make(map[string]string) - if method, ok := runtime.RPCMethod(ctx); ok { - md["method"] = method // /grpc.gateway.examples.internal.proto.examplepb.LoginService/Login - } - if pattern, ok := runtime.HTTPPathPattern(ctx); ok { - md["pattern"] = pattern // /v1/example/login - } - md["Authorization"] = "Bearer " + ctxAuth.GetRawAccessTokenFromContext(ctx, nil) - return metadata.New(md) - }), - ) + httpMux := http.NewServeMux() // Create http server - httpServer, err := newHTTPServer(config, connectRPC.Mux, grpcGatewayMux, authN, logger) + httpServer, err := newHTTPServer(config, connectRPC.Mux, httpMux, authN, logger) if err != nil { return nil, fmt.Errorf("failed to create http server: %w", err) } o := OpenTDFServer{ - AuthN: authN, - GRPCGatewayMux: grpcGatewayMux, - HTTPServer: httpServer, - CacheManager: cacheManager, - ConnectRPC: connectRPC, + AuthN: authN, + HTTPMux: httpMux, + HTTPServer: httpServer, + CacheManager: cacheManager, + ConnectRPC: connectRPC, ConnectRPCInProcess: &inProcessServer{ logger: logger.With("ipc_server", "true"), srv: memhttp.New(connectRPCIpc.Mux), @@ -345,56 +333,22 @@ func NewOpenTDFServer(config Config, logger *logger.Logger, cacheManager *cache. return &o, nil } -// Custom response writer to add deprecation header -type grpcGatewayResponseWriter struct { - w http.ResponseWriter - code int - wroteHeader bool -} - -func (rw *grpcGatewayResponseWriter) Header() http.Header { - return rw.w.Header() -} - -func (rw *grpcGatewayResponseWriter) WriteHeader(statusCode int) { - gRPCGatewayDeprecationDate := fmt.Sprintf("@%d", time.Date(2025, time.March, 25, 0, 0, 0, 0, time.UTC).Unix()) - if !rw.wroteHeader { - rw.w.Header().Set("Deprecation", gRPCGatewayDeprecationDate) - rw.wroteHeader = true - rw.w.WriteHeader(statusCode) - } - rw.code = statusCode -} - -func (rw *grpcGatewayResponseWriter) Write(data []byte) (int, error) { - // Ensure headers are written before any data - if !rw.wroteHeader { - rw.WriteHeader(http.StatusOK) - } - return rw.w.Write(data) -} - -// newHTTPServer creates a new http server with the given handler and grpc server -func newHTTPServer(c Config, connectRPC http.Handler, originalGrpcGateway http.Handler, a *auth.Authentication, l *logger.Logger) (*http.Server, error) { +// newHTTPServer creates a new http server with the given Connect RPC and extra HTTP handlers. +func newHTTPServer(c Config, connectRPC http.Handler, extraHTTP http.Handler, a *auth.Authentication, l *logger.Logger) (*http.Server, error) { var ( err error tc *tls.Config ) - // Adds deprecation header to any grpcGateway responses. - var grpcGateway http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - grpcRW := &grpcGatewayResponseWriter{w: w, code: http.StatusOK} - originalGrpcGateway.ServeHTTP(grpcRW, r) - }) + httpHandler := extraHTTP - // Add authN interceptor to extra handlers + // Add authN interceptor to extra handlers. if c.Auth.Enabled { - grpcGateway = a.MuxHandler(grpcGateway) + httpHandler = a.MuxHandler(httpHandler) } else { l.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP set `enforceDPoP = false`") } - // Note: The grpc-gateway handlers are getting chained together in reverse. So the last handler is the first to be called. // CORS if c.CORS.Enabled { // Compute effective values by merging base and additional lists @@ -403,7 +357,8 @@ func newHTTPServer(c Config, connectRPC http.Handler, originalGrpcGateway http.H effectiveExposed := c.CORS.EffectiveExposedHeaders() // Log effective CORS config for operator visibility - l.Info("CORS middleware enabled", + l.Info( + "CORS middleware enabled", slog.Any("allowed_origins", c.CORS.AllowedOrigins), slog.Any("effective_methods", effectiveMethods), slog.Any("effective_headers", effectiveHeaders), @@ -432,12 +387,12 @@ func newHTTPServer(c Config, connectRPC http.Handler, originalGrpcGateway http.H // Apply CORS to connectRPC and extra handlers connectRPC = corsHandler.Handler(connectRPC) - grpcGateway = corsHandler.Handler(grpcGateway) + httpHandler = corsHandler.Handler(httpHandler) } // Enable pprof if c.EnablePprof { - grpcGateway = pprofHandler(grpcGateway) + httpHandler = pprofHandler(httpHandler) // Need to extend write timeout to collect pprof data. if c.HTTPServerConfig.WriteTimeout < 30*time.Second { c.HTTPServerConfig.WriteTimeout = 30 * time.Second //nolint:mnd // easier to read that we are overriding the default @@ -446,13 +401,13 @@ func newHTTPServer(c Config, connectRPC http.Handler, originalGrpcGateway http.H var handler http.Handler if !c.TLS.Enabled { - handler = h2c.NewHandler(routeConnectRPCRequests(connectRPC, grpcGateway), &http2.Server{}) + handler = h2c.NewHandler(routeConnectRPCRequests(connectRPC, httpHandler), &http2.Server{}) } else { tc, err = loadTLSConfig(c.TLS) if err != nil { return nil, fmt.Errorf("failed to load tls config: %w", err) } - handler = routeConnectRPCRequests(connectRPC, grpcGateway) + handler = routeConnectRPCRequests(connectRPC, httpHandler) } if c.HTTPServerConfig.ReadTimeout == 0 { @@ -509,45 +464,34 @@ func pprofHandler(h http.Handler) http.Handler { }) } -func newConnectRPCIPC(c Config, a *auth.Authentication, logger *logger.Logger) (*ConnectRPC, error) { +func newConnectRPC(c Config, authInt connect.Interceptor, ints []connect.Interceptor, logger *logger.Logger) (*ConnectRPC, error) { interceptors := make([]connect.HandlerOption, 0) - if c.Auth.Enabled { - interceptors = append(interceptors, connect.WithInterceptors(a.IPCUnaryServerInterceptor())) - } else { - logger.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP you can set `enforceDpop = false`") - } - - // Add protovalidate interceptor - vaidationInterceptor, err := validate.NewInterceptor() + // OTel tracing and metrics for incoming Connect requests, before all other interceptors + serverTraceInt, err := tracing.ConnectServerTraceInterceptor() if err != nil { - return nil, fmt.Errorf("failed to create validation interceptor: %w", err) + return nil, fmt.Errorf("failed to create server trace interceptor: %w", err) } - - interceptors = append(interceptors, connect.WithInterceptors(vaidationInterceptor, audit.ContextServerInterceptor(logger.Logger))) - - return &ConnectRPC{ - Interceptors: interceptors, - Mux: http.NewServeMux(), - }, nil -} - -func newConnectRPC(c Config, a *auth.Authentication, logger *logger.Logger) (*ConnectRPC, error) { - interceptors := make([]connect.HandlerOption, 0) + interceptors = append(interceptors, connect.WithInterceptors(serverTraceInt)) if c.Auth.Enabled { - interceptors = append(interceptors, connect.WithInterceptors(a.ConnectUnaryServerInterceptor())) + if authInt == nil { + return nil, errors.New("authentication enabled but no interceptor provided") + } + interceptors = append(interceptors, connect.WithInterceptors(authInt)) } else { logger.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP you can set `enforceDpop = false`") } // Add protovalidate interceptor - vaidationInterceptor, err := validate.NewInterceptor() - if err != nil { - return nil, fmt.Errorf("failed to create validation interceptor: %w", err) - } + validationInterceptor := validate.NewInterceptor() - interceptors = append(interceptors, connect.WithInterceptors(vaidationInterceptor, audit.ContextServerInterceptor(logger.Logger))) + interceptors = append(interceptors, connect.WithInterceptors(validationInterceptor, audit.ContextServerInterceptor(logger.Logger))) + + // Add any additional interceptors provided programmatically AFTER the default ones, so they have access needed context + if len(ints) > 0 { + interceptors = append(interceptors, connect.WithInterceptors(ints...)) + } return &ConnectRPC{ Interceptors: interceptors, @@ -604,6 +548,13 @@ func (s OpenTDFServer) Stop() { func (s inProcessServer) Conn() *sdk.ConnectRPCConnection { var clientInterceptors []connect.Interceptor + // OTel tracing and metrics for outbound IPC Connect RPCs + if clientTraceInt, err := tracing.ConnectClientTraceInterceptor(); err != nil { + s.logger.Error("failed to create IPC client trace interceptor", slog.String("error", err.Error())) + } else { + clientInterceptors = append(clientInterceptors, clientTraceInt) + } + // Add audit interceptor clientInterceptors = append(clientInterceptors, sdkAudit.MetadataAddingConnectInterceptor()) @@ -622,32 +573,6 @@ func (s inProcessServer) Conn() *sdk.ConnectRPCConnection { return &conn } -func (s inProcessServer) GrpcConn() *grpc.ClientConn { - var clientInterceptors []grpc.UnaryClientInterceptor - - // Add audit interceptor - clientInterceptors = append(clientInterceptors, sdkAudit.MetadataAddingClientInterceptor) - - defaultOptions := []grpc.DialOption{ - grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(s.maxCallRecvMsgSize), - grpc.MaxCallSendMsgSize(s.maxCallSendMsgSize), - ), - grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { - conn, err := s.srv.Listener.DialContext(ctx, "tcp", "http://localhost:8080") - if err != nil { - return nil, fmt.Errorf("failed to dial in process grpc server: %w", err) - } - return conn, nil - }), - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithChainUnaryInterceptor(clientInterceptors...), - } - - conn, _ := grpc.NewClient("passthrough:///", defaultOptions...) - return conn -} - func (s *inProcessServer) WithContextDialer() grpc.DialOption { return grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { conn, err := s.srv.Listener.DialContext(ctx, "tcp", "http://localhost:8080") diff --git a/service/internal/server/server_test.go b/service/internal/server/server_test.go index d18bd60665..12884d1302 100644 --- a/service/internal/server/server_test.go +++ b/service/internal/server/server_test.go @@ -1,14 +1,19 @@ package server import ( + "context" "net/http" "net/http/httptest" "strings" "testing" + "connectrpc.com/connect" "github.com/go-chi/cors" + "github.com/opentdf/platform/service/internal/auth" + "github.com/opentdf/platform/service/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/emptypb" ) func TestMergeStringSlices(t *testing.T) { @@ -522,3 +527,167 @@ func TestCORSMiddleware_SpecificOrigins(t *testing.T) { }) } } + +// noopInterceptor returns a connect.UnaryInterceptorFunc that passes through. +func noopInterceptor() connect.Interceptor { + return connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return next(ctx, req) + } + }) +} + +func TestNewConnectRPC(t *testing.T) { + testLogger := logger.CreateTestLogger() + + tests := []struct { + name string + authEnabled bool + authInt connect.Interceptor + extraInts []connect.Interceptor + wantErr bool + wantIntLen int + wantDescription string + }{ + { + name: "auth enabled with extras", + authEnabled: true, + authInt: noopInterceptor(), + extraInts: []connect.Interceptor{noopInterceptor(), noopInterceptor()}, + wantIntLen: 4, + wantDescription: "1 trace + 1 auth + 1 extras + 1 validation/audit", + }, + { + name: "auth enabled no extras", + authEnabled: true, + authInt: noopInterceptor(), + extraInts: nil, + wantIntLen: 3, + wantDescription: "1 trace + 1 auth + 1 validation/audit", + }, + { + name: "auth disabled no extras", + authEnabled: false, + authInt: nil, + extraInts: nil, + wantIntLen: 2, + wantDescription: "1 trace + 1 validation/audit only", + }, + { + name: "auth disabled with extras", + authEnabled: false, + authInt: nil, + extraInts: []connect.Interceptor{noopInterceptor()}, + wantIntLen: 3, + wantDescription: "1 trace + 1 extras + 1 validation/audit", + }, + { + name: "auth enabled but nil authInt returns error", + authEnabled: true, + authInt: nil, + extraInts: nil, + wantErr: true, + }, + { + name: "auth enabled nil authInt with extras returns error", + authEnabled: true, + authInt: nil, + extraInts: []connect.Interceptor{noopInterceptor()}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := Config{ + Auth: auth.Config{Enabled: tt.authEnabled}, + } + + result, err := newConnectRPC(cfg, tt.authInt, tt.extraInts, testLogger) + if tt.wantErr { + require.Error(t, err) + assert.Nil(t, result) + return + } + require.NoError(t, err) + require.NotNil(t, result) + assert.NotNil(t, result.Mux, "Mux must be initialized") + assert.Len(t, result.Interceptors, tt.wantIntLen, tt.wantDescription) + }) + } +} + +func TestNewConnectRPC_InterceptorOrdering(t *testing.T) { + testLogger := logger.CreateTestLogger() + + var callOrder []string + + makeInterceptor := func(name string) connect.Interceptor { + return connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + callOrder = append(callOrder, name) + return next(ctx, req) + } + }) + } + + tests := []struct { + name string + authEnabled bool + authInt connect.Interceptor + extras []connect.Interceptor + wantOrder []string + }{ + { + name: "extras run after auth interceptor", + authEnabled: true, + authInt: makeInterceptor("auth"), + extras: []connect.Interceptor{makeInterceptor("extra1"), makeInterceptor("extra2")}, + wantOrder: []string{"auth", "extra1", "extra2"}, + }, + { + name: "extras run after defaults without auth", + authEnabled: false, + extras: []connect.Interceptor{makeInterceptor("extra1"), makeInterceptor("extra2")}, + wantOrder: []string{"extra1", "extra2"}, + }, + { + name: "single extra runs after auth", + authEnabled: true, + authInt: makeInterceptor("auth"), + extras: []connect.Interceptor{makeInterceptor("extra1")}, + wantOrder: []string{"auth", "extra1"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + callOrder = nil + + cfg := Config{Auth: auth.Config{Enabled: tt.authEnabled}} + result, err := newConnectRPC(cfg, tt.authInt, tt.extras, testLogger) + require.NoError(t, err) + + // Register a minimal unary handler to exercise the interceptor chain + handler := connect.NewUnaryHandler( + "/test.v1.TestService/Method", + func(_ context.Context, _ *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { + return connect.NewResponse(&emptypb.Empty{}), nil + }, + result.Interceptors..., + ) + result.Mux.Handle("/test.v1.TestService/", handler) + + // Send a connect-protocol unary request + req := httptest.NewRequest(http.MethodPost, "/test.v1.TestService/Method", strings.NewReader("{}")) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Connect-Protocol-Version", "1") + rr := httptest.NewRecorder() + result.Mux.ServeHTTP(rr, req) + + require.Equal(t, http.StatusOK, rr.Code, "handler should succeed") + assert.Equal(t, tt.wantOrder, callOrder, + "extra interceptors must run after built-in interceptors") + }) + } +} diff --git a/service/internal/subjectmappingbuiltin/subject_mapping_builtin.go b/service/internal/subjectmappingbuiltin/subject_mapping_builtin.go index c66f62ac77..d849c0afce 100644 --- a/service/internal/subjectmappingbuiltin/subject_mapping_builtin.go +++ b/service/internal/subjectmappingbuiltin/subject_mapping_builtin.go @@ -8,9 +8,9 @@ import ( "log/slog" "strings" - "github.com/open-policy-agent/opa/ast" - "github.com/open-policy-agent/opa/rego" - "github.com/open-policy-agent/opa/types" + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/rego" + "github.com/open-policy-agent/opa/v1/types" "github.com/opentdf/platform/lib/flattening" "github.com/opentdf/platform/protocol/go/entityresolution" "github.com/opentdf/platform/protocol/go/policy" diff --git a/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions.go b/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions.go index 491d1bb8f7..dcc18fa743 100644 --- a/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions.go +++ b/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions.go @@ -2,6 +2,7 @@ package subjectmappingbuiltin import ( "fmt" + "log/slog" "maps" "slices" "strings" @@ -19,10 +20,11 @@ type EntityIDsToEntitlements map[string]AttributeValueFQNsToActions func EvaluateSubjectMappingMultipleEntitiesWithActions( attributeMappings map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue, entityRepresentations []*entityresolutionV2.EntityRepresentation, + l *slog.Logger, ) (EntityIDsToEntitlements, error) { results := make(map[string]AttributeValueFQNsToActions, len(entityRepresentations)) for _, er := range entityRepresentations { - entitlements, err := EvaluateSubjectMappingsWithActions(attributeMappings, er) + entitlements, err := EvaluateSubjectMappingsWithActions(attributeMappings, er, l) if err != nil { return nil, err } @@ -36,6 +38,7 @@ func EvaluateSubjectMappingMultipleEntitiesWithActions( func EvaluateSubjectMappingsWithActions( resolveableAttributes map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue, entityRepresentation *entityresolutionV2.EntityRepresentation, + l *slog.Logger, ) (AttributeValueFQNsToActions, error) { jsonEntities := entityRepresentation.GetAdditionalProps() entitlementsSet := make(map[string][]*policy.Action) @@ -65,18 +68,13 @@ func EvaluateSubjectMappingsWithActions( // each subject mapping that is true should permit the actions on the mapped value if subjectMappingResult { - // add value FQN to the entitlements set if _, ok := entitlementsSet[valueFQN]; !ok { entitlementsSet[valueFQN] = make([]*policy.Action, 0) } - actions := subjectMapping.GetActions() - - // Cache each action by name to deduplicate - m := make(map[string]*policy.Action, len(actions)) - for _, action := range actions { - m[strings.ToLower(action.GetName())] = action - } - entitlementsSet[valueFQN] = append(entitlementsSet[valueFQN], slices.Collect(maps.Values(m))...) + entitlementsSet[valueFQN] = append( + entitlementsSet[valueFQN], + dedupeSubjectMappingActions(subjectMapping.GetActions(), l)..., + ) } } } @@ -84,3 +82,86 @@ func EvaluateSubjectMappingsWithActions( return entitlementsSet, nil } + +// dedupeSubjectMappingActions caches actions by lowercased name and returns +// the deduplicated set. In normal operation, same-name conflicting actions +// should be prevented earlier by policy service create/update validation, +// which enforces namespace consistency between subject mappings and referenced +// actions. This extra conflict check is defensive for unexpected or mixed +// legacy states in-memory; if encountered, keep deterministic behavior and log. +func dedupeSubjectMappingActions(actions []*policy.Action, l *slog.Logger) []*policy.Action { + m := make(map[string]*policy.Action, len(actions)) + for _, action := range actions { + key := strings.ToLower(action.GetName()) + existing, ok := m[key] + if !ok { + m[key] = action + continue + } + if actionsConflict(existing, action) && l != nil { + l.Warn( + "subject mapping action name collision with conflicting identity; using deterministic preference", + slog.String("action_name", key), + slog.Any("existing_action", existing), + slog.Any("candidate_action", action), + ) + } + m[key] = preferAction(existing, action) + } + return slices.Collect(maps.Values(m)) +} + +func actionsConflict(existing *policy.Action, candidate *policy.Action) bool { + if existing == nil || candidate == nil { + return false + } + + if existing.GetId() != candidate.GetId() { + return true + } + + existingNS := existing.GetNamespace() + candidateNS := candidate.GetNamespace() + if existingNS == nil && candidateNS == nil { + return false + } + if (existingNS == nil) != (candidateNS == nil) { + return true + } + + if existingNS.GetId() != candidateNS.GetId() { + return true + } + + return !strings.EqualFold(existingNS.GetFqn(), candidateNS.GetFqn()) +} + +func preferAction(existing *policy.Action, candidate *policy.Action) *policy.Action { + if existing == nil { + return candidate + } + if candidate == nil { + return existing + } + + if existing.GetId() == "" && candidate.GetId() != "" { + return candidate + } + if existing.GetId() != "" && candidate.GetId() == "" { + return existing + } + + existingNS := existing.GetNamespace() + candidateNS := candidate.GetNamespace() + existingHasNS := existingNS != nil && (existingNS.GetId() != "" || existingNS.GetFqn() != "") + candidateHasNS := candidateNS != nil && (candidateNS.GetId() != "" || candidateNS.GetFqn() != "") + + if !existingHasNS && candidateHasNS { + return candidate + } + if existingHasNS && !candidateHasNS { + return existing + } + + return existing +} diff --git a/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions_test.go b/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions_test.go index 2b5a86c1a4..de31db4c9c 100644 --- a/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions_test.go +++ b/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions_test.go @@ -143,7 +143,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_SingleEntity(t *testi ), } - result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions(attributeMappings, []*entityresolutionV2.EntityRepresentation{engineeringEntity}) + result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions(attributeMappings, []*entityresolutionV2.EntityRepresentation{engineeringEntity}, nil) require.NoError(t, err) assert.Len(t, result, 1) @@ -187,6 +187,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_MultipleEntities(t *t result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions( attributeMappings, []*entityresolutionV2.EntityRepresentation{engineeringEntity, salesEntity}, + nil, ) // Validate results @@ -235,6 +236,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_NoMatchingEntities(t result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions( attributeMappings, []*entityresolutionV2.EntityRepresentation{marketingEntity}, + nil, ) // Validate results @@ -269,6 +271,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_MultipleAttributes(t result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions( attributeMappings, []*entityresolutionV2.EntityRepresentation{engineeringEntity}, + nil, ) // Validate results @@ -356,7 +359,7 @@ func TestEvaluateSubjectMappingsWithActions_OneGoodResolution(t *testing.T) { ), } // Execute function - entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, entity) + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, entity, nil) require.NoError(t, err) assert.Len(t, entitlements, 1) @@ -422,7 +425,7 @@ func TestEvaluateSubjectMappingsWithActions_MultipleMatchingSubjectMappings(t *t } // Execute function - entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, multiMatchEntity) + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, multiMatchEntity, nil) // Validate results require.NoError(t, err) @@ -450,6 +453,37 @@ func TestEvaluateSubjectMappingsWithActions_MultipleMatchingSubjectMappings(t *t assert.Equal(t, actions.ActionNameRead, internalActions[0].GetName()) } +func TestEvaluateSubjectMappingsWithActions_DeduplicatesConflictingActionNamesDeterministically(t *testing.T) { + entity := createEntityRepresentation("eng-entity", map[string]interface{}{ + "department": "engineering", + }) + + ns := &policy.Namespace{Id: "11111111-1111-1111-1111-111111111111", Fqn: "https://example.com"} + + sm := &policy.SubjectMapping{ + SubjectConditionSet: departmentEngineeringSM.GetSubjectConditionSet(), + Actions: []*policy.Action{ + {Name: actions.ActionNameRead}, + {Id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Name: actions.ActionNameRead, Namespace: ns}, + }, + } + + attributeMappings := map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue{ + classConfFQN: createAttributeMapping(classConfFQN, sm), + } + + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, entity, nil) + require.NoError(t, err) + + actionsList, exists := entitlements[classConfFQN] + require.True(t, exists) + require.Len(t, actionsList, 1) + + assert.Equal(t, actions.ActionNameRead, actionsList[0].GetName()) + assert.Equal(t, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", actionsList[0].GetId()) + assert.Equal(t, ns.GetId(), actionsList[0].GetNamespace().GetId()) +} + func TestEvaluateSubjectMappingsWithActions_NoMatchingSubjectMappings(t *testing.T) { // Setup test data with entity that doesn't match any subject mappings marketingEntity := createEntityRepresentation("marketing-entity", map[string]interface{}{ @@ -470,7 +504,7 @@ func TestEvaluateSubjectMappingsWithActions_NoMatchingSubjectMappings(t *testing } // Execute function - entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, marketingEntity) + entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, marketingEntity, nil) // Validate results require.NoError(t, err) @@ -556,7 +590,7 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr } // Test senior engineer - seniorEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, seniorEngEntity) + seniorEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, seniorEngEntity, nil) require.NoError(t, err) assert.Empty(t, seniorEntitlements) seniorActions, exists := seniorEntitlements[classRestrictedFQN] @@ -564,7 +598,7 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr assert.Empty(t, seniorActions) // Test principal engineer with admin - adminEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, principalEngWithAdmin) + adminEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, principalEngWithAdmin, nil) require.NoError(t, err) assert.Len(t, adminEntitlements, 1) seniorWithAdminActions, exists := adminEntitlements[classRestrictedFQN] @@ -578,7 +612,7 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr assert.Contains(t, actionNames, actions.ActionNameDelete) // Test senior engineer with admin in a different index - adminEntitlementsBadIndex, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, seniorEngWithAdminEntityInBadIndex) + adminEntitlementsBadIndex, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, seniorEngWithAdminEntityInBadIndex, nil) require.NoError(t, err) assert.Empty(t, adminEntitlementsBadIndex) adminActionsBadIndex, exists := adminEntitlementsBadIndex[classRestrictedFQN] @@ -586,7 +620,7 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr assert.Empty(t, adminActionsBadIndex) // Test non-engineering admin - nonEngAdminEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, nonEngAdminEntity) + nonEngAdminEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, nonEngAdminEntity, nil) require.NoError(t, err) assert.Empty(t, nonEngAdminEntitlements) nonEngAdminActions, exists := nonEngAdminEntitlements[classRestrictedFQN] diff --git a/service/kas/access/provider.go b/service/kas/access/provider.go index e9c01dbe0c..89f90b5e85 100644 --- a/service/kas/access/provider.go +++ b/service/kas/access/provider.go @@ -52,13 +52,15 @@ type KASConfig struct { // Enabling is required to parse KAOs with the `ec-wrapped` type, // and (currently) also enables responding with ECIES encrypted responses. ECTDFEnabled bool `mapstructure:"ec_tdf_enabled" json:"ec_tdf_enabled"` + HybridTDFEnabled bool `mapstructure:"hybrid_tdf_enabled" json:"hybrid_tdf_enabled"` Preview Preview `mapstructure:"preview" json:"preview"` RegisteredKASURI string `mapstructure:"registered_kas_uri" json:"registered_kas_uri"` } type Preview struct { - ECTDFEnabled bool `mapstructure:"ec_tdf_enabled" json:"ec_tdf_enabled"` - KeyManagement bool `mapstructure:"key_management" json:"key_management"` + ECTDFEnabled bool `mapstructure:"ec_tdf_enabled" json:"ec_tdf_enabled"` + HybridTDFEnabled bool `mapstructure:"hybrid_tdf_enabled" json:"hybrid_tdf_enabled"` + KeyManagement bool `mapstructure:"key_management" json:"key_management"` } // Specifies the preferred/default key for a given algorithm type. @@ -143,13 +145,14 @@ func (kasCfg KASConfig) String() string { } return fmt.Sprintf( - "KASConfig{Keyring:%v, ECCertID:%q, RSACertID:%q, RootKey:%s, KeyCacheExpiration:%s, ECTDFEnabled:%t, Preview:%+v, RegisteredKASURI:%q}", + "KASConfig{Keyring:%v, ECCertID:%q, RSACertID:%q, RootKey:%s, KeyCacheExpiration:%s, ECTDFEnabled:%t, HybridTDFEnabled:%t, Preview:%+v, RegisteredKASURI:%q}", kasCfg.Keyring, kasCfg.ECCertID, kasCfg.RSACertID, rootKeySummary, kasCfg.KeyCacheExpiration, kasCfg.ECTDFEnabled, + kasCfg.HybridTDFEnabled, kasCfg.Preview, kasCfg.RegisteredKASURI, ) @@ -168,6 +171,7 @@ func (kasCfg KASConfig) LogValue() slog.Value { slog.String("root_key", rootKeyVal), slog.Duration("key_cache_expiration", kasCfg.KeyCacheExpiration), slog.Bool("ec_tdf_enabled", kasCfg.ECTDFEnabled), + slog.Bool("hybrid_tdf_enabled", kasCfg.HybridTDFEnabled), slog.Any("preview", kasCfg.Preview), slog.String("registered_kas_uri", kasCfg.RegisteredKASURI), ) diff --git a/service/kas/access/publicKey.go b/service/kas/access/publicKey.go index eb5b1fe780..d7f381c307 100644 --- a/service/kas/access/publicKey.go +++ b/service/kas/access/publicKey.go @@ -76,9 +76,8 @@ func (p *Provider) LegacyPublicKey(ctx context.Context, req *connect.Request[kas p.Logger.ErrorContext(ctx, "keyDetails.ExportCertificate failed", slog.Any("error", err)) return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrConfig, errors.New("configuration error"))) } - case security.AlgorithmRSA2048: - fallthrough - case "": + case security.AlgorithmRSA2048, security.AlgorithmHPQTXWing, + security.AlgorithmHPQTSecp256r1MLKEM768, security.AlgorithmHPQTSecp384r1MLKEM1024, "": // For RSA keys, return the public key in PKCS8 format pem, err = keyDetails.ExportPublicKey(ctx, trust.KeyTypePKCS8) if err != nil { @@ -153,6 +152,14 @@ func (p *Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Pub // For EC keys, export the public key ecPublicKeyPem, err := keyDetails.ExportPublicKey(ctx, trust.KeyTypePKCS8) return r(ecPublicKeyPem, kid, err) + case security.AlgorithmHPQTXWing, + security.AlgorithmHPQTSecp256r1MLKEM768, + security.AlgorithmHPQTSecp384r1MLKEM1024: + switch fmt { + case "pkcs8", "": + publicKeyPEM, err := keyDetails.ExportPublicKey(ctx, trust.KeyTypePKCS8) + return r(publicKeyPEM, kid, err) + } case security.AlgorithmRSA2048: fallthrough case "": diff --git a/service/kas/access/publicKey_test.go b/service/kas/access/publicKey_test.go index a9be66def2..dd8bac58f7 100644 --- a/service/kas/access/publicKey_test.go +++ b/service/kas/access/publicKey_test.go @@ -154,7 +154,7 @@ func (m *MockSecurityProvider) DeriveKey(_ context.Context, _ trust.KeyDetails, return nil, errors.New("not implemented for tests") } -func (m *MockSecurityProvider) GenerateECSessionKey(_ context.Context, _ string) (trust.Encapsulator, error) { +func (m *MockSecurityProvider) GenerateECSessionKey(_ context.Context, _ string) (ocrypto.Encapsulator, error) { return nil, errors.New("not implemented for tests") } diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 92d4efadea..aba7ffa84e 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -1,7 +1,6 @@ package access import ( - "bytes" "context" "crypto" "crypto/ecdsa" @@ -29,7 +28,6 @@ import ( "github.com/opentdf/platform/lib/ocrypto" "github.com/opentdf/platform/protocol/go/entity" kaspb "github.com/opentdf/platform/protocol/go/kas" - "github.com/opentdf/platform/sdk" "github.com/opentdf/platform/service/internal/security" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/logger/audit" @@ -44,7 +42,6 @@ import ( const ( kTDF3Algorithm = "rsa:2048" - kNanoAlgorithm = "ec:secp256r1" kFailedStatus = "fail" kPermitStatus = "permit" additionalRewrapContextHeader = "X-Rewrap-Additional-Context" @@ -85,10 +82,6 @@ type kaoResult struct { // Optional: Present for EC wrapped responses EphemeralPublicKey []byte RequiredObligations []string - - // Only populated for Nano auditing - KeyID string - PolicyBinding string } // From policy ID to KAO ID to result @@ -103,15 +96,23 @@ type AdditionalRewrapContext struct { } const ( - kNanoTDFGMACLength = 8 - ErrUser = Error("request error") - ErrInternal = Error("internal error") - - ErrNanoTDFPolicyModeUnsupported = Error("unsupported policy mode") - + ErrUser = Error("request error") + ErrInternal = Error("internal error") errNoValidKeyAccessObjects = Error("no valid KAOs") ) +// Error helpers for KAS rewrap responses. +// +// SECURITY: Policy binding verification and DEK decryption failures MUST use the +// generic "bad request" message to avoid leaking information about computations +// involving secret key material. Non-secret failures (malformed input, unsupported +// key types, missing fields) SHOULD use descriptive messages so the SDK can +// distinguish misconfiguration from potential tamper. +// +// The SDK matches on the substring "desc = bad request" in serialized per-KAO +// errors to identify potential tamper (see sdk/tdferrors.go kasGenericBadRequest). +// Do not change the generic "bad request" message without updating the SDK +// constant, and do not use "bad request" in descriptive error messages. func err400(s string) error { return connect.NewError(connect.CodeInvalidArgument, errors.Join(ErrUser, status.Error(codes.InvalidArgument, s))) } @@ -294,7 +295,7 @@ func extractAndConvertV1SRTBody(body []byte) (kaspb.UnsignedRewrapRequest, error } kao := requestBody.KeyAccess - // ignore errors, maybe nanoTDF + // Ignore errors: legacy requests may omit or use non-standard policy binding formats. binding, _ := extractPolicyBinding(kao.PolicyBinding) reqs := []*kaspb.UnsignedRewrapRequest_WithPolicyRequest{ @@ -443,7 +444,7 @@ func verifyPolicyBinding(ctx context.Context, policy []byte, kao *kaspb.Unsigned if !hmac.Equal(actualHMAC, expectedHMAC) { //nolint:sloglint // usage of camelCase is intentional logger.WarnContext(ctx, "policy hmac mismatch", slog.String("policyBinding", policyBinding)) - return err400("bad request") + return err400("bad request") // Generic: involves secret key material } return nil @@ -563,12 +564,9 @@ func (p *Provider) Rewrap(ctx context.Context, req *connect.Request[kaspb.Rewrap resp := &kaspb.RewrapResponse{} - var nanoReqs []*kaspb.UnsignedRewrapRequest_WithPolicyRequest var tdf3Reqs []*kaspb.UnsignedRewrapRequest_WithPolicyRequest for _, req := range body.GetRequests() { switch { - case req.GetAlgorithm() == kNanoAlgorithm: - nanoReqs = append(nanoReqs, req) case req.GetAlgorithm() == "": req.Algorithm = kTDF3Algorithm tdf3Reqs = append(tdf3Reqs, req) @@ -580,23 +578,14 @@ func (p *Provider) Rewrap(ctx context.Context, req *connect.Request[kaspb.Rewrap additionalRewrapContext, err := getAdditionalRewrapContext(req.Header()) if err != nil { p.Logger.WarnContext(ctx, "failed to get additional rewrap context", slog.Any("error", err)) - return nil, err400(err.Error()) + return nil, err400("failed to get additional rewrap context") } - if len(tdf3Reqs) > 0 { - resp.SessionPublicKey, results, err = p.tdf3Rewrap(ctx, tdf3Reqs, body.GetClientPublicKey(), entityInfo, additionalRewrapContext) - if err != nil { - p.Logger.WarnContext(ctx, "status 400, tdf3 rewrap failure", slog.Any("error", err)) - return nil, err - } - addResultsToResponse(resp, results) - } else { - resp.SessionPublicKey, results, err = p.nanoTDFRewrap(ctx, nanoReqs, body.GetClientPublicKey(), entityInfo, additionalRewrapContext) - if err != nil { - p.Logger.WarnContext(ctx, "status 400, nanoTDF rewrap failure", slog.Any("error", err)) - return nil, err - } - addResultsToResponse(resp, results) + resp.SessionPublicKey, results, err = p.tdf3Rewrap(ctx, tdf3Reqs, body.GetClientPublicKey(), entityInfo, additionalRewrapContext) + if err != nil { + p.Logger.WarnContext(ctx, "status 400, tdf3 rewrap failure", slog.Any("error", err)) + return nil, err } + addResultsToResponse(resp, results) if isV1 { if len(results) != 1 { @@ -656,14 +645,14 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned for _, kao := range req.GetKeyAccessObjects() { if policyErr != nil { - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("bad request")) // Generic: corrupted policy body may indicate tamper continue } // Check if KeyAccessObject is nil if kao.GetKeyAccessObject() == nil { p.Logger.WarnContext(ctx, "key access object is nil", slog.String("kao_id", kao.GetKeyAccessObjectId())) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("key access object is nil")) continue } @@ -671,7 +660,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned wrappedKey := kao.GetKeyAccessObject().GetWrappedKey() if len(wrappedKey) == 0 { p.Logger.WarnContext(ctx, "wrapped key is empty", slog.String("kao_id", kao.GetKeyAccessObjectId())) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("wrapped key is empty")) continue } @@ -682,7 +671,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned if !p.ECTDFEnabled && !p.Preview.ECTDFEnabled { p.Logger.WarnContext(ctx, "ec-wrapped not enabled") - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("ec-wrapped not enabled")) continue } @@ -697,7 +686,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned slog.Any("kao", kao), slog.Any("error", err), ) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid ephemeral public key")) continue } @@ -708,7 +697,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned slog.Any("kao", kao), slog.Any("error", err), ) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("unsupported EC key size")) continue } @@ -720,7 +709,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned slog.Any("kao", kao), slog.Any("error", err), ) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid ephemeral public key PEM")) continue } @@ -731,14 +720,14 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned slog.Any("kao", kao), slog.Any("error", err), ) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid ephemeral public key")) continue } ecPub, ok := pub.(*ecdsa.PublicKey) if !ok { p.Logger.WarnContext(ctx, "not an EC public key", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("ephemeral key is not EC")) continue } @@ -746,7 +735,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned compressedKey, err := ocrypto.CompressedECPublicKey(mode, *ecPub) if err != nil { p.Logger.WarnContext(ctx, "failed to compress public key", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid EC public key")) continue } @@ -757,6 +746,20 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned failedKAORewrap(results, kao, err400("bad request")) continue } + case "hybrid-wrapped": + if !p.HybridTDFEnabled && !p.Preview.HybridTDFEnabled { + p.Logger.WarnContext(ctx, "hybrid-wrapped not enabled") + failedKAORewrap(results, kao, err400("bad request")) + continue + } + + kid := trust.KeyIdentifier(kao.GetKeyAccessObject().GetKid()) + dek, err = p.KeyDelegator.Decrypt(ctx, kid, kao.GetKeyAccessObject().GetWrappedKey(), nil) + if err != nil { + p.Logger.WarnContext(ctx, "failed to decrypt hybrid key", slog.Any("error", err)) + failedKAORewrap(results, kao, err400("bad request")) + continue + } case "wrapped": var kidsToCheck []trust.KeyIdentifier if kao.GetKeyAccessObject().GetKid() != "" { @@ -766,7 +769,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned kidsToCheck = p.listLegacyKeys(ctx) if len(kidsToCheck) == 0 { p.Logger.WarnContext(ctx, "failure to find legacy kids for rsa") - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("no legacy key IDs found")) continue } } @@ -785,12 +788,12 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned p.Logger.WarnContext(ctx, "unsupported key type", slog.String("key_type", keyType), slog.String("kao_id", kao.GetKeyAccessObjectId())) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("unsupported key type")) continue } if err != nil { p.Logger.WarnContext(ctx, "failure to decrypt dek", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("bad request")) // Generic: involves secret key material continue } @@ -807,7 +810,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned n, err := base64.StdEncoding.Decode(policyBinding, []byte(policyBindingB64Encoded)) if err != nil { p.Logger.WarnContext(ctx, "invalid policy binding encoding", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("bad request")) // Generic: malformed binding may indicate tamper continue } if n == 64 { //nolint:mnd // 32 bytes of hex encoded data = 256 bit sha-2 @@ -823,7 +826,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned // Verify policy binding using the UnwrappedKeyData interface if err := dek.VerifyBinding(ctx, []byte(req.GetPolicy().GetBody()), policyBinding); err != nil { p.Logger.WarnContext(ctx, "failure to verify policy binding", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("bad request")) // Generic: involves secret key material continue } @@ -889,12 +892,12 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew continue } policy, kaoResults, err := p.verifyRewrapRequests(ctx, req) - if err != nil && !errors.Is(err, errNoValidKeyAccessObjects) { - return "", nil, err400("invalid request") - } policyID := req.GetPolicy().GetId() results[policyID] = kaoResults if err != nil { + // Store per-KAO results even on error so tamper signals (e.g. corrupted + // policy body → generic "bad request") reach the SDK rather than being + // replaced by a top-level "invalid request". p.Logger.WarnContext(ctx, "rewrap: verifyRewrapRequests failed", slog.String("policy_id", policyID), @@ -1012,233 +1015,6 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew return sessionKey, results, nil } -func (p *Provider) nanoTDFRewrap(ctx context.Context, requests []*kaspb.UnsignedRewrapRequest_WithPolicyRequest, clientPublicKey string, entityInfo *entityInfo, additionalRewrapContext *AdditionalRewrapContext) (string, policyKAOResults, error) { - ctx, span := p.Start(ctx, "nanoTDFRewrap") - defer span.End() - - results := make(policyKAOResults) - - var policies []*Policy - policyReqs := make(map[*Policy]*kaspb.UnsignedRewrapRequest_WithPolicyRequest) - - for _, req := range requests { - policy, kaoResults, err := p.verifyNanoRewrapRequests(ctx, req) - if err != nil { - return "", nil, err400("invalid request") - } - results[req.GetPolicy().GetId()] = kaoResults - if policy != nil { - policies = append(policies, policy) - policyReqs[policy] = req - } - } - // do the access check - tok := &entity.Token{ - EphemeralId: "rewrap-tok", - Jwt: entityInfo.Token, - } - - pdpAccessResults, accessErr := p.canAccess(ctx, tok, policies, additionalRewrapContext.Obligations.FulfillableFQNs) - if accessErr != nil { - failAllKaos(requests, results, err500("could not perform access")) - return "", results, nil - } - - sessionKey, err := p.KeyDelegator.GenerateECSessionKey(ctx, clientPublicKey) - if err != nil { - p.Logger.WarnContext(ctx, "failure in GenerateNanoTDFSessionKey", slog.Any("error", err)) - failAllKaos(requests, results, err400("keypair mismatch")) - return "", results, nil - } - sessionKeyPEM, err := sessionKey.PublicKeyAsPEM() - if err != nil { - p.Logger.WarnContext(ctx, "failure in PublicKeyToPem", slog.Any("error", err)) - failAllKaos(requests, results, err500("")) - return "", results, nil - } - - for _, pdpAccess := range pdpAccessResults { - policy := pdpAccess.Policy - requiredObligationsForPolicy := pdpAccess.RequiredObligations - req, ok := policyReqs[policy] - if !ok { // this should not happen - continue - } - kaoResults, ok := results[req.GetPolicy().GetId()] - if !ok { // this should not happen - //nolint:sloglint // reference to key is intentional - p.Logger.WarnContext(ctx, "policy not found in policyReq response", "policy.uuid", policy.UUID) - continue - } - access := pdpAccess.Access - - // Audit the Nano Rewrap - kasPolicy := ConvertToAuditKasPolicy(*policy) - - for _, kao := range req.GetKeyAccessObjects() { - kaoInfo := kaoResults[kao.GetKeyAccessObjectId()] - if kaoInfo.Error != nil { - continue - } - - auditEventParams := audit.RewrapAuditEventParams{ - Policy: kasPolicy, - IsSuccess: access, - TDFFormat: "Nano", - Algorithm: req.GetAlgorithm(), - KeyID: kaoInfo.KeyID, - PolicyBinding: kaoInfo.PolicyBinding, - } - - if !access { - p.Logger.Audit.RewrapFailure(ctx, auditEventParams) - failedKAORewrapWithObligations(kaoResults, kao, err403("forbidden"), requiredObligationsForPolicy) - continue - } - cipherText, err := kaoInfo.DEK.Export(sessionKey) - if err != nil { - p.Logger.Audit.RewrapFailure(ctx, auditEventParams) - failedKAORewrap(kaoResults, kao, err403("forbidden")) - continue - } - - kaoResults[kao.GetKeyAccessObjectId()] = kaoResult{ - ID: kao.GetKeyAccessObjectId(), - Encapped: cipherText, - RequiredObligations: requiredObligationsForPolicy, - } - - p.Logger.Audit.RewrapSuccess(ctx, auditEventParams) - } - } - return sessionKeyPEM, results, nil -} - -func (p *Provider) verifyNanoRewrapRequests(ctx context.Context, req *kaspb.UnsignedRewrapRequest_WithPolicyRequest) (*Policy, map[string]kaoResult, error) { - results := make(map[string]kaoResult) - - // Check if req is nil - if req == nil { - p.Logger.WarnContext(ctx, "request is nil") - return nil, nil, errors.New("request is nil") - } - - for _, kao := range req.GetKeyAccessObjects() { - // there should never be multiple KAOs in policy - if len(req.GetKeyAccessObjects()) != 1 { - failedKAORewrap(results, kao, err400("NanoTDFs should not have multiple KAOs per Policy")) - continue - } - - headerReader := bytes.NewReader(kao.GetKeyAccessObject().GetHeader()) - header, _, err := sdk.NewNanoTDFHeaderFromReader(headerReader) - if err != nil { - failedKAORewrap(results, kao, fmt.Errorf("failed to parse NanoTDF header: %w", err)) - return nil, results, nil - } - // Lookup KID from nano header - kid, err := header.GetKasURL().GetIdentifier() - if err != nil { - p.Logger.DebugContext(ctx, - "nanoTDFRewrap GetIdentifier", - slog.String("kid", kid), - slog.Any("error", err), - ) - // legacy nano with KID - kid, err = p.lookupKid(ctx, security.AlgorithmECP256R1) - if err != nil { - p.Logger.ErrorContext(ctx, "failure to find default kid for ec", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) - continue - } - p.Logger.DebugContext(ctx, "nanoTDFRewrap lookupKid", slog.String("kid", kid)) - } - p.Logger.DebugContext(ctx, "nanoTDFRewrap", slog.String("kid", kid)) - ecCurve, err := header.ECCurve() - if err != nil { - failedKAORewrap(results, kao, fmt.Errorf("ECCurve failed: %w", err)) - return nil, results, nil - } - - symmetricKey, err := p.KeyDelegator.DeriveKey(ctx, trust.KeyIdentifier(kid), header.EphemeralKey, ecCurve) - if err != nil { - failedKAORewrap(results, kao, fmt.Errorf("failed to generate symmetric key: %w", err)) - return nil, results, nil - } - - // extract the policy - policy, err := extractNanoPolicy(symmetricKey, header) - if err != nil { - failedKAORewrap(results, kao, fmt.Errorf("Error extracting policy: %w", err)) - return nil, results, nil - } - - // check the policy binding - binding, err := header.PolicyBinding() - if err != nil { - failedKAORewrap(results, kao, fmt.Errorf("failed to retrieve policy binding: %w", err)) - return nil, results, nil - } - - verify, err := binding.Verify() - if err != nil { - failedKAORewrap(results, kao, fmt.Errorf("error verifying policy binding: %w", err)) - return nil, results, nil - } - - if !verify { - failedKAORewrap(results, kao, errors.New("policy binding verification failed")) - return nil, results, nil - } - results[kao.GetKeyAccessObjectId()] = kaoResult{ - ID: kao.GetKeyAccessObjectId(), - DEK: symmetricKey, - KeyID: kid, - PolicyBinding: binding.String(), - } - return policy, results, nil - } - return nil, results, nil -} - -func extractNanoPolicy(symmetricKey ocrypto.ProtectedKey, header sdk.NanoTDFHeader) (*Policy, error) { - const ( - kIvLen = 12 - ) - - var policy Policy - switch header.PolicyMode { - case sdk.NanoTDFPolicyModePlainText: - err := json.Unmarshal(header.PolicyBody, &policy) - if err != nil { - return nil, fmt.Errorf("error unmarshalling plaintext policy: %w", err) - } - return &policy, nil - - case sdk.NanoTDFPolicyModeEncrypted: - iv := make([]byte, kIvLen) - tagSize, err := sdk.SizeOfAuthTagForCipher(header.GetCipher()) - if err != nil { - return nil, fmt.Errorf("SizeOfAuthTagForCipher failed: %w", err) - } - - policyData, err := symmetricKey.DecryptAESGCM(iv, header.PolicyBody, tagSize) - if err != nil { - return nil, fmt.Errorf("error decrypting policy body: %w", err) - } - - err = json.Unmarshal(policyData, &policy) - if err != nil { - return nil, fmt.Errorf("error unmarshalling encrypted policy: %w", err) - } - return &policy, nil - case sdk.NanoTDFPolicyModeRemote, sdk.NanoTDFPolicyModeEncryptedPolicyKeyAccess: - default: - // noop - } - return nil, errors.Join(fmt.Errorf("unsupported policy mode: %d", header.PolicyMode), ErrNanoTDFPolicyModeUnsupported) -} - func failAllKaos(reqs []*kaspb.UnsignedRewrapRequest_WithPolicyRequest, results policyKAOResults, err error) { for _, req := range reqs { for _, kao := range req.GetKeyAccessObjects() { diff --git a/service/kas/access/rewrap_test.go b/service/kas/access/rewrap_test.go index 269051468c..bd62647483 100644 --- a/service/kas/access/rewrap_test.go +++ b/service/kas/access/rewrap_test.go @@ -394,7 +394,7 @@ type PolicyBinding struct { func keyAccessWrappedRaw(t *testing.T, policyBindingAsString bool) kaspb.UnsignedRewrapRequest_WithKeyAccessObject { policyBytes := fauxPolicyBytes(t) - asym, err := ocrypto.NewAsymEncryption(rsaPublicAlt) + asym, err := ocrypto.FromPublicPEM(rsaPublicAlt) require.NoError(t, err, "rewrap: NewAsymEncryption failed") wrappedKey, err := asym.Encrypt([]byte(plainKey)) require.NoError(t, err, "rewrap: encryptWithPublicKey failed") @@ -1431,119 +1431,3 @@ func TestVerifyRewrapRequests(t *testing.T) { }) } } - -func TestVerifyNanoRewrapRequests(t *testing.T) { - testLogger := logger.CreateTestLogger() - - tests := []struct { - name string - setupProvider func() *Provider - request *kaspb.UnsignedRewrapRequest_WithPolicyRequest - expectError bool - errorMessage string - checkResultErrors bool // Whether to check for errors in the results map - expectedResultCount int // Expected number of results when checkResultErrors is true - }{ - { - name: "nil request should return error", - setupProvider: func() *Provider { - return &Provider{ - Logger: testLogger, - } - }, - request: nil, - expectError: true, - errorMessage: "request is nil", - checkResultErrors: false, - expectedResultCount: 0, - }, - { - name: "multiple KAOs should return error", - setupProvider: func() *Provider { - return &Provider{ - Logger: testLogger, - } - }, - request: &kaspb.UnsignedRewrapRequest_WithPolicyRequest{ - Policy: &kaspb.UnsignedRewrapRequest_WithPolicy{ - Id: "test-policy", - Body: "test-body", - }, - KeyAccessObjects: []*kaspb.UnsignedRewrapRequest_WithKeyAccessObject{ - { - KeyAccessObjectId: "test-kao-1", - KeyAccessObject: &kaspb.KeyAccess{ - Header: []byte("fake-header-1"), - }, - }, - { - KeyAccessObjectId: "test-kao-2", - KeyAccessObject: &kaspb.KeyAccess{ - Header: []byte("fake-header-2"), - }, - }, - }, - }, - expectError: false, // Error goes into results, not returned directly - errorMessage: "NanoTDFs should not have multiple KAOs per Policy", - checkResultErrors: true, // Check for errors in the results map - expectedResultCount: 2, // Should have two KAO results with errors - }, - { - name: "invalid NanoTDF header should fail gracefully", - setupProvider: func() *Provider { - return &Provider{ - Logger: testLogger, - } - }, - request: &kaspb.UnsignedRewrapRequest_WithPolicyRequest{ - Policy: &kaspb.UnsignedRewrapRequest_WithPolicy{ - Id: "test-policy", - Body: "test-body", - }, - KeyAccessObjects: []*kaspb.UnsignedRewrapRequest_WithKeyAccessObject{ - { - KeyAccessObjectId: "test-kao", - KeyAccessObject: &kaspb.KeyAccess{ - Header: []byte("invalid-nano-header"), // Invalid header that will fail parsing - }, - }, - }, - }, - expectError: false, // Function returns nil, results, nil (fails gracefully) - errorMessage: "", - checkResultErrors: false, - expectedResultCount: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - provider := tt.setupProvider() - ctx := t.Context() - - // Test that the function doesn't panic and returns appropriate errors - var err error - var results map[string]kaoResult - - assert.NotPanics(t, func() { - _, results, err = provider.verifyNanoRewrapRequests(ctx, tt.request) - }, "Function should not panic: "+tt.name) - - if tt.expectError { - require.Error(t, err, "Expected error but got none: "+tt.name) - assert.Contains(t, err.Error(), tt.errorMessage, "Error message should contain expected text: "+tt.errorMessage) - } else { - assert.NotNil(t, results, "Results should not be nil") - if tt.checkResultErrors { - // Check for errors stored in results map - assert.Len(t, results, tt.expectedResultCount, "Should have expected number of KAO results with errors") - for _, result := range results { - require.Error(t, result.Error, "KAO result should contain error") - assert.Contains(t, result.Error.Error(), tt.errorMessage, "Error should contain expected message") - } - } - } - }) - } -} diff --git a/service/kas/kas.go b/service/kas/kas.go index 2e0960862e..77d0bbffcf 100644 --- a/service/kas/kas.go +++ b/service/kas/kas.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/go-viper/mapstructure/v2" + "github.com/opentdf/platform/lib/ocrypto" kaspb "github.com/opentdf/platform/protocol/go/kas" "github.com/opentdf/platform/protocol/go/kas/kasconnect" "github.com/opentdf/platform/service/internal/security" @@ -21,14 +22,15 @@ import ( ) func OnConfigUpdate(p *access.Provider) serviceregistry.OnConfigUpdateHook { - return func(_ context.Context, cfg config.ServiceConfig) error { + return func(ctx context.Context, cfg config.ServiceConfig) error { var kasCfg access.KASConfig if err := mapstructure.Decode(cfg, &kasCfg); err != nil { return fmt.Errorf("invalid kas cfg [%v] %w", cfg, err) } p.ApplyConfig(kasCfg, p.SecurityConfig()) - p.Logger.Info("kas config reloaded") + p.Logger.TraceContext(ctx, "kas config reloaded", slog.Any("config", kasCfg)) + logSupportedMechanisms(ctx, p.Logger, p.KeyDelegator, &kasCfg) return nil } @@ -39,11 +41,10 @@ func NewRegistration() *serviceregistry.Service[kasconnect.AccessServiceHandler] onConfigUpdate := OnConfigUpdate(p) return &serviceregistry.Service[kasconnect.AccessServiceHandler]{ ServiceOptions: serviceregistry.ServiceOptions[kasconnect.AccessServiceHandler]{ - Namespace: "kas", - ServiceDesc: &kaspb.AccessService_ServiceDesc, - ConnectRPCFunc: kasconnect.NewAccessServiceHandler, - GRPCGatewayFunc: kaspb.RegisterAccessServiceHandler, - OnConfigUpdate: onConfigUpdate, + Namespace: "kas", + ServiceDesc: &kaspb.AccessService_ServiceDesc, + ConnectRPCFunc: kasconnect.NewAccessServiceHandler, + OnConfigUpdate: onConfigUpdate, RegisterFunc: func(srp serviceregistry.RegistrationParams) (kasconnect.AccessServiceHandler, serviceregistry.HandlerServer) { var kasCfg access.KASConfig if err := mapstructure.Decode(srp.Config, &kasCfg); err != nil { @@ -76,18 +77,18 @@ func NewRegistration() *serviceregistry.Service[kasconnect.AccessServiceHandler] // Configure new delegation service p.KeyDelegator = trust.NewDelegatingKeyService(NewPlatformKeyIndexer(srp.SDK, kasURL.String(), srp.Logger), srp.Logger, cacheClient) for _, manager := range srp.KeyManagerCtxFactories { - p.KeyDelegator.RegisterKeyManagerCtx(manager.Name, manager.Factory) + p.KeyDelegator.RegisterKeyManagerCtxWithAlgorithms(manager.Name, manager.Factory, manager.SupportedAlgorithms) kmgrs = append(kmgrs, manager.Name) } // Register Basic Key Manager - p.KeyDelegator.RegisterKeyManagerCtx(security.BasicManagerName, func(_ context.Context, opts *trust.KeyManagerFactoryOptions) (trust.KeyManager, error) { + p.KeyDelegator.RegisterKeyManagerCtxWithAlgorithms(security.BasicManagerName, func(_ context.Context, opts *trust.KeyManagerFactoryOptions) (trust.KeyManager, error) { bm, err := security.NewBasicManager(opts.Logger, opts.Cache, kasCfg.RootKey) if err != nil { return nil, err } return bm, nil - }) + }, security.BasicManagerSupportedAlgorithms) kmgrs = append(kmgrs, security.BasicManagerName) // Explicitly set the default manager for session key generation. // This should be configurable, e.g., defaulting to BasicManager or an HSM if available. @@ -95,14 +96,14 @@ func NewRegistration() *serviceregistry.Service[kasconnect.AccessServiceHandler] } else { // Set up both the legacy CryptoProvider and the new SecurityProvider kasCfg.UpgradeMapToKeyring(srp.OTDF.CryptoProvider) - p.CryptoProvider = srp.OTDF.CryptoProvider + p.CryptoProvider = srp.OTDF.CryptoProvider //nolint:staticcheck // Legacy field retained during migration. - inProcessService := initSecurityProviderAdapter(p.CryptoProvider, kasCfg, srp.Logger) + inProcessService := initSecurityProviderAdapter(p.CryptoProvider, kasCfg, srp.Logger) //nolint:staticcheck // Legacy field retained during migration. p.KeyDelegator = trust.NewDelegatingKeyService(inProcessService, srp.Logger, nil) - p.KeyDelegator.RegisterKeyManagerCtx(inProcessService.Name(), func(_ context.Context, _ *trust.KeyManagerFactoryOptions) (trust.KeyManager, error) { + p.KeyDelegator.RegisterKeyManagerCtxWithAlgorithms(inProcessService.Name(), func(_ context.Context, _ *trust.KeyManagerFactoryOptions) (trust.KeyManager, error) { return inProcessService, nil - }) + }, security.InProcessSupportedAlgorithms) // Set default for non-key-management mode p.KeyDelegator.SetDefaultMode(inProcessService.Name(), "", nil) kmgrs = append(kmgrs, inProcessService.Name()) @@ -114,7 +115,8 @@ func NewRegistration() *serviceregistry.Service[kasconnect.AccessServiceHandler] p.ApplyConfig(kasCfg, srp.Security) p.Tracer = srp.Tracer - srp.Logger.Debug("kas config", slog.Any("config", kasCfg)) + srp.Logger.Debug("kas config loaded", slog.Any("config", kasCfg)) + logSupportedMechanisms(context.Background(), srp.Logger, p.KeyDelegator, &kasCfg) if err := srp.RegisterReadinessCheck("kas", p.IsReady); err != nil { srp.Logger.Error("failed to register kas readiness check", slog.String("error", err.Error())) @@ -181,6 +183,39 @@ func determineKASURL(srp serviceregistry.RegistrationParams, kasCfg access.KASCo return kasURL, nil } +// logSupportedMechanisms emits a single INFO entry listing the cryptographic +// mechanisms this KAS instance is configured to serve. The mechanism set is +// sourced from the trust KeyManagers (what they could serve if a key were +// provisioned) and filtered by the same preview-feature gates rewrap.go +// enforces, so the log only advertises algorithms rewrap would actually accept. +func logSupportedMechanisms(ctx context.Context, l *logger.Logger, kd *trust.DelegatingKeyService, kasCfg *access.KASConfig) { + if l == nil || kd == nil || kasCfg == nil { + return + } + mechanisms := filterMechanismsByPreview(kd.SupportedAlgorithms(ctx), kasCfg) + l.InfoContext(ctx, "kas trust mechanisms initialized", slog.Any("mechanisms", mechanisms)) +} + +// filterMechanismsByPreview drops algorithms whose corresponding rewrap path is +// disabled. Keep aligned with the gating in service/kas/access/rewrap.go for +// "ec-wrapped" and "hybrid-wrapped" key access objects. +func filterMechanismsByPreview(algs []ocrypto.KeyType, kasCfg *access.KASConfig) []ocrypto.KeyType { + ecEnabled := kasCfg.ECTDFEnabled || kasCfg.Preview.ECTDFEnabled + hybridEnabled := kasCfg.HybridTDFEnabled || kasCfg.Preview.HybridTDFEnabled + + out := make([]ocrypto.KeyType, 0, len(algs)) + for _, a := range algs { + switch { + case !ecEnabled && ocrypto.IsECKeyType(a): + continue + case !hybridEnabled && ocrypto.IsHybridKeyType(a): + continue + } + out = append(out, a) + } + return out +} + func initSecurityProviderAdapter(cryptoProvider *security.StandardCrypto, kasCfg access.KASConfig, l *logger.Logger) trust.KeyService { var defaults []string var legacies []string @@ -192,7 +227,7 @@ func initSecurityProviderAdapter(cryptoProvider *security.StandardCrypto, kasCfg } } if len(defaults) == 0 && len(legacies) == 0 { - for _, alg := range []string{security.AlgorithmECP256R1, security.AlgorithmRSA2048} { + for _, alg := range []string{security.AlgorithmECP256R1, security.AlgorithmRSA2048, security.AlgorithmHPQTXWing} { kid := cryptoProvider.FindKID(alg) if kid != "" { defaults = append(defaults, kid) diff --git a/service/kas/kas.proto b/service/kas/kas.proto index 6945a1208a..6168a6a5f8 100644 --- a/service/kas/kas.proto +++ b/service/kas/kas.proto @@ -2,21 +2,8 @@ syntax = "proto3"; package kas; -import "google/api/annotations.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/wrappers.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "OpenTDF Key Access Service"; - version: "1.5.0"; - license: { - name: "BSD 3-Clause Clear"; - url: "https://github.com/opentdf/backend/blob/master/LICENSE"; - }; - }; -}; message InfoRequest { // Intentionally empty. May include features later. @@ -68,7 +55,7 @@ message KeyAccess { // Type of key wrapping used for the data encryption key // Required: Always - // Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped) + // Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped), 'hybrid-wrapped' (experimental X-Wing-wrapped) string key_type = 4 [json_name = "type"]; // URL of the Key Access Server that can unwrap this key @@ -94,15 +81,14 @@ message KeyAccess { // This is the core cryptographic material needed for TDF decryption bytes wrapped_key = 8; - // Complete NanoTDF header containing all metadata and policy information - // Required: NanoTDF only - // ZTDF: Omitted (policy and metadata are separate) + // Complete header containing all metadata and policy information (for formats that embed it) + // Optional: Not used by ZTDF (policy and metadata are separate) // Contains magic bytes, version, algorithm, policy, and ephemeral key information bytes header = 9; // Ephemeral public key for ECDH key derivation (ec-wrapped type only) // Required: When key_type="ec-wrapped" (experimental ECDH-based ZTDF) - // Omitted: When key_type="wrapped" (RSA-based ZTDF) + // Omitted: When key_type="wrapped" or key_type="hybrid-wrapped" // Should be a PEM-encoded PKCS#8 (ASN.1) formatted public key // Used to derive the symmetric key for unwrapping the DEK string ephemeral_public_key = 10; @@ -142,8 +128,7 @@ message UnsignedRewrapRequest { message WithPolicyRequest { // List of Key Access Objects associated with this policy // Required: Always (at least one) - // NanoTDF: Exactly one KAO per policy - // ZTDF: One or more KAOs per policy + // Some formats require exactly one KAO per policy repeated WithKeyAccessObject key_access_objects = 1; // Policy information for this group of KAOs @@ -152,7 +137,7 @@ message UnsignedRewrapRequest { // Cryptographic algorithm identifier for the TDF type // Optional: Defaults to rsa:2048 if omitted - // Values: "ec:secp256r1" (NanoTDF), "rsa:2048" (ZTDF), "" (defaults to rsa:2048) + // Values: "ec:secp256r1" (EC-based), "rsa:2048" (RSA-based), "" (defaults to rsa:2048) // Example: "ec:secp256r1" string algorithm = 3; } @@ -183,9 +168,9 @@ message UnsignedRewrapRequest { string algorithm = 5 [deprecated = true]; } message PublicKeyRequest { - string algorithm = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "algorithm type rsa: or ec:"}]; - string fmt = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "response format"}]; - string v = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "request version"}]; + string algorithm = 1; + string fmt = 2; + string v = 3; } message PublicKeyResponse { @@ -259,8 +244,8 @@ message RewrapResponse { bytes entity_wrapped_key = 2 [deprecated = true]; // KAS's ephemeral session public key in PEM format - // Required: For EC-based operations (NanoTDF and ZTDF with key_type="ec-wrapped") - // Optional: Empty for RSA-based ZTDF (key_type="wrapped") + // Required: For EC-based operations (key_type="ec-wrapped") + // Optional: Empty for RSA-based or X-Wing-based ZTDF (key_type="wrapped" or key_type="hybrid-wrapped") // Used by client to perform ECDH key agreement and decrypt the kas_wrapped_key values string session_public_key = 3; @@ -277,11 +262,6 @@ message RewrapResponse { // Get app info from the root path service AccessService { rpc PublicKey(PublicKeyRequest) returns (PublicKeyResponse) { - option (google.api.http) = {get: "/kas/v2/kas_public_key"}; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: {key: "200"} - }; option idempotency_level = NO_SIDE_EFFECTS; } @@ -291,23 +271,9 @@ service AccessService { // // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME rpc LegacyPublicKey(LegacyPublicKeyRequest) returns (google.protobuf.StringValue) { - option (google.api.http) = {get: "/kas/kas_public_key"}; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: {key: "200"} - }; option idempotency_level = NO_SIDE_EFFECTS; option deprecated = true; } - rpc Rewrap(RewrapRequest) returns (RewrapResponse) { - option (google.api.http) = { - post: "/kas/v2/rewrap" - body: "*"; - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: {key: "200"} - }; - } + rpc Rewrap(RewrapRequest) returns (RewrapResponse) {} } diff --git a/service/kas/kas_test.go b/service/kas/kas_test.go new file mode 100644 index 0000000000..4772de138b --- /dev/null +++ b/service/kas/kas_test.go @@ -0,0 +1,226 @@ +package kas + +import ( + "bytes" + "context" + "crypto/elliptic" + "encoding/json" + "errors" + "log/slog" + "strings" + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/service/kas/access" + "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/logger/audit" + "github.com/opentdf/platform/service/trust" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFilterMechanismsByPreview(t *testing.T) { + allAlgs := []ocrypto.KeyType{ + "rsa:2048", + "rsa:4096", + "ec:secp256r1", + "ec:secp384r1", + "hpqt:xwing", + "hpqt:secp256r1-mlkem768", + } + + tests := []struct { + name string + algs []ocrypto.KeyType + cfg *access.KASConfig + want []ocrypto.KeyType + }{ + { + name: "both flags off drops ec and hpqt", + algs: allAlgs, + cfg: &access.KASConfig{}, + want: []ocrypto.KeyType{"rsa:2048", "rsa:4096"}, + }, + { + name: "top-level ec flag on keeps ec only", + algs: allAlgs, + cfg: &access.KASConfig{ECTDFEnabled: true}, + want: []ocrypto.KeyType{"rsa:2048", "rsa:4096", "ec:secp256r1", "ec:secp384r1"}, + }, + { + name: "preview ec flag on keeps ec only", + algs: allAlgs, + cfg: &access.KASConfig{Preview: access.Preview{ECTDFEnabled: true}}, + want: []ocrypto.KeyType{"rsa:2048", "rsa:4096", "ec:secp256r1", "ec:secp384r1"}, + }, + { + name: "top-level hybrid flag on keeps hpqt only", + algs: allAlgs, + cfg: &access.KASConfig{HybridTDFEnabled: true}, + want: []ocrypto.KeyType{"rsa:2048", "rsa:4096", "hpqt:xwing", "hpqt:secp256r1-mlkem768"}, + }, + { + name: "preview hybrid flag on keeps hpqt only", + algs: allAlgs, + cfg: &access.KASConfig{Preview: access.Preview{HybridTDFEnabled: true}}, + want: []ocrypto.KeyType{"rsa:2048", "rsa:4096", "hpqt:xwing", "hpqt:secp256r1-mlkem768"}, + }, + { + name: "both flags on keeps everything", + algs: allAlgs, + cfg: &access.KASConfig{ECTDFEnabled: true, HybridTDFEnabled: true}, + want: allAlgs, + }, + { + name: "both preview flags on keeps everything", + algs: allAlgs, + cfg: &access.KASConfig{Preview: access.Preview{ + ECTDFEnabled: true, + HybridTDFEnabled: true, + }}, + want: allAlgs, + }, + { + name: "empty input returns empty", + algs: []ocrypto.KeyType{}, + cfg: &access.KASConfig{ECTDFEnabled: true, HybridTDFEnabled: true}, + want: []ocrypto.KeyType{}, + }, + { + name: "rsa always passes through with no flags", + algs: []ocrypto.KeyType{"rsa:2048"}, + cfg: &access.KASConfig{}, + want: []ocrypto.KeyType{"rsa:2048"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := filterMechanismsByPreview(tc.algs, tc.cfg) + assert.Equal(t, tc.want, got) + }) + } +} + +// fakeKeyManager is a minimal trust.KeyManager used to satisfy registration. +// SupportedAlgorithms now reads from registered metadata, not from the manager, +// so this type doesn't need to declare any algorithms itself. +type fakeKeyManager struct { + name string +} + +func (m *fakeKeyManager) Name() string { return m.name } +func (m *fakeKeyManager) Decrypt(_ context.Context, _ trust.KeyDetails, _, _ []byte) (ocrypto.ProtectedKey, error) { + return nil, nil //nolint:nilnil // unused in this test +} + +func (m *fakeKeyManager) DeriveKey(_ context.Context, _ trust.KeyDetails, _ []byte, _ elliptic.Curve) (ocrypto.ProtectedKey, error) { + return nil, nil //nolint:nilnil // unused in this test +} + +func (m *fakeKeyManager) GenerateECSessionKey(_ context.Context, _ string) (ocrypto.Encapsulator, error) { + return nil, nil //nolint:nilnil // unused in this test +} +func (m *fakeKeyManager) Close() {} + +// stubKeyIndex satisfies trust.KeyIndex without any backing storage. +type stubKeyIndex struct{} + +func (stubKeyIndex) String() string { return "stubKeyIndex" } +func (stubKeyIndex) LogValue() slog.Value { return slog.StringValue("stubKeyIndex") } +func (stubKeyIndex) FindKeyByAlgorithm(_ context.Context, _ string, _ bool) (trust.KeyDetails, error) { + return nil, errors.New("not implemented") +} + +func (stubKeyIndex) FindKeyByID(_ context.Context, _ trust.KeyIdentifier) (trust.KeyDetails, error) { + return nil, errors.New("not implemented") +} +func (stubKeyIndex) ListKeys(_ context.Context) ([]trust.KeyDetails, error) { return nil, nil } +func (stubKeyIndex) ListKeysWith(_ context.Context, _ trust.ListKeyOptions) ([]trust.KeyDetails, error) { + return nil, nil +} + +func newBufferLogger() (*logger.Logger, *bytes.Buffer) { + buf := &bytes.Buffer{} + handler := slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + auditBase := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})) + return &logger.Logger{ + Logger: slog.New(handler), + Audit: audit.CreateAuditLogger(*auditBase), + }, buf +} + +func TestLogSupportedMechanisms_EmitsInfoLine(t *testing.T) { + l, buf := newBufferLogger() + + kd := trust.NewDelegatingKeyService(stubKeyIndex{}, l, nil) + kd.RegisterKeyManagerCtxWithAlgorithms("fake", func(_ context.Context, _ *trust.KeyManagerFactoryOptions) (trust.KeyManager, error) { + return &fakeKeyManager{name: "fake"}, nil + }, []ocrypto.KeyType{"rsa:2048", "ec:secp256r1", "hpqt:xwing"}) + kd.SetDefaultMode("fake", "", nil) + + tests := []struct { + name string + cfg *access.KASConfig + wantMechanisms []string + }{ + { + name: "no preview flags", + cfg: &access.KASConfig{}, + wantMechanisms: []string{"rsa:2048"}, + }, + { + name: "both preview flags", + cfg: &access.KASConfig{Preview: access.Preview{ECTDFEnabled: true, HybridTDFEnabled: true}}, + wantMechanisms: []string{"ec:secp256r1", "hpqt:xwing", "rsa:2048"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + buf.Reset() + logSupportedMechanisms(context.Background(), l, kd, tc.cfg) + + data := strings.TrimSpace(buf.String()) + require.NotEmpty(t, data) + lines := strings.Split(data, "\n") + + var found map[string]any + for _, line := range lines { + var record map[string]any + require.NoError(t, json.Unmarshal([]byte(line), &record)) + if msg, _ := record["msg"].(string); msg == "kas trust mechanisms initialized" { + found = record + break + } + } + require.NotNil(t, found, "expected log record with msg=kas trust mechanisms initialized") + require.Equal(t, "INFO", found["level"]) + + rawMechs, ok := found["mechanisms"].([]any) + require.True(t, ok, "mechanisms field should be a slice") + gotMechs := make([]string, 0, len(rawMechs)) + for _, m := range rawMechs { + s, isStr := m.(string) + require.True(t, isStr) + gotMechs = append(gotMechs, s) + } + assert.Equal(t, tc.wantMechanisms, gotMechs) + }) + } +} + +func TestLogSupportedMechanisms_NilSafe(t *testing.T) { + l, buf := newBufferLogger() + kd := trust.NewDelegatingKeyService(stubKeyIndex{}, l, nil) + + // All three permutations of nil arg should be a no-op. + logSupportedMechanisms(context.Background(), nil, kd, &access.KASConfig{}) + logSupportedMechanisms(context.Background(), l, nil, &access.KASConfig{}) + logSupportedMechanisms(context.Background(), l, kd, nil) + + assert.Empty(t, buf.String(), "no log output expected when args are nil") +} + +// Compile-time check: fakeKeyManager satisfies trust.KeyManager. +var _ trust.KeyManager = (*fakeKeyManager)(nil) diff --git a/service/kas/key_indexer.go b/service/kas/key_indexer.go index 021b3f418a..9c336e2bae 100644 --- a/service/kas/key_indexer.go +++ b/service/kas/key_indexer.go @@ -46,42 +46,6 @@ func NewPlatformKeyIndexer(sdk *sdk.SDK, kasURI string, l *logger.Logger) *KeyIn } } -func convertEnumToAlg(alg policy.Algorithm) ocrypto.KeyType { - switch alg { - case policy.Algorithm_ALGORITHM_RSA_2048: - return ocrypto.RSA2048Key - case policy.Algorithm_ALGORITHM_RSA_4096: - return ocrypto.RSA4096Key - case policy.Algorithm_ALGORITHM_EC_P256: - return ocrypto.EC256Key - case policy.Algorithm_ALGORITHM_EC_P384: - return ocrypto.EC384Key - case policy.Algorithm_ALGORITHM_EC_P521: - return ocrypto.EC521Key - case policy.Algorithm_ALGORITHM_UNSPECIFIED: - fallthrough - default: - return "" - } -} - -func convertAlgToEnum(alg string) (policy.Algorithm, error) { - switch alg { - case string(ocrypto.RSA2048Key): - return policy.Algorithm_ALGORITHM_RSA_2048, nil - case string(ocrypto.RSA4096Key): - return policy.Algorithm_ALGORITHM_RSA_4096, nil - case string(ocrypto.EC256Key): - return policy.Algorithm_ALGORITHM_EC_P256, nil - case string(ocrypto.EC384Key): - return policy.Algorithm_ALGORITHM_EC_P384, nil - case string(ocrypto.EC521Key): - return policy.Algorithm_ALGORITHM_EC_P521, nil - default: - return policy.Algorithm_ALGORITHM_UNSPECIFIED, fmt.Errorf("unsupported algorithm: %s", alg) - } -} - func (p *KeyIndexer) String() string { return fmt.Sprintf("PlatformKeyIndexer[%s]", p.kasURI) } @@ -91,7 +55,11 @@ func (p *KeyIndexer) LogValue() slog.Value { } func (p *KeyIndexer) FindKeyByAlgorithm(ctx context.Context, algorithm string, includeLegacy bool) (trust.KeyDetails, error) { - alg, err := convertAlgToEnum(algorithm) + kt, err := ocrypto.ParseKeyType(algorithm) + if err != nil { + return nil, err + } + alg, err := sdk.KeyTypeToPolicyAlgorithm(kt) if err != nil { return nil, err } @@ -192,7 +160,13 @@ func (p *KeyAdapter) ID() trust.KeyIdentifier { // Might need to convert this to a standard format func (p *KeyAdapter) Algorithm() ocrypto.KeyType { - return convertEnumToAlg(p.key.GetKey().GetKeyAlgorithm()) + kt, err := sdk.PolicyAlgorithmToKeyType(p.key.GetKey().GetKeyAlgorithm()) + if err != nil { + p.log.Error("unable to format key with alg", + slog.String("kid", p.key.GetKey().GetKeyId()), + slog.Any("err", err)) + } + return kt } func (p *KeyAdapter) IsLegacy() bool { diff --git a/service/kas/key_indexer_test.go b/service/kas/key_indexer_test.go index abd8f7f200..eba2f8278d 100644 --- a/service/kas/key_indexer_test.go +++ b/service/kas/key_indexer_test.go @@ -39,7 +39,7 @@ func (m *MockKeyAccessServerRegistryClient) DeleteKeyAccessServer(context.Contex return nil, errors.New("not implemented") } -func (m *MockKeyAccessServerRegistryClient) ListKeyAccessServerGrants(context.Context, *kasregistry.ListKeyAccessServerGrantsRequest) (*kasregistry.ListKeyAccessServerGrantsResponse, error) { +func (m *MockKeyAccessServerRegistryClient) ListKeyAccessServerGrants(context.Context, *kasregistry.ListKeyAccessServerGrantsRequest) (*kasregistry.ListKeyAccessServerGrantsResponse, error) { //nolint:staticcheck // Compatibility test for deprecated RPC. return nil, errors.New("not implemented") } diff --git a/service/logger/audit/constants.go b/service/logger/audit/constants.go index 900da74c21..9a0bb2db1b 100644 --- a/service/logger/audit/constants.go +++ b/service/logger/audit/constants.go @@ -32,7 +32,6 @@ const ( ObjectTypeKasAttributeDefinitionKeyAssignment ObjectTypeKasAttributeValueKeyAssignment ObjectTypeKasAttributeNamespaceKeyAssignment - ObjectTypeNamespaceCertificate ) func (ot ObjectType) String() string { @@ -62,7 +61,6 @@ func (ot ObjectType) String() string { "kas_attribute_definition_key_assignment", "kas_attribute_value_key_assignment", "kas_attribute_namespace_key_assignment", - "namespace_certificate", }[ot] } diff --git a/service/logger/audit/helpers_test.go b/service/logger/audit/helpers_test.go index cc2181c785..1d064052ce 100644 --- a/service/logger/audit/helpers_test.go +++ b/service/logger/audit/helpers_test.go @@ -15,7 +15,7 @@ const ( TestUserAgent = "test-user-agent" TestActorID = "test-actor-id" - TestTDFFormat = "nano" + TestTDFFormat = "ztdf" TestAlgorithm = "rsa" TestPolicyBinding = "test-policy-binding" ) diff --git a/service/pkg/authz/role_provider.go b/service/pkg/authz/role_provider.go new file mode 100644 index 0000000000..0146abf549 --- /dev/null +++ b/service/pkg/authz/role_provider.go @@ -0,0 +1,30 @@ +package authz + +import ( + "context" + + "github.com/lestrrat-go/jwx/v2/jwt" +) + +// RoleProvider returns role/group identifiers used as Casbin subjects. +type RoleProvider interface { + Roles(ctx context.Context, token jwt.Token, req RoleRequest) ([]string, error) +} + +// RoleProviderFactory constructs a RoleProvider at startup. +type RoleProviderFactory func(ctx context.Context, cfg ProviderConfig) (RoleProvider, error) + +// ProviderConfig carries provider-specific configuration and claim selectors. +type ProviderConfig struct { + Config map[string]any + UsernameClaim string + GroupsClaim string + ClientIDClaim string +} + +// RoleRequest provides request context to role providers. +type RoleRequest struct { + Issuer string + Resource string + Action string +} diff --git a/service/pkg/cache/cache.go b/service/pkg/cache/cache.go index db32985b00..9f3fc975e4 100644 --- a/service/pkg/cache/cache.go +++ b/service/pkg/cache/cache.go @@ -6,7 +6,7 @@ import ( "log/slog" "time" - "github.com/dgraph-io/ristretto" + "github.com/dgraph-io/ristretto/v2" "github.com/eko/gocache/lib/v4/cache" "github.com/eko/gocache/lib/v4/store" ristretto_store "github.com/eko/gocache/store/ristretto/v4" @@ -22,7 +22,7 @@ var ( // Manager is a cache manager for any value. type Manager struct { cache *cache.Cache[any] - underlyingStore *ristretto.Cache + underlyingStore *ristretto.Cache[string, any] } // Cache is a cache implementation using gocache for any value type. @@ -44,7 +44,7 @@ func NewCacheManager(maxCost int64) (*Manager, error) { if err != nil { return nil, err } - config := &ristretto.Config{ + config := &ristretto.Config[string, any]{ NumCounters: numCounters, // number of keys to track frequency of (10x max items) MaxCost: maxCost, // maximum cost of cache (e.g., 1<<20 for 1MB) BufferItems: bufferItems, // number of keys per Get buffer. @@ -152,7 +152,7 @@ func TestCacheClient(expiration time.Duration) (*Cache, error) { if err != nil { return nil, err } - config := &ristretto.Config{ + config := &ristretto.Config[string, any]{ NumCounters: numCounters, MaxCost: testMaxCost, BufferItems: bufferItems, diff --git a/service/pkg/config/config.go b/service/pkg/config/config.go index b3bcdff64d..008d1a9cff 100644 --- a/service/pkg/config/config.go +++ b/service/pkg/config/config.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/go-playground/validator/v10" + "github.com/go-viper/mapstructure/v2" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/db" @@ -155,8 +156,8 @@ func (c *Config) AddOnConfigChangeHook(hook ChangeHook) { c.onConfigChangeHooks = append(c.onConfigChangeHooks, hook) } -// Watch starts watching the configuration for changes in all config loaders. -func (c *Config) Watch(ctx context.Context) error { +// WatchWithNamespaces starts watching the configuration for changes in all config loaders, and provides the namespace info to the loaders. +func (c *Config) WatchWithNamespaces(ctx context.Context, namespaces []NamespaceInfo) error { if len(c.loaders) == 0 { return nil } @@ -176,13 +177,18 @@ func (c *Config) Watch(ctx context.Context) error { // Now call the user-provided hooks with the new configuration. return c.OnChange(ctx) } - if err := loader.Watch(ctx, c, onChangeCallback); err != nil { + if err := loader.Watch(ctx, c, onChangeCallback, namespaces); err != nil { return err } } return nil } +// Watch starts watching the configuration for changes in all config loaders. +func (c *Config) Watch(ctx context.Context) error { + return c.WatchWithNamespaces(ctx, []NamespaceInfo{}) +} + // Close invokes close method on all config loaders. func (c *Config) Close(ctx context.Context) error { if len(c.loaders) == 0 { @@ -275,7 +281,8 @@ func (c *Config) Reload(ctx context.Context) error { // Unmarshal the merged configuration into the main config struct `c` // so it's available for the next iteration of the dependency loop. - if err := orderedViper.Unmarshal(c); err != nil { + // TextUnmarshallerHookFunc enables custom types with UnmarshalText to decode from strings. + if err := orderedViper.Unmarshal(c, viper.DecodeHook(mapstructure.TextUnmarshallerHookFunc())); err != nil { return errors.Join(err, ErrUnmarshallingConfig) } diff --git a/service/pkg/config/config_file_loader.go b/service/pkg/config/config_file_loader.go index 06db0be3fa..aec09c4937 100644 --- a/service/pkg/config/config_file_loader.go +++ b/service/pkg/config/config_file_loader.go @@ -63,7 +63,7 @@ func (l *FileLoader) Load(_ Config) error { } // Watch starts watching the config file for configuration changes -func (l *FileLoader) Watch(ctx context.Context, _ *Config, onChange func(context.Context) error) error { +func (l *FileLoader) Watch(ctx context.Context, _ *Config, onChange func(context.Context) error, _ []NamespaceInfo) error { l.viper.WatchConfig() // If config changes, trigger the main config reload function diff --git a/service/pkg/config/config_test.go b/service/pkg/config/config_test.go index 5fdeb191bf..4ce51adc42 100644 --- a/service/pkg/config/config_test.go +++ b/service/pkg/config/config_test.go @@ -19,7 +19,7 @@ type MockLoader struct { loadFn func(Config) error getFn func(string) (any, error) getConfigKeysFn func() ([]string, error) - watchFn func(context.Context, *Config, func(context.Context) error) error + watchFn func(context.Context, *Config, func(context.Context) error, []NamespaceInfo) error closeFn func() error getNameFn func() string @@ -51,10 +51,10 @@ func (l *MockLoader) GetConfigKeys() ([]string, error) { return nil, nil } -func (l *MockLoader) Watch(ctx context.Context, config *Config, onChange func(context.Context) error) error { +func (l *MockLoader) Watch(ctx context.Context, config *Config, onChange func(context.Context) error, namespaces []NamespaceInfo) error { l.watchCalled = true if l.watchFn != nil { - if err := l.watchFn(ctx, config, onChange); err != nil { + if err := l.watchFn(ctx, config, onChange, namespaces); err != nil { return err } l.onChangeCalled = true @@ -123,7 +123,7 @@ func TestConfig_Watch(t *testing.T) { config := &Config{} loader := newMockLoader() // Mock loader to call onChange - loader.watchFn = func(_ context.Context, _ *Config, _ func(context.Context) error) error { + loader.watchFn = func(_ context.Context, _ *Config, _ func(context.Context) error, _ []NamespaceInfo) error { return nil } config.AddLoader(loader) @@ -139,7 +139,7 @@ func TestConfig_Watch(t *testing.T) { config := &Config{} expectedErr := errors.New("watch error") loader := newMockLoader() - loader.watchFn = func(_ context.Context, _ *Config, _ func(context.Context) error) error { + loader.watchFn = func(_ context.Context, _ *Config, _ func(context.Context) error, _ []NamespaceInfo) error { return expectedErr } config.AddLoader(loader) @@ -150,6 +150,32 @@ func TestConfig_Watch(t *testing.T) { assert.True(t, loader.watchCalled) assert.False(t, loader.onChangeCalled) }) + + t.Run("Loader receives NamespaceInfo", func(t *testing.T) { + config := &Config{} + loader := newMockLoader() + testNamespaces := []NamespaceInfo{ + { + Name: "test-ns", + Enabled: true, + Services: []ServiceInfo{ + {Namespace: "test-ns", Name: "test-svc"}, + }, + }, + } + + var receivedNamespaces []NamespaceInfo + loader.watchFn = func(_ context.Context, _ *Config, _ func(context.Context) error, namespaces []NamespaceInfo) error { + receivedNamespaces = namespaces + return nil + } + config.AddLoader(loader) + + err := config.WatchWithNamespaces(ctx, testNamespaces) + + require.NoError(t, err) + assert.Equal(t, testNamespaces, receivedNamespaces) + }) } func TestConfig_Close(t *testing.T) { diff --git a/service/pkg/config/default_settings_loader.go b/service/pkg/config/default_settings_loader.go index 392959d802..9d93a46ce9 100644 --- a/service/pkg/config/default_settings_loader.go +++ b/service/pkg/config/default_settings_loader.go @@ -90,7 +90,7 @@ func (l *DefaultSettingsLoader) Load(_ Config) error { return nil } -func (l *DefaultSettingsLoader) Watch(_ context.Context, _ *Config, _ func(context.Context) error) error { +func (l *DefaultSettingsLoader) Watch(_ context.Context, _ *Config, _ func(context.Context) error, _ []NamespaceInfo) error { return nil } diff --git a/service/pkg/config/environment_value_loader.go b/service/pkg/config/environment_value_loader.go index d2821362c7..741fb97d8b 100644 --- a/service/pkg/config/environment_value_loader.go +++ b/service/pkg/config/environment_value_loader.go @@ -95,7 +95,7 @@ func (l *EnvironmentValueLoader) Load(_ Config) error { } // Watch starts watching the config file for configuration changes -func (l *EnvironmentValueLoader) Watch(_ context.Context, _ *Config, _ func(context.Context) error) error { +func (l *EnvironmentValueLoader) Watch(_ context.Context, _ *Config, _ func(context.Context) error, _ []NamespaceInfo) error { // Environment variables can't be watched. return nil } diff --git a/service/pkg/config/legacy_loader.go b/service/pkg/config/legacy_loader.go index 6c5f651a4f..a9c2ff5a50 100644 --- a/service/pkg/config/legacy_loader.go +++ b/service/pkg/config/legacy_loader.go @@ -75,7 +75,7 @@ func (l *LegacyLoader) Load(cfg Config) error { } // Watch starts watching the config file for configuration changes -func (l *LegacyLoader) Watch(ctx context.Context, _ *Config, onChange func(context.Context) error) error { +func (l *LegacyLoader) Watch(ctx context.Context, _ *Config, onChange func(context.Context) error, _ []NamespaceInfo) error { l.viper.WatchConfig() // If config changes, trigger the main config reload function diff --git a/service/pkg/config/loader.go b/service/pkg/config/loader.go index 00361d3bd3..14ac7bc0ea 100644 --- a/service/pkg/config/loader.go +++ b/service/pkg/config/loader.go @@ -4,6 +4,19 @@ import ( "context" ) +// ServiceInfo represents minimal information about a service for configuration loaders +type ServiceInfo struct { + Namespace string + Name string +} + +// NamespaceInfo represents minimal information about a namespace for configuration loaders +type NamespaceInfo struct { + Name string + Enabled bool + Services []ServiceInfo +} + // Loader defines the interface for loading and managing configuration type Loader interface { // Get fetches a particular config value by dot-delimited key @@ -12,8 +25,10 @@ type Loader interface { GetConfigKeys() ([]string, error) // Load is called to load/refresh the configuration from its source Load(mostRecentConfig Config) error - // Watch starts watching for configuration changes and invokes an onChange callback - Watch(ctx context.Context, cfg *Config, onChange func(context.Context) error) error + // Watch starts watching for configuration changes and invokes an onChange callback. + // It receives information about the registered namespaces and services to determine + // if watching is required for this loader. + Watch(ctx context.Context, cfg *Config, onChange func(context.Context) error, namespaces []NamespaceInfo) error // Close closes the configuration loader Close() error // Name returns the name of the configuration loader diff --git a/service/pkg/db/errors.go b/service/pkg/db/errors.go index 0598c6bcb5..353addc0d7 100644 --- a/service/pkg/db/errors.go +++ b/service/pkg/db/errors.go @@ -20,6 +20,7 @@ var ( ErrForeignKeyViolation = errors.New("ErrForeignKeyViolation: value is referenced by another table") ErrRestrictViolation = errors.New("ErrRestrictViolation: value cannot be deleted due to restriction") ErrNotFound = errors.New("ErrNotFound: value not found") + ErrAttributeValueInactive = errors.New("ErrAttributeValueInactive: attribute value inactive") ErrEnumValueInvalid = errors.New("ErrEnumValueInvalid: not a valid enum value") ErrUUIDInvalid = errors.New("ErrUUIDInvalid: value not a valid UUID") ErrMissingValue = errors.New("ErrMissingValue: value must be included") @@ -32,6 +33,7 @@ var ( ErrCannotUpdateToUnspecified = errors.New("ErrCannotUpdateToUnspecified: cannot update to unspecified value") ErrKeyRotationFailed = errors.New("ErrTextKeyRotationFailed: key rotation failed") ErrExpectedBase64EncodedValue = errors.New("ErrExpectedBase64EncodedValue: expected base64 encoded value") + ErrUnencryptedPrivateKey = errors.New("ErrUnencryptedPrivateKey: unencrypted private key not allowed") ErrMarshalValueFailed = errors.New("ErrMashalValueFailed: failed to marshal value") ErrUnmarshalValueFailed = errors.New("ErrUnmarshalValueFailed: failed to unmarshal value") ErrNamespaceMismatch = errors.New("ErrNamespaceMismatch: namespace mismatch") @@ -40,7 +42,6 @@ var ( ErrInvalidOblTriParam = errors.New("ErrInvalidOblTriParam: either the obligation value, attribute value, or action provided was not found") ErrCheckViolation = errors.New("ErrCheckViolation: check constraint violation") ErrFqnMismatch = errors.New("ErrFqnMismatch: FQN mismatch") - ErrInvalidCertificate = errors.New("ErrInvalidCertificate: invalid certificate") ) // Get helpful error message for PostgreSQL violation @@ -128,6 +129,7 @@ const ( ErrorTextUpdateToUnspecified = "cannot update to unspecified value" ErrTextKeyRotationFailed = "key rotation failed" ErrorTextExpectedBase64EncodedValue = "expected base64 encoded value" + ErrorTextUnencryptedPrivateKey = "unencrypted private key not allowed" ErrorTextMarshalFailed = "failed to marshal value" ErrorTextUnmarsalFailed = "failed to unmarshal value" ErrorTextNamespaceMismatch = "namespace mismatch" @@ -135,7 +137,7 @@ const ( ErrorTextKIDMismatch = "key id mismatch" ErrorTextInvalidOblTrigParam = "either the obligation value, attribute value, or action provided is invalid" ErrorTextFqnMismatch = "fqn mismatch" - ErrorTextInvalidCertificate = "invalid certificate" + ErrorTextInactiveAttributeValue = "inactive attribute value" ) func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackErr string, logs ...any) error { @@ -188,6 +190,10 @@ func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackEr l.ErrorContext(ctx, ErrorTextExpectedBase64EncodedValue, logs...) return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextExpectedBase64EncodedValue)) } + if errors.Is(err, ErrUnencryptedPrivateKey) { + l.ErrorContext(ctx, ErrorTextUnencryptedPrivateKey, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextUnencryptedPrivateKey)) + } if errors.Is(err, ErrMarshalValueFailed) { l.ErrorContext(ctx, ErrorTextMarshalFailed, logs...) return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextMarshalFailed)) @@ -208,9 +214,13 @@ func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackEr l.ErrorContext(ctx, ErrorTextFqnMismatch, logs...) return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextFqnMismatch)) } - if errors.Is(err, ErrInvalidCertificate) { - l.ErrorContext(ctx, ErrorTextInvalidCertificate, logs...) - return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextInvalidCertificate)) + if errors.Is(err, ErrAttributeValueInactive) { + l.ErrorContext(ctx, ErrorTextInactiveAttributeValue, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextInactiveAttributeValue)) + } + if errors.Is(err, ErrNamespaceMismatch) { + l.ErrorContext(ctx, ErrorTextNamespaceMismatch, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextNamespaceMismatch)) } l.ErrorContext(ctx, "request error", append(logs, slog.Any("error", err))...) diff --git a/service/pkg/db/marshalHelpers.go b/service/pkg/db/marshalHelpers.go index 9d8ca510e2..31ce86bc0b 100644 --- a/service/pkg/db/marshalHelpers.go +++ b/service/pkg/db/marshalHelpers.go @@ -8,6 +8,7 @@ import ( "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/opentdf/platform/sdk" "google.golang.org/protobuf/encoding/protojson" ) @@ -146,22 +147,11 @@ func KasKeysProtoJSON(keysJSON []byte) ([]*policy.KasKey, error) { } func FormatAlg(alg policy.Algorithm) (string, error) { - switch alg { - case policy.Algorithm_ALGORITHM_RSA_2048: - return "rsa:2048", nil - case policy.Algorithm_ALGORITHM_RSA_4096: - return "rsa:4096", nil - case policy.Algorithm_ALGORITHM_EC_P256: - return "ec:secp256r1", nil - case policy.Algorithm_ALGORITHM_EC_P384: - return "ec:secp384r1", nil - case policy.Algorithm_ALGORITHM_EC_P521: - return "ec:secp512r1", nil - case policy.Algorithm_ALGORITHM_UNSPECIFIED: - fallthrough - default: + kt, err := sdk.PolicyAlgorithmToKeyType(alg) + if err != nil { return "", fmt.Errorf("unsupported algorithm: %s", alg) } + return string(kt), nil } func SimpleKasKeysProtoJSON(keysJSON []byte) ([]*policy.SimpleKasKey, error) { @@ -194,34 +184,3 @@ func UnmarshalSimpleKasKey(keysJSON []byte) (*policy.SimpleKasKey, error) { } return key, nil } - -func CertificatesProtoJSON(certsJSON []byte) ([]*policy.Certificate, error) { - var ( - certs []*policy.Certificate - raw []json.RawMessage - ) - if err := json.Unmarshal(certsJSON, &raw); err != nil { - return nil, err - } - for _, r := range raw { - c, err := UnmarshalCertificate([]byte(r)) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal certificate: %w", err) - } - if c != nil { - certs = append(certs, c) - } - } - return certs, nil -} - -func UnmarshalCertificate(certJSON []byte) (*policy.Certificate, error) { - var cert *policy.Certificate - if certJSON != nil { - cert = &policy.Certificate{} - if err := protojson.Unmarshal(certJSON, cert); err != nil { - return nil, err - } - } - return cert, nil -} diff --git a/service/pkg/db/marshalHelpers_test.go b/service/pkg/db/marshalHelpers_test.go new file mode 100644 index 0000000000..16a4234e8d --- /dev/null +++ b/service/pkg/db/marshalHelpers_test.go @@ -0,0 +1,71 @@ +package db + +import ( + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// reverseAlgMap mirrors the SDK's getKasKeyAlg mapping: ocrypto.KeyType string → policy.Algorithm. +// If FormatAlg produces a string that isn't in this map, the SDK would return ALGORITHM_UNSPECIFIED. +var reverseAlgMap = map[string]policy.Algorithm{ + string(ocrypto.RSA2048Key): policy.Algorithm_ALGORITHM_RSA_2048, + string(ocrypto.RSA4096Key): policy.Algorithm_ALGORITHM_RSA_4096, + string(ocrypto.EC256Key): policy.Algorithm_ALGORITHM_EC_P256, + string(ocrypto.EC384Key): policy.Algorithm_ALGORITHM_EC_P384, + string(ocrypto.EC521Key): policy.Algorithm_ALGORITHM_EC_P521, + string(ocrypto.HybridXWingKey): policy.Algorithm_ALGORITHM_HPQT_XWING, + string(ocrypto.HybridSecp256r1MLKEM768Key): policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, + string(ocrypto.HybridSecp384r1MLKEM1024Key): policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, +} + +func TestFormatAlg_RoundTrip(t *testing.T) { + // Every supported algorithm must survive a round-trip: + // enum → FormatAlg(enum) → reverseAlgMap[result] → must equal original enum + // This proves FormatAlg produces strings the SDK's getKasKeyAlg can parse. + supportedAlgs := []struct { + name string + alg policy.Algorithm + }{ + {"RSA-2048", policy.Algorithm_ALGORITHM_RSA_2048}, + {"RSA-4096", policy.Algorithm_ALGORITHM_RSA_4096}, + {"EC-P256", policy.Algorithm_ALGORITHM_EC_P256}, + {"EC-P384", policy.Algorithm_ALGORITHM_EC_P384}, + {"EC-P521", policy.Algorithm_ALGORITHM_EC_P521}, + {"HPQT-XWing", policy.Algorithm_ALGORITHM_HPQT_XWING}, + {"HPQT-P256-MLKEM768", policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768}, + {"HPQT-P384-MLKEM1024", policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024}, + } + + for _, tc := range supportedAlgs { + t.Run(tc.name, func(t *testing.T) { + formatted, err := FormatAlg(tc.alg) + require.NoError(t, err, "FormatAlg should not error for %s", tc.name) + + roundTripped, ok := reverseAlgMap[formatted] + require.True(t, ok, "FormatAlg returned %q which is not a known ocrypto.KeyType string", formatted) + assert.Equal(t, tc.alg, roundTripped, "round-trip mismatch: FormatAlg(%s) = %q maps back to %s, not %s", + tc.name, formatted, roundTripped, tc.alg) + }) + } +} + +func TestFormatAlg_Unsupported(t *testing.T) { + unsupported := []struct { + name string + alg policy.Algorithm + }{ + {"Unspecified", policy.Algorithm_ALGORITHM_UNSPECIFIED}, + {"Invalid", policy.Algorithm(99)}, + } + + for _, tc := range unsupported { + t.Run(tc.name, func(t *testing.T) { + _, err := FormatAlg(tc.alg) + require.Error(t, err) + }) + } +} diff --git a/service/pkg/server/options.go b/service/pkg/server/options.go index 65ee1f8e0b..db3952ceef 100644 --- a/service/pkg/server/options.go +++ b/service/pkg/server/options.go @@ -3,7 +3,9 @@ package server import ( "context" + "connectrpc.com/connect" "github.com/casbin/casbin/v2/persist" + "github.com/opentdf/platform/service/pkg/authz" "github.com/opentdf/platform/service/pkg/config" "github.com/opentdf/platform/service/pkg/serviceregistry" "github.com/opentdf/platform/service/trust" @@ -24,8 +26,14 @@ type StartConfig struct { configLoaders []config.Loader configLoaderOrder []string + extraConnectInterceptors []connect.Interceptor + extraIPCInterceptors []connect.Interceptor + trustKeyManagerCtxs []trust.NamedKeyManagerCtxFactory + authzRoleProvider authz.RoleProvider + authzRoleProviderFactories map[string]authz.RoleProviderFactory + // CORS additive configuration - appended to YAML/env config values additionalCORSHeaders []string additionalCORSMethods []string @@ -127,6 +135,25 @@ func WithCasbinAdapter(adapter persist.Adapter) StartOptions { } } +// WithAuthZRoleProvider option sets a role provider directly. +func WithAuthZRoleProvider(provider authz.RoleProvider) StartOptions { + return func(c StartConfig) StartConfig { + c.authzRoleProvider = provider + return c + } +} + +// WithAuthZRoleProviderFactory option registers a named role provider factory. +func WithAuthZRoleProviderFactory(name string, factory authz.RoleProviderFactory) StartOptions { + return func(c StartConfig) StartConfig { + if c.authzRoleProviderFactories == nil { + c.authzRoleProviderFactories = make(map[string]authz.RoleProviderFactory) + } + c.authzRoleProviderFactories[name] = factory + return c + } +} + // WithAdditionalConfigLoader option adds an additional configuration loader to the server. func WithAdditionalConfigLoader(loader config.Loader) StartOptions { return func(c StartConfig) StartConfig { @@ -143,6 +170,22 @@ func WithConfigLoaderOrder(loaderOrder []string) StartOptions { } } +// WithConnectInterceptors appends additional Connect interceptors (server-side) applied to all RPCs. +func WithConnectInterceptors(interceptors ...connect.Interceptor) StartOptions { + return func(c StartConfig) StartConfig { + c.extraConnectInterceptors = append(c.extraConnectInterceptors, interceptors...) + return c + } +} + +// WithIPCInterceptors appends additional Connect interceptors for in-process IPC server. +func WithIPCInterceptors(interceptors ...connect.Interceptor) StartOptions { + return func(c StartConfig) StartConfig { + c.extraIPCInterceptors = append(c.extraIPCInterceptors, interceptors...) + return c + } +} + // WithTrustKeyManagerFactories option provides factories for creating trust key managers. // Use WithTrustKeyManagerCtxFactories instead. // EXPERIMENTAL diff --git a/service/pkg/server/options_test.go b/service/pkg/server/options_test.go new file mode 100644 index 0000000000..ff17bb4b12 --- /dev/null +++ b/service/pkg/server/options_test.go @@ -0,0 +1,167 @@ +package server + +import ( + "context" + "testing" + + "connectrpc.com/connect" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/opentdf/platform/service/pkg/authz" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// noopInterceptor returns a connect.UnaryInterceptorFunc that passes through. +func noopInterceptor() connect.Interceptor { + return connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return next(ctx, req) + } + }) +} + +func TestWithConnectInterceptors(t *testing.T) { + tests := []struct { + name string + apply func(*StartConfig) + wantCount int + }{ + { + name: "single interceptor is appended", + apply: func(c *StartConfig) { + *c = WithConnectInterceptors(noopInterceptor())(*c) + }, + wantCount: 1, + }, + { + name: "multiple interceptors are appended in order", + apply: func(c *StartConfig) { + *c = WithConnectInterceptors(noopInterceptor(), noopInterceptor(), noopInterceptor())(*c) + }, + wantCount: 3, + }, + { + name: "calling twice accumulates interceptors", + apply: func(c *StartConfig) { + *c = WithConnectInterceptors(noopInterceptor())(*c) + *c = WithConnectInterceptors(noopInterceptor(), noopInterceptor())(*c) + }, + wantCount: 3, + }, + { + name: "empty call leaves slice nil", + apply: func(c *StartConfig) { + *c = WithConnectInterceptors()(*c) + }, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cfg StartConfig + tt.apply(&cfg) + + if tt.wantCount == 0 { + assert.Nil(t, cfg.extraConnectInterceptors) + } else { + require.Len(t, cfg.extraConnectInterceptors, tt.wantCount) + } + // Must not affect IPC interceptors + assert.Nil(t, cfg.extraIPCInterceptors) + }) + } +} + +func TestWithIPCInterceptors(t *testing.T) { + tests := []struct { + name string + apply func(*StartConfig) + wantCount int + }{ + { + name: "single interceptor is appended", + apply: func(c *StartConfig) { + *c = WithIPCInterceptors(noopInterceptor())(*c) + }, + wantCount: 1, + }, + { + name: "multiple interceptors are appended in order", + apply: func(c *StartConfig) { + *c = WithIPCInterceptors(noopInterceptor(), noopInterceptor(), noopInterceptor())(*c) + }, + wantCount: 3, + }, + { + name: "calling twice accumulates interceptors", + apply: func(c *StartConfig) { + *c = WithIPCInterceptors(noopInterceptor())(*c) + *c = WithIPCInterceptors(noopInterceptor(), noopInterceptor())(*c) + }, + wantCount: 3, + }, + { + name: "empty call leaves slice nil", + apply: func(c *StartConfig) { + *c = WithIPCInterceptors()(*c) + }, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cfg StartConfig + tt.apply(&cfg) + + if tt.wantCount == 0 { + assert.Nil(t, cfg.extraIPCInterceptors) + } else { + require.Len(t, cfg.extraIPCInterceptors, tt.wantCount) + } + // Must not affect Connect interceptors + assert.Nil(t, cfg.extraConnectInterceptors) + }) + } +} + +func TestWithConnectAndIPCInterceptorsTogether(t *testing.T) { + var cfg StartConfig + cfg = WithConnectInterceptors(noopInterceptor(), noopInterceptor())(cfg) + cfg = WithIPCInterceptors(noopInterceptor())(cfg) + + require.Len(t, cfg.extraConnectInterceptors, 2, "expected 2 connect interceptors") + require.Len(t, cfg.extraIPCInterceptors, 1, "expected 1 IPC interceptor") + + // Verify slices are independent (not sharing backing array) + assert.NotSame(t, + &cfg.extraConnectInterceptors[0], + &cfg.extraIPCInterceptors[0], + "connect and IPC interceptor slices must be independent", + ) +} + +type noopRoleProvider struct{} + +func (noopRoleProvider) Roles(_ context.Context, _ jwt.Token, _ authz.RoleRequest) ([]string, error) { + return nil, nil +} + +func TestWithAuthZRoleProvider(t *testing.T) { + var cfg StartConfig + cfg = WithAuthZRoleProvider(noopRoleProvider{})(cfg) + + require.NotNil(t, cfg.authzRoleProvider) + assert.Nil(t, cfg.authzRoleProviderFactories) +} + +func TestWithAuthZRoleProviderFactory(t *testing.T) { + var cfg StartConfig + cfg = WithAuthZRoleProviderFactory("mock", func(_ context.Context, _ authz.ProviderConfig) (authz.RoleProvider, error) { + return noopRoleProvider{}, nil + })(cfg) + + require.NotNil(t, cfg.authzRoleProviderFactories) + require.Contains(t, cfg.authzRoleProviderFactories, "mock") +} diff --git a/service/pkg/server/services.go b/service/pkg/server/services.go index 5af961893a..858f6aa25d 100644 --- a/service/pkg/server/services.go +++ b/service/pkg/server/services.go @@ -13,6 +13,8 @@ import ( "github.com/opentdf/platform/service/entityresolution" entityresolutionV2 "github.com/opentdf/platform/service/entityresolution/v2" "github.com/opentdf/platform/service/health" + authn "github.com/opentdf/platform/service/internal/auth" + "github.com/opentdf/platform/service/internal/auth/authz" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/kas" logging "github.com/opentdf/platform/service/logger" @@ -125,15 +127,14 @@ type startServicesParams struct { reg *serviceregistry.Registry cacheManager *cache.Manager keyManagerCtxFactories []trust.NamedKeyManagerCtxFactory + authzResolverRegistry *authz.ResolverRegistry } // startServices iterates through the registered namespaces and starts the services // based on the configuration and namespace mode. It creates a new service logger -// and a database client if required. It registers the services with the gRPC server, -// in-process gRPC server, and gRPC gateway. Finally, it logs the status of each service. -func startServices(ctx context.Context, params startServicesParams) (func(), error) { - var gatewayCleanup func() - +// and a database client if required. It registers the services with the external +// and in-process Connect RPC servers plus any extra HTTP handlers. +func startServices(ctx context.Context, params startServicesParams) error { cfg := params.cfg otdf := params.otdf client := params.client @@ -152,7 +153,8 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err // Skip the namespace if the mode is not enabled if !modeEnabled { - logger.Info("skipping namespace", + logger.Info( + "skipping namespace", slog.String("namespace", ns), slog.String("mode", namespace.Mode), ) @@ -186,7 +188,7 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err var err error svcDBClient, err = newServiceDBClient(ctx, cfg.Logger, cfg.DB, tracer, ns, svc.DBMigrations()) if err != nil { - return func() {}, err + return err } } if svc.GetVersion() != "" { @@ -195,7 +197,8 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err // Function to create a cache given cache options createCacheClient := func(options cache.Options) (*cache.Cache, error) { - slog.Info("creating cache client for", + slog.Info( + "creating cache client for", slog.String("namespace", ns), slog.String("service", svc.GetServiceDesc().ServiceName), ) @@ -206,6 +209,18 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err return cacheClient, nil } + // Create a scoped authz resolver registry for this service + // This ensures services can only register resolvers for their own methods + var scopedAuthzRegistry *authz.ScopedResolverRegistry + if params.authzResolverRegistry != nil { + scopedAuthzRegistry = params.authzResolverRegistry.ScopedForService(svc.GetServiceDesc()) + } + + var accessTokenVerifier authn.AccessTokenVerifier + if otdf != nil && otdf.AuthN != nil { + accessTokenVerifier = otdf.AuthN.AccessTokenVerifier() + } + err = svc.Start(ctx, serviceregistry.RegistrationParams{ Config: cfg.Services[svc.GetNamespace()], Security: &cfg.Security, @@ -216,15 +231,17 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err RegisterReadinessCheck: health.RegisterReadinessCheck, OTDF: otdf, // TODO: REMOVE THIS Tracer: tracer, + AccessTokenVerifier: accessTokenVerifier, NewCacheClient: createCacheClient, KeyManagerCtxFactories: keyManagerCtxFactories, + AuthzResolverRegistry: scopedAuthzRegistry, }) if err != nil { - return func() {}, err + return err } if err := svc.RegisterConfigUpdateHook(ctx, cfg.AddOnConfigChangeHook); err != nil { - return func() {}, fmt.Errorf("failed to register config update hook: %w", err) + return fmt.Errorf("failed to register config update hook: %w", err) } // Register Connect RPC Services @@ -237,23 +254,8 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err logger.Info("service did not register a connect-rpc handler", slog.String("namespace", ns)) } - // Register GRPC Gateway Handler using the in-process connect rpc - grpcConn := otdf.ConnectRPCInProcess.GrpcConn() - err := svc.RegisterGRPCGatewayHandler(ctx, otdf.GRPCGatewayMux, otdf.ConnectRPCInProcess.GrpcConn()) - if err != nil { - logger.Info("service did not register a grpc gateway handler", slog.String("namespace", ns)) - } else if gatewayCleanup == nil { - gatewayCleanup = func() { - slog.Debug("executing cleanup") - if grpcConn != nil { - grpcConn.Close() - } - slog.Info("cleanup complete") - } - } - // Register Extra Handlers - if err := svc.RegisterHTTPHandlers(ctx, otdf.GRPCGatewayMux); err != nil { + if err := svc.RegisterHTTPHandlers(ctx, otdf.HTTPMux); err != nil { logger.Info("service did not register extra http handlers", slog.String("namespace", ns)) } @@ -261,18 +263,16 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err "service running", slog.String("namespace", ns), slog.String("service", svc.GetServiceDesc().ServiceName), - slog.Group("database", + slog.Group( + "database", slog.Any("required", svcDBClient != nil), - slog.Any("migrationStatus", determineStatusOfMigration(svcDBClient)), + slog.Any("migration_status", determineStatusOfMigration(svcDBClient)), ), ) } } - if gatewayCleanup == nil { - gatewayCleanup = func() {} - } - return gatewayCleanup, nil + return nil } func extractServiceLoggerConfig(cfg config.ServiceConfig) (string, error) { @@ -293,7 +293,8 @@ func extractServiceLoggerConfig(cfg config.ServiceConfig) (string, error) { func newServiceDBClient(ctx context.Context, logCfg logging.Config, dbCfg db.Config, trace trace.Tracer, ns string, migrations *embed.FS) (*db.Client, error) { var err error - client, err := db.New(ctx, dbCfg, logCfg, &trace, + client, err := db.New( + ctx, dbCfg, logCfg, &trace, db.WithService(ns), db.WithMigrations(migrations), ) diff --git a/service/pkg/server/services_test.go b/service/pkg/server/services_test.go index 0fc7130161..f78aced894 100644 --- a/service/pkg/server/services_test.go +++ b/service/pkg/server/services_test.go @@ -6,7 +6,6 @@ import ( "net/http" "testing" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/config" @@ -25,7 +24,7 @@ type mockTestServiceOptions struct { serviceName string serviceHandlerType any serviceObject any - serviceHandler func(ctx context.Context, mux *runtime.ServeMux) error + serviceHandler func(ctx context.Context, mux *http.ServeMux) error dbRegister serviceregistry.DBRegister } @@ -41,7 +40,7 @@ func mockTestServiceRegistry(opts mockTestServiceOptions) (serviceregistry.IServ namespace: "test", serviceName: "TestService", serviceHandlerType: (*interface{})(nil), - serviceHandler: func(_ context.Context, _ *runtime.ServeMux) error { + serviceHandler: func(_ context.Context, _ *http.ServeMux) error { return nil }, } @@ -74,7 +73,7 @@ func mockTestServiceRegistry(opts mockTestServiceOptions) (serviceregistry.IServ if ts, ok = opts.serviceObject.(TestService); !ok { panic("serviceObject is not a TestService") } - return ts, func(ctx context.Context, mux *runtime.ServeMux) error { + return ts, func(ctx context.Context, mux *http.ServeMux) error { spy.wasCalled = true spy.callParams = append(spy.callParams, srp, ctx, mux, ts) return serviceHandler(ctx, mux) @@ -260,7 +259,7 @@ func (suite *ServiceTestSuite) TestStartServicesWithVariousCases() { newLogger, err := logger.NewLogger(logger.Config{Output: "stdout", Level: "info", Type: "json"}) suite.Require().NoError(err) - cleanup, err := startServices(ctx, startServicesParams{ + err = startServices(ctx, startServicesParams{ cfg: &config.Config{ Mode: []string{"test"}, Logger: logger.Config{Output: "stdout", Level: "info", Type: "json"}, @@ -284,9 +283,6 @@ func (suite *ServiceTestSuite) TestStartServicesWithVariousCases() { reg: registry, }) - // call cleanup function - defer cleanup() - suite.Require().NoError(err) // require.NotNil(t, cF) // assert.Lenf(t, services, 2, "expected 2 services enabled, got %d", len(services)) @@ -607,11 +603,7 @@ func (m *mockOrderTrackingService) RegisterConnectRPCServiceHandler(context.Cont return nil } -func (m *mockOrderTrackingService) RegisterGRPCGatewayHandler(context.Context, *runtime.ServeMux, *grpc.ClientConn) error { - return nil -} - -func (m *mockOrderTrackingService) RegisterHTTPHandlers(context.Context, *runtime.ServeMux) error { +func (m *mockOrderTrackingService) RegisterHTTPHandlers(context.Context, *http.ServeMux) error { return nil } @@ -650,7 +642,7 @@ func (suite *ServiceTestSuite) TestStartServices_StartsInRegistrationOrder() { newLogger, err := logger.NewLogger(logger.Config{Output: "stdout", Level: "info", Type: "json"}) suite.Require().NoError(err) - cleanup, err := startServices(ctx, startServicesParams{ + err = startServices(ctx, startServicesParams{ cfg: &config.Config{ Mode: []string{"test"}, // Enable the mode for our test services Services: map[string]config.ServiceConfig{ @@ -664,7 +656,6 @@ func (suite *ServiceTestSuite) TestStartServices_StartsInRegistrationOrder() { reg: registry, }) suite.Require().NoError(err) - defer cleanup() // The startServices function iterates through namespaces in the order they were first registered, // and then through the services within that namespace in their registration order. @@ -828,8 +819,12 @@ func (suite *ServiceTestSuite) Test_Extra_Services_With_Mode_Negation() { namespace: tc.extraCoreNamespace, serviceName: "ExtraCoreService", serviceObject: TestService{}, - serviceHandler: func(_ context.Context, mux *runtime.ServeMux) error { - return mux.HandlePath(http.MethodGet, "/extracore/status", TestService{}.TestHandler) + serviceHandler: func(_ context.Context, mux *http.ServeMux) error { + ts := TestService{} + mux.HandleFunc("/extracore/status", func(w http.ResponseWriter, r *http.Request) { + ts.TestHandler(w, r, nil) + }) + return nil }, }) extraCoreServices = append(extraCoreServices, extraCoreService) @@ -842,8 +837,12 @@ func (suite *ServiceTestSuite) Test_Extra_Services_With_Mode_Negation() { namespace: tc.extraServiceNamespace, serviceName: "ExtraService", serviceObject: TestService{}, - serviceHandler: func(_ context.Context, mux *runtime.ServeMux) error { - return mux.HandlePath(http.MethodGet, "/extraservice/status", TestService{}.TestHandler) + serviceHandler: func(_ context.Context, mux *http.ServeMux) error { + ts := TestService{} + mux.HandleFunc("/extraservice/status", func(w http.ResponseWriter, r *http.Request) { + ts.TestHandler(w, r, nil) + }) + return nil }, }) extraServices = append(extraServices, extraService) diff --git a/service/pkg/server/start.go b/service/pkg/server/start.go index 1cf129c3d2..d1be8c37a2 100644 --- a/service/pkg/server/start.go +++ b/service/pkg/server/start.go @@ -18,6 +18,7 @@ import ( "github.com/opentdf/platform/sdk/auth/oauth" "github.com/opentdf/platform/sdk/httputil" "github.com/opentdf/platform/service/internal/auth" + "github.com/opentdf/platform/service/internal/auth/authz" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/cache" @@ -45,7 +46,7 @@ func Start(f ...StartOptions) error { ctx := context.Background() - slog.Debug("loading configuration from environment") + slog.Log(ctx, logger.LevelTrace, "loading configuration from environment") loaderOrder := []string{ config.LoaderNameLegacy, config.LoaderNameDefaultSettings, @@ -124,7 +125,7 @@ func Start(f ...StartOptions) error { } defer shutdown() - logger.Debug("config loaded", slog.Any("config", cfg.LogValue())) + logger.Trace("config loaded", slog.Any("config", cfg.LogValue())) // Configure cache manager logger.Info("creating cache manager") @@ -151,6 +152,10 @@ func Start(f ...StartOptions) error { cfg.Server.Auth.IPCReauthRoutes = startConfig.IPCReauthRoutes } + // Programmatic Connect/IPC interceptors (not config-driven) + cfg.Server.ExtraConnectInterceptors = append(cfg.Server.ExtraConnectInterceptors, startConfig.extraConnectInterceptors...) + cfg.Server.ExtraIPCInterceptors = append(cfg.Server.ExtraIPCInterceptors, startConfig.extraIPCInterceptors...) + // Set Default Policy if startConfig.builtinPolicyOverride != "" { cfg.Server.Auth.Policy.Builtin = startConfig.builtinPolicyOverride @@ -161,6 +166,14 @@ func Start(f ...StartOptions) error { cfg.Server.Auth.Policy.Adapter = startConfig.casbinAdapter } + // Set AuthZ role provider overrides + if startConfig.authzRoleProvider != nil { + cfg.Server.Auth.RoleProvider = startConfig.authzRoleProvider + } + if startConfig.authzRoleProviderFactories != nil { + cfg.Server.Auth.RoleProviderFactories = startConfig.authzRoleProviderFactories + } + // Apply additional CORS configuration from programmatic options // These are appended to the YAML config values; deduplication happens in Effective*() methods if len(startConfig.additionalCORSHeaders) > 0 { @@ -176,6 +189,11 @@ func Start(f ...StartOptions) error { cfg.Server.CORS.AdditionalExposedHeaders = append(cfg.Server.CORS.AdditionalExposedHeaders, startConfig.additionalCORSExposedHeaders...) } + // Create the global authz resolver registry before the server/authenticator. + // Services receive scoped views of this same registry during startup. + authzResolverRegistry := authz.NewResolverRegistry() + cfg.Server.AuthzResolverRegistry = authzResolverRegistry + // Create new server for grpc & http. Also will support in process grpc potentially too logger.Debug("initializing opentdf server") cfg.Server.WellKnownConfigRegister = wellknown.RegisterConfiguration @@ -255,7 +273,7 @@ func Start(f ...StartOptions) error { } // provide token endpoint -- sdk cannot discover it since well-known service isnt running yet - sdkOptions = append(sdkOptions, sdk.WithTokenEndpoint(oidcconfig.TokenEndpoint)) + sdkOptions = append(sdkOptions, sdk.WithTokenEndpoint(oidcconfig.TokenEndpoint)) //nolint:staticcheck // Backward-compatible explicit token endpoint option. } // Configure SDK based on mode @@ -271,7 +289,7 @@ func Start(f ...StartOptions) error { defer client.Close() logger.Info("starting services") - gatewayCleanup, err := startServices(ctx, startServicesParams{ + err = startServices(ctx, startServicesParams{ cfg: cfg, otdf: otdf, client: client, @@ -279,15 +297,31 @@ func Start(f ...StartOptions) error { logger: logger, reg: svcRegistry, cacheManager: cacheManager, + authzResolverRegistry: authzResolverRegistry, }) if err != nil { logger.Error("issue starting services", slog.String("error", err.Error())) return fmt.Errorf("issue starting services: %w", err) } - defer gatewayCleanup() // Start watching the configuration for changes with registered config change service hooks - if err := cfg.Watch(ctx); err != nil { + var watchInfo []config.NamespaceInfo + for _, nsInfo := range svcRegistry.GetNamespaces() { + var services []config.ServiceInfo + for _, svc := range nsInfo.Namespace.Services { + services = append(services, config.ServiceInfo{ + Namespace: svc.GetNamespace(), + Name: svc.GetServiceDesc().ServiceName, + }) + } + watchInfo = append(watchInfo, config.NamespaceInfo{ + Name: nsInfo.Name, + Enabled: nsInfo.Namespace.IsEnabled(cfg.Mode), + Services: services, + }) + } + + if err := cfg.WatchWithNamespaces(ctx, watchInfo); err != nil { return fmt.Errorf("failed to watch configuration: %w", err) } defer cfg.Close(ctx) @@ -360,6 +394,13 @@ func setupERSConnection(cfg *config.Config, oidcconfig *auth.OIDCConfiguration, ersConnectRPCConn := &sdk.ConnectRPCConnection{} + // OTel tracing and metrics for outbound ERS Connect RPCs (outermost interceptor) + if ersTraceInt, err := tracing.ConnectClientTraceInterceptor(); err != nil { + logger.Error("failed to create ERS trace interceptor", slog.String("error", err.Error())) + } else { + ersConnectRPCConn.Options = append(ersConnectRPCConn.Options, connect.WithInterceptors(ersTraceInt)) + } + // Configure TLS tlsConfig := configureTLSForERS(cfg, ersConnectRPCConn) diff --git a/service/pkg/server/start_test.go b/service/pkg/server/start_test.go index bff90e197a..2b5ce2dced 100644 --- a/service/pkg/server/start_test.go +++ b/service/pkg/server/start_test.go @@ -15,7 +15,7 @@ import ( "testing" "time" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/opentdf/platform/lib/ocrypto" "github.com/opentdf/platform/service/internal/auth" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/logger" @@ -135,17 +135,13 @@ func mockKeycloakServer() *httptest.Server { } func mockOpenTDFServer() (*server.OpenTDFServer, error) { - discoveryEndpoint := mockKeycloakServer() // Create new opentdf server return server.NewOpenTDFServer(server.Config{ WellKnownConfigRegister: func(_ string, _ any) error { return nil }, Auth: auth.Config{ - AuthNConfig: auth.AuthNConfig{ - Issuer: discoveryEndpoint.URL, - Audience: "test", - }, + Enabled: false, PublicRoutes: []string{"/testpath/*"}, }, Port: 43481, @@ -228,7 +224,9 @@ func TestStartTestSuite(t *testing.T) { } func (s *StartTestSuite) SetupSuite() { - // Create dummy KAS key files in testdata + // Generate fresh KAS key material for every run so nothing is committed + // alongside the source tree. all-no-config.yaml expects all of these paths + // to resolve, including the hybrid PQ key pairs added in PR #3276. keyFiles := map[string]string{ "kas-private.pem": dummyRsaPrivate, "kas-cert.pem": dummyRsaPublic, // Using public key as cert for dummy purposes @@ -236,6 +234,23 @@ func (s *StartTestSuite) SetupSuite() { "kas-ec-cert.pem": dummyEcCert, } + hybridPairs := []struct { + name string + newPair func() (priv, pub string, err error) + priv string + pub string + }{ + {"X-Wing", testXWingPair, "kas-xwing-private.pem", "kas-xwing-public.pem"}, + {"P-256+ML-KEM-768", testP256MLKEM768Pair, "kas-p256mlkem768-private.pem", "kas-p256mlkem768-public.pem"}, + {"P-384+ML-KEM-1024", testP384MLKEM1024Pair, "kas-p384mlkem1024-private.pem", "kas-p384mlkem1024-public.pem"}, + } + for _, p := range hybridPairs { + priv, pub, err := p.newPair() + s.Require().NoErrorf(err, "Failed to generate %s key pair", p.name) + keyFiles[p.priv] = priv + keyFiles[p.pub] = pub + } + for filename, content := range keyFiles { filePath := filepath.Join("testdata", filename) err := os.WriteFile(filePath, []byte(content), 0o600) @@ -250,7 +265,7 @@ func (s *StartTestSuite) TearDownSuite() { s.Require().NoError(err, "Failed to read testdata directory") for _, entry := range entries { - if !entry.IsDir() { + if entry.IsDir() { continue } if entry.Name() == ignoreFile { @@ -261,6 +276,47 @@ func (s *StartTestSuite) TearDownSuite() { } } +func testXWingPair() (string, string, error) { + kp, err := ocrypto.NewXWingKeyPair() + if err != nil { + return "", "", err + } + return testHybridPEMs(kp) +} + +func testP256MLKEM768Pair() (string, string, error) { + kp, err := ocrypto.NewP256MLKEM768KeyPair() + if err != nil { + return "", "", err + } + return testHybridPEMs(kp) +} + +func testP384MLKEM1024Pair() (string, string, error) { + kp, err := ocrypto.NewP384MLKEM1024KeyPair() + if err != nil { + return "", "", err + } + return testHybridPEMs(kp) +} + +type pemKeyPair interface { + PrivateKeyInPemFormat() (string, error) + PublicKeyInPemFormat() (string, error) +} + +func testHybridPEMs(kp pemKeyPair) (string, string, error) { + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} + func (s *StartTestSuite) Test_Start_When_Extra_Service_Registered() { testCases := []struct { name string @@ -278,7 +334,7 @@ func (s *StartTestSuite) Test_Start_When_Extra_Service_Registered() { name: "And_Mode_Core", mode: []string{"core"}, status: http.StatusNotFound, - responseBody: "{\"code\":5,\"message\":\"Not Found\",\"details\":[]}", + responseBody: "404 page not found\n", }, { name: "And_Mode_Core_Plus_Test", @@ -296,7 +352,7 @@ func (s *StartTestSuite) Test_Start_When_Extra_Service_Registered() { name: "And_Mode_Kas", mode: []string{"kas"}, status: http.StatusNotFound, - responseBody: "{\"code\":5,\"message\":\"Not Found\",\"details\":[]}", + responseBody: "404 page not found\n", }, { name: "And_Mode_Kas_Plus_Test", @@ -308,7 +364,7 @@ func (s *StartTestSuite) Test_Start_When_Extra_Service_Registered() { name: "And_Mode_EntityResolution", mode: []string{"entityresolution"}, status: http.StatusNotFound, - responseBody: "{\"code\":5,\"message\":\"Not Found\",\"details\":[]}", + responseBody: "404 page not found\n", }, { name: "And_Mode_EntityResolution_Plus_Test", @@ -320,7 +376,7 @@ func (s *StartTestSuite) Test_Start_When_Extra_Service_Registered() { name: "And_Mode_Unknown", mode: []string{"unknown"}, status: http.StatusNotFound, - responseBody: "{\"code\":5,\"message\":\"Not Found\",\"details\":[]}", + responseBody: "404 page not found\n", }, { name: "And_Mode_Unknown_Plus_Test", @@ -343,8 +399,11 @@ func (s *StartTestSuite) Test_Start_When_Extra_Service_Registered() { ts := TestService{} registerTestService, _ := mockTestServiceRegistry(mockTestServiceOptions{ serviceObject: ts, - serviceHandler: func(_ context.Context, mux *runtime.ServeMux) error { - return mux.HandlePath(http.MethodGet, "/healthz", ts.TestHandler) + serviceHandler: func(_ context.Context, mux *http.ServeMux) error { + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + ts.TestHandler(w, r, nil) + }) + return nil }, }) @@ -353,7 +412,7 @@ func (s *StartTestSuite) Test_Start_When_Extra_Service_Registered() { require.NoError(t, err) // Start services with test service - cleanup, err := startServices(context.Background(), startServicesParams{ + err = startServices(context.Background(), startServicesParams{ cfg: &config.Config{ Mode: tc.mode, Services: map[string]config.ServiceConfig{ @@ -368,7 +427,6 @@ func (s *StartTestSuite) Test_Start_When_Extra_Service_Registered() { cacheManager: &cache.Manager{}, }) require.NoError(t, err) - defer cleanup() require.NoError(t, s.Start()) defer s.Stop() @@ -485,14 +543,14 @@ func (s *StartTestSuite) Test_Start_Mode_Config_Success() { { "core,entityresolution without sdk_config", map[string]interface{}{ - "mode": "core,entityresolution", "server.auth.issuer": discoveryEndpoint.URL, + "mode": []string{"core", "entityresolution"}, "server.auth.issuer": discoveryEndpoint.URL, }, "all-no-config-*.yaml", }, { "core,entityresolution,kas without sdk_config", map[string]interface{}{ - "mode": "core,entityresolution,kas", "server.auth.issuer": discoveryEndpoint.URL, + "mode": []string{"core", "entityresolution", "kas"}, "server.auth.issuer": discoveryEndpoint.URL, }, "all-no-config-*.yaml", }, diff --git a/service/pkg/server/testdata/all-no-config.yaml b/service/pkg/server/testdata/all-no-config.yaml index d6db2ae334..29dcd5fe8c 100644 --- a/service/pkg/server/testdata/all-no-config.yaml +++ b/service/pkg/server/testdata/all-no-config.yaml @@ -16,6 +16,12 @@ services: - kid: r1 alg: rsa:2048 legacy: true + - kid: x1 + alg: hpqt:xwing + - kid: h1 + alg: hpqt:secp256r1-mlkem768 + - kid: h2 + alg: hpqt:secp384r1-mlkem1024 entityresolution: log_level: info url: http://localhost:8888/auth @@ -117,3 +123,15 @@ server: alg: ec:secp256r1 private: ./testdata/kas-ec-private.pem cert: ./testdata/kas-ec-cert.pem + - kid: x1 + alg: hpqt:xwing + private: ./testdata/kas-xwing-private.pem + cert: ./testdata/kas-xwing-public.pem + - kid: h1 + alg: hpqt:secp256r1-mlkem768 + private: ./testdata/kas-p256mlkem768-private.pem + cert: ./testdata/kas-p256mlkem768-public.pem + - kid: h2 + alg: hpqt:secp384r1-mlkem1024 + private: ./testdata/kas-p384mlkem1024-private.pem + cert: ./testdata/kas-p384mlkem1024-public.pem diff --git a/service/pkg/serviceregistry/serviceregistry.go b/service/pkg/serviceregistry/serviceregistry.go index 85e95f1c01..e372a8613e 100644 --- a/service/pkg/serviceregistry/serviceregistry.go +++ b/service/pkg/serviceregistry/serviceregistry.go @@ -12,11 +12,12 @@ import ( "sync" "connectrpc.com/connect" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/opentdf/platform/sdk" + authn "github.com/opentdf/platform/service/internal/auth" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" + "github.com/opentdf/platform/service/internal/auth/authz" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/cache" @@ -47,6 +48,8 @@ type RegistrationParams struct { // Logger is the logger that can be used to log messages. This logger is scoped to the service Logger *logger.Logger trace.Tracer + // AccessTokenVerifier validates request tokens using the platform's shared auth configuration. + AccessTokenVerifier authn.AccessTokenVerifier // NewCacheClient is a function that can be used to create a new cache instance for the service NewCacheClient func(cache.Options) (*cache.Cache, error) @@ -64,9 +67,26 @@ type RegistrationParams struct { // service. This is useful for services that need to perform some initialization before they are // ready to serve requests. This function should be called in the RegisterFunc function. RegisterReadinessCheck func(namespace string, check func(context.Context) error) error + + // AuthzResolverRegistry allows services to register authorization resolvers per-method. + // This registry is scoped to the service's namespace - services can only register + // resolvers for their own methods (validated against ServiceDesc). + // + // Services should register resolvers in RegisterFunc where db client and other dependencies + // are available. The resolver will be called by the auth interceptor at request time. + // + // Example: + // srp.AuthzResolverRegistry.MustRegister("UpdateAttribute", + // func(ctx context.Context, req connect.AnyRequest) (authz.ResolverContext, error) { + // msg := req.Any().(*pb.UpdateAttributeRequest) + // // ... resolve dimensions using db client ... + // }, + // ) + AuthzResolverRegistry *authz.ScopedResolverRegistry } + type ( - HandlerServer func(ctx context.Context, mux *runtime.ServeMux) error + HandlerServer func(ctx context.Context, mux *http.ServeMux) error RegisterFunc[S any] func(RegistrationParams) (impl S, HandlerServer HandlerServer) // Allow services to implement handling for config changes as direced by caller OnConfigUpdateHook func(context.Context, config.ServiceConfig) error @@ -93,8 +113,7 @@ type IService interface { Shutdown() error RegisterConfigUpdateHook(ctx context.Context, hookAppender func(config.ChangeHook)) error RegisterConnectRPCServiceHandler(context.Context, *server.ConnectRPC) error - RegisterGRPCGatewayHandler(context.Context, *runtime.ServeMux, *grpc.ClientConn) error - RegisterHTTPHandlers(context.Context, *runtime.ServeMux) error + RegisterHTTPHandlers(context.Context, *http.ServeMux) error } // Service is a struct that holds the registration information for a service as well as the state @@ -127,8 +146,6 @@ type ServiceOptions[S any] struct { httpHandlerFunc HandlerServer // ConnectRPCServiceHandler is the function that will be called to register the service with the ConnectRPCFunc func(S, ...connect.HandlerOption) (string, http.Handler) - // Deprecated: Registers a gRPC service with the gRPC gateway - GRPCGatewayFunc func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error // DB is optional and used to register the service with a database DB DBRegister } @@ -192,7 +209,7 @@ func (s Service[S]) RegisterConfigUpdateHook(ctx context.Context, hookAppender f // If no hook is registered, exit if s.OnConfigUpdate != nil { var onChange config.ChangeHook = func(cfg config.ServicesMap) error { - slog.Debug("service config change hook called", + slog.Log(ctx, logger.LevelTrace, "service config change hook called", slog.String("namespace", s.GetNamespace()), slog.String("service", s.GetServiceDesc().ServiceName), ) @@ -213,30 +230,14 @@ func (s Service[S]) RegisterConnectRPCServiceHandler(_ context.Context, connectR return nil } -// Deprecated: RegisterHTTPServer is deprecated and should not be used going forward. -// We will be looking onto other alternatives like bufconnect to replace this. -// RegisterHTTPServer registers an HTTP server with the service. -// It takes a context, a ServeMux, and an implementation function as parameters. -// If the service did not register a handler, it returns an error. -func (s *Service[S]) RegisterHTTPHandlers(ctx context.Context, mux *runtime.ServeMux) error { +// RegisterHTTPHandlers registers extra HTTP handlers with the platform HTTP mux. +func (s *Service[S]) RegisterHTTPHandlers(ctx context.Context, mux *http.ServeMux) error { if s.httpHandlerFunc == nil { return errors.New("service did not register any handlers") } return s.httpHandlerFunc(ctx, mux) } -// Deprecated: RegisterConnectRPCServiceHandler is deprecated and should not be used going forward. -// We will be looking onto other alternatives like bufconnect to replace this. -// RegisterConnectRPCServiceHandler registers an HTTP server with the service. -// It takes a context, a ServeMux, and an implementation function as parameters. -// If the service did not register a handler, it returns an error. -func (s Service[S]) RegisterGRPCGatewayHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - if s.GRPCGatewayFunc == nil { - return errors.New("service did not register a handler") - } - return s.GRPCGatewayFunc(ctx, mux, conn) -} - // namespace represents a namespace in the service registry. type Namespace struct { Mode string diff --git a/service/pkg/util/dotnotation.go b/service/pkg/util/dotnotation.go new file mode 100644 index 0000000000..737dda4cd6 --- /dev/null +++ b/service/pkg/util/dotnotation.go @@ -0,0 +1,37 @@ +package util + +import "strings" + +// Dotnotation retrieves a value from a nested map using dot notation keys. +// Returns nil for empty keys, malformed paths (leading/trailing/double dots), +// or if the path doesn't exist in the map. +func Dotnotation(m map[string]interface{}, key string) interface{} { + if key == "" { + return nil + } + keys := strings.Split(key, ".") + // Filter out empty segments from leading/trailing/double dots + filtered := keys[:0] + for _, k := range keys { + if k != "" { + filtered = append(filtered, k) + } + } + if len(filtered) == 0 { + return nil + } + for i, k := range filtered { + if i == len(filtered)-1 { + return m[k] + } + if m[k] == nil { + return nil + } + var ok bool + m, ok = m[k].(map[string]interface{}) + if !ok { + return nil + } + } + return nil +} diff --git a/service/pkg/util/dotnotation_test.go b/service/pkg/util/dotnotation_test.go new file mode 100644 index 0000000000..fa0b4855fa --- /dev/null +++ b/service/pkg/util/dotnotation_test.go @@ -0,0 +1,38 @@ +package util + +import ( + "testing" +) + +func TestDotnotation(t *testing.T) { + tests := []struct { + name string + input map[string]any + key string + expected any + }{ + // Basic cases + {name: "valid key", input: map[string]any{"a": map[string]any{"b": 1}}, key: "a.b", expected: 1}, + {name: "non-existent key", input: map[string]any{"a": map[string]any{"b": 1}}, key: "a.c", expected: nil}, + {name: "nested map", input: map[string]any{"a": map[string]any{"b": map[string]any{"c": 2}}}, key: "a.b.c", expected: 2}, + {name: "invalid key type", input: map[string]any{"a": 1}, key: "a.b", expected: nil}, + {name: "top level key", input: map[string]any{"a": "value"}, key: "a", expected: "value"}, + {name: "nil map value", input: map[string]any{"a": nil}, key: "a.b", expected: nil}, + // Edge cases for malformed keys + {name: "empty key", input: map[string]any{"a": 1}, key: "", expected: nil}, + {name: "trailing dot", input: map[string]any{"a": 1}, key: "a.", expected: 1}, + {name: "leading dot", input: map[string]any{"a": 1}, key: ".a", expected: 1}, + {name: "double dot", input: map[string]any{"a": map[string]any{"b": 1}}, key: "a..b", expected: 1}, + {name: "only dots", input: map[string]any{"a": 1}, key: "...", expected: nil}, + {name: "whitespace key", input: map[string]any{" ": 1}, key: " ", expected: 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Dotnotation(tt.input, tt.key) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} diff --git a/service/policy/actions/actions.go b/service/policy/actions/actions.go index fe263f1030..6c4f5ca5e9 100644 --- a/service/policy/actions/actions.go +++ b/service/policy/actions/actions.go @@ -2,6 +2,7 @@ package actions import ( "context" + "errors" "fmt" "log/slog" @@ -112,6 +113,10 @@ func (a *ActionService) ListActions(ctx context.Context, req *connect.Request[ac func (a *ActionService) CreateAction(ctx context.Context, req *connect.Request[actions.CreateActionRequest]) (*connect.Response[actions.CreateActionResponse], error) { a.logger.DebugContext(ctx, "creating action", slog.String("name", req.Msg.GetName())) + if a.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("either namespace_id or namespace_fqn must be provided")) + } + auditParams := audit.PolicyEventParams{ ActionType: audit.ActionTypeCreate, ObjectType: audit.ObjectTypeAction, diff --git a/service/policy/actions/actions.proto b/service/policy/actions/actions.proto index 8dc5e427ce..7065e2d503 100644 --- a/service/policy/actions/actions.proto +++ b/service/policy/actions/actions.proto @@ -12,6 +12,8 @@ import "policy/selectors.proto"; */ message GetActionRequest { + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + // Required oneof identifier { option (buf.validate.oneof).required = true; @@ -25,6 +27,22 @@ message GetActionRequest { } ]; } + + // Optional namespace ID to scope name-based lookup. + // If omitted for name-based lookup, action search is limited to legacy (namespace_id = NULL) actions. + string namespace_id = 3 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Optional namespace FQN to scope name-based lookup. + // If omitted for name-based lookup, action search is limited to legacy (namespace_id = NULL) actions. + string namespace_fqn = 4 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string = { + min_len : 1 + uri : true + } + ]; } message GetActionResponse { policy.Action action = 1; @@ -34,6 +52,21 @@ message GetActionResponse { } message ListActionsRequest { + // Optional + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + + // ID of the namespace to scope results. If omitted, returns actions across namespaces. + string namespace_id = 1 [ + (buf.validate.field).string.uuid = true + ]; + // FQN of the namespace to scope results. If omitted, returns actions across namespaces. + string namespace_fqn = 2 [ + (buf.validate.field).string = { + min_len : 1 + uri : true + } + ]; + // Optional policy.PageRequest pagination = 10; } @@ -47,6 +80,9 @@ message ListActionsResponse { // Create a new Custom action name with optional metadata. // Creation of Standard actions is not supported. message CreateActionRequest { + // Optional + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + // Required string name = 1 [ (buf.validate.field).required = true, @@ -58,6 +94,18 @@ message CreateActionRequest { } ]; + // Optional namespace ID for the custom action. + // If omitted, create targets legacy (namespace_id = NULL) behavior unless enforced by server config. + string namespace_id = 2 [(buf.validate.field).string.uuid = true]; + // Optional namespace FQN for the custom action. + // If omitted, create targets legacy (namespace_id = NULL) behavior unless enforced by server config. + string namespace_fqn = 3 [ + (buf.validate.field).string = { + min_len : 1 + uri : true + } + ]; + // Optional common.MetadataMutable metadata = 100; } diff --git a/service/policy/actions/actions_test.go b/service/policy/actions/actions_test.go index 6288123c3b..fd5daf39ef 100644 --- a/service/policy/actions/actions_test.go +++ b/service/policy/actions/actions_test.go @@ -13,6 +13,7 @@ import ( const ( validUUID = "00000000-0000-0000-0000-000000000000" + validNamespaceFQN = "https://example.com" errMessageUUID = "string.uuid" errMessageMaxLength = "string.max_len" errMessageActionNameFormat = "action_name_format" @@ -61,7 +62,8 @@ func (s *ActionSuite) Test_CreateActionRequest_Fails() { for _, name := range actionNamesInvalidFormat { s.Run(name, func() { req := &actions.CreateActionRequest{ - Name: name, + Name: name, + NamespaceId: validUUID, } err := s.v.Validate(req) s.Require().Error(err) @@ -71,7 +73,8 @@ func (s *ActionSuite) Test_CreateActionRequest_Fails() { // no name req := &actions.CreateActionRequest{ - Name: "", + Name: "", + NamespaceId: validUUID, } err := s.v.Validate(req) s.Require().Error(err) @@ -79,32 +82,60 @@ func (s *ActionSuite) Test_CreateActionRequest_Fails() { // too long req = &actions.CreateActionRequest{ - Name: strings.Repeat("a", 254), + Name: strings.Repeat("a", 254), + NamespaceId: validUUID, } err = s.v.Validate(req) s.Require().Error(err) s.Require().Contains(err.Error(), errMessageMaxLength) + + // invalid namespace id + req = &actions.CreateActionRequest{ + Name: "valid_name", + NamespaceId: "invalid-uuid", + } + err = s.v.Validate(req) + s.Require().Error(err) + s.Require().Contains(err.Error(), errMessageUUID) + + // invalid namespace fqn + req = &actions.CreateActionRequest{ + Name: "valid_name", + NamespaceFqn: "not-a-uri", + } + err = s.v.Validate(req) + s.Require().Error(err) + s.Require().Contains(err.Error(), errMessageURI) } func (s *ActionSuite) Test_CreateActionRequest_Succeeds() { for _, name := range validNames { s.Run(name, func() { req := &actions.CreateActionRequest{ - Name: name, + Name: name, + NamespaceFqn: validNamespaceFQN, } err := s.v.Validate(req) s.Require().NoError(err) }) } - // with metadata + // with no namespace req := &actions.CreateActionRequest{ Name: "valid_name", + } + err := s.v.Validate(req) + s.Require().NoError(err) + + // with metadata + req = &actions.CreateActionRequest{ + Name: "valid_name", + NamespaceId: validUUID, Metadata: &common.MetadataMutable{ Labels: map[string]string{"key": "value"}, }, } - err := s.v.Validate(req) + err = s.v.Validate(req) s.Require().NoError(err) } @@ -135,6 +166,7 @@ func (s *ActionSuite) Test_GetAction_Fails() { Identifier: &actions.GetActionRequest_Id{ Id: "", }, + NamespaceId: validUUID, } err := s.v.Validate(req) s.Require().Error(err) @@ -151,6 +183,7 @@ func (s *ActionSuite) Test_GetAction_Fails() { Identifier: &actions.GetActionRequest_Name{ Name: name, }, + NamespaceFqn: validNamespaceFQN, } err := s.v.Validate(req) s.Require().Error(err) @@ -163,30 +196,72 @@ func (s *ActionSuite) Test_GetAction_Fails() { Identifier: &actions.GetActionRequest_Name{ Name: strings.Repeat("a", 254), }, + NamespaceId: validUUID, } err = s.v.Validate(req) s.Require().Error(err) s.Require().Contains(err.Error(), errMessageMaxLength) + + // invalid namespace id + req = &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{ + Name: "valid_name", + }, + NamespaceId: "invalid-uuid", + } + err = s.v.Validate(req) + s.Require().Error(err) + s.Require().Contains(err.Error(), errMessageUUID) + + // invalid namespace fqn + req = &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{ + Name: "valid_name", + }, + NamespaceFqn: "not-a-uri", + } + err = s.v.Validate(req) + s.Require().Error(err) + s.Require().Contains(err.Error(), errMessageURI) } func (s *ActionSuite) Test_ListActions_Succeeds() { + reqNoNamespace := &actions.ListActionsRequest{} + err := s.v.Validate(reqNoNamespace) + s.Require().NoError(err) + reqPaginated := &actions.ListActionsRequest{ + NamespaceId: validUUID, Pagination: &policy.PageRequest{ Limit: 1, }, } - err := s.v.Validate(reqPaginated) + err = s.v.Validate(reqPaginated) s.Require().NoError(err) reqPaginated.Pagination.Offset = 100 err = s.v.Validate(reqPaginated) s.Require().NoError(err) - reqNoPagination := &actions.ListActionsRequest{} + reqNoPagination := &actions.ListActionsRequest{NamespaceFqn: validNamespaceFQN} err = s.v.Validate(reqNoPagination) s.Require().NoError(err) } +func (s *ActionSuite) Test_ListActions_Fails() { + // invalid namespace id + req := &actions.ListActionsRequest{NamespaceId: "invalid-uuid"} + err := s.v.Validate(req) + s.Require().Error(err) + s.Require().Contains(err.Error(), errMessageUUID) + + // invalid namespace fqn + req = &actions.ListActionsRequest{NamespaceFqn: "not-a-uri"} + err = s.v.Validate(req) + s.Require().Error(err) + s.Require().Contains(err.Error(), errMessageURI) +} + func (s *ActionSuite) Test_UpdateActionRequest_Succeeds() { req := &actions.UpdateActionRequest{ Id: validUUID, diff --git a/service/policy/attributes/attributes.go b/service/policy/attributes/attributes.go index 6eabaf1f75..f2ae52a672 100644 --- a/service/policy/attributes/attributes.go +++ b/service/policy/attributes/attributes.go @@ -5,13 +5,17 @@ import ( "errors" "fmt" "log/slog" + "net/url" + "time" "connectrpc.com/connect" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/opentdf/platform/protocol/go/policy/attributes/attributesconnect" + "github.com/opentdf/platform/service/internal/auth/authz" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/logger/audit" + "github.com/opentdf/platform/service/pkg/cache" "github.com/opentdf/platform/service/pkg/config" "github.com/opentdf/platform/service/pkg/db" "github.com/opentdf/platform/service/pkg/serviceregistry" @@ -20,10 +24,21 @@ import ( "go.opentelemetry.io/otel/trace" ) +const ( + // defaultResolverCacheExpiration is the default TTL for cross-request resolver cache entries. + // This cache stores frequently-accessed data (attributes, namespaces) to avoid repeated DB lookups. + defaultResolverCacheExpiration = 5 * time.Minute +) + +var ErrDeprecatedListAttributeValues = errors.New("deprecated: ListAttributeValues has been removed. Use GetAttribute instead") + type AttributesService struct { //nolint:revive // AttributesService is a valid name for this struct dbClient policydb.PolicyDBClient logger *logger.Logger config *policyconfig.Config + // cache is the cross-request cache for resolver lookups (attributes, namespaces). + // Uses cache.Manager for memory-bounded storage with automatic eviction. + cache *cache.Cache trace.Tracer } @@ -49,12 +64,11 @@ func NewRegistration(ns string, dbRegister serviceregistry.DBRegister) *servicer return &serviceregistry.Service[attributesconnect.AttributesServiceHandler]{ Close: as.Close, ServiceOptions: serviceregistry.ServiceOptions[attributesconnect.AttributesServiceHandler]{ - Namespace: ns, - DB: dbRegister, - ServiceDesc: &attributes.AttributesService_ServiceDesc, - ConnectRPCFunc: attributesconnect.NewAttributesServiceHandler, - GRPCGatewayFunc: attributes.RegisterAttributesServiceHandler, - OnConfigUpdate: onUpdateConfigHook, + Namespace: ns, + DB: dbRegister, + ServiceDesc: &attributes.AttributesService_ServiceDesc, + ConnectRPCFunc: attributesconnect.NewAttributesServiceHandler, + OnConfigUpdate: onUpdateConfigHook, RegisterFunc: func(srp serviceregistry.RegistrationParams) (attributesconnect.AttributesServiceHandler, serviceregistry.HandlerServer) { logger := srp.Logger cfg, err := policyconfig.GetSharedPolicyConfig(srp.Config) @@ -66,6 +80,30 @@ func NewRegistration(ns string, dbRegister serviceregistry.DBRegister) *servicer as.logger = logger as.dbClient = policydb.NewClient(srp.DBClient, logger, int32(cfg.ListRequestLimitMax), int32(cfg.ListRequestLimitDefault)) as.config = cfg + + // Create cross-request cache for resolver lookups + // This cache stores attributes and namespaces to avoid repeated DB queries across requests + resolverCache, err := srp.NewCacheClient(cache.Options{ + Expiration: defaultResolverCacheExpiration, + }) + if err != nil { + logger.Error("error creating resolver cache", slog.String("error", err.Error())) + panic(err) + } + as.cache = resolverCache + + // Register authz resolvers per-method + // Each resolver extracts authorization dimensions from the request, performing DB lookups as needed. + // The resolver is called by the auth interceptor before the handler. + if srp.AuthzResolverRegistry != nil { + srp.AuthzResolverRegistry.MustRegister("CreateAttribute", as.createAttributeAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("GetAttribute", as.getAttributeAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("GetAttributeValuesByFqns", as.getAttributeValuesByFqnsAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("ListAttributes", as.listAttributesAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("UpdateAttribute", as.updateAttributeAuthzResolver) + srp.AuthzResolverRegistry.MustRegister("DeactivateAttribute", as.deactivateAttributeAuthzResolver) + } + return as, nil }, }, @@ -78,6 +116,12 @@ func (s *AttributesService) Close() { s.dbClient.Close() } +/// +/// Attribute Definitions +/// + +// --- CreateAttribute --- + func (s *AttributesService) CreateAttribute(ctx context.Context, req *connect.Request[attributes.CreateAttributeRequest], ) (*connect.Response[attributes.CreateAttributeResponse], error) { @@ -112,6 +156,8 @@ func (s *AttributesService) CreateAttribute(ctx context.Context, return connect.NewResponse(rsp), nil } +// --- ListAttributes --- + func (s *AttributesService) ListAttributes(ctx context.Context, req *connect.Request[attributes.ListAttributesRequest], ) (*connect.Response[attributes.ListAttributesResponse], error) { @@ -129,6 +175,8 @@ func (s *AttributesService) ListAttributes(ctx context.Context, return connect.NewResponse(rsp), nil } +// --- GetAttribute --- + func (s *AttributesService) GetAttribute(ctx context.Context, req *connect.Request[attributes.GetAttributeRequest], ) (*connect.Response[attributes.GetAttributeResponse], error) { @@ -137,8 +185,16 @@ func (s *AttributesService) GetAttribute(ctx context.Context, rsp := &attributes.GetAttributeResponse{} - var identifier any + // Check if attribute was already fetched by authz resolver (avoid duplicate DB query) + if cached := authz.GetResolvedDataFromContext(ctx, ResolverCacheKeyAttribute); cached != nil { + if attr, ok := cached.(*policy.Attribute); ok { + rsp.Attribute = attr + return connect.NewResponse(rsp), nil + } + } + // Fallback to DB query if not cached (e.g., v1 authz mode or no resolver) + var identifier any if req.Msg.GetId() != "" { //nolint:staticcheck // Id can still be used until removed identifier = req.Msg.GetId() //nolint:staticcheck // Id can still be used until removed } else { @@ -151,9 +207,11 @@ func (s *AttributesService) GetAttribute(ctx context.Context, } rsp.Attribute = item - return connect.NewResponse(rsp), err + return connect.NewResponse(rsp), nil } +// --- GetAttributeValuesByFqns --- + func (s *AttributesService) GetAttributeValuesByFqns(ctx context.Context, req *connect.Request[attributes.GetAttributeValuesByFqnsRequest], ) (*connect.Response[attributes.GetAttributeValuesByFqnsResponse], error) { @@ -171,6 +229,8 @@ func (s *AttributesService) GetAttributeValuesByFqns(ctx context.Context, return connect.NewResponse(rsp), nil } +// --- UpdateAttribute --- + func (s *AttributesService) UpdateAttribute(ctx context.Context, req *connect.Request[attributes.UpdateAttributeRequest], ) (*connect.Response[attributes.UpdateAttributeResponse], error) { @@ -183,10 +243,20 @@ func (s *AttributesService) UpdateAttribute(ctx context.Context, ObjectID: attributeID, } - original, err := s.dbClient.GetAttribute(ctx, attributeID) - if err != nil { - s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("id", attributeID)) + // Check if attribute was already fetched by authz resolver (avoid duplicate DB query) + var original *policy.Attribute + if cached := authz.GetResolvedDataFromContext(ctx, ResolverCacheKeyAttribute); cached != nil { + original, _ = cached.(*policy.Attribute) + } + + // Fallback to DB query if not cached (e.g., v1 authz mode or no resolver) + if original == nil { + var err error + original, err = s.dbClient.GetAttribute(ctx, attributeID) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("id", attributeID)) + } } updated, err := s.dbClient.UpdateAttribute(ctx, attributeID, req.Msg) @@ -195,6 +265,9 @@ func (s *AttributesService) UpdateAttribute(ctx context.Context, return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("id", req.Msg.GetId()), slog.String("attribute", req.Msg.String())) } + // Invalidate cross-request cache after successful update + s.invalidateAttributeCache(ctx, attributeID) + auditParams.Original = original auditParams.Updated = updated s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) @@ -206,6 +279,8 @@ func (s *AttributesService) UpdateAttribute(ctx context.Context, return connect.NewResponse(rsp), nil } +// --- DeactivateAttribute --- + func (s *AttributesService) DeactivateAttribute(ctx context.Context, req *connect.Request[attributes.DeactivateAttributeRequest], ) (*connect.Response[attributes.DeactivateAttributeResponse], error) { @@ -218,10 +293,20 @@ func (s *AttributesService) DeactivateAttribute(ctx context.Context, ObjectID: attributeID, } - original, err := s.dbClient.GetAttribute(ctx, attributeID) - if err != nil { - s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("id", attributeID)) + // Check if attribute was already fetched by authz resolver (avoid duplicate DB query) + var original *policy.Attribute + if cached := authz.GetResolvedDataFromContext(ctx, ResolverCacheKeyAttribute); cached != nil { + original, _ = cached.(*policy.Attribute) + } + + // Fallback to DB query if not cached (e.g., v1 authz mode or no resolver) + if original == nil { + var err error + original, err = s.dbClient.GetAttribute(ctx, attributeID) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("id", attributeID)) + } } updated, err := s.dbClient.DeactivateAttribute(ctx, attributeID) @@ -230,6 +315,9 @@ func (s *AttributesService) DeactivateAttribute(ctx context.Context, return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextDeactivationFailed, slog.String("id", attributeID)) } + // Invalidate cross-request cache after successful deactivation + s.invalidateAttributeCache(ctx, attributeID) + auditParams.Original = original auditParams.Updated = updated s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) @@ -274,19 +362,8 @@ func (s *AttributesService) CreateAttributeValue(ctx context.Context, req *conne return connect.NewResponse(rsp), nil } -func (s *AttributesService) ListAttributeValues(ctx context.Context, req *connect.Request[attributes.ListAttributeValuesRequest]) (*connect.Response[attributes.ListAttributeValuesResponse], error) { - state := req.Msg.GetState().String() - s.logger.DebugContext(ctx, - "listing attribute values", - slog.String("attribute_id", req.Msg.GetAttributeId()), - slog.String("state", state), - ) - rsp, err := s.dbClient.ListAttributeValues(ctx, req.Msg) - if err != nil { - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextListRetrievalFailed, slog.String("attributeId", req.Msg.GetAttributeId())) - } - - return connect.NewResponse(rsp), nil +func (s *AttributesService) ListAttributeValues(_ context.Context, _ *connect.Request[attributes.ListAttributeValuesRequest]) (*connect.Response[attributes.ListAttributeValuesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, ErrDeprecatedListAttributeValues) } func (s *AttributesService) GetAttributeValue(ctx context.Context, req *connect.Request[attributes.GetAttributeValueRequest]) (*connect.Response[attributes.GetAttributeValueResponse], error) { @@ -374,12 +451,12 @@ func (s *AttributesService) DeactivateAttributeValue(ctx context.Context, req *c return connect.NewResponse(rsp), nil } -func (s *AttributesService) AssignKeyAccessServerToAttribute(_ context.Context, _ *connect.Request[attributes.AssignKeyAccessServerToAttributeRequest]) (*connect.Response[attributes.AssignKeyAccessServerToAttributeResponse], error) { +func (s *AttributesService) AssignKeyAccessServerToAttribute(_ context.Context, _ *connect.Request[attributes.AssignKeyAccessServerToAttributeRequest]) (*connect.Response[attributes.AssignKeyAccessServerToAttributeResponse], error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. return nil, connect.NewError(connect.CodeUnimplemented, errors.New("this compatibility stub will be removed entirely in the following release")) } -func (s *AttributesService) RemoveKeyAccessServerFromAttribute(ctx context.Context, req *connect.Request[attributes.RemoveKeyAccessServerFromAttributeRequest]) (*connect.Response[attributes.RemoveKeyAccessServerFromAttributeResponse], error) { - rsp := &attributes.RemoveKeyAccessServerFromAttributeResponse{} +func (s *AttributesService) RemoveKeyAccessServerFromAttribute(ctx context.Context, req *connect.Request[attributes.RemoveKeyAccessServerFromAttributeRequest]) (*connect.Response[attributes.RemoveKeyAccessServerFromAttributeResponse], error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + rsp := &attributes.RemoveKeyAccessServerFromAttributeResponse{} //nolint:staticcheck // Deprecated response retained for compatibility endpoint. auditParams := audit.PolicyEventParams{ ActionType: audit.ActionTypeDelete, @@ -389,7 +466,7 @@ func (s *AttributesService) RemoveKeyAccessServerFromAttribute(ctx context.Conte attributeKas, err := s.dbClient.RemoveKeyAccessServerFromAttribute(ctx, req.Msg.GetAttributeKeyAccessServer()) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("attributeKas", req.Msg.GetAttributeKeyAccessServer().String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("attribute_kas", req.Msg.GetAttributeKeyAccessServer().String())) } auditParams.ObjectID = attributeKas.GetAttributeId() @@ -402,12 +479,12 @@ func (s *AttributesService) RemoveKeyAccessServerFromAttribute(ctx context.Conte return connect.NewResponse(rsp), nil } -func (s *AttributesService) AssignKeyAccessServerToValue(_ context.Context, _ *connect.Request[attributes.AssignKeyAccessServerToValueRequest]) (*connect.Response[attributes.AssignKeyAccessServerToValueResponse], error) { +func (s *AttributesService) AssignKeyAccessServerToValue(_ context.Context, _ *connect.Request[attributes.AssignKeyAccessServerToValueRequest]) (*connect.Response[attributes.AssignKeyAccessServerToValueResponse], error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. return nil, connect.NewError(connect.CodeUnimplemented, errors.New("this compatibility stub will be removed entirely in the following release")) } -func (s *AttributesService) RemoveKeyAccessServerFromValue(ctx context.Context, req *connect.Request[attributes.RemoveKeyAccessServerFromValueRequest]) (*connect.Response[attributes.RemoveKeyAccessServerFromValueResponse], error) { - rsp := &attributes.RemoveKeyAccessServerFromValueResponse{} +func (s *AttributesService) RemoveKeyAccessServerFromValue(ctx context.Context, req *connect.Request[attributes.RemoveKeyAccessServerFromValueRequest]) (*connect.Response[attributes.RemoveKeyAccessServerFromValueResponse], error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. + rsp := &attributes.RemoveKeyAccessServerFromValueResponse{} //nolint:staticcheck // Deprecated response retained for compatibility endpoint. auditParams := audit.PolicyEventParams{ ActionType: audit.ActionTypeDelete, @@ -417,7 +494,7 @@ func (s *AttributesService) RemoveKeyAccessServerFromValue(ctx context.Context, valueKas, err := s.dbClient.RemoveKeyAccessServerFromValue(ctx, req.Msg.GetValueKeyAccessServer()) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("attributeValueKas", req.Msg.GetValueKeyAccessServer().String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("attribute_value_kas", req.Msg.GetValueKeyAccessServer().String())) } auditParams.ObjectID = valueKas.GetValueId() @@ -440,7 +517,7 @@ func (s *AttributesService) AssignPublicKeyToAttribute(ctx context.Context, r *c ak, err := s.dbClient.AssignPublicKeyToAttribute(ctx, r.Msg.GetAttributeKey()) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("attributeKey", r.Msg.GetAttributeKey().String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("attribute_key", r.Msg.GetAttributeKey().String())) } auditParams.ObjectID = ak.GetAttributeId() @@ -462,7 +539,7 @@ func (s *AttributesService) RemovePublicKeyFromAttribute(ctx context.Context, r ak, err := s.dbClient.RemovePublicKeyFromAttribute(ctx, r.Msg.GetAttributeKey()) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextDeletionFailed, slog.String("attributeKey", r.Msg.GetAttributeKey().String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextDeletionFailed, slog.String("attribute_key", r.Msg.GetAttributeKey().String())) } auditParams.ObjectID = ak.GetAttributeId() @@ -483,7 +560,7 @@ func (s *AttributesService) AssignPublicKeyToValue(ctx context.Context, r *conne vk, err := s.dbClient.AssignPublicKeyToValue(ctx, r.Msg.GetValueKey()) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("attributeKey", r.Msg.GetValueKey().String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("value_key", r.Msg.GetValueKey().String())) } auditParams.ObjectID = vk.GetValueId() @@ -505,7 +582,7 @@ func (s *AttributesService) RemovePublicKeyFromValue(ctx context.Context, r *con vk, err := s.dbClient.RemovePublicKeyFromValue(ctx, r.Msg.GetValueKey()) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextDeletionFailed, slog.String("attributeKey", r.Msg.GetValueKey().String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextDeletionFailed, slog.String("value_key", r.Msg.GetValueKey().String())) } auditParams.ObjectID = vk.GetValueId() @@ -515,3 +592,305 @@ func (s *AttributesService) RemovePublicKeyFromValue(ctx context.Context, r *con return connect.NewResponse(rsp), nil } + +/// +/// Authz Resolvers +/// +/// These methods resolve authorization dimensions from requests. +/// They are placed at the end of the file per linting rules (unexported methods after exported). + +// ResolverCacheKeyAttribute is the key used to cache fetched attributes in the ResolverContext. +// Handlers can retrieve the cached attribute via authz.GetResolvedDataFromContext(ctx, ResolverCacheKeyAttribute). +const ResolverCacheKeyAttribute = "attribute" + +// Cache key prefixes for cross-request caching +const ( + cacheKeyPrefixAttributeByID = "attr:id:" + cacheKeyPrefixNamespaceByID = "ns:id:" +) + +// getAttributeFromCache attempts to get an attribute from the cross-request cache. +// Returns nil if not found or on cache error (cache miss is not an error condition). +func (s *AttributesService) getAttributeFromCache(ctx context.Context, id string) *policy.Attribute { + if s.cache == nil { + return nil + } + cached, err := s.cache.Get(ctx, cacheKeyPrefixAttributeByID+id) + if err != nil { + return nil // Cache miss - not an error + } + if attr, ok := cached.(*policy.Attribute); ok { + return attr + } + return nil +} + +// setAttributeInCache stores an attribute in the cross-request cache. +// Errors are logged but not returned (cache failures should not break the flow). +func (s *AttributesService) setAttributeInCache(ctx context.Context, attr *policy.Attribute) { + if s.cache == nil || attr == nil || attr.GetId() == "" { + return + } + if err := s.cache.Set(ctx, cacheKeyPrefixAttributeByID+attr.GetId(), attr, nil); err != nil { + s.logger.WarnContext( + ctx, + "failed to cache attribute", + slog.String("id", attr.GetId()), + slog.Any("error", err), + ) + } +} + +// invalidateAttributeCache removes an attribute from the cross-request cache. +// Called after mutations (update, deactivate) to ensure stale data is not served. +func (s *AttributesService) invalidateAttributeCache(ctx context.Context, id string) { + if s.cache == nil || id == "" { + return + } + if err := s.cache.Delete(ctx, cacheKeyPrefixAttributeByID+id); err != nil { + s.logger.WarnContext( + ctx, + "failed to invalidate attribute cache", + slog.String("id", id), + slog.Any("error", err), + ) + } +} + +// getNamespaceFromCache attempts to get a namespace from the cross-request cache. +func (s *AttributesService) getNamespaceFromCache(ctx context.Context, id string) *policy.Namespace { + if s.cache == nil { + return nil + } + cached, err := s.cache.Get(ctx, cacheKeyPrefixNamespaceByID+id) + if err != nil { + return nil + } + if ns, ok := cached.(*policy.Namespace); ok { + return ns + } + return nil +} + +// setNamespaceInCache stores a namespace in the cross-request cache. +func (s *AttributesService) setNamespaceInCache(ctx context.Context, ns *policy.Namespace) { + if s.cache == nil || ns == nil || ns.GetId() == "" { + return + } + if err := s.cache.Set(ctx, cacheKeyPrefixNamespaceByID+ns.GetId(), ns, nil); err != nil { + s.logger.WarnContext( + ctx, + "failed to cache namespace", + slog.String("id", ns.GetId()), + slog.Any("error", err), + ) + } +} + +// getAttributeCached retrieves an attribute, checking cross-request cache first, then DB. +// On cache miss, the attribute is fetched from DB and stored in the cache. +func (s *AttributesService) getAttributeCached(ctx context.Context, id string) (*policy.Attribute, error) { + // Check cross-request cache first + if attr := s.getAttributeFromCache(ctx, id); attr != nil { + s.logger.TraceContext(ctx, "resolver cache hit for attribute", slog.String("id", id)) + return attr, nil + } + + // Cache miss - fetch from DB + attr, err := s.dbClient.GetAttribute(ctx, id) + if err != nil { + return nil, err + } + + // Store in cross-request cache for future requests + s.setAttributeInCache(ctx, attr) + + return attr, nil +} + +// getNamespaceCached retrieves a namespace, checking cross-request cache first, then DB. +func (s *AttributesService) getNamespaceCached(ctx context.Context, id string) (*policy.Namespace, error) { + // Check cross-request cache first + if ns := s.getNamespaceFromCache(ctx, id); ns != nil { + s.logger.TraceContext(ctx, "resolver cache hit for namespace", slog.String("id", id)) + return ns, nil + } + + // Cache miss - fetch from DB + ns, err := s.dbClient.GetNamespace(ctx, id) + if err != nil { + return nil, err + } + + // Store in cross-request cache for future requests + s.setNamespaceInCache(ctx, ns) + + return ns, nil +} + +// createAttributeAuthzResolver resolves namespace from the request's namespace_id. +func (s *AttributesService) createAttributeAuthzResolver(ctx context.Context, req connect.AnyRequest) (authz.ResolverContext, error) { + resolverCtx := authz.NewResolverContext() + msg, ok := req.Any().(*attributes.CreateAttributeRequest) + if !ok { + return resolverCtx, fmt.Errorf("unexpected request type: %T", req.Any()) + } + + // Use cached namespace lookup + ns, err := s.getNamespaceCached(ctx, msg.GetNamespaceId()) + if err != nil { + return resolverCtx, fmt.Errorf("failed to resolve namespace for authz: %w", err) + } + + res := resolverCtx.NewResource() + res.AddDimension("namespace", ns.GetName()) + + return resolverCtx, nil +} + +// listAttributesAuthzResolver resolves optional namespace filter. +func (s *AttributesService) listAttributesAuthzResolver(_ context.Context, req connect.AnyRequest) (authz.ResolverContext, error) { + resolverCtx := authz.NewResolverContext() + msg, ok := req.Any().(*attributes.ListAttributesRequest) + if !ok { + return resolverCtx, fmt.Errorf("unexpected request type: %T", req.Any()) + } + + res := resolverCtx.NewResource() + // Namespace filter is optional - empty means "all accessible namespaces" + if ns := msg.GetNamespace(); ns != "" { + res.AddDimension("namespace", ns) + } + + return resolverCtx, nil +} + +// getAttributeAuthzResolver resolves namespace from attribute lookup. +func (s *AttributesService) getAttributeAuthzResolver(ctx context.Context, req connect.AnyRequest) (authz.ResolverContext, error) { + resolverCtx := authz.NewResolverContext() + msg, ok := req.Any().(*attributes.GetAttributeRequest) + if !ok { + return resolverCtx, fmt.Errorf("unexpected request type: %T", req.Any()) + } + + // Determine the identifier (deprecated ID, new attribute_id, or FQN) + var id string + switch { + case msg.GetId() != "": //nolint:staticcheck // Id can still be used until removed + id = msg.GetId() //nolint:staticcheck // Id can still be used until removed + case msg.GetAttributeId() != "": + id = msg.GetAttributeId() + case msg.GetFqn() != "": + // FQN lookups can't use ID-based cache; fall back to DB + attr, err := s.dbClient.GetAttribute(ctx, msg.GetIdentifier()) + if err != nil { + return resolverCtx, fmt.Errorf("failed to resolve attribute for authz: %w", err) + } + // Cache by ID for future lookups + s.setAttributeInCache(ctx, attr) + + res := resolverCtx.NewResource() + res.AddDimension("namespace", attr.GetNamespace().GetName()) + res.AddDimension("attribute", attr.GetName()) + resolverCtx.SetResolvedData(ResolverCacheKeyAttribute, attr) + return resolverCtx, nil + default: + // No valid identifier provided + return resolverCtx, errors.New("no valid identifier provided for attribute lookup") + } + + // Use cached attribute lookup (checks cross-request cache, then DB) + attr, err := s.getAttributeCached(ctx, id) + if err != nil { + return resolverCtx, fmt.Errorf("failed to resolve attribute for authz: %w", err) + } + + res := resolverCtx.NewResource() + res.AddDimension("namespace", attr.GetNamespace().GetName()) + res.AddDimension("attribute", attr.GetName()) + + // Store in per-request cache for handler reuse (avoids duplicate DB query within same request) + resolverCtx.SetResolvedData(ResolverCacheKeyAttribute, attr) + + return resolverCtx, nil +} + +// updateAttributeAuthzResolver resolves namespace from attribute lookup. +func (s *AttributesService) updateAttributeAuthzResolver(ctx context.Context, req connect.AnyRequest) (authz.ResolverContext, error) { + resolverCtx := authz.NewResolverContext() + msg, ok := req.Any().(*attributes.UpdateAttributeRequest) + if !ok { + return resolverCtx, fmt.Errorf("unexpected request type: %T", req.Any()) + } + + // Use cached attribute lookup (checks cross-request cache, then DB) + attr, err := s.getAttributeCached(ctx, msg.GetId()) + if err != nil { + return resolverCtx, fmt.Errorf("failed to resolve attribute for authz: %w", err) + } + + res := resolverCtx.NewResource() + res.AddDimension("namespace", attr.GetNamespace().GetName()) + res.AddDimension("attribute", attr.GetName()) + + // Store in per-request cache for handler reuse (avoids duplicate DB query within same request) + resolverCtx.SetResolvedData(ResolverCacheKeyAttribute, attr) + + return resolverCtx, nil +} + +// deactivateAttributeAuthzResolver resolves namespace from attribute lookup. +func (s *AttributesService) deactivateAttributeAuthzResolver(ctx context.Context, req connect.AnyRequest) (authz.ResolverContext, error) { + resolverCtx := authz.NewResolverContext() + msg, ok := req.Any().(*attributes.DeactivateAttributeRequest) + if !ok { + return resolverCtx, fmt.Errorf("unexpected request type: %T", req.Any()) + } + + // Use cached attribute lookup (checks cross-request cache, then DB) + attr, err := s.getAttributeCached(ctx, msg.GetId()) + if err != nil { + return resolverCtx, fmt.Errorf("failed to resolve attribute for authz: %w", err) + } + + res := resolverCtx.NewResource() + res.AddDimension("namespace", attr.GetNamespace().GetName()) + res.AddDimension("attribute", attr.GetName()) + + // Store in per-request cache for handler reuse (avoids duplicate DB query within same request) + resolverCtx.SetResolvedData(ResolverCacheKeyAttribute, attr) + + return resolverCtx, nil +} + +// getAttributeValuesByFqnsAuthzResolver resolves namespaces from FQNs. +// FQN format: https:///attr//value/ +// Since FQNs can span multiple namespaces, this creates a resource per unique namespace. +func (s *AttributesService) getAttributeValuesByFqnsAuthzResolver(_ context.Context, req connect.AnyRequest) (authz.ResolverContext, error) { + resolverCtx := authz.NewResolverContext() + msg, ok := req.Any().(*attributes.GetAttributeValuesByFqnsRequest) + if !ok { + return resolverCtx, fmt.Errorf("unexpected request type: %T", req.Any()) + } + + // Extract unique namespaces from FQNs + // FQN format: https:///attr//value/ + namespaces := make(map[string]struct{}) + for _, fqn := range msg.GetFqns() { + parsed, err := url.Parse(fqn) + if err != nil { + continue // Skip malformed FQNs; DB will validate later + } + if parsed.Host != "" { + namespaces[parsed.Host] = struct{}{} + } + } + + // Create a resource for each unique namespace + for ns := range namespaces { + res := resolverCtx.NewResource() + res.AddDimension("namespace", ns) + } + + return resolverCtx, nil +} diff --git a/service/policy/attributes/attributes.proto b/service/policy/attributes/attributes.proto index 2dbf98e0b9..f43caa5130 100644 --- a/service/policy/attributes/attributes.proto +++ b/service/policy/attributes/attributes.proto @@ -4,7 +4,7 @@ package policy.attributes; import "buf/validate/validate.proto"; import "common/common.proto"; -import "google/api/annotations.proto"; +import "google/protobuf/wrappers.proto"; import "policy/objects.proto"; import "policy/selectors.proto"; @@ -62,6 +62,18 @@ message ValueKey { Attribute Service Definitions */ +enum SortAttributesType { + SORT_ATTRIBUTES_TYPE_UNSPECIFIED = 0; + SORT_ATTRIBUTES_TYPE_NAME = 1; + SORT_ATTRIBUTES_TYPE_CREATED_AT = 2; + SORT_ATTRIBUTES_TYPE_UPDATED_AT = 3; +} + +message AttributesSort { + SortAttributesType field = 1 [(buf.validate.field).enum.defined_only = true]; + policy.SortDirection direction = 2 [(buf.validate.field).enum.defined_only = true]; +} + message ListAttributesRequest { // Optional // ACTIVE by default when not specified @@ -72,6 +84,12 @@ message ListAttributesRequest { // Optional policy.PageRequest pagination = 10; + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + repeated AttributesSort sort = 11 [(buf.validate.field).repeated.max_items = 1]; } message ListAttributesResponse { repeated policy.Attribute attributes = 1; @@ -145,6 +163,14 @@ message CreateAttributeRequest { } }]; + // Optional + // Setting allow_traversal=true allows TDF creation to be front-loaded, meaning a customer + // can create encrypted content with an attribute definitions key mapping before + // creating the attribute values needed to decrypt. + // Content will be able to be encrypted with missing attribute values, + // but will not be able to be decrypted until such attribute values exist. + google.protobuf.BoolValue allow_traversal = 5; + // Optional common.MetadataMutable metadata = 100; } @@ -225,6 +251,17 @@ message ListAttributeValuesResponse { policy.PageResponse pagination = 10; } +message AttributeValueObligationTriggerRequest { + // Required. Existing obligation value to associate with the newly created attribute value. + common.IdFqnIdentifier obligation_value = 1 [(buf.validate.field).required = true]; + // Required. Action that, together with the newly created attribute value, triggers the obligation value. + common.IdNameIdentifier action = 2 [(buf.validate.field).required = true]; + // Optional. Request context for the obligation trigger. + policy.RequestContext context = 11; + // Optional. Common metadata for the obligation trigger. + common.MetadataMutable metadata = 100; +} + message CreateAttributeValueRequest { // Required string attribute_id = 1 [(buf.validate.field).string.uuid = true]; @@ -243,6 +280,10 @@ message CreateAttributeValueRequest { reserved "members"; reserved 3; + // Optional + // Existing obligation values to trigger for the newly created attribute value. + repeated AttributeValueObligationTriggerRequest obligation_triggers = 11; + // Optional // Common metadata common.MetadataMutable metadata = 100; @@ -403,7 +444,10 @@ service AttributesService { rpc ListAttributes(ListAttributesRequest) returns (ListAttributesResponse) { option idempotency_level = NO_SIDE_EFFECTS; } + // Deprecated + // Use GetAttribute rpc ListAttributeValues(ListAttributeValuesRequest) returns (ListAttributeValuesResponse) { + option deprecated = true; option idempotency_level = NO_SIDE_EFFECTS; } @@ -411,7 +455,6 @@ service AttributesService { option idempotency_level = NO_SIDE_EFFECTS; } rpc GetAttributeValuesByFqns(GetAttributeValuesByFqnsRequest) returns (GetAttributeValuesByFqnsResponse) { - option (google.api.http) = {get: "/attributes/*/fqn"}; option idempotency_level = NO_SIDE_EFFECTS; } diff --git a/service/policy/attributes/attributes_test.go b/service/policy/attributes/attributes_test.go index da6dd51dbb..1b68c6dc4e 100644 --- a/service/policy/attributes/attributes_test.go +++ b/service/policy/attributes/attributes_test.go @@ -6,9 +6,11 @@ import ( "testing" "buf.build/go/protovalidate" + "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/wrapperspb" ) func getValidator() protovalidate.Validator { @@ -73,6 +75,20 @@ func TestCreateAttribute_WithValues_Valid_Succeeds(t *testing.T) { require.NoError(t, err) } +func TestCreateAttribute_AllowTraversal_Valid_Succeeds(t *testing.T) { + req := &attributes.CreateAttributeRequest{ + Name: validName, + NamespaceId: validUUID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + AllowTraversal: &wrapperspb.BoolValue{Value: true}, + } + + v := getValidator() + err := v.Validate(req) + + require.NoError(t, err) +} + func TestCreateAttribute_NameTooLong_Fails(t *testing.T) { name := strings.Repeat("a", 254) req := &attributes.CreateAttributeRequest{ @@ -355,6 +371,127 @@ func TestCreateAttributeValue_Valid_Succeeds(t *testing.T) { require.NoError(t, err) } +func TestCreateAttributeValue_WithObligationTriggers_Request(t *testing.T) { + validFQN := "https://example.com/obl/test/value/value1" + validRequestContext := &policy.RequestContext{ + Pep: &policy.PolicyEnforcementPoint{ + ClientId: "client-id", + }, + } + + testCases := []struct { + name string + req *attributes.CreateAttributeValueRequest + expectError bool + errorMessage string + }{ + { + name: "valid with obligation trigger ids", + req: &attributes.CreateAttributeValueRequest{ + AttributeId: validUUID, + Value: validValue1, + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + ObligationValue: &common.IdFqnIdentifier{Id: validUUID}, + Action: &common.IdNameIdentifier{Id: validUUID}, + Context: validRequestContext, + Metadata: &common.MetadataMutable{ + Labels: map[string]string{"source": "inline"}, + }, + }, + }, + }, + expectError: false, + }, + { + name: "valid with obligation trigger fqn and action name", + req: &attributes.CreateAttributeValueRequest{ + AttributeId: validUUID, + Value: validValue1, + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + ObligationValue: &common.IdFqnIdentifier{Fqn: validFQN}, + Action: &common.IdNameIdentifier{Name: "read"}, + }, + }, + }, + expectError: false, + }, + { + name: "invalid trigger with invalid obligation value id", + req: &attributes.CreateAttributeValueRequest{ + AttributeId: validUUID, + Value: validValue1, + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + ObligationValue: &common.IdFqnIdentifier{Id: "invalid-uuid"}, + Action: &common.IdNameIdentifier{Id: validUUID}, + }, + }, + }, + expectError: true, + errorMessage: "obligation_value.id", + }, + { + name: "invalid trigger with missing obligation value", + req: &attributes.CreateAttributeValueRequest{ + AttributeId: validUUID, + Value: validValue1, + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + Action: &common.IdNameIdentifier{Id: validUUID}, + }, + }, + }, + expectError: true, + errorMessage: "obligation_value", + }, + { + name: "invalid trigger with invalid action id", + req: &attributes.CreateAttributeValueRequest{ + AttributeId: validUUID, + Value: validValue1, + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + ObligationValue: &common.IdFqnIdentifier{Id: validUUID}, + Action: &common.IdNameIdentifier{Id: "invalid-uuid"}, + }, + }, + }, + expectError: true, + errorMessage: "action.id", + }, + { + name: "invalid trigger with missing action", + req: &attributes.CreateAttributeValueRequest{ + AttributeId: validUUID, + Value: validValue1, + ObligationTriggers: []*attributes.AttributeValueObligationTriggerRequest{ + { + ObligationValue: &common.IdFqnIdentifier{Id: validUUID}, + }, + }, + }, + expectError: true, + errorMessage: "action", + }, + } + + v := getValidator() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := v.Validate(tc.req) + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errorMessage) + } else { + require.NoError(t, err) + } + }) + } +} + func TestCreateAttributeValue_ValueTooLong_Fails(t *testing.T) { value := strings.Repeat("a", 254) req := &attributes.CreateAttributeValueRequest{ @@ -878,3 +1015,39 @@ func Test_RemovePublicKeyFromValue(t *testing.T) { }) } } + +func Test_ListAttributesRequest_Sort(t *testing.T) { + v := getValidator() + + // no sort — valid + req := &attributes.ListAttributesRequest{} + require.NoError(t, v.Validate(req)) + + // one sort item — valid + req = &attributes.ListAttributesRequest{ + Sort: []*attributes.AttributesSort{ + { + Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + } + require.NoError(t, v.Validate(req)) + + // two sort items — exceeds max_items = 1 + req = &attributes.ListAttributesRequest{ + Sort: []*attributes.AttributesSort{ + { + Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + { + Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME, + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + } + err := v.Validate(req) + require.Error(t, err) + require.Contains(t, err.Error(), "sort") +} diff --git a/service/policy/config/config.go b/service/policy/config/config.go index 0f7a74dace..d237001aa7 100644 --- a/service/policy/config/config.go +++ b/service/policy/config/config.go @@ -15,6 +15,8 @@ type Config struct { ListRequestLimitDefault int `mapstructure:"list_request_limit_default" default:"1000"` // Maximum pagination list limit allowed by policy services ListRequestLimitMax int `mapstructure:"list_request_limit_max" default:"2500"` + // Enable support for namespaced policies. If false, namespace fields are ignored and treated as null. + NamespacedPolicy bool `mapstructure:"namespaced_policy" default:"false"` } func (c Config) Validate() error { diff --git a/service/policy/db/actions.go b/service/policy/db/actions.go index ec91ef640d..fc212dd126 100644 --- a/service/policy/db/actions.go +++ b/service/policy/db/actions.go @@ -2,7 +2,6 @@ package db import ( "context" - "errors" "fmt" "strings" @@ -44,6 +43,17 @@ func (c PolicyDBClient) GetAction(ctx context.Context, req *actions.GetActionReq getActionParams.ID = pgtypeUUID(req.GetId()) case req.GetName() != "": getActionParams.Name = pgtypeText(strings.ToLower(req.GetName())) + + namespaceID := req.GetNamespaceId() + if len(namespaceID) > 0 { + parsedID := pgtypeUUID(namespaceID) + if !parsedID.Valid { + return nil, db.ErrUUIDInvalid + } + getActionParams.NamespaceID = parsedID + } else if req.GetNamespaceFqn() != "" { + getActionParams.NamespaceFqn = pgtypeText(req.GetNamespaceFqn()) + } default: return nil, db.ErrSelectIdentifierInvalid } @@ -58,10 +68,16 @@ func (c PolicyDBClient) GetAction(ctx context.Context, req *actions.GetActionReq return nil, db.WrapIfKnownInvalidQueryErr(err) } + namespace, err := hydrateNamespaceFromInterface(got.Namespace) + if err != nil { + return nil, err + } + return &policy.Action{ - Id: got.ID, - Name: got.Name, - Metadata: metadata, + Id: got.ID, + Name: got.Name, + Metadata: metadata, + Namespace: namespace, }, nil } @@ -74,8 +90,10 @@ func (c PolicyDBClient) ListActions(ctx context.Context, req *actions.ListAction } list, err := c.queries.listActions(ctx, listActionsParams{ - Limit: limit, - Offset: offset, + NamespaceID: pgtypeUUID(req.GetNamespaceId()), + NamespaceFqn: pgtypeText(req.GetNamespaceFqn()), + Limit: limit, + Offset: offset, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -90,10 +108,15 @@ func (c PolicyDBClient) ListActions(ctx context.Context, req *actions.ListAction if err := unmarshalMetadata(a.Metadata, metadata); err != nil { return nil, err } + namespace, err := hydrateNamespaceFromInterface(a.Namespace) + if err != nil { + return nil, err + } action := &policy.Action{ - Id: a.ID, - Name: a.Name, - Metadata: metadata, + Id: a.ID, + Name: a.Name, + Metadata: metadata, + Namespace: namespace, } if a.IsStandard { actionsStandard = append(actionsStandard, action) @@ -121,13 +144,28 @@ func (c PolicyDBClient) ListActions(ctx context.Context, req *actions.ListAction } func (c PolicyDBClient) CreateAction(ctx context.Context, req *actions.CreateActionRequest) (*policy.Action, error) { + name := strings.ToLower(req.GetName()) + if ActionStandard(name).IsValid() { + return nil, fmt.Errorf("cannot create standard action %s: %w", name, db.ErrRestrictViolation) + } + + namespaceID := req.GetNamespaceId() + namespaceFQN := req.GetNamespaceFqn() + useID := len(namespaceID) > 0 + parsedID := pgtypeUUID(namespaceID) + if useID && !parsedID.Valid { + return nil, db.ErrUUIDInvalid + } + metadataJSON, _, err := db.MarshalCreateMetadata(req.GetMetadata()) if err != nil { return nil, err } createParams := createCustomActionParams{ - Name: strings.ToLower(req.GetName()), - Metadata: metadataJSON, + Name: name, + Metadata: metadataJSON, + NamespaceID: parsedID, + NamespaceFqn: pgtypeText(namespaceFQN), } createdID, err := c.queries.createCustomAction(ctx, createParams) @@ -143,6 +181,13 @@ func (c PolicyDBClient) CreateAction(ctx context.Context, req *actions.CreateAct } func (c PolicyDBClient) UpdateAction(ctx context.Context, req *actions.UpdateActionRequest) (*policy.Action, error) { + if req.GetName() != "" { + name := strings.ToLower(req.GetName()) + if ActionStandard(name).IsValid() { + return nil, fmt.Errorf("cannot rename custom action to standard action %s: %w", name, db.ErrRestrictViolation) + } + } + // if extend we need to merge the metadata metadataJSON, metadata, err := db.MarshalUpdateMetadata(req.GetMetadata(), req.GetMetadataUpdateBehavior(), func() (*common.Metadata, error) { a, err := c.GetAction(ctx, &actions.GetActionRequest{ @@ -174,38 +219,39 @@ func (c PolicyDBClient) UpdateAction(ctx context.Context, req *actions.UpdateAct return nil, db.ErrNotFound } - return &policy.Action{ - Id: req.GetId(), - Name: req.GetName(), - Metadata: metadata, - }, nil + updated, err := c.GetAction(ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Id{ + Id: req.GetId(), + }, + }) + if err != nil { + return nil, err + } + if metadata != nil { + updated.Metadata = metadata + } + + return updated, nil } func (c PolicyDBClient) DeleteAction(ctx context.Context, req *actions.DeleteActionRequest) (*policy.Action, error) { + got, err := c.GetAction(ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Id{ + Id: req.GetId(), + }, + }) + if err != nil { + return nil, err + } + count, err := c.queries.deleteCustomAction(ctx, req.GetId()) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) } - // if did not delete, was either not found or was a standard action + // if not deleted, it is a standard action because existence was verified above if count == 0 { - got, err := c.GetAction(ctx, &actions.GetActionRequest{ - Identifier: &actions.GetActionRequest_Id{ - Id: req.GetId(), - }, - }) - // not found - if err != nil && errors.Is(err, db.ErrNotFound) { - return nil, err - } - // standard action - name := strings.ToLower(got.GetName()) - if ActionStandard(name).IsValid() { - return nil, fmt.Errorf("cannot delete standard action %s: %w", name, db.ErrRestrictViolation) - } - return nil, db.ErrNotFound + return nil, fmt.Errorf("cannot delete standard action %s: %w", got.GetName(), db.ErrRestrictViolation) } - return &policy.Action{ - Id: req.GetId(), - }, nil + return got, nil } diff --git a/service/policy/db/actions.sql.go b/service/policy/db/actions.sql.go index 1505f69b8b..e3648ce58b 100644 --- a/service/policy/db/actions.sql.go +++ b/service/policy/db/actions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: actions.sql package db @@ -12,23 +12,64 @@ import ( ) const createCustomAction = `-- name: createCustomAction :one -INSERT INTO actions (name, metadata, is_standard) -VALUES ($1, $2, FALSE) +WITH ns AS ( + SELECT + $3::uuid AS id, + $4::text AS fqn +) +INSERT INTO actions (name, metadata, is_standard, namespace_id) +SELECT + $1, + $2, + FALSE, + COALESCE(ns.id, fqns.namespace_id) +FROM ns +LEFT JOIN attribute_fqns fqns ON fqns.fqn = ns.fqn AND ns.id IS NULL +WHERE + (ns.id IS NULL AND ns.fqn IS NULL) + OR + (ns.id IS NOT NULL) + OR + (ns.fqn IS NOT NULL AND fqns.namespace_id IS NOT NULL) RETURNING id ` type createCustomActionParams struct { - Name string `json:"name"` - Metadata []byte `json:"metadata"` + Name string `json:"name"` + Metadata []byte `json:"metadata"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` } // createCustomAction // -// INSERT INTO actions (name, metadata, is_standard) -// VALUES ($1, $2, FALSE) +// WITH ns AS ( +// SELECT +// $3::uuid AS id, +// $4::text AS fqn +// ) +// INSERT INTO actions (name, metadata, is_standard, namespace_id) +// SELECT +// $1, +// $2, +// FALSE, +// COALESCE(ns.id, fqns.namespace_id) +// FROM ns +// LEFT JOIN attribute_fqns fqns ON fqns.fqn = ns.fqn AND ns.id IS NULL +// WHERE +// (ns.id IS NULL AND ns.fqn IS NULL) +// OR +// (ns.id IS NOT NULL) +// OR +// (ns.fqn IS NOT NULL AND fqns.namespace_id IS NOT NULL) // RETURNING id func (q *Queries) createCustomAction(ctx context.Context, arg createCustomActionParams) (string, error) { - row := q.db.QueryRow(ctx, createCustomAction, arg.Name, arg.Metadata) + row := q.db.QueryRow(ctx, createCustomAction, + arg.Name, + arg.Metadata, + arg.NamespaceID, + arg.NamespaceFqn, + ) var id string err := row.Scan(&id) return id, err @@ -39,15 +80,16 @@ WITH input_actions AS ( SELECT unnest($1::text[]) AS name ), new_actions AS ( - INSERT INTO actions (name, is_standard) + INSERT INTO actions (name, is_standard, namespace_id) SELECT input.name, - FALSE -- custom actions + FALSE, -- custom actions + NULL FROM input_actions input WHERE NOT EXISTS ( - SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) + SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) AND a.namespace_id IS NULL ) - ON CONFLICT (name) DO NOTHING + ON CONFLICT (name) WHERE namespace_id IS NULL DO NOTHING RETURNING id, name, is_standard, created_at ), all_actions AS ( @@ -56,6 +98,7 @@ all_actions AS ( TRUE AS pre_existing FROM actions a JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) + WHERE a.namespace_id IS NULL UNION ALL @@ -88,15 +131,16 @@ type createOrListActionsByNameRow struct { // SELECT unnest($1::text[]) AS name // ), // new_actions AS ( -// INSERT INTO actions (name, is_standard) +// INSERT INTO actions (name, is_standard, namespace_id) // SELECT // input.name, -// FALSE -- custom actions +// FALSE, -- custom actions +// NULL // FROM input_actions input // WHERE NOT EXISTS ( -// SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) +// SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) AND a.namespace_id IS NULL // ) -// ON CONFLICT (name) DO NOTHING +// ON CONFLICT (name) WHERE namespace_id IS NULL DO NOTHING // RETURNING id, name, is_standard, created_at // ), // all_actions AS ( @@ -105,6 +149,7 @@ type createOrListActionsByNameRow struct { // TRUE AS pre_existing // FROM actions a // JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) +// WHERE a.namespace_id IS NULL // // UNION ALL // @@ -147,6 +192,106 @@ func (q *Queries) createOrListActionsByName(ctx context.Context, actionNames []s return items, nil } +const createOrListActionsByNameInNamespace = `-- name: createOrListActionsByNameInNamespace :many +WITH resolved_namespace AS ( + SELECT n.id + FROM attribute_namespaces n + WHERE n.id = $1::uuid + LIMIT 1 +), +input_actions AS ( + SELECT unnest($2::text[]) AS name +), +existing_actions AS ( + SELECT a.id, a.name, a.is_standard, a.created_at + FROM actions a + JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) + WHERE a.namespace_id = (SELECT id FROM resolved_namespace) +), +new_actions AS ( + INSERT INTO actions (name, is_standard, namespace_id) + SELECT input.name, FALSE, (SELECT id FROM resolved_namespace) + FROM input_actions input + WHERE NOT EXISTS ( + SELECT 1 FROM existing_actions ea WHERE LOWER(ea.name) = LOWER(input.name) + ) + ON CONFLICT (namespace_id, name) WHERE namespace_id IS NOT NULL DO NOTHING + RETURNING id, name, is_standard, created_at +) +SELECT id, name, is_standard, created_at FROM existing_actions +UNION ALL +SELECT id, name, is_standard, created_at FROM new_actions +ORDER BY name +` + +type createOrListActionsByNameInNamespaceParams struct { + NamespaceID string `json:"namespace_id"` + ActionNames []string `json:"action_names"` +} + +type createOrListActionsByNameInNamespaceRow struct { + ID string `json:"id"` + Name string `json:"name"` + IsStandard bool `json:"is_standard"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +// createOrListActionsByNameInNamespace +// +// WITH resolved_namespace AS ( +// SELECT n.id +// FROM attribute_namespaces n +// WHERE n.id = $1::uuid +// LIMIT 1 +// ), +// input_actions AS ( +// SELECT unnest($2::text[]) AS name +// ), +// existing_actions AS ( +// SELECT a.id, a.name, a.is_standard, a.created_at +// FROM actions a +// JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) +// WHERE a.namespace_id = (SELECT id FROM resolved_namespace) +// ), +// new_actions AS ( +// INSERT INTO actions (name, is_standard, namespace_id) +// SELECT input.name, FALSE, (SELECT id FROM resolved_namespace) +// FROM input_actions input +// WHERE NOT EXISTS ( +// SELECT 1 FROM existing_actions ea WHERE LOWER(ea.name) = LOWER(input.name) +// ) +// ON CONFLICT (namespace_id, name) WHERE namespace_id IS NOT NULL DO NOTHING +// RETURNING id, name, is_standard, created_at +// ) +// SELECT id, name, is_standard, created_at FROM existing_actions +// UNION ALL +// SELECT id, name, is_standard, created_at FROM new_actions +// ORDER BY name +func (q *Queries) createOrListActionsByNameInNamespace(ctx context.Context, arg createOrListActionsByNameInNamespaceParams) ([]createOrListActionsByNameInNamespaceRow, error) { + rows, err := q.db.Query(ctx, createOrListActionsByNameInNamespace, arg.NamespaceID, arg.ActionNames) + if err != nil { + return nil, err + } + defer rows.Close() + var items []createOrListActionsByNameInNamespaceRow + for rows.Next() { + var i createOrListActionsByNameInNamespaceRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.IsStandard, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const deleteCustomAction = `-- name: deleteCustomAction :execrows DELETE FROM actions WHERE id = $1 @@ -167,56 +312,206 @@ func (q *Queries) deleteCustomAction(ctx context.Context, id string) (int64, err } const getAction = `-- name: getAction :one +WITH resolved_namespace AS ( + SELECT + n.id, + n.name, + fqns.fqn + FROM attribute_namespaces n + LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL + WHERE + ($3::uuid IS NOT NULL AND n.id = $3::uuid) + OR + ($4::text IS NOT NULL AND fqns.fqn = $4::text) + LIMIT 1 +) SELECT a.id, a.name, a.is_standard, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata + JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata, + CASE + WHEN a.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + END AS namespace FROM actions a +LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +LEFT JOIN resolved_namespace rn ON TRUE WHERE - ($1::uuid IS NULL OR a.id = $1::uuid) - AND ($2::text IS NULL OR a.name = $2::text) + ( + ($1::uuid IS NOT NULL AND a.id = $1::uuid) + OR + ( + $2::text IS NOT NULL + AND a.name = $2::text + AND ( + (rn.id IS NOT NULL AND a.namespace_id = rn.id) + OR + (rn.id IS NULL AND a.namespace_id IS NULL) + ) + ) + ) +ORDER BY + CASE + WHEN a.namespace_id = rn.id THEN 0 + WHEN a.is_standard = TRUE THEN 1 + ELSE 2 + END, + a.created_at DESC +LIMIT 1 ` type getActionParams struct { - ID pgtype.UUID `json:"id"` - Name pgtype.Text `json:"name"` + ID pgtype.UUID `json:"id"` + Name pgtype.Text `json:"name"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` } type getActionRow struct { - ID string `json:"id"` - Name string `json:"name"` - IsStandard bool `json:"is_standard"` - Metadata []byte `json:"metadata"` + ID string `json:"id"` + Name string `json:"name"` + IsStandard bool `json:"is_standard"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` } // getAction // +// WITH resolved_namespace AS ( +// SELECT +// n.id, +// n.name, +// fqns.fqn +// FROM attribute_namespaces n +// LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL +// WHERE +// ($3::uuid IS NOT NULL AND n.id = $3::uuid) +// OR +// ($4::text IS NOT NULL AND fqns.fqn = $4::text) +// LIMIT 1 +// ) // SELECT // a.id, // a.name, // a.is_standard, -// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata +// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata, +// CASE +// WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', ns_fqns.fqn +// ) +// END AS namespace // FROM actions a +// LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +// LEFT JOIN resolved_namespace rn ON TRUE // WHERE -// ($1::uuid IS NULL OR a.id = $1::uuid) -// AND ($2::text IS NULL OR a.name = $2::text) +// ( +// ($1::uuid IS NOT NULL AND a.id = $1::uuid) +// OR +// ( +// $2::text IS NOT NULL +// AND a.name = $2::text +// AND ( +// (rn.id IS NOT NULL AND a.namespace_id = rn.id) +// OR +// (rn.id IS NULL AND a.namespace_id IS NULL) +// ) +// ) +// ) +// ORDER BY +// CASE +// WHEN a.namespace_id = rn.id THEN 0 +// WHEN a.is_standard = TRUE THEN 1 +// ELSE 2 +// END, +// a.created_at DESC +// LIMIT 1 func (q *Queries) getAction(ctx context.Context, arg getActionParams) (getActionRow, error) { - row := q.db.QueryRow(ctx, getAction, arg.ID, arg.Name) + row := q.db.QueryRow(ctx, getAction, + arg.ID, + arg.Name, + arg.NamespaceID, + arg.NamespaceFqn, + ) var i getActionRow err := row.Scan( &i.ID, &i.Name, &i.IsStandard, &i.Metadata, + &i.Namespace, ) return i, err } +const getActionsByIDs = `-- name: getActionsByIDs :many +SELECT + a.id, + a.is_standard, + a.namespace_id +FROM actions + a +WHERE a.id = ANY($1::uuid[]) +` + +type getActionsByIDsRow struct { + ID string `json:"id"` + IsStandard bool `json:"is_standard"` + NamespaceID pgtype.UUID `json:"namespace_id"` +} + +// getActionsByIDs +// +// SELECT +// a.id, +// a.is_standard, +// a.namespace_id +// FROM actions +// a +// WHERE a.id = ANY($1::uuid[]) +func (q *Queries) getActionsByIDs(ctx context.Context, ids []string) ([]getActionsByIDsRow, error) { + rows, err := q.db.Query(ctx, getActionsByIDs, ids) + if err != nil { + return nil, err + } + defer rows.Close() + var items []getActionsByIDsRow + for rows.Next() { + var i getActionsByIDsRow + if err := rows.Scan(&i.ID, &i.IsStandard, &i.NamespaceID); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listActions = `-- name: listActions :many -WITH counted AS ( - SELECT COUNT(id) AS total FROM actions +WITH resolved_namespace AS ( + SELECT + n.id, + n.name, + fqns.fqn + FROM attribute_namespaces n + LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL + WHERE + ($1::uuid IS NOT NULL AND n.id = $1::uuid) + OR + ($2::text IS NOT NULL AND fqns.fqn = $2::text) + LIMIT 1 ) SELECT a.id, @@ -227,32 +522,75 @@ SELECT 'updated_at', a.updated_at )) as metadata, a.is_standard, - counted.total + CASE + WHEN a.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + END AS namespace, + COUNT(*) OVER() as total FROM actions a -CROSS JOIN counted -LIMIT $2 -OFFSET $1 +LEFT JOIN resolved_namespace rn ON TRUE +LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +WHERE + ( + $1::uuid IS NULL + AND $2::text IS NULL + ) + OR ( + a.namespace_id = rn.id + OR ( + rn.id IS NOT NULL + AND a.is_standard = TRUE + AND a.namespace_id IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM actions ax + WHERE ax.name = a.name + AND ax.namespace_id = rn.id + ) + ) + ) +ORDER BY a.created_at DESC +LIMIT $4 +OFFSET $3 ` type listActionsParams struct { - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` } type listActionsRow struct { - ID string `json:"id"` - Name string `json:"name"` - Metadata []byte `json:"metadata"` - IsStandard bool `json:"is_standard"` - Total int64 `json:"total"` + ID string `json:"id"` + Name string `json:"name"` + Metadata []byte `json:"metadata"` + IsStandard bool `json:"is_standard"` + Namespace interface{} `json:"namespace"` + Total int64 `json:"total"` } // -------------------------------------------------------------- // ACTIONS // -------------------------------------------------------------- // -// WITH counted AS ( -// SELECT COUNT(id) AS total FROM actions +// WITH resolved_namespace AS ( +// SELECT +// n.id, +// n.name, +// fqns.fqn +// FROM attribute_namespaces n +// LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL +// WHERE +// ($1::uuid IS NOT NULL AND n.id = $1::uuid) +// OR +// ($2::text IS NOT NULL AND fqns.fqn = $2::text) +// LIMIT 1 // ) // SELECT // a.id, @@ -263,13 +601,48 @@ type listActionsRow struct { // 'updated_at', a.updated_at // )) as metadata, // a.is_standard, -// counted.total +// CASE +// WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', ns_fqns.fqn +// ) +// END AS namespace, +// COUNT(*) OVER() as total // FROM actions a -// CROSS JOIN counted -// LIMIT $2 -// OFFSET $1 +// LEFT JOIN resolved_namespace rn ON TRUE +// LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +// WHERE +// ( +// $1::uuid IS NULL +// AND $2::text IS NULL +// ) +// OR ( +// a.namespace_id = rn.id +// OR ( +// rn.id IS NOT NULL +// AND a.is_standard = TRUE +// AND a.namespace_id IS NULL +// AND NOT EXISTS ( +// SELECT 1 +// FROM actions ax +// WHERE ax.name = a.name +// AND ax.namespace_id = rn.id +// ) +// ) +// ) +// ORDER BY a.created_at DESC +// LIMIT $4 +// OFFSET $3 func (q *Queries) listActions(ctx context.Context, arg listActionsParams) ([]listActionsRow, error) { - rows, err := q.db.Query(ctx, listActions, arg.Offset, arg.Limit) + rows, err := q.db.Query(ctx, listActions, + arg.NamespaceID, + arg.NamespaceFqn, + arg.Offset, + arg.Limit, + ) if err != nil { return nil, err } @@ -282,6 +655,7 @@ func (q *Queries) listActions(ctx context.Context, arg listActionsParams) ([]lis &i.Name, &i.Metadata, &i.IsStandard, + &i.Namespace, &i.Total, ); err != nil { return nil, err @@ -294,6 +668,33 @@ func (q *Queries) listActions(ctx context.Context, arg listActionsParams) ([]lis return items, nil } +const seedStandardActionsForNamespace = `-- name: seedStandardActionsForNamespace :execrows +INSERT INTO actions (name, is_standard, namespace_id) +VALUES + ('create', TRUE, $1), + ('read', TRUE, $1), + ('update', TRUE, $1), + ('delete', TRUE, $1) +ON CONFLICT (namespace_id, name) WHERE namespace_id IS NOT NULL DO NOTHING +` + +// seedStandardActionsForNamespace +// +// INSERT INTO actions (name, is_standard, namespace_id) +// VALUES +// ('create', TRUE, $1), +// ('read', TRUE, $1), +// ('update', TRUE, $1), +// ('delete', TRUE, $1) +// ON CONFLICT (namespace_id, name) WHERE namespace_id IS NOT NULL DO NOTHING +func (q *Queries) seedStandardActionsForNamespace(ctx context.Context, namespaceID pgtype.UUID) (int64, error) { + result, err := q.db.Exec(ctx, seedStandardActionsForNamespace, namespaceID) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + const updateCustomAction = `-- name: updateCustomAction :execrows UPDATE actions SET diff --git a/service/policy/db/attribute_fqn.go b/service/policy/db/attribute_fqn.go index f6dde5eb64..32551dcb26 100644 --- a/service/policy/db/attribute_fqn.go +++ b/service/policy/db/attribute_fqn.go @@ -6,7 +6,9 @@ import ( "log/slog" "strings" + "github.com/opentdf/platform/lib/identifier" "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/opentdf/platform/protocol/go/policy/namespaces" "github.com/opentdf/platform/service/pkg/db" @@ -79,6 +81,8 @@ func (c *PolicyDBClient) GetAttributesByValueFqns(ctx context.Context, r *attrib defer span.End() list := make(map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue, len(fqns)) + definitionFqns := make(map[string]string) + queryFqnsSet := make(map[string]struct{}, len(fqns)) for i, fqn := range fqns { // normalize to lower case @@ -89,19 +93,46 @@ func (c *PolicyDBClient) GetAttributesByValueFqns(ctx context.Context, r *attrib // prepopulate response map for easy lookup list[fqn] = nil + + queryFqnsSet[fqn] = struct{}{} + if defFqn := definitionFqnFromValueFqn(fqn); defFqn != "" { + definitionFqns[fqn] = defFqn + queryFqnsSet[defFqn] = struct{}{} + } + } + + queryFqns := make([]string, 0, len(queryFqnsSet)) + for fqn := range queryFqnsSet { + queryFqns = append(queryFqns, fqn) } - // get all attribute values by FQN - attrs, err := c.ListAttributesByFqns(ctx, fqns) + // get all attributes by value or definition FQN + attrs, err := c.ListAttributesByFqns(ctx, queryFqns, true) if err != nil { return nil, err } + defByFqn := make(map[string]*policy.Attribute, len(attrs)) + // loop through attributes to find values that match the requested FQNs for _, attr := range attrs { - for _, val := range attr.GetValues() { + if attr == nil { + continue + } + + values := attr.GetValues() + // Ensure that only active values are within the attribute object + activeValues := make([]*policy.Value, 0) + for _, val := range values { valFqn := val.GetFqn() + isActive := val.GetActive().GetValue() + if isActive { + activeValues = append(activeValues, val) + } if _, ok := list[valFqn]; ok { + if !isActive { + return nil, fmt.Errorf("value fqn [%s] inactive: %w", valFqn, db.ErrAttributeValueInactive) + } // update response map with attribute and value pair if value FQN found list[valFqn] = &attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue{ Attribute: attr, @@ -109,6 +140,31 @@ func (c *PolicyDBClient) GetAttributesByValueFqns(ctx context.Context, r *attrib } } } + + if len(activeValues) != len(values) { + attr.Values = activeValues + } + + if attr.GetFqn() != "" { + defByFqn[attr.GetFqn()] = attr + } + } + + // if value is missing, attempt to resolve the attribute definition + for valueFqn, defFqn := range definitionFqns { + if list[valueFqn] != nil { + continue + } + if attr, ok := defByFqn[defFqn]; ok { + if attr.GetAllowTraversal().GetValue() { + c.logger.DebugContext(ctx, "value missing but allow_traversal is true, using definition", + slog.String("value_fqn", valueFqn), + slog.String("def_fqn", attr.GetFqn())) + list[valueFqn] = &attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue{ + Attribute: attr, + } + } + } } // Map and Merge Grants & Keys @@ -124,11 +180,13 @@ func (c *PolicyDBClient) GetAttributesByValueFqns(ctx context.Context, r *attrib } pair.GetAttribute().Grants = attrGrants - valGrants, err := mapKasKeysToGrants(pair.GetValue().GetKasKeys(), pair.GetValue().GetGrants(), c.logger) - if err != nil { - return nil, fmt.Errorf("could not map & merge value grants and keys: %w", err) + if pair.GetValue() != nil { + valGrants, err := mapKasKeysToGrants(pair.GetValue().GetKasKeys(), pair.GetValue().GetGrants(), c.logger) + if err != nil { + return nil, fmt.Errorf("could not map & merge value grants and keys: %w", err) + } + pair.GetValue().Grants = valGrants } - pair.GetValue().Grants = valGrants nsGrants, err := mapKasKeysToGrants(pair.GetAttribute().GetNamespace().GetKasKeys(), pair.GetAttribute().GetNamespace().GetGrants(), c.logger) if err != nil { @@ -146,3 +204,25 @@ func (c *PolicyDBClient) GetAttributesByValueFqns(ctx context.Context, r *attrib return list, nil } + +func definitionFqnFromValueFqn(valueFqn string) string { + httpPrefix := "http://" + httpsPrefix := "https://" + hadHTTP := strings.HasPrefix(valueFqn, httpPrefix) + if hadHTTP { + valueFqn = httpsPrefix + strings.TrimPrefix(valueFqn, httpPrefix) + } + parsed, err := identifier.Parse[*identifier.FullyQualifiedAttribute](valueFqn) + if err != nil { + return "" + } + if parsed.Value == "" { + return "" + } + parsed.Value = "" + defFqn := parsed.FQN() + if hadHTTP { + defFqn = httpPrefix + strings.TrimPrefix(defFqn, httpsPrefix) + } + return defFqn +} diff --git a/service/policy/db/attribute_fqn.sql.go b/service/policy/db/attribute_fqn.sql.go index ad4ed6b043..6765e8ef51 100644 --- a/service/policy/db/attribute_fqn.sql.go +++ b/service/policy/db/attribute_fqn.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: attribute_fqn.sql package db diff --git a/service/policy/db/attribute_fqn_test.go b/service/policy/db/attribute_fqn_test.go new file mode 100644 index 0000000000..0601ed48cb --- /dev/null +++ b/service/policy/db/attribute_fqn_test.go @@ -0,0 +1,49 @@ +package db + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefinitionFqnFromValueFqn(t *testing.T) { + tests := []struct { + name string + valueFqn string + want string + }{ + { + name: "https value fqn", + valueFqn: "https://example.com/attr/foo/value/bar", + want: "https://example.com/attr/foo", + }, + { + name: "http value fqn", + valueFqn: "http://example.com/attr/foo/value/bar", + want: "http://example.com/attr/foo", + }, + { + name: "definition fqn", + valueFqn: "https://example.com/attr/foo", + want: "", + }, + { + name: "invalid fqn", + valueFqn: "not-a-fqn", + want: "", + }, + { + name: "empty string", + valueFqn: "", + want: "", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := definitionFqnFromValueFqn(tc.valueFqn) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/service/policy/db/attribute_values.go b/service/policy/db/attribute_values.go index d8be648fed..5d5e18b751 100644 --- a/service/policy/db/attribute_values.go +++ b/service/policy/db/attribute_values.go @@ -7,10 +7,10 @@ import ( "log/slog" "strings" - "github.com/jackc/pgx/v5/pgtype" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/obligations" "github.com/opentdf/platform/protocol/go/policy/unsafe" "github.com/opentdf/platform/service/pkg/db" "google.golang.org/protobuf/types/known/wrapperspb" @@ -39,6 +39,19 @@ func (c PolicyDBClient) CreateAttributeValue(ctx context.Context, attributeID st return nil, db.WrapIfKnownInvalidQueryErr(err) } + for _, trigger := range r.GetObligationTriggers() { + _, err = c.CreateObligationTrigger(ctx, &obligations.AddObligationTriggerRequest{ + ObligationValue: trigger.GetObligationValue(), + Action: trigger.GetAction(), + AttributeValue: &common.IdFqnIdentifier{Id: createdID}, + Context: trigger.GetContext(), + Metadata: trigger.GetMetadata(), + }) + if err != nil { + return nil, err + } + } + return c.GetAttributeValue(ctx, createdID) } @@ -122,97 +135,6 @@ func (c PolicyDBClient) GetAttributeValue(ctx context.Context, identifier any) ( }, nil } -func (c PolicyDBClient) ListAttributeValues(ctx context.Context, r *attributes.ListAttributeValuesRequest) (*attributes.ListAttributeValuesResponse, error) { - state := getDBStateTypeTransformedEnum(r.GetState()) - limit, offset := c.getRequestedLimitOffset(r.GetPagination()) - - maxLimit := c.listCfg.limitMax - if maxLimit > 0 && limit > maxLimit { - return nil, db.ErrListLimitTooLarge - } - - active := pgtype.Bool{ - Valid: false, - } - - if state != stateAny { - active = pgtypeBool(state == stateActive) - } - - list, err := c.queries.listAttributeValues(ctx, listAttributeValuesParams{ - AttributeDefinitionID: pgtypeUUID(r.GetAttributeId()), - Active: active, - Limit: limit, - Offset: offset, - }) - if err != nil { - return nil, db.WrapIfKnownInvalidQueryErr(err) - } - - attributeValues := make([]*policy.Value, len(list)) - - for i, av := range list { - metadata := &common.Metadata{} - if err := unmarshalMetadata(av.Metadata, metadata); err != nil { - return nil, err - } - - attributeValues[i] = &policy.Value{ - Id: av.ID, - Value: av.Value, - Active: &wrapperspb.BoolValue{Value: av.Active}, - Metadata: metadata, - Attribute: &policy.Attribute{ - Id: av.AttributeDefinitionID, - }, - Fqn: av.Fqn.String, - } - } - var total int32 - var nextOffset int32 - if len(list) > 0 { - total = int32(list[0].Total) - nextOffset = getNextOffset(offset, limit, total) - } - - return &attributes.ListAttributeValuesResponse{ - Values: attributeValues, - Pagination: &policy.PageResponse{ - CurrentOffset: offset, - Total: total, - NextOffset: nextOffset, - }, - }, nil -} - -// Loads all attribute values into memory by making iterative db roundtrip requests of defaultObjectListAllLimit size -func (c PolicyDBClient) ListAllAttributeValues(ctx context.Context) ([]*policy.Value, error) { - var nextOffset int32 - valsList := make([]*policy.Value, 0) - - for { - listed, err := c.ListAttributeValues(ctx, &attributes.ListAttributeValuesRequest{ - State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, - Pagination: &policy.PageRequest{ - Limit: c.listCfg.limitMax, - Offset: nextOffset, - }, - }) - if err != nil { - return nil, fmt.Errorf("failed to list all attributes: %w", err) - } - - nextOffset = listed.GetPagination().GetNextOffset() - valsList = append(valsList, listed.GetValues()...) - - // offset becomes zero when list is exhausted - if nextOffset <= 0 { - break - } - } - return valsList, nil -} - func (c PolicyDBClient) UpdateAttributeValue(ctx context.Context, r *attributes.UpdateAttributeValueRequest) (*policy.Value, error) { id := r.GetId() metadataJSON, metadata, err := db.MarshalUpdateMetadata(r.GetMetadata(), r.GetMetadataUpdateBehavior(), func() (*common.Metadata, error) { @@ -324,7 +246,7 @@ func (c PolicyDBClient) UnsafeDeleteAttributeValue(ctx context.Context, toDelete }, nil } -func (c PolicyDBClient) RemoveKeyAccessServerFromValue(ctx context.Context, k *attributes.ValueKeyAccessServer) (*attributes.ValueKeyAccessServer, error) { +func (c PolicyDBClient) RemoveKeyAccessServerFromValue(ctx context.Context, k *attributes.ValueKeyAccessServer) (*attributes.ValueKeyAccessServer, error) { //nolint:staticcheck // Compatibility path for deprecated protobuf type. count, err := c.queries.removeKeyAccessServerFromAttributeValue(ctx, removeKeyAccessServerFromAttributeValueParams{ AttributeValueID: k.GetValueId(), KeyAccessServerID: k.GetKeyAccessServerId(), diff --git a/service/policy/db/attribute_values.sql.go b/service/policy/db/attribute_values.sql.go index 612aeb87ee..1611d803a1 100644 --- a/service/policy/db/attribute_values.sql.go +++ b/service/policy/db/attribute_values.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: attribute_values.sql package db @@ -74,6 +74,7 @@ func (q *Queries) deleteAttributeValue(ctx context.Context, id string) (int64, e } const getAttributeValue = `-- name: getAttributeValue :one + WITH obligation_triggers_agg AS ( SELECT ot.obligation_value_id, @@ -88,6 +89,11 @@ WITH obligation_triggers_agg AS ( 'id', av.id, 'fqn', av_fqns.fqn ), + 'namespace', JSONB_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSONB_BUILD_ARRAY( JSONB_BUILD_OBJECT( @@ -103,6 +109,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av.id = av_fqns.value_id GROUP BY ot.obligation_value_id ), @@ -190,7 +198,7 @@ LEFT JOIN ( ) value_keys ON av.id = value_keys.value_id LEFT JOIN attribute_obligations ao ON av.id = ao.attribute_value_id WHERE ($1::uuid IS NULL OR av.id = $1::uuid) - AND ($2::text IS NULL OR REGEXP_REPLACE(fqns.fqn, '^https?://', '') = REGEXP_REPLACE($2::text, '^https?://', '')) + AND ($2::text IS NULL OR REGEXP_REPLACE(fqns.fqn, '^https://', '') = REGEXP_REPLACE($2::text, '^https://', '')) GROUP BY av.id, fqns.fqn, value_keys.keys, ao.obligations ` @@ -211,7 +219,9 @@ type getAttributeValueRow struct { Obligations []byte `json:"obligations"` } -// getAttributeValue +// -------------------------------------------------------------- +// ATTRIBUTE VALUES +// -------------------------------------------------------------- // // WITH obligation_triggers_agg AS ( // SELECT @@ -227,6 +237,11 @@ type getAttributeValueRow struct { // 'id', av.id, // 'fqn', av_fqns.fqn // ), +// 'namespace', JSONB_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), // 'context', CASE // WHEN ot.client_id IS NOT NULL THEN JSONB_BUILD_ARRAY( // JSONB_BUILD_OBJECT( @@ -242,6 +257,8 @@ type getAttributeValueRow struct { // FROM obligation_triggers ot // JOIN actions a ON ot.action_id = a.id // JOIN attribute_values av ON ot.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id // LEFT JOIN attribute_fqns av_fqns ON av.id = av_fqns.value_id // GROUP BY ot.obligation_value_id // ), @@ -329,7 +346,7 @@ type getAttributeValueRow struct { // ) value_keys ON av.id = value_keys.value_id // LEFT JOIN attribute_obligations ao ON av.id = ao.attribute_value_id // WHERE ($1::uuid IS NULL OR av.id = $1::uuid) -// AND ($2::text IS NULL OR REGEXP_REPLACE(fqns.fqn, '^https?://', '') = REGEXP_REPLACE($2::text, '^https?://', '')) +// AND ($2::text IS NULL OR REGEXP_REPLACE(fqns.fqn, '^https://', '') = REGEXP_REPLACE($2::text, '^https://', '')) // GROUP BY av.id, fqns.fqn, value_keys.keys, ao.obligations func (q *Queries) getAttributeValue(ctx context.Context, arg getAttributeValueParams) (getAttributeValueRow, error) { row := q.db.QueryRow(ctx, getAttributeValue, arg.ID, arg.Fqn) @@ -348,86 +365,34 @@ func (q *Queries) getAttributeValue(ctx context.Context, arg getAttributeValuePa return i, err } -const listAttributeValues = `-- name: listAttributeValues :many - -SELECT - COUNT(*) OVER() AS total, - av.id, - av.value, - av.active, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', av.metadata -> 'labels', 'created_at', av.created_at, 'updated_at', av.updated_at)) as metadata, - av.attribute_definition_id, - fqns.fqn +const getAttributeValueNamespaceIDs = `-- name: getAttributeValueNamespaceIDs :many +SELECT av.id AS attribute_value_id, ad.namespace_id FROM attribute_values av -LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id -WHERE ( - ($1::BOOLEAN IS NULL OR av.active = $1) AND - ($2::uuid IS NULL OR av.attribute_definition_id = $2::uuid) -) -LIMIT $4 -OFFSET $3 +JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +WHERE av.id = ANY($1::uuid[]) ` -type listAttributeValuesParams struct { - Active pgtype.Bool `json:"active"` - AttributeDefinitionID pgtype.UUID `json:"attribute_definition_id"` - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` -} - -type listAttributeValuesRow struct { - Total int64 `json:"total"` - ID string `json:"id"` - Value string `json:"value"` - Active bool `json:"active"` - Metadata []byte `json:"metadata"` - AttributeDefinitionID string `json:"attribute_definition_id"` - Fqn pgtype.Text `json:"fqn"` +type getAttributeValueNamespaceIDsRow struct { + AttributeValueID string `json:"attribute_value_id"` + NamespaceID string `json:"namespace_id"` } -// -------------------------------------------------------------- -// ATTRIBUTE VALUES -// -------------------------------------------------------------- +// getAttributeValueNamespaceIDs // -// SELECT -// COUNT(*) OVER() AS total, -// av.id, -// av.value, -// av.active, -// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', av.metadata -> 'labels', 'created_at', av.created_at, 'updated_at', av.updated_at)) as metadata, -// av.attribute_definition_id, -// fqns.fqn +// SELECT av.id AS attribute_value_id, ad.namespace_id // FROM attribute_values av -// LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id -// WHERE ( -// ($1::BOOLEAN IS NULL OR av.active = $1) AND -// ($2::uuid IS NULL OR av.attribute_definition_id = $2::uuid) -// ) -// LIMIT $4 -// OFFSET $3 -func (q *Queries) listAttributeValues(ctx context.Context, arg listAttributeValuesParams) ([]listAttributeValuesRow, error) { - rows, err := q.db.Query(ctx, listAttributeValues, - arg.Active, - arg.AttributeDefinitionID, - arg.Offset, - arg.Limit, - ) +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// WHERE av.id = ANY($1::uuid[]) +func (q *Queries) getAttributeValueNamespaceIDs(ctx context.Context, ids []string) ([]getAttributeValueNamespaceIDsRow, error) { + rows, err := q.db.Query(ctx, getAttributeValueNamespaceIDs, ids) if err != nil { return nil, err } defer rows.Close() - var items []listAttributeValuesRow + var items []getAttributeValueNamespaceIDsRow for rows.Next() { - var i listAttributeValuesRow - if err := rows.Scan( - &i.Total, - &i.ID, - &i.Value, - &i.Active, - &i.Metadata, - &i.AttributeDefinitionID, - &i.Fqn, - ); err != nil { + var i getAttributeValueNamespaceIDsRow + if err := rows.Scan(&i.AttributeValueID, &i.NamespaceID); err != nil { return nil, err } items = append(items, i) diff --git a/service/policy/db/attributes.go b/service/policy/db/attributes.go index 954feffacb..3fb9c47e57 100644 --- a/service/policy/db/attributes.go +++ b/service/policy/db/attributes.go @@ -54,16 +54,17 @@ func attributesValuesProtojson(valuesJSON []byte) ([]*policy.Value, error) { } type attributeQueryRow struct { - id string - name string - rule string - metadataJSON []byte - namespaceID string - active bool - namespaceName string - valuesJSON []byte - grantsJSON []byte - fqn sql.NullString + id string + name string + rule string + allowTraversal bool + metadataJSON []byte + namespaceID string + active bool + namespaceName string + valuesJSON []byte + grantsJSON []byte + fqn sql.NullString } func hydrateAttribute(row *attributeQueryRow) (*policy.Attribute, error) { @@ -97,15 +98,16 @@ func hydrateAttribute(row *attributeQueryRow) (*policy.Attribute, error) { } attr := &policy.Attribute{ - Id: row.id, - Name: row.name, - Rule: attributesRuleTypeEnumTransformOut(row.rule), - Values: values, - Active: &wrapperspb.BoolValue{Value: row.active}, - Metadata: metadata, - Namespace: ns, - Grants: grants, - Fqn: row.fqn.String, + Id: row.id, + Name: row.name, + Rule: attributesRuleTypeEnumTransformOut(row.rule), + Values: values, + AllowTraversal: &wrapperspb.BoolValue{Value: row.allowTraversal}, + Active: &wrapperspb.BoolValue{Value: row.active}, + Metadata: metadata, + Namespace: ns, + Grants: grants, + Fqn: row.fqn.String, } return attr, nil @@ -144,12 +146,16 @@ func (c PolicyDBClient) ListAttributes(ctx context.Context, r *attributes.ListAt } } + sortField, sortDirection := GetAttributesSortParams(r.GetSort()) + list, err := c.queries.listAttributesDetail(ctx, listAttributesDetailParams{ Active: active, NamespaceID: pgtypeUUID(namespaceID), NamespaceName: pgtypeText(namespaceName), Limit: limit, Offset: offset, + SortField: sortField, + SortDirection: sortDirection, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -159,15 +165,16 @@ func (c PolicyDBClient) ListAttributes(ctx context.Context, r *attributes.ListAt for i, attr := range list { policyAttributes[i], err = hydrateAttribute(&attributeQueryRow{ - id: attr.ID, - name: attr.AttributeName, - rule: string(attr.Rule), - active: attr.Active, - metadataJSON: attr.Metadata, - namespaceID: attr.NamespaceID, - namespaceName: attr.NamespaceName.String, - valuesJSON: attr.Values, - fqn: sql.NullString(attr.Fqn), + id: attr.ID, + name: attr.AttributeName, + rule: string(attr.Rule), + allowTraversal: attr.AllowTraversal, + active: attr.Active, + metadataJSON: attr.Metadata, + namespaceID: attr.NamespaceID, + namespaceName: attr.NamespaceName.String, + valuesJSON: attr.Values, + fqn: sql.NullString(attr.Fqn), }) if err != nil { return nil, err @@ -228,16 +235,17 @@ func (c PolicyDBClient) GetAttribute(ctx context.Context, identifier any) (*poli } policyAttr, err := hydrateAttribute(&attributeQueryRow{ - id: attr.ID, - name: attr.AttributeName, - rule: string(attr.Rule), - active: attr.Active, - metadataJSON: attr.Metadata, - namespaceID: attr.NamespaceID, - namespaceName: attr.NamespaceName.String, - valuesJSON: attr.Values, - grantsJSON: attr.Grants, - fqn: sql.NullString(attr.Fqn), + id: attr.ID, + name: attr.AttributeName, + rule: string(attr.Rule), + allowTraversal: attr.AllowTraversal, + active: attr.Active, + metadataJSON: attr.Metadata, + namespaceID: attr.NamespaceID, + namespaceName: attr.NamespaceName.String, + valuesJSON: attr.Values, + grantsJSON: attr.Grants, + fqn: sql.NullString(attr.Fqn), }) if err != nil { return nil, err @@ -255,8 +263,11 @@ func (c PolicyDBClient) GetAttribute(ctx context.Context, identifier any) (*poli return policyAttr, nil } -func (c PolicyDBClient) ListAttributesByFqns(ctx context.Context, fqns []string) ([]*policy.Attribute, error) { - list, err := c.queries.listAttributesByDefOrValueFqns(ctx, fqns) +func (c PolicyDBClient) ListAttributesByFqns(ctx context.Context, fqns []string, includeInactiveValues bool) ([]*policy.Attribute, error) { + list, err := c.queries.listAttributesByDefOrValueFqns(ctx, listAttributesByDefOrValueFqnsParams{ + Fqns: fqns, + IncludeInactiveValues: includeInactiveValues, + }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) } @@ -313,15 +324,16 @@ func (c PolicyDBClient) ListAttributesByFqns(ctx context.Context, fqns []string) ns.Grants = nsGrants attrs[i] = &policy.Attribute{ - Id: attr.ID, - Name: attr.Name, - Rule: attributesRuleTypeEnumTransformOut(string(attr.Rule)), - Fqn: attr.Fqn, - Active: &wrapperspb.BoolValue{Value: attr.Active}, - Namespace: ns, - Grants: grants, - Values: values, - KasKeys: keys, + Id: attr.ID, + Name: attr.Name, + Rule: attributesRuleTypeEnumTransformOut(string(attr.Rule)), + AllowTraversal: &wrapperspb.BoolValue{Value: attr.AllowTraversal}, + Fqn: attr.Fqn, + Active: &wrapperspb.BoolValue{Value: attr.Active}, + Namespace: ns, + Grants: grants, + Values: values, + KasKeys: keys, } } @@ -329,7 +341,7 @@ func (c PolicyDBClient) ListAttributesByFqns(ctx context.Context, fqns []string) } func (c PolicyDBClient) GetAttributeByFqn(ctx context.Context, fqn string) (*policy.Attribute, error) { - list, err := c.ListAttributesByFqns(ctx, []string{strings.ToLower(fqn)}) + list, err := c.ListAttributesByFqns(ctx, []string{strings.ToLower(fqn)}, false) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) } @@ -354,13 +366,14 @@ func (c PolicyDBClient) GetAttributesByNamespace(ctx context.Context, namespaceI for i, attr := range list { policyAttributes[i], err = hydrateAttribute(&attributeQueryRow{ - id: attr.ID, - name: attr.AttributeName, - rule: string(attr.Rule), - active: attr.Active, - metadataJSON: attr.Metadata, - namespaceID: attr.NamespaceID, - namespaceName: attr.NamespaceName.String, + id: attr.ID, + name: attr.AttributeName, + rule: string(attr.Rule), + allowTraversal: attr.AllowTraversal, + active: attr.Active, + metadataJSON: attr.Metadata, + namespaceID: attr.NamespaceID, + namespaceName: attr.NamespaceName.String, }) if err != nil { return nil, err @@ -380,10 +393,11 @@ func (c PolicyDBClient) CreateAttribute(ctx context.Context, r *attributes.Creat ruleString := attributesRuleTypeEnumTransformIn(r.GetRule().String()) createdID, err := c.queries.createAttribute(ctx, createAttributeParams{ - NamespaceID: namespaceID, - Name: name, - Rule: AttributeDefinitionRule(ruleString), - Metadata: metadataJSON, + NamespaceID: namespaceID, + Name: name, + Rule: AttributeDefinitionRule(ruleString), + Metadata: metadataJSON, + AllowTraversal: r.GetAllowTraversal().GetValue(), }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -414,6 +428,12 @@ func (c PolicyDBClient) UnsafeUpdateAttribute(ctx context.Context, r *unsafe.Uns id := r.GetId() name := strings.ToLower(r.GetName()) rule := r.GetRule() + var allowTraversal pgtype.Bool + if r.GetAllowTraversal() == nil { + allowTraversal = pgtype.Bool{Valid: false} + } else { + allowTraversal = pgtypeBool(r.GetAllowTraversal().GetValue()) + } before, err := c.GetAttribute(ctx, id) if err != nil { return nil, err @@ -454,7 +474,8 @@ func (c PolicyDBClient) UnsafeUpdateAttribute(ctx context.Context, r *unsafe.Uns AttributeDefinitionRule: AttributeDefinitionRule(ruleString), Valid: ruleString != "", }, - ValuesOrder: r.GetValuesOrder(), + AllowTraversal: allowTraversal, + ValuesOrder: r.GetValuesOrder(), }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -568,7 +589,7 @@ func (c PolicyDBClient) UnsafeDeleteAttribute(ctx context.Context, existing *pol /// Key Access Server assignments /// -func (c PolicyDBClient) RemoveKeyAccessServerFromAttribute(ctx context.Context, k *attributes.AttributeKeyAccessServer) (*attributes.AttributeKeyAccessServer, error) { +func (c PolicyDBClient) RemoveKeyAccessServerFromAttribute(ctx context.Context, k *attributes.AttributeKeyAccessServer) (*attributes.AttributeKeyAccessServer, error) { //nolint:staticcheck // Compatibility path for deprecated protobuf type. count, err := c.queries.removeKeyAccessServerFromAttribute(ctx, removeKeyAccessServerFromAttributeParams{ AttributeDefinitionID: k.GetAttributeId(), KeyAccessServerID: k.GetKeyAccessServerId(), diff --git a/service/policy/db/attributes.sql.go b/service/policy/db/attributes.sql.go index a00d7efcdc..a7a235a25f 100644 --- a/service/policy/db/attributes.sql.go +++ b/service/policy/db/attributes.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: attributes.sql package db @@ -35,22 +35,23 @@ func (q *Queries) assignPublicKeyToAttributeDefinition(ctx context.Context, arg } const createAttribute = `-- name: createAttribute :one -INSERT INTO attribute_definitions (namespace_id, name, rule, metadata) -VALUES ($1, $2, $3, $4) +INSERT INTO attribute_definitions (namespace_id, name, rule, metadata, allow_traversal) +VALUES ($1, $2, $3, $4, $5) RETURNING id ` type createAttributeParams struct { - NamespaceID string `json:"namespace_id"` - Name string `json:"name"` - Rule AttributeDefinitionRule `json:"rule"` - Metadata []byte `json:"metadata"` + NamespaceID string `json:"namespace_id"` + Name string `json:"name"` + Rule AttributeDefinitionRule `json:"rule"` + Metadata []byte `json:"metadata"` + AllowTraversal bool `json:"allow_traversal"` } // createAttribute // -// INSERT INTO attribute_definitions (namespace_id, name, rule, metadata) -// VALUES ($1, $2, $3, $4) +// INSERT INTO attribute_definitions (namespace_id, name, rule, metadata, allow_traversal) +// VALUES ($1, $2, $3, $4, $5) // RETURNING id func (q *Queries) createAttribute(ctx context.Context, arg createAttributeParams) (string, error) { row := q.db.QueryRow(ctx, createAttribute, @@ -58,6 +59,7 @@ func (q *Queries) createAttribute(ctx context.Context, arg createAttributeParams arg.Name, arg.Rule, arg.Metadata, + arg.AllowTraversal, ) var id string err := row.Scan(&id) @@ -84,6 +86,7 @@ SELECT ad.id, ad.name as attribute_name, ad.rule, + ad.allow_traversal, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, ad.namespace_id, ad.active, @@ -95,7 +98,7 @@ SELECT 'active', avt.active, 'fqn', CONCAT(fqns.fqn, '/value/', avt.value) ) ORDER BY ARRAY_POSITION(ad.values_order, avt.id) - ) AS values, + ) FILTER (WHERE avt.id IS NOT NULL) AS values, JSONB_AGG( DISTINCT JSONB_BUILD_OBJECT( 'id', kas.id, @@ -140,7 +143,7 @@ LEFT JOIN ( GROUP BY k.definition_id ) defk ON ad.id = defk.definition_id WHERE ($1::uuid IS NULL OR ad.id = $1::uuid) - AND ($2::text IS NULL OR REGEXP_REPLACE(fqns.fqn, '^https?://', '') = REGEXP_REPLACE($2::text, '^https?://', '')) + AND ($2::text IS NULL OR REGEXP_REPLACE(fqns.fqn, '^https://', '') = REGEXP_REPLACE($2::text, '^https://', '')) GROUP BY ad.id, n.name, fqns.fqn, defk.keys ` @@ -150,17 +153,18 @@ type getAttributeParams struct { } type getAttributeRow struct { - ID string `json:"id"` - AttributeName string `json:"attribute_name"` - Rule AttributeDefinitionRule `json:"rule"` - Metadata []byte `json:"metadata"` - NamespaceID string `json:"namespace_id"` - Active bool `json:"active"` - NamespaceName pgtype.Text `json:"namespace_name"` - Values []byte `json:"values"` - Grants []byte `json:"grants"` - Fqn pgtype.Text `json:"fqn"` - Keys []byte `json:"keys"` + ID string `json:"id"` + AttributeName string `json:"attribute_name"` + Rule AttributeDefinitionRule `json:"rule"` + AllowTraversal bool `json:"allow_traversal"` + Metadata []byte `json:"metadata"` + NamespaceID string `json:"namespace_id"` + Active bool `json:"active"` + NamespaceName pgtype.Text `json:"namespace_name"` + Values []byte `json:"values"` + Grants []byte `json:"grants"` + Fqn pgtype.Text `json:"fqn"` + Keys []byte `json:"keys"` } // getAttribute @@ -169,6 +173,7 @@ type getAttributeRow struct { // ad.id, // ad.name as attribute_name, // ad.rule, +// ad.allow_traversal, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, // ad.namespace_id, // ad.active, @@ -180,7 +185,7 @@ type getAttributeRow struct { // 'active', avt.active, // 'fqn', CONCAT(fqns.fqn, '/value/', avt.value) // ) ORDER BY ARRAY_POSITION(ad.values_order, avt.id) -// ) AS values, +// ) FILTER (WHERE avt.id IS NOT NULL) AS values, // JSONB_AGG( // DISTINCT JSONB_BUILD_OBJECT( // 'id', kas.id, @@ -225,7 +230,7 @@ type getAttributeRow struct { // GROUP BY k.definition_id // ) defk ON ad.id = defk.definition_id // WHERE ($1::uuid IS NULL OR ad.id = $1::uuid) -// AND ($2::text IS NULL OR REGEXP_REPLACE(fqns.fqn, '^https?://', '') = REGEXP_REPLACE($2::text, '^https?://', '')) +// AND ($2::text IS NULL OR REGEXP_REPLACE(fqns.fqn, '^https://', '') = REGEXP_REPLACE($2::text, '^https://', '')) // GROUP BY ad.id, n.name, fqns.fqn, defk.keys func (q *Queries) getAttribute(ctx context.Context, arg getAttributeParams) (getAttributeRow, error) { row := q.db.QueryRow(ctx, getAttribute, arg.ID, arg.Fqn) @@ -234,6 +239,7 @@ func (q *Queries) getAttribute(ctx context.Context, arg getAttributeParams) (get &i.ID, &i.AttributeName, &i.Rule, + &i.AllowTraversal, &i.Metadata, &i.NamespaceID, &i.Active, @@ -253,8 +259,10 @@ WITH target_definition AS ( ad.namespace_id, ad.name, ad.rule, + ad.allow_traversal, ad.active, ad.values_order, + ad.created_at, JSONB_AGG( DISTINCT JSONB_BUILD_OBJECT( 'id', kas.id, @@ -289,7 +297,7 @@ WITH target_definition AS ( ) defk ON ad.id = defk.definition_id WHERE fqns.fqn = ANY($1::TEXT[]) AND ad.active = TRUE - GROUP BY ad.id, defk.keys + GROUP BY ad.id, ad.created_at, defk.keys ), namespaces AS ( SELECT @@ -450,13 +458,14 @@ values AS ( INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id GROUP BY k.value_id ) value_keys ON av.id = value_keys.value_id - WHERE av.active = TRUE + WHERE (av.active = TRUE OR $2::BOOLEAN = TRUE) GROUP BY av.attribute_definition_id ) SELECT td.id, td.name, td.rule, + td.allow_traversal, td.active, n.namespace, fqns.fqn, @@ -468,18 +477,25 @@ INNER JOIN attribute_fqns fqns ON td.id = fqns.attribute_id INNER JOIN namespaces n ON td.namespace_id = n.id LEFT JOIN values ON td.id = values.attribute_definition_id WHERE fqns.value_id IS NULL +ORDER BY td.created_at DESC ` +type listAttributesByDefOrValueFqnsParams struct { + Fqns []string `json:"fqns"` + IncludeInactiveValues bool `json:"include_inactive_values"` +} + type listAttributesByDefOrValueFqnsRow struct { - ID string `json:"id"` - Name string `json:"name"` - Rule AttributeDefinitionRule `json:"rule"` - Active bool `json:"active"` - Namespace []byte `json:"namespace"` - Fqn string `json:"fqn"` - Values []byte `json:"values"` - Grants []byte `json:"grants"` - Keys []byte `json:"keys"` + ID string `json:"id"` + Name string `json:"name"` + Rule AttributeDefinitionRule `json:"rule"` + AllowTraversal bool `json:"allow_traversal"` + Active bool `json:"active"` + Namespace []byte `json:"namespace"` + Fqn string `json:"fqn"` + Values []byte `json:"values"` + Grants []byte `json:"grants"` + Keys []byte `json:"keys"` } // get the attribute definition for the provided value or definition fqn @@ -490,8 +506,10 @@ type listAttributesByDefOrValueFqnsRow struct { // ad.namespace_id, // ad.name, // ad.rule, +// ad.allow_traversal, // ad.active, // ad.values_order, +// ad.created_at, // JSONB_AGG( // DISTINCT JSONB_BUILD_OBJECT( // 'id', kas.id, @@ -526,7 +544,7 @@ type listAttributesByDefOrValueFqnsRow struct { // ) defk ON ad.id = defk.definition_id // WHERE fqns.fqn = ANY($1::TEXT[]) // AND ad.active = TRUE -// GROUP BY ad.id, defk.keys +// GROUP BY ad.id, ad.created_at, defk.keys // ), // namespaces AS ( // SELECT @@ -687,13 +705,14 @@ type listAttributesByDefOrValueFqnsRow struct { // INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id // GROUP BY k.value_id // ) value_keys ON av.id = value_keys.value_id -// WHERE av.active = TRUE +// WHERE (av.active = TRUE OR $2::BOOLEAN = TRUE) // GROUP BY av.attribute_definition_id // ) // SELECT // td.id, // td.name, // td.rule, +// td.allow_traversal, // td.active, // n.namespace, // fqns.fqn, @@ -705,8 +724,9 @@ type listAttributesByDefOrValueFqnsRow struct { // INNER JOIN namespaces n ON td.namespace_id = n.id // LEFT JOIN values ON td.id = values.attribute_definition_id // WHERE fqns.value_id IS NULL -func (q *Queries) listAttributesByDefOrValueFqns(ctx context.Context, fqns []string) ([]listAttributesByDefOrValueFqnsRow, error) { - rows, err := q.db.Query(ctx, listAttributesByDefOrValueFqns, fqns) +// ORDER BY td.created_at DESC +func (q *Queries) listAttributesByDefOrValueFqns(ctx context.Context, arg listAttributesByDefOrValueFqnsParams) ([]listAttributesByDefOrValueFqnsRow, error) { + rows, err := q.db.Query(ctx, listAttributesByDefOrValueFqns, arg.Fqns, arg.IncludeInactiveValues) if err != nil { return nil, err } @@ -718,6 +738,7 @@ func (q *Queries) listAttributesByDefOrValueFqns(ctx context.Context, fqns []str &i.ID, &i.Name, &i.Rule, + &i.AllowTraversal, &i.Active, &i.Namespace, &i.Fqn, @@ -737,10 +758,16 @@ func (q *Queries) listAttributesByDefOrValueFqns(ctx context.Context, fqns []str const listAttributesDetail = `-- name: listAttributesDetail :many +WITH params AS ( + SELECT + COALESCE(NULLIF($6::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($7::text, ''), 'DESC') AS resolved_direction +) SELECT ad.id, ad.name as attribute_name, ad.rule, + ad.allow_traversal, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, ad.namespace_id, ad.active, @@ -752,7 +779,7 @@ SELECT 'active', avt.active, 'fqn', CONCAT(fqns.fqn, '/value/', avt.value) ) ORDER BY ARRAY_POSITION(ad.values_order, avt.id) - ) AS values, + ) FILTER (WHERE avt.id IS NOT NULL) AS values, fqns.fqn, COUNT(*) OVER() AS total FROM attribute_definitions ad @@ -767,12 +794,21 @@ LEFT JOIN ( GROUP BY av.id ) avt ON avt.attribute_definition_id = ad.id LEFT JOIN attribute_fqns fqns ON fqns.attribute_id = ad.id AND fqns.value_id IS NULL +CROSS JOIN params p WHERE ($1::BOOLEAN IS NULL OR ad.active = $1) AND - ($2::uuid IS NULL OR ad.namespace_id = $2::uuid) AND - ($3::text IS NULL OR n.name = $3::text) -GROUP BY ad.id, n.name, fqns.fqn -LIMIT $5 + ($2::uuid IS NULL OR ad.namespace_id = $2::uuid) AND + ($3::text IS NULL OR n.name = $3::text) +GROUP BY ad.id, n.name, fqns.fqn, p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ad.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ad.name END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ad.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ad.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ad.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ad.updated_at END DESC, + ad.id ASC +LIMIT $5 OFFSET $4 ` @@ -782,29 +818,38 @@ type listAttributesDetailParams struct { NamespaceName pgtype.Text `json:"namespace_name"` Offset int32 `json:"offset_"` Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` } type listAttributesDetailRow struct { - ID string `json:"id"` - AttributeName string `json:"attribute_name"` - Rule AttributeDefinitionRule `json:"rule"` - Metadata []byte `json:"metadata"` - NamespaceID string `json:"namespace_id"` - Active bool `json:"active"` - NamespaceName pgtype.Text `json:"namespace_name"` - Values []byte `json:"values"` - Fqn pgtype.Text `json:"fqn"` - Total int64 `json:"total"` + ID string `json:"id"` + AttributeName string `json:"attribute_name"` + Rule AttributeDefinitionRule `json:"rule"` + AllowTraversal bool `json:"allow_traversal"` + Metadata []byte `json:"metadata"` + NamespaceID string `json:"namespace_id"` + Active bool `json:"active"` + NamespaceName pgtype.Text `json:"namespace_name"` + Values []byte `json:"values"` + Fqn pgtype.Text `json:"fqn"` + Total int64 `json:"total"` } // -------------------------------------------------------------- // ATTRIBUTES // -------------------------------------------------------------- // +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($6::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($7::text, ''), 'DESC') AS resolved_direction +// ) // SELECT // ad.id, // ad.name as attribute_name, // ad.rule, +// ad.allow_traversal, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, // ad.namespace_id, // ad.active, @@ -816,7 +861,7 @@ type listAttributesDetailRow struct { // 'active', avt.active, // 'fqn', CONCAT(fqns.fqn, '/value/', avt.value) // ) ORDER BY ARRAY_POSITION(ad.values_order, avt.id) -// ) AS values, +// ) FILTER (WHERE avt.id IS NOT NULL) AS values, // fqns.fqn, // COUNT(*) OVER() AS total // FROM attribute_definitions ad @@ -831,11 +876,20 @@ type listAttributesDetailRow struct { // GROUP BY av.id // ) avt ON avt.attribute_definition_id = ad.id // LEFT JOIN attribute_fqns fqns ON fqns.attribute_id = ad.id AND fqns.value_id IS NULL +// CROSS JOIN params p // WHERE // ($1::BOOLEAN IS NULL OR ad.active = $1) AND // ($2::uuid IS NULL OR ad.namespace_id = $2::uuid) AND // ($3::text IS NULL OR n.name = $3::text) -// GROUP BY ad.id, n.name, fqns.fqn +// GROUP BY ad.id, n.name, fqns.fqn, p.resolved_field, p.resolved_direction +// ORDER BY +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ad.name END ASC, +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ad.name END DESC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ad.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ad.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ad.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ad.updated_at END DESC, +// ad.id ASC // LIMIT $5 // OFFSET $4 func (q *Queries) listAttributesDetail(ctx context.Context, arg listAttributesDetailParams) ([]listAttributesDetailRow, error) { @@ -845,6 +899,8 @@ func (q *Queries) listAttributesDetail(ctx context.Context, arg listAttributesDe arg.NamespaceName, arg.Offset, arg.Limit, + arg.SortField, + arg.SortDirection, ) if err != nil { return nil, err @@ -857,6 +913,7 @@ func (q *Queries) listAttributesDetail(ctx context.Context, arg listAttributesDe &i.ID, &i.AttributeName, &i.Rule, + &i.AllowTraversal, &i.Metadata, &i.NamespaceID, &i.Active, @@ -876,10 +933,16 @@ func (q *Queries) listAttributesDetail(ctx context.Context, arg listAttributesDe } const listAttributesSummary = `-- name: listAttributesSummary :many +WITH params AS ( + SELECT + COALESCE(NULLIF($4::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($5::text, ''), 'DESC') AS resolved_direction +) SELECT ad.id, ad.name as attribute_name, ad.rule, + ad.allow_traversal, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, ad.namespace_id, ad.active, @@ -887,35 +950,53 @@ SELECT COUNT(*) OVER() AS total FROM attribute_definitions ad LEFT JOIN attribute_namespaces n ON n.id = ad.namespace_id +CROSS JOIN params p WHERE ad.namespace_id = $1 -GROUP BY ad.id, n.name -LIMIT $3 +GROUP BY ad.id, n.name, p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ad.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ad.name END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ad.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ad.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ad.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ad.updated_at END DESC, + ad.id ASC +LIMIT $3 OFFSET $2 ` type listAttributesSummaryParams struct { - NamespaceID string `json:"namespace_id"` - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + NamespaceID string `json:"namespace_id"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` } type listAttributesSummaryRow struct { - ID string `json:"id"` - AttributeName string `json:"attribute_name"` - Rule AttributeDefinitionRule `json:"rule"` - Metadata []byte `json:"metadata"` - NamespaceID string `json:"namespace_id"` - Active bool `json:"active"` - NamespaceName pgtype.Text `json:"namespace_name"` - Total int64 `json:"total"` + ID string `json:"id"` + AttributeName string `json:"attribute_name"` + Rule AttributeDefinitionRule `json:"rule"` + AllowTraversal bool `json:"allow_traversal"` + Metadata []byte `json:"metadata"` + NamespaceID string `json:"namespace_id"` + Active bool `json:"active"` + NamespaceName pgtype.Text `json:"namespace_name"` + Total int64 `json:"total"` } // listAttributesSummary // +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($4::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($5::text, ''), 'DESC') AS resolved_direction +// ) // SELECT // ad.id, // ad.name as attribute_name, // ad.rule, +// ad.allow_traversal, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, // ad.namespace_id, // ad.active, @@ -923,12 +1004,27 @@ type listAttributesSummaryRow struct { // COUNT(*) OVER() AS total // FROM attribute_definitions ad // LEFT JOIN attribute_namespaces n ON n.id = ad.namespace_id +// CROSS JOIN params p // WHERE ad.namespace_id = $1 -// GROUP BY ad.id, n.name +// GROUP BY ad.id, n.name, p.resolved_field, p.resolved_direction +// ORDER BY +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ad.name END ASC, +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ad.name END DESC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ad.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ad.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ad.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ad.updated_at END DESC, +// ad.id ASC // LIMIT $3 // OFFSET $2 func (q *Queries) listAttributesSummary(ctx context.Context, arg listAttributesSummaryParams) ([]listAttributesSummaryRow, error) { - rows, err := q.db.Query(ctx, listAttributesSummary, arg.NamespaceID, arg.Offset, arg.Limit) + rows, err := q.db.Query(ctx, listAttributesSummary, + arg.NamespaceID, + arg.Offset, + arg.Limit, + arg.SortField, + arg.SortDirection, + ) if err != nil { return nil, err } @@ -940,6 +1036,7 @@ func (q *Queries) listAttributesSummary(ctx context.Context, arg listAttributesS &i.ID, &i.AttributeName, &i.Rule, + &i.AllowTraversal, &i.Metadata, &i.NamespaceID, &i.Active, @@ -1045,17 +1142,19 @@ SET rule = COALESCE($3, rule), values_order = COALESCE($4, values_order), metadata = COALESCE($5, metadata), - active = COALESCE($6, active) + active = COALESCE($6, active), + allow_traversal = COALESCE($7, allow_traversal) WHERE id = $1 ` type updateAttributeParams struct { - ID string `json:"id"` - Name pgtype.Text `json:"name"` - Rule NullAttributeDefinitionRule `json:"rule"` - ValuesOrder []string `json:"values_order"` - Metadata []byte `json:"metadata"` - Active pgtype.Bool `json:"active"` + ID string `json:"id"` + Name pgtype.Text `json:"name"` + Rule NullAttributeDefinitionRule `json:"rule"` + ValuesOrder []string `json:"values_order"` + Metadata []byte `json:"metadata"` + Active pgtype.Bool `json:"active"` + AllowTraversal pgtype.Bool `json:"allow_traversal"` } // updateAttribute: Unsafe and Safe Updates both @@ -1066,7 +1165,8 @@ type updateAttributeParams struct { // rule = COALESCE($3, rule), // values_order = COALESCE($4, values_order), // metadata = COALESCE($5, metadata), -// active = COALESCE($6, active) +// active = COALESCE($6, active), +// allow_traversal = COALESCE($7, allow_traversal) // WHERE id = $1 func (q *Queries) updateAttribute(ctx context.Context, arg updateAttributeParams) (int64, error) { result, err := q.db.Exec(ctx, updateAttribute, @@ -1076,6 +1176,7 @@ func (q *Queries) updateAttribute(ctx context.Context, arg updateAttributeParams arg.ValuesOrder, arg.Metadata, arg.Active, + arg.AllowTraversal, ) if err != nil { return 0, err diff --git a/service/policy/db/copyfrom.go b/service/policy/db/copyfrom.go index 2f450ac823..e89a426d77 100644 --- a/service/policy/db/copyfrom.go +++ b/service/policy/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: copyfrom.go package db @@ -39,9 +39,7 @@ func (r iteratorForcreateRegisteredResourceActionAttributeValues) Err() error { return nil } -// -------------------------------------------------------------- -// Registered Resource Action Attribute Values -// -------------------------------------------------------------- +// createRegisteredResourceActionAttributeValues // // INSERT INTO registered_resource_action_attribute_values (registered_resource_value_id, action_id, attribute_value_id) // VALUES ($1, $2, $3) diff --git a/service/policy/db/db.go b/service/policy/db/db.go index 95f1a604c9..d24a61d024 100644 --- a/service/policy/db/db.go +++ b/service/policy/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 package db diff --git a/service/policy/db/grant_mappings.go b/service/policy/db/grant_mappings.go index 11cd413e9d..7cd8830f92 100644 --- a/service/policy/db/grant_mappings.go +++ b/service/policy/db/grant_mappings.go @@ -22,6 +22,12 @@ func mapAlgorithmToKasPublicKeyAlg(alg policy.Algorithm) policy.KasPublicKeyAlgE return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 case policy.Algorithm_ALGORITHM_EC_P521: // ALGORITHM_EC_P521 is an alias return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + case policy.Algorithm_ALGORITHM_HPQT_XWING: + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING + case policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768: + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 + case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 case policy.Algorithm_ALGORITHM_UNSPECIFIED: return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED default: @@ -64,7 +70,7 @@ func mapKasKeysToGrants(keys []*policy.SimpleKasKey, existingGrants []*policy.Ke // KAS URI already exists, merge/add the public key if existingKas.GetPublicKey().GetCached() == nil { // Initialize if PublicKey or Cached part is missing - existingKas.PublicKey = &policy.PublicKey{ + existingKas.PublicKey = &policy.PublicKey{ //nolint:staticcheck // Legacy single-key field maintained for compatibility. PublicKey: &policy.PublicKey_Cached{ Cached: &policy.KasPublicKeySet{Keys: []*policy.KasPublicKey{}}, }, diff --git a/service/policy/db/key_access_server_registry.go b/service/policy/db/key_access_server_registry.go index 03b3a14009..ef9eabc83c 100644 --- a/service/policy/db/key_access_server_registry.go +++ b/service/policy/db/key_access_server_registry.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/jackc/pgx/v5/pgtype" + "github.com/opentdf/platform/lib/ocrypto" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" @@ -41,9 +42,13 @@ func (c PolicyDBClient) ListKeyAccessServers(ctx context.Context, r *kasregistry return nil, db.ErrListLimitTooLarge } + sortField, sortDirection := GetKeyAccessServersSortParams(r.GetSort()) + list, err := c.queries.listKeyAccessServers(ctx, listKeyAccessServersParams{ - Offset: offset, - Limit: limit, + Offset: offset, + Limit: limit, + SortField: sortField, + SortDirection: sortDirection, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -75,7 +80,7 @@ func (c PolicyDBClient) ListKeyAccessServers(ctx context.Context, r *kasregistry keyAccessServer.Id = kas.ID keyAccessServer.Uri = kas.Uri - keyAccessServer.PublicKey = publicKey + keyAccessServer.PublicKey = publicKey //nolint:staticcheck // Legacy single-key field maintained for compatibility. keyAccessServer.Name = kas.KasName.String keyAccessServer.Metadata = metadata keyAccessServer.KasKeys = keys @@ -293,7 +298,7 @@ func (c PolicyDBClient) DeleteKeyAccessServer(ctx context.Context, id string) (* }, nil } -func (c PolicyDBClient) ListKeyAccessServerGrants(ctx context.Context, r *kasregistry.ListKeyAccessServerGrantsRequest) (*kasregistry.ListKeyAccessServerGrantsResponse, error) { +func (c PolicyDBClient) ListKeyAccessServerGrants(ctx context.Context, r *kasregistry.ListKeyAccessServerGrantsRequest) (*kasregistry.ListKeyAccessServerGrantsResponse, error) { //nolint:staticcheck // Compatibility path for deprecated RPC. limit, offset := c.getRequestedLimitOffset(r.GetPagination()) maxLimit := c.listCfg.limitMax if maxLimit > 0 && limit > maxLimit { @@ -349,7 +354,7 @@ func (c PolicyDBClient) ListKeyAccessServerGrants(ctx context.Context, r *kasreg total = int32(listRows[0].Total) nextOffset = getNextOffset(offset, limit, total) } - return &kasregistry.ListKeyAccessServerGrantsResponse{ + return &kasregistry.ListKeyAccessServerGrantsResponse{ //nolint:staticcheck // Compatibility path for deprecated RPC. Grants: grants, Pagination: &policy.PageResponse{ CurrentOffset: params.Offset, @@ -373,8 +378,15 @@ func (c PolicyDBClient) CreateKey(ctx context.Context, r *kasregistry.CreateKeyR if !isValidBase64(r.GetPublicKeyCtx().GetPem()) { return nil, errors.Join(errors.New("public key ctx"), db.ErrExpectedBase64EncodedValue) } - if (mode == int32(policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY) || mode == int32(policy.KeyMode_KEY_MODE_PROVIDER_ROOT_KEY)) && !isValidBase64(r.GetPrivateKeyCtx().GetWrappedKey()) { - return nil, errors.Join(errors.New("private key ctx"), db.ErrExpectedBase64EncodedValue) + if mode == int32(policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY) || mode == int32(policy.KeyMode_KEY_MODE_PROVIDER_ROOT_KEY) { + wrappedKey := r.GetPrivateKeyCtx().GetWrappedKey() + decodedKey, err := base64.StdEncoding.DecodeString(wrappedKey) + if err != nil { + return nil, errors.Join(errors.New("private key ctx"), db.ErrExpectedBase64EncodedValue) + } + if ocrypto.IsPEMOrDERPrivateKey(decodedKey) { + return nil, errors.Join(errors.New("private key ctx"), db.ErrUnencryptedPrivateKey) + } } // Marshal private key and public key context @@ -545,9 +557,47 @@ func (c PolicyDBClient) ListKeys(ctx context.Context, r *kasregistry.ListKeysReq return nil, db.ErrListLimitTooLarge } - kasID := pgtypeUUID(r.GetKasId()) - kasURI := pgtypeText(r.GetKasUri()) - kasName := pgtypeText(strings.ToLower(r.GetKasName())) + var ( + kasID pgtype.UUID + kasURI pgtype.Text + kasName pgtype.Text + ) + hasKasFilter := false + + switch f := r.GetKasFilter().(type) { + case *kasregistry.ListKeysRequest_KasId: + hasKasFilter = true + kasID = pgtypeUUID(f.KasId) + if !kasID.Valid { + return nil, db.ErrUUIDInvalid + } + case *kasregistry.ListKeysRequest_KasUri: + hasKasFilter = true + kasURI = pgtypeText(f.KasUri) + if !kasURI.Valid { + return nil, db.ErrSelectIdentifierInvalid + } + case *kasregistry.ListKeysRequest_KasName: + hasKasFilter = true + kasName = pgtypeText(strings.ToLower(f.KasName)) + if !kasName.Valid { + return nil, db.ErrSelectIdentifierInvalid + } + } + + if hasKasFilter { + exists, err := c.queries.keyAccessServerExists(ctx, keyAccessServerExistsParams{ + KasID: kasID, + KasName: kasName, + KasUri: kasURI, + }) + if err != nil { + return nil, db.WrapIfKnownInvalidQueryErr(err) + } + if !exists { + return nil, db.ErrNotFound + } + } algo := pgtypeInt4(int32(r.GetKeyAlgorithm()), r.GetKeyAlgorithm() != policy.Algorithm_ALGORITHM_UNSPECIFIED) var legacy pgtype.Bool @@ -558,14 +608,18 @@ func (c PolicyDBClient) ListKeys(ctx context.Context, r *kasregistry.ListKeysReq legacy = pgtypeBool(r.GetLegacy()) } + sortField, sortDirection := GetKasKeysSortParams(r.GetSort()) + params := listKeysParams{ - Legacy: legacy, - KeyAlgorithm: algo, - KasID: kasID, - KasUri: kasURI, - KasName: kasName, - Offset: offset, - Limit: limit, + Legacy: legacy, + KeyAlgorithm: algo, + KasID: kasID, + KasUri: kasURI, + KasName: kasName, + Offset: offset, + Limit: limit, + SortField: sortField, + SortDirection: sortDirection, } listRows, err := c.queries.listKeys(ctx, params) diff --git a/service/policy/db/key_access_server_registry.sql.go b/service/policy/db/key_access_server_registry.sql.go index cc4eee316d..f7b709306f 100644 --- a/service/policy/db/key_access_server_registry.sql.go +++ b/service/policy/db/key_access_server_registry.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: key_access_server_registry.sql package db @@ -412,6 +412,38 @@ func (q *Queries) getKeyAccessServer(ctx context.Context, arg getKeyAccessServer return i, err } +const keyAccessServerExists = `-- name: keyAccessServerExists :one +SELECT EXISTS ( + SELECT 1 + FROM key_access_servers AS kas + WHERE ($1::uuid IS NULL OR kas.id = $1::uuid) + AND ($2::text IS NULL OR kas.name = $2::text) + AND ($3::text IS NULL OR kas.uri = $3::text) +) +` + +type keyAccessServerExistsParams struct { + KasID pgtype.UUID `json:"kas_id"` + KasName pgtype.Text `json:"kas_name"` + KasUri pgtype.Text `json:"kas_uri"` +} + +// keyAccessServerExists +// +// SELECT EXISTS ( +// SELECT 1 +// FROM key_access_servers AS kas +// WHERE ($1::uuid IS NULL OR kas.id = $1::uuid) +// AND ($2::text IS NULL OR kas.name = $2::text) +// AND ($3::text IS NULL OR kas.uri = $3::text) +// ) +func (q *Queries) keyAccessServerExists(ctx context.Context, arg keyAccessServerExistsParams) (bool, error) { + row := q.db.QueryRow(ctx, keyAccessServerExists, arg.KasID, arg.KasName, arg.KasUri) + var exists bool + err := row.Scan(&exists) + return exists, err +} + const listKeyAccessServerGrants = `-- name: listKeyAccessServerGrants :many WITH listed AS ( SELECT @@ -604,7 +636,12 @@ func (q *Queries) listKeyAccessServerGrants(ctx context.Context, arg listKeyAcce } const listKeyAccessServers = `-- name: listKeyAccessServers :many -WITH counted AS ( +WITH params AS ( + SELECT + COALESCE(NULLIF($3::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($4::text, ''), 'DESC') AS resolved_direction +), +counted AS ( SELECT COUNT(kas.id) AS total FROM key_access_servers AS kas ) @@ -618,6 +655,7 @@ SELECT kas.id, counted.total FROM key_access_servers AS kas CROSS JOIN counted +CROSS JOIN params p LEFT JOIN ( SELECT kask.key_access_server_id, @@ -636,13 +674,25 @@ LEFT JOIN ( INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id GROUP BY kask.key_access_server_id ) kask_keys ON kas.id = kask_keys.key_access_server_id -LIMIT $2 +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN kas.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN kas.name END DESC, + CASE WHEN p.resolved_field = 'uri' AND p.resolved_direction = 'ASC' THEN kas.uri END ASC, + CASE WHEN p.resolved_field = 'uri' AND p.resolved_direction = 'DESC' THEN kas.uri END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN kas.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN kas.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN kas.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN kas.updated_at END DESC, + kas.id ASC +LIMIT $2 OFFSET $1 ` type listKeyAccessServersParams struct { - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` } type listKeyAccessServersRow struct { @@ -658,7 +708,12 @@ type listKeyAccessServersRow struct { // listKeyAccessServers // -// WITH counted AS ( +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($3::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($4::text, ''), 'DESC') AS resolved_direction +// ), +// counted AS ( // SELECT COUNT(kas.id) AS total // FROM key_access_servers AS kas // ) @@ -672,6 +727,7 @@ type listKeyAccessServersRow struct { // counted.total // FROM key_access_servers AS kas // CROSS JOIN counted +// CROSS JOIN params p // LEFT JOIN ( // SELECT // kask.key_access_server_id, @@ -690,10 +746,25 @@ type listKeyAccessServersRow struct { // INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id // GROUP BY kask.key_access_server_id // ) kask_keys ON kas.id = kask_keys.key_access_server_id +// ORDER BY +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN kas.name END ASC, +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN kas.name END DESC, +// CASE WHEN p.resolved_field = 'uri' AND p.resolved_direction = 'ASC' THEN kas.uri END ASC, +// CASE WHEN p.resolved_field = 'uri' AND p.resolved_direction = 'DESC' THEN kas.uri END DESC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN kas.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN kas.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN kas.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN kas.updated_at END DESC, +// kas.id ASC // LIMIT $2 // OFFSET $1 func (q *Queries) listKeyAccessServers(ctx context.Context, arg listKeyAccessServersParams) ([]listKeyAccessServersRow, error) { - rows, err := q.db.Query(ctx, listKeyAccessServers, arg.Offset, arg.Limit) + rows, err := q.db.Query(ctx, listKeyAccessServers, + arg.Offset, + arg.Limit, + arg.SortField, + arg.SortDirection, + ) if err != nil { return nil, err } @@ -824,7 +895,7 @@ CROSS JOIN keys_with_mappings_count kwmc LEFT JOIN namespace_mappings nm ON fk.id = nm.key_id LEFT JOIN definition_mappings dm ON fk.id = dm.key_id LEFT JOIN value_mappings vm ON fk.id = vm.key_id -ORDER BY fk.created_at +ORDER BY fk.created_at DESC LIMIT $2 OFFSET $1 ` @@ -952,7 +1023,7 @@ type listKeyMappingsRow struct { // LEFT JOIN namespace_mappings nm ON fk.id = nm.key_id // LEFT JOIN definition_mappings dm ON fk.id = dm.key_id // LEFT JOIN value_mappings vm ON fk.id = vm.key_id -// ORDER BY fk.created_at +// ORDER BY fk.created_at DESC // LIMIT $2 // OFFSET $1 func (q *Queries) listKeyMappings(ctx context.Context, arg listKeyMappingsParams) ([]listKeyMappingsRow, error) { @@ -991,14 +1062,19 @@ func (q *Queries) listKeyMappings(ctx context.Context, arg listKeyMappingsParams } const listKeys = `-- name: listKeys :many -WITH listed AS ( +WITH params AS ( + SELECT + COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +), +listed AS ( SELECT kas.id AS kas_id, kas.uri AS kas_uri FROM key_access_servers AS kas - WHERE ($5::uuid IS NULL OR kas.id = $5::uuid) - AND ($6::text IS NULL OR kas.name = $6::text) - AND ($7::text IS NULL OR kas.uri = $7::text) + WHERE ($7::uuid IS NULL OR kas.id = $7::uuid) + AND ($8::text IS NULL OR kas.name = $8::text) + AND ($9::text IS NULL OR kas.uri = $9::text) ) SELECT COUNT(*) OVER () AS total, @@ -1026,24 +1102,34 @@ SELECT FROM key_access_server_keys AS kask INNER JOIN listed ON kask.key_access_server_id = listed.kas_id -LEFT JOIN +CROSS JOIN params p +LEFT JOIN provider_config as pc ON kask.provider_config_id = pc.id WHERE ($1::integer IS NULL OR kask.key_algorithm = $1::integer) AND ($2::boolean IS NULL OR kask.legacy = $2::boolean) -ORDER BY kask.created_at DESC -LIMIT $4 +ORDER BY + CASE WHEN p.resolved_field = 'key_id' AND p.resolved_direction = 'ASC' THEN kask.key_id END ASC, + CASE WHEN p.resolved_field = 'key_id' AND p.resolved_direction = 'DESC' THEN kask.key_id END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN kask.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN kask.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN kask.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN kask.updated_at END DESC, + kask.id ASC +LIMIT $4 OFFSET $3 ` type listKeysParams struct { - KeyAlgorithm pgtype.Int4 `json:"key_algorithm"` - Legacy pgtype.Bool `json:"legacy"` - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` - KasID pgtype.UUID `json:"kas_id"` - KasName pgtype.Text `json:"kas_name"` - KasUri pgtype.Text `json:"kas_uri"` + KeyAlgorithm pgtype.Int4 `json:"key_algorithm"` + Legacy pgtype.Bool `json:"legacy"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` + KasID pgtype.UUID `json:"kas_id"` + KasName pgtype.Text `json:"kas_name"` + KasUri pgtype.Text `json:"kas_uri"` } type listKeysRow struct { @@ -1067,14 +1153,19 @@ type listKeysRow struct { // listKeys // -// WITH listed AS ( +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +// ), +// listed AS ( // SELECT // kas.id AS kas_id, // kas.uri AS kas_uri // FROM key_access_servers AS kas -// WHERE ($5::uuid IS NULL OR kas.id = $5::uuid) -// AND ($6::text IS NULL OR kas.name = $6::text) -// AND ($7::text IS NULL OR kas.uri = $7::text) +// WHERE ($7::uuid IS NULL OR kas.id = $7::uuid) +// AND ($8::text IS NULL OR kas.name = $8::text) +// AND ($9::text IS NULL OR kas.uri = $9::text) // ) // SELECT // COUNT(*) OVER () AS total, @@ -1102,12 +1193,20 @@ type listKeysRow struct { // FROM key_access_server_keys AS kask // INNER JOIN // listed ON kask.key_access_server_id = listed.kas_id +// CROSS JOIN params p // LEFT JOIN // provider_config as pc ON kask.provider_config_id = pc.id // WHERE // ($1::integer IS NULL OR kask.key_algorithm = $1::integer) // AND ($2::boolean IS NULL OR kask.legacy = $2::boolean) -// ORDER BY kask.created_at DESC +// ORDER BY +// CASE WHEN p.resolved_field = 'key_id' AND p.resolved_direction = 'ASC' THEN kask.key_id END ASC, +// CASE WHEN p.resolved_field = 'key_id' AND p.resolved_direction = 'DESC' THEN kask.key_id END DESC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN kask.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN kask.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN kask.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN kask.updated_at END DESC, +// kask.id ASC // LIMIT $4 // OFFSET $3 func (q *Queries) listKeys(ctx context.Context, arg listKeysParams) ([]listKeysRow, error) { @@ -1116,6 +1215,8 @@ func (q *Queries) listKeys(ctx context.Context, arg listKeysParams) ([]listKeysR arg.Legacy, arg.Offset, arg.Limit, + arg.SortField, + arg.SortDirection, arg.KasID, arg.KasName, arg.KasUri, diff --git a/service/policy/db/key_management.sql.go b/service/policy/db/key_management.sql.go index 6e265e292f..eae00e6412 100644 --- a/service/policy/db/key_management.sql.go +++ b/service/policy/db/key_management.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: key_management.sql package db @@ -171,6 +171,7 @@ SELECT counted.total FROM provider_config AS pc CROSS JOIN counted +ORDER BY pc.created_at DESC LIMIT $2 OFFSET $1 ` @@ -204,6 +205,7 @@ type listProviderConfigsRow struct { // counted.total // FROM provider_config AS pc // CROSS JOIN counted +// ORDER BY pc.created_at DESC // LIMIT $2 // OFFSET $1 func (q *Queries) listProviderConfigs(ctx context.Context, arg listProviderConfigsParams) ([]listProviderConfigsRow, error) { diff --git a/service/policy/db/migrations/20240213000000_create_attribute_fqn.sql b/service/policy/db/migrations/20240213000000_create_attribute_fqn.sql index ca7324ab55..72ae2000b8 100644 --- a/service/policy/db/migrations/20240213000000_create_attribute_fqn.sql +++ b/service/policy/db/migrations/20240213000000_create_attribute_fqn.sql @@ -15,7 +15,4 @@ CREATE TABLE IF NOT EXISTS attribute_fqns ( -- +goose Down -DROP TABLE attribute_fqn; - --- +goose StatementBegin --- +goose StatementEnd +DROP TABLE IF EXISTS attribute_fqns; diff --git a/service/policy/db/migrations/20240305000000_add_subject_condition_sets.sql b/service/policy/db/migrations/20240305000000_add_subject_condition_sets.sql index 4f881cdc84..ca1ab89f39 100644 --- a/service/policy/db/migrations/20240305000000_add_subject_condition_sets.sql +++ b/service/policy/db/migrations/20240305000000_add_subject_condition_sets.sql @@ -114,9 +114,6 @@ WHERE subject_mappings.subject_condition_set_id = subject_mappings_migration_dat ALTER TABLE IF EXISTS subject_mappings DROP COLUMN subject_condition_set_id, DROP COLUMN actions; -DROP TRIGGER subject_condition_set_updated_at; -DROP TABLE subject_condition_set; -CREATE TYPE subject_mappings_operator AS ENUM ('UNSPECIFIED', 'IN', 'NOT_IN'); - --- +goose StatementBegin --- +goose StatementEnd \ No newline at end of file +DROP TRIGGER IF EXISTS subject_condition_set_updated_at ON subject_condition_set; +DROP TABLE IF EXISTS subject_condition_set; +CREATE TYPE subject_mappings_operator AS ENUM ('UNSPECIFIED', 'IN', 'NOT_IN'); \ No newline at end of file diff --git a/service/policy/db/migrations/20240402000000_preserve_value_order.sql b/service/policy/db/migrations/20240402000000_preserve_value_order.sql index 6bac721f5b..56185f715d 100644 --- a/service/policy/db/migrations/20240402000000_preserve_value_order.sql +++ b/service/policy/db/migrations/20240402000000_preserve_value_order.sql @@ -37,12 +37,12 @@ EXECUTE FUNCTION update_definition_delete_values_order(); -- +goose StatementBegin -DROP FUNCTION update_definition_add_values_order; -DROP TRIGGER trigger_update_definition_add_values_order; +DROP TRIGGER IF EXISTS trigger_update_definition_add_values_order ON attribute_values; +DROP FUNCTION IF EXISTS update_definition_add_values_order; -DROP FUNCTION update_definition_delete_values_order; -DROP TRIGGER trigger_update_definition_delete_values_order; +DROP TRIGGER IF EXISTS trigger_update_definition_delete_values_order ON attribute_values; +DROP FUNCTION IF EXISTS update_definition_delete_values_order; -ALTER TABLE attribute_definitions DROP COLUMN values_order; +ALTER TABLE attribute_definitions DROP COLUMN IF EXISTS values_order; -- +goose StatementEnd diff --git a/service/policy/db/migrations/20240618000000_add_delete_cascade.sql b/service/policy/db/migrations/20240618000000_add_delete_cascade.sql index c618dc8893..c34bc655c8 100644 --- a/service/policy/db/migrations/20240618000000_add_delete_cascade.sql +++ b/service/policy/db/migrations/20240618000000_add_delete_cascade.sql @@ -233,20 +233,27 @@ ADD CONSTRAINT attribute_value_key_access_grants_attribute_value_id_fkey FOREIGN KEY (attribute_value_id) REFERENCES attribute_values (id); +-- attribute_value_members may have been dropped and recreated by a later +-- migration (20240813000000) with default constraint names, so these +-- _cascades constraints may not exist. ALTER TABLE attribute_value_members -DROP CONSTRAINT attr_val_members_value_id_fkey_cascades; +DROP CONSTRAINT IF EXISTS attr_val_members_value_id_fkey_cascades; -ALTER TABLE attribute_value_members -ADD CONSTRAINT attribute_value_members_value_id_fkey -FOREIGN KEY (value_id) -REFERENCES attribute_values (id); +DO $$ BEGIN + ALTER TABLE attribute_value_members + ADD CONSTRAINT attribute_value_members_value_id_fkey + FOREIGN KEY (value_id) REFERENCES attribute_values (id); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; ALTER TABLE attribute_value_members -DROP CONSTRAINT attr_val_members_member_id_fkey_cascades; - -ALTER TABLE attribute_value_members -ADD CONSTRAINT attribute_value_members_member_id_fkey -FOREIGN KEY (member_id) -REFERENCES attribute_values (id); +DROP CONSTRAINT IF EXISTS attr_val_members_member_id_fkey_cascades; + +DO $$ BEGIN + ALTER TABLE attribute_value_members + ADD CONSTRAINT attribute_value_members_member_id_fkey + FOREIGN KEY (member_id) REFERENCES attribute_values (id); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; -- +goose StatementEnd \ No newline at end of file diff --git a/service/policy/db/migrations/20241125220354_keys_table.sql b/service/policy/db/migrations/20241125220354_keys_table.sql index aa6c435db1..50cbe77de4 100644 --- a/service/policy/db/migrations/20241125220354_keys_table.sql +++ b/service/policy/db/migrations/20241125220354_keys_table.sql @@ -306,20 +306,20 @@ DROP TRIGGER IF EXISTS trigger_update_was_mapped_namespace ON attribute_namespac DROP TRIGGER IF EXISTS trigger_update_was_mapped_definition ON attribute_definition_public_key_map; -DROP TRIGGER IF EXISTS trigger_update_was_mapped_value ON attribute_value_key_map; +DROP TRIGGER IF EXISTS trigger_update_was_mapped_value ON attribute_value_public_key_map; -DROP TRIGGER IF EXISTS maintain_active_key; +DROP TRIGGER IF EXISTS maintain_active_key ON public_keys; DROP FUNCTION IF EXISTS update_active_key; -DROP FUNCTION IF EXISTS update_was_mapped (); +DROP FUNCTION IF EXISTS update_was_mapped; -DROP TABLE public_keys; +DROP TABLE IF EXISTS attribute_namespace_public_key_map; -DROP TABLE attribute_namespace_public_key_map; +DROP TABLE IF EXISTS attribute_definition_public_key_map; -DROP TABLE attribute_definition_public_key_map; +DROP TABLE IF EXISTS attribute_value_public_key_map; -DROP TABLE attribute_value_public_key_map; +DROP TABLE IF EXISTS public_keys; -- +goose StatementEnd \ No newline at end of file diff --git a/service/policy/db/migrations/20260120000000_add_attribute_definition_allow_traversal.md b/service/policy/db/migrations/20260120000000_add_attribute_definition_allow_traversal.md new file mode 100644 index 0000000000..b538677794 --- /dev/null +++ b/service/policy/db/migrations/20260120000000_add_attribute_definition_allow_traversal.md @@ -0,0 +1,17 @@ +# Add Allow Traversal to Attribute Definitions + +This migration adds a boolean flag to attribute definitions so policy logic can explicitly allow or deny attribute traversal. + +## Schema Changes + +- **Table**: `attribute_definitions` +- **Column added**: `allow_traversal BOOLEAN NOT NULL DEFAULT FALSE` (comment: "Whether or not to allow platform to return the definition key when encrypting, if the value specified is missing.") + +## Behavior + +- Existing rows default to `allow_traversal = FALSE`. +- New rows must provide a value or accept the default `FALSE`. + +## Rollback + +- Drops the `allow_traversal` column from `attribute_definitions`. diff --git a/service/policy/db/migrations/20260120000000_add_attribute_definition_allow_traversal.sql b/service/policy/db/migrations/20260120000000_add_attribute_definition_allow_traversal.sql new file mode 100644 index 0000000000..21a9ff9d72 --- /dev/null +++ b/service/policy/db/migrations/20260120000000_add_attribute_definition_allow_traversal.sql @@ -0,0 +1,12 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE attribute_definitions + ADD COLUMN IF NOT EXISTS allow_traversal BOOLEAN NOT NULL DEFAULT FALSE; +COMMENT ON COLUMN attribute_definitions.allow_traversal IS 'Whether or not to allow platform to return the definition key when encrypting, if the value specified is missing.'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE attribute_definitions + DROP COLUMN IF EXISTS allow_traversal; +-- +goose StatementEnd diff --git a/service/policy/db/migrations/20260204000000_drop_namespace_certificates.sql b/service/policy/db/migrations/20260204000000_drop_namespace_certificates.sql new file mode 100644 index 0000000000..47cf488a31 --- /dev/null +++ b/service/policy/db/migrations/20260204000000_drop_namespace_certificates.sql @@ -0,0 +1,45 @@ +-- +goose Up +-- +goose StatementBegin + +DROP TRIGGER IF EXISTS certificates_updated_at ON certificates; +DROP TABLE IF EXISTS attribute_namespace_certificates; +DROP TABLE IF EXISTS certificates; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +CREATE TABLE IF NOT EXISTS certificates +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + pem TEXT NOT NULL, + metadata JSONB, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE certificates IS 'Table to store X.509 certificates for chain of trust (root only)'; +COMMENT ON COLUMN certificates.id IS 'Unique identifier for the certificate'; +COMMENT ON COLUMN certificates.pem IS 'PEM format - Base64-encoded DER certificate (not PEM; no headers/footers)'; +COMMENT ON COLUMN certificates.metadata IS 'Optional metadata for the certificate'; +COMMENT ON COLUMN certificates.created_at IS 'Timestamp when the certificate was created'; +COMMENT ON COLUMN certificates.updated_at IS 'Timestamp when the certificate was last updated'; + +CREATE TABLE IF NOT EXISTS attribute_namespace_certificates +( + namespace_id UUID NOT NULL REFERENCES attribute_namespaces(id) ON DELETE CASCADE, + certificate_id UUID NOT NULL REFERENCES certificates(id) ON DELETE CASCADE, + PRIMARY KEY (namespace_id, certificate_id) +); + +COMMENT ON TABLE attribute_namespace_certificates IS 'Junction table to map root certificates to attribute namespaces'; +COMMENT ON COLUMN attribute_namespace_certificates.namespace_id IS 'Foreign key to the namespace'; +COMMENT ON COLUMN attribute_namespace_certificates.certificate_id IS 'Foreign key to the certificate'; + +CREATE TRIGGER certificates_updated_at + BEFORE UPDATE ON certificates + FOR EACH ROW + EXECUTE FUNCTION update_updated_at(); + +-- +goose StatementEnd diff --git a/service/policy/db/migrations/20260302000000_add_namespace_to_registered_resources.sql b/service/policy/db/migrations/20260302000000_add_namespace_to_registered_resources.sql new file mode 100644 index 0000000000..adbcf44ee1 --- /dev/null +++ b/service/policy/db/migrations/20260302000000_add_namespace_to_registered_resources.sql @@ -0,0 +1,36 @@ +-- +goose Up +-- +goose StatementBegin + +-- Add nullable namespace_id column to registered_resources +ALTER TABLE registered_resources + ADD COLUMN namespace_id UUID REFERENCES attribute_namespaces(id) ON DELETE CASCADE; + +-- Drop existing global uniqueness constraint +ALTER TABLE registered_resources DROP CONSTRAINT registered_resources_name_key; + +-- Namespaced RRs: unique name within namespace +CREATE UNIQUE INDEX registered_resources_namespace_name_unique + ON registered_resources(namespace_id, name) WHERE namespace_id IS NOT NULL; + +-- Legacy RRs (no namespace): unique name globally +CREATE UNIQUE INDEX registered_resources_name_unique + ON registered_resources(name) WHERE namespace_id IS NULL; + +-- Index for namespace-scoped queries +CREATE INDEX idx_registered_resources_namespace + ON registered_resources(namespace_id); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DROP INDEX IF EXISTS idx_registered_resources_namespace; +DROP INDEX IF EXISTS registered_resources_name_unique; +DROP INDEX IF EXISTS registered_resources_namespace_name_unique; + +ALTER TABLE registered_resources ADD CONSTRAINT registered_resources_name_key UNIQUE (name); + +ALTER TABLE registered_resources DROP COLUMN IF EXISTS namespace_id; + +-- +goose StatementEnd diff --git a/service/policy/db/migrations/20260306000000_make_obligation_trigger_uniqueness_client_aware.md b/service/policy/db/migrations/20260306000000_make_obligation_trigger_uniqueness_client_aware.md new file mode 100644 index 0000000000..ff768c0a11 --- /dev/null +++ b/service/policy/db/migrations/20260306000000_make_obligation_trigger_uniqueness_client_aware.md @@ -0,0 +1,31 @@ +# Make Obligation Trigger Uniqueness Client-Aware + +This migration updates uniqueness semantics for `obligation_triggers` after the +introduction of optional `client_id` scoping. + +## Why + +The previous unique constraint only considered: + +- `obligation_value_id` +- `action_id` +- `attribute_value_id` + +That prevented creating multiple triggers for different PEP clients when all +other fields were the same. + +## Changes + +1. Drop the existing table-level unique constraint on: + - `(obligation_value_id, action_id, attribute_value_id)` + - Includes handling historical truncated constraint names in Postgres. +2. Add partial unique index for unscoped triggers: + - `(obligation_value_id, action_id, attribute_value_id)` where `client_id IS NULL` +3. Add partial unique index for client-scoped triggers: + - `(obligation_value_id, action_id, attribute_value_id, client_id)` where `client_id IS NOT NULL` + +## Resulting Behavior + +- Allows one unscoped trigger per obligation/action/attribute tuple. +- Allows one scoped trigger per unique `client_id` for the same tuple. +- Prevents duplicate scoped triggers for the same `client_id`. diff --git a/service/policy/db/migrations/20260306000000_make_obligation_trigger_uniqueness_client_aware.sql b/service/policy/db/migrations/20260306000000_make_obligation_trigger_uniqueness_client_aware.sql new file mode 100644 index 0000000000..0f95bc22e3 --- /dev/null +++ b/service/policy/db/migrations/20260306000000_make_obligation_trigger_uniqueness_client_aware.sql @@ -0,0 +1,30 @@ +-- +goose Up +-- +goose StatementBegin +-- Make trigger uniqueness aware of optional client_id scoping. +ALTER TABLE IF EXISTS obligation_triggers +DROP CONSTRAINT IF EXISTS obligation_triggers_obligation_value_id_action_id_attribute_value_id_key; + +ALTER TABLE IF EXISTS obligation_triggers +DROP CONSTRAINT IF EXISTS obligation_triggers_obligation_value_id_action_id_attribute_key; + +ALTER TABLE IF EXISTS obligation_triggers +DROP CONSTRAINT IF EXISTS obligation_triggers_obligation_value_id_action_id_attribute_val; + +CREATE UNIQUE INDEX IF NOT EXISTS obligation_triggers_unscoped_unique_idx +ON obligation_triggers (obligation_value_id, action_id, attribute_value_id) +WHERE client_id IS NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS obligation_triggers_scoped_unique_idx +ON obligation_triggers (obligation_value_id, action_id, attribute_value_id, client_id) +WHERE client_id IS NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS obligation_triggers_scoped_unique_idx; +DROP INDEX IF EXISTS obligation_triggers_unscoped_unique_idx; + +ALTER TABLE IF EXISTS obligation_triggers +ADD CONSTRAINT obligation_triggers_obligation_value_id_action_id_attribute_key +UNIQUE (obligation_value_id, action_id, attribute_value_id); +-- +goose StatementEnd diff --git a/service/policy/db/migrations/20260312000000_add_namespace_to_actions.md b/service/policy/db/migrations/20260312000000_add_namespace_to_actions.md new file mode 100644 index 0000000000..4c112e16e6 --- /dev/null +++ b/service/policy/db/migrations/20260312000000_add_namespace_to_actions.md @@ -0,0 +1,45 @@ +# Add Namespace to Actions + +This migration introduces namespace-scoped actions by adding `namespace_id` to `actions` and updating uniqueness semantics. + +## Why + +Policy objects are namespaced, and action references need to support same-namespace resolution. This migration enables actions to be scoped per namespace while preserving legacy/global actions. + +## Up Migration + +The Up migration: + +- Adds nullable `namespace_id` to `actions` (`REFERENCES attribute_namespaces(id) ON DELETE CASCADE`). +- Replaces global `UNIQUE(name)` with two partial uniqueness constraints: + - `UNIQUE(namespace_id, name)` when `namespace_id IS NOT NULL` (namespaced actions) + - `UNIQUE(name)` when `namespace_id IS NULL` (legacy/global actions) +- Adds `idx_actions_namespace_id` for namespaced lookup performance. + +## Down Migration + +The Down migration is data-aware and performs a deterministic canonicalization before restoring global-only action semantics. + +Steps: + +1. Build an action-id remap (`old_action_id -> canonical_action_id`) by action name. +2. Canonical action selection order per name is: + - prefer global (`namespace_id IS NULL`) + - then earliest `created_at` + - then smallest `id` +3. Remap and deduplicate references in: + - `subject_mapping_actions` + - `registered_resource_action_attribute_values` + - `obligation_triggers` +4. Delete duplicate (non-canonical) action rows. +5. Drop namespace indexes, restore global `UNIQUE(name)`, and drop `namespace_id`. + +## Rollback Semantics + +Rollback is intentionally **lossy** with respect to namespace-level action identity. + +- Actions sharing the same `name` are collapsed to one canonical global action row. +- Referencing rows are rewritten to canonical action ids. +- Distinct namespace-scoped action ids for the same name are not preserved after rollback. + +This behavior is required to safely re-establish global `UNIQUE(name)` without orphaning references. diff --git a/service/policy/db/migrations/20260312000000_add_namespace_to_actions.sql b/service/policy/db/migrations/20260312000000_add_namespace_to_actions.sql new file mode 100644 index 0000000000..c7d17d3aff --- /dev/null +++ b/service/policy/db/migrations/20260312000000_add_namespace_to_actions.sql @@ -0,0 +1,188 @@ +-- +goose Up +-- +goose StatementBegin + +-- Add nullable namespace_id column to actions for namespace-scoped custom actions. +-- Keep nullable for legacy custom actions and standard CRUD actions. +ALTER TABLE actions + ADD COLUMN namespace_id UUID REFERENCES attribute_namespaces(id) ON DELETE CASCADE; + +-- Drop existing global uniqueness constraint. +ALTER TABLE actions DROP CONSTRAINT actions_name_unique; + +-- Namespaced custom actions: unique name per namespace. +CREATE UNIQUE INDEX actions_namespace_name_unique + ON actions(namespace_id, name) WHERE namespace_id IS NOT NULL; + +-- Legacy/global actions (including standard CRUD actions): unique name globally. +CREATE UNIQUE INDEX actions_name_unique + ON actions(name) WHERE namespace_id IS NULL; + +-- Index for namespace-scoped action queries. +CREATE INDEX idx_actions_namespace_id + ON actions(namespace_id); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +-- Roll back namespace-scoped actions to global actions by canonicalizing action IDs +-- by action name, remapping references, and deleting duplicate action rows. +-- Canonical action selection prefers legacy/global actions (namespace_id IS NULL), +-- then earliest created_at, then smallest id for deterministic behavior. +-- +-- Operator note (optional, run manually outside migration): +-- Preview action-id remaps this Down migration will apply: +-- +-- WITH canonical AS ( +-- SELECT +-- id, +-- name, +-- FIRST_VALUE(id) OVER ( +-- PARTITION BY name +-- ORDER BY (namespace_id IS NULL) DESC, created_at ASC, id ASC +-- ) AS canonical_id +-- FROM actions +-- ) +-- SELECT name, id AS old_action_id, canonical_id AS new_action_id +-- FROM canonical +-- WHERE id <> canonical_id +-- ORDER BY name, old_action_id; +-- +-- Preview impacted row counts by table: +-- SELECT COUNT(*) FROM subject_mapping_actions sma JOIN actions a ON a.id = sma.action_id WHERE a.namespace_id IS NOT NULL; +-- SELECT COUNT(*) FROM registered_resource_action_attribute_values rr JOIN actions a ON a.id = rr.action_id WHERE a.namespace_id IS NOT NULL; +-- SELECT COUNT(*) FROM obligation_triggers ot JOIN actions a ON a.id = ot.action_id WHERE a.namespace_id IS NOT NULL; +-- +-- Post-down verification queries: +-- SELECT name, COUNT(*) FROM actions GROUP BY name HAVING COUNT(*) > 1; +-- SELECT COUNT(*) FROM subject_mapping_actions sma LEFT JOIN actions a ON a.id = sma.action_id WHERE a.id IS NULL; +-- SELECT COUNT(*) FROM registered_resource_action_attribute_values rr LEFT JOIN actions a ON a.id = rr.action_id WHERE a.id IS NULL; +-- SELECT COUNT(*) FROM obligation_triggers ot LEFT JOIN actions a ON a.id = ot.action_id WHERE a.id IS NULL; + +CREATE TEMP TABLE action_id_remap AS +WITH canonical AS ( + SELECT + id, + name, + FIRST_VALUE(id) OVER ( + PARTITION BY name + ORDER BY (namespace_id IS NULL) DESC, created_at ASC, id ASC + ) AS canonical_id + FROM actions +) +SELECT id AS old_action_id, canonical_id AS new_action_id +FROM canonical +WHERE id <> canonical_id; + +-- subject_mapping_actions references actions(id) and has PK(subject_mapping_id, action_id). +-- Rebuild table contents in deduplicated form after remapping action ids. +CREATE TEMP TABLE subject_mapping_actions_dedup AS +SELECT + sma.subject_mapping_id, + COALESCE(r.new_action_id, sma.action_id) AS action_id, + MIN(sma.created_at) AS created_at +FROM subject_mapping_actions sma +LEFT JOIN action_id_remap r ON r.old_action_id = sma.action_id +GROUP BY sma.subject_mapping_id, COALESCE(r.new_action_id, sma.action_id); + +DELETE FROM subject_mapping_actions; + +INSERT INTO subject_mapping_actions (subject_mapping_id, action_id, created_at) +SELECT subject_mapping_id, action_id, created_at +FROM subject_mapping_actions_dedup; + +-- registered_resource_action_attribute_values references actions(id) and has +-- UNIQUE(registered_resource_value_id, action_id, attribute_value_id). +CREATE TEMP TABLE rr_aav_dedup AS +SELECT DISTINCT ON ( + rr.registered_resource_value_id, + COALESCE(r.new_action_id, rr.action_id), + rr.attribute_value_id +) + rr.id, + rr.registered_resource_value_id, + COALESCE(r.new_action_id, rr.action_id) AS action_id, + rr.attribute_value_id, + rr.created_at, + rr.updated_at +FROM registered_resource_action_attribute_values rr +LEFT JOIN action_id_remap r ON r.old_action_id = rr.action_id +ORDER BY rr.registered_resource_value_id, COALESCE(r.new_action_id, rr.action_id), rr.attribute_value_id, (r.new_action_id IS NOT NULL) ASC, rr.id; + +DELETE FROM registered_resource_action_attribute_values; + +INSERT INTO registered_resource_action_attribute_values ( + id, + registered_resource_value_id, + action_id, + attribute_value_id, + created_at, + updated_at +) +SELECT + id, + registered_resource_value_id, + action_id, + attribute_value_id, + created_at, + updated_at +FROM rr_aav_dedup; + +-- obligation_triggers references actions(id) and has client-aware uniqueness. +CREATE TEMP TABLE obligation_triggers_dedup AS +SELECT DISTINCT ON ( + ot.obligation_value_id, + COALESCE(r.new_action_id, ot.action_id), + ot.attribute_value_id, + ot.client_id +) + ot.id, + ot.obligation_value_id, + COALESCE(r.new_action_id, ot.action_id) AS action_id, + ot.attribute_value_id, + ot.client_id, + ot.metadata, + ot.created_at, + ot.updated_at +FROM obligation_triggers ot +LEFT JOIN action_id_remap r ON r.old_action_id = ot.action_id +ORDER BY ot.obligation_value_id, COALESCE(r.new_action_id, ot.action_id), ot.attribute_value_id, ot.client_id, (r.new_action_id IS NOT NULL) ASC, ot.id; + +DELETE FROM obligation_triggers; + +INSERT INTO obligation_triggers ( + id, + obligation_value_id, + action_id, + attribute_value_id, + client_id, + metadata, + created_at, + updated_at +) +SELECT + id, + obligation_value_id, + action_id, + attribute_value_id, + client_id, + metadata, + created_at, + updated_at +FROM obligation_triggers_dedup; + +-- Remove duplicate actions after references are remapped. +DELETE FROM actions a +USING action_id_remap r +WHERE a.id = r.old_action_id; + +DROP INDEX IF EXISTS idx_actions_namespace_id; +DROP INDEX IF EXISTS actions_name_unique; +DROP INDEX IF EXISTS actions_namespace_name_unique; + +ALTER TABLE actions ADD CONSTRAINT actions_name_unique UNIQUE (name); + +ALTER TABLE actions DROP COLUMN IF EXISTS namespace_id; + +-- +goose StatementEnd diff --git a/service/policy/db/migrations/20260318000000_add_namespace_to_subject_mappings.md b/service/policy/db/migrations/20260318000000_add_namespace_to_subject_mappings.md new file mode 100644 index 0000000000..228978638d --- /dev/null +++ b/service/policy/db/migrations/20260318000000_add_namespace_to_subject_mappings.md @@ -0,0 +1,30 @@ +# Add Namespace to Subject Mappings and Subject Condition Sets + +This migration adds optional namespace scoping to `subject_mappings` and `subject_condition_set`. + +## Why + +Subject mappings and subject condition sets were previously global — there was no way to scope them +to a specific namespace. This change allows them to be associated with a namespace, enabling +namespace-scoped list filtering. + +The columns are nullable to preserve backwards compatibility with existing records that have no +namespace association. + +## Changes + +1. `subject_condition_set` — add nullable `namespace_id` column: + - Foreign key to `attribute_namespaces(id)` with `ON DELETE CASCADE` + - Index `idx_subject_condition_set_namespace_id` for efficient namespace-filtered queries + +2. `subject_mappings` — add nullable `namespace_id` column: + - Foreign key to `attribute_namespaces(id)` with `ON DELETE CASCADE` + - Index `idx_subject_mappings_namespace_id` for efficient namespace-filtered queries + +## Resulting Behavior + +- Existing records with `namespace_id = NULL` are unscoped and returned in all list queries where no namespace filter is given. +- New records can optionally be associated with a namespace at creation time (by ID or FQN). +- List queries accept an optional namespace filter; when provided, only records matching that + namespace are returned. +- Deleting a namespace cascades to remove all associated subject mappings and subject condition sets. diff --git a/service/policy/db/migrations/20260318000000_add_namespace_to_subject_mappings.sql b/service/policy/db/migrations/20260318000000_add_namespace_to_subject_mappings.sql new file mode 100644 index 0000000000..37944596c1 --- /dev/null +++ b/service/policy/db/migrations/20260318000000_add_namespace_to_subject_mappings.sql @@ -0,0 +1,33 @@ +-- +goose Up +-- +goose StatementBegin + +-- Add nullable namespace_id column to subject_condition_set for namespace-scoped SCSes. +-- Keep nullable for backwards compatibility with existing SCSes not yet migrated to a namespace. +ALTER TABLE subject_condition_set + ADD COLUMN namespace_id UUID REFERENCES attribute_namespaces(id) ON DELETE CASCADE; + +-- Index for namespace-scoped SCS queries. +CREATE INDEX idx_subject_condition_set_namespace_id + ON subject_condition_set(namespace_id); + +-- Add nullable namespace_id column to subject_mappings for namespace-scoped SMs. +-- Keep nullable for backwards compatibility with existing SMs not yet migrated to a namespace. +ALTER TABLE subject_mappings + ADD COLUMN namespace_id UUID REFERENCES attribute_namespaces(id) ON DELETE CASCADE; + +-- Index for namespace-scoped SM queries. +CREATE INDEX idx_subject_mappings_namespace_id + ON subject_mappings(namespace_id); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DROP INDEX IF EXISTS idx_subject_mappings_namespace_id; +ALTER TABLE subject_mappings DROP COLUMN IF EXISTS namespace_id; + +DROP INDEX IF EXISTS idx_subject_condition_set_namespace_id; +ALTER TABLE subject_condition_set DROP COLUMN IF EXISTS namespace_id; + +-- +goose StatementEnd diff --git a/service/policy/db/migrations/20260331000000_seed_standard_actions_for_existing_namespaces.md b/service/policy/db/migrations/20260331000000_seed_standard_actions_for_existing_namespaces.md new file mode 100644 index 0000000000..18f8db319a --- /dev/null +++ b/service/policy/db/migrations/20260331000000_seed_standard_actions_for_existing_namespaces.md @@ -0,0 +1,29 @@ +# Seed Standard Actions for Existing Namespaces + +This migration retroactively seeds the four standard actions (create, read, update, delete) into all namespaces that already exist. + +## Why + +When a namespace is created, `CreateNamespace()` automatically calls `seedStandardActionsForNamespace()` to insert the standard CRUD actions scoped to that namespace. However, namespaces created before action namespacing was introduced (`20260312000000_add_namespace_to_actions.sql`) do not have namespace-scoped standard actions — they only have the legacy global standard actions. + +The otdfctl policy migration tooling requires that every namespace already has standard actions before migrating existing unnamespaced policy (SMs, SCSs, RRs) into namespaces. Without this seed, migrated policy referencing standard actions would have no same-namespace action to rewrite references to. + +## Changes + +A single idempotent `INSERT ... ON CONFLICT DO NOTHING` that cross-joins all existing namespaces with the four standard action names and inserts any missing rows into the `actions` table. + +## Resulting Behavior + +- Every existing namespace will have namespace-scoped standard actions (create, read, update, delete) with `is_standard = TRUE`. +- New namespaces continue to receive standard actions via `CreateNamespace()` as before. +- Running this migration on a system where some or all namespaces already have standard actions is safe — conflicts are silently ignored. + +## Rollback + +The Down migration is intentionally a no-op. Namespace-scoped standard actions are required for namespace correctness and policy reference rewrites; deleting them on rollback could break existing namespaces. + +## Operational Rollback Note + +Rolling back past `20260312000000_add_namespace_to_actions.sql` now performs an automatic action-id canonicalization and reference remap by action name before restoring global `UNIQUE(name)` semantics. This allows namespace-scoped duplicates (including standard actions) to be merged safely for rollback. + +This rollback remains intentionally lossy with respect to namespace-level action identity: actions sharing the same name are collapsed to one global action id, and policy references are rewritten to that canonical action. diff --git a/service/policy/db/migrations/20260331000000_seed_standard_actions_for_existing_namespaces.sql b/service/policy/db/migrations/20260331000000_seed_standard_actions_for_existing_namespaces.sql new file mode 100644 index 0000000000..c7005d9d4e --- /dev/null +++ b/service/policy/db/migrations/20260331000000_seed_standard_actions_for_existing_namespaces.sql @@ -0,0 +1,23 @@ +-- +goose Up +-- +goose StatementBegin + +-- Seed standard actions (create, read, update, delete) into all existing namespaces. +-- New namespaces already receive standard actions automatically via CreateNamespace(). +-- This migration retroactively seeds them for namespaces created before action namespacing was introduced. +-- ON CONFLICT DO NOTHING makes this idempotent. +INSERT INTO actions (name, is_standard, namespace_id) +SELECT a.name, TRUE, ns.id +FROM attribute_namespaces ns +CROSS JOIN (VALUES ('create'), ('read'), ('update'), ('delete')) AS a(name) +ON CONFLICT (namespace_id, name) WHERE namespace_id IS NOT NULL DO NOTHING; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +-- No-op: namespace-scoped standard actions are required for namespace policy +-- evaluation and reference rewrites. Removing them can break existing namespaces. +SELECT 1; + +-- +goose StatementEnd diff --git a/service/policy/db/models.go b/service/policy/db/models.go index e0409af9e3..fb0616f43f 100644 --- a/service/policy/db/models.go +++ b/service/policy/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 package db @@ -64,9 +64,10 @@ type Action struct { // Whether the action is standard (proto-enum) or custom (user-defined). IsStandard bool `json:"is_standard"` // Metadata for the action (see protos for structure) - Metadata []byte `json:"metadata"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Metadata []byte `json:"metadata"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + NamespaceID pgtype.UUID `json:"namespace_id"` } // View to retrieve active public keys mapped to attribute definitions @@ -132,6 +133,8 @@ type AttributeDefinition struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` // Order of value ids for the attribute (important for hierarchy rule) ValuesOrder []string `json:"values_order"` + // Whether or not to allow platform to return the definition key when encrypting, if the value specified is missing. + AllowTraversal bool `json:"allow_traversal"` } // Table to store the grants of key access servers (KASs) to attribute definitions @@ -178,14 +181,6 @@ type AttributeNamespace struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` } -// Junction table to map root certificates to attribute namespaces -type AttributeNamespaceCertificate struct { - // Foreign key to the namespace - NamespaceID string `json:"namespace_id"` - // Foreign key to the certificate - CertificateID string `json:"certificate_id"` -} - // Table to store the grants of key access servers (KASs) to attribute namespaces type AttributeNamespaceKeyAccessGrant struct { // Foreign key to the namespace of the KAS grant @@ -239,20 +234,6 @@ type BaseKey struct { KeyAccessServerKeyID pgtype.UUID `json:"key_access_server_key_id"` } -// Table to store X.509 certificates for chain of trust (root only) -type Certificate struct { - // Unique identifier for the certificate - ID string `json:"id"` - // PEM format - Base64-encoded DER certificate (not PEM; no headers/footers) - Pem string `json:"pem"` - // Optional metadata for the certificate - Metadata []byte `json:"metadata"` - // Timestamp when the certificate was created - CreatedAt pgtype.Timestamptz `json:"created_at"` - // Timestamp when the certificate was last updated - UpdatedAt pgtype.Timestamptz `json:"updated_at"` -} - // Table to store the known registrations of key access servers (KASs) type KeyAccessServer struct { // Primary key for the table @@ -366,7 +347,8 @@ type RegisteredResource struct { // Timestamp when the record was created CreatedAt pgtype.Timestamptz `json:"created_at"` // Timestamp when the record was last updated - UpdatedAt pgtype.Timestamptz `json:"updated_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + NamespaceID pgtype.UUID `json:"namespace_id"` } // Table to store the linkage of registered resource values to actions and attribute values @@ -441,7 +423,8 @@ type SubjectConditionSet struct { CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` // Array of cached selector values extracted from the condition JSONB and maintained via trigger. - SelectorValues []string `json:"selector_values"` + SelectorValues []string `json:"selector_values"` + NamespaceID pgtype.UUID `json:"namespace_id"` } // Table to store conditions that logically entitle subject entity representations to attribute values @@ -456,6 +439,7 @@ type SubjectMapping struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` // Foreign key to the condition set that entitles the subject entity to the attribute value SubjectConditionSetID pgtype.UUID `json:"subject_condition_set_id"` + NamespaceID pgtype.UUID `json:"namespace_id"` } type SubjectMappingAction struct { diff --git a/service/policy/db/namespaces.go b/service/policy/db/namespaces.go index 0d03ba1903..fd0a399e34 100644 --- a/service/policy/db/namespaces.go +++ b/service/policy/db/namespaces.go @@ -2,13 +2,10 @@ package db import ( "context" - "crypto/x509" - "encoding/pem" "errors" "fmt" "log/slog" "strings" - "time" "github.com/jackc/pgx/v5/pgtype" "github.com/opentdf/platform/protocol/go/common" @@ -73,24 +70,14 @@ func (c PolicyDBClient) GetNamespace(ctx context.Context, identifier any) (*poli } } - var certs []*policy.Certificate - if len(ns.Certs) > 0 { - certs, err = db.CertificatesProtoJSON(ns.Certs) - if err != nil { - c.logger.ErrorContext(ctx, "could not unmarshal certificates", slog.Any("error", err)) - return nil, err - } - } - return &policy.Namespace{ - Id: ns.ID, - Name: ns.Name, - Active: &wrapperspb.BoolValue{Value: ns.Active}, - Grants: grants, - Metadata: metadata, - Fqn: ns.Fqn.String, - KasKeys: keys, - RootCerts: certs, + Id: ns.ID, + Name: ns.Name, + Active: &wrapperspb.BoolValue{Value: ns.Active}, + Grants: grants, + Metadata: metadata, + Fqn: ns.Fqn.String, + KasKeys: keys, }, nil } @@ -110,10 +97,14 @@ func (c PolicyDBClient) ListNamespaces(ctx context.Context, r *namespaces.ListNa active = pgtypeBool(state == stateActive) } + sortField, sortDirection := GetNamespacesSortParams(r.GetSort()) + list, err := c.queries.listNamespaces(ctx, listNamespacesParams{ - Active: active, - Limit: limit, - Offset: offset, + Active: active, + Limit: limit, + Offset: offset, + SortField: sortField, + SortDirection: sortDirection, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -202,6 +193,11 @@ func (c PolicyDBClient) CreateNamespace(ctx context.Context, r *namespaces.Creat return nil, db.WrapIfKnownInvalidQueryErr(err) } + _, err = c.queries.seedStandardActionsForNamespace(ctx, pgtypeUUID(createdID)) + if err != nil { + return nil, db.WrapIfKnownInvalidQueryErr(err) + } + return c.GetNamespace(ctx, createdID) } @@ -350,7 +346,7 @@ func (c PolicyDBClient) UnsafeDeleteNamespace(ctx context.Context, existing *pol }, nil } -func (c PolicyDBClient) RemoveKeyAccessServerFromNamespace(ctx context.Context, k *namespaces.NamespaceKeyAccessServer) (*namespaces.NamespaceKeyAccessServer, error) { +func (c PolicyDBClient) RemoveKeyAccessServerFromNamespace(ctx context.Context, k *namespaces.NamespaceKeyAccessServer) (*namespaces.NamespaceKeyAccessServer, error) { //nolint:staticcheck // Compatibility path for deprecated protobuf type. count, err := c.queries.removeKeyAccessServerFromNamespace(ctx, removeKeyAccessServerFromNamespaceParams{ NamespaceID: k.GetNamespaceId(), KeyAccessServerID: k.GetKeyAccessServerId(), @@ -399,227 +395,3 @@ func (c PolicyDBClient) RemovePublicKeyFromNamespace(ctx context.Context, k *nam KeyId: k.GetKeyId(), }, nil } - -// validateRootCertificate validates that the PEM string is a valid PEM-encoded root certificate -func validateRootCertificate(pemStr string) error { - // Check that the PEM string contains "BEGIN CERTIFICATE" - if !strings.Contains(pemStr, "BEGIN CERTIFICATE") { - return errors.Join(db.ErrInvalidCertificate, errors.New("invalid PEM format: must contain BEGIN CERTIFICATE marker")) - } - - // Check that the PEM string contains newlines (proper PEM formatting) - if !strings.Contains(pemStr, "\n") { - return errors.Join(db.ErrInvalidCertificate, errors.New("invalid PEM format: must contain newlines")) - } - - // Decode PEM block - block, _ := pem.Decode([]byte(pemStr)) - if block == nil { - return errors.Join(db.ErrInvalidCertificate, errors.New("invalid PEM format: failed to decode PEM block")) - } - - // Verify it's a CERTIFICATE type - if block.Type != "CERTIFICATE" { - return errors.Join(db.ErrInvalidCertificate, fmt.Errorf("invalid PEM type: expected CERTIFICATE, got %s", block.Type)) - } - - // Parse the certificate - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return errors.Join(db.ErrInvalidCertificate, fmt.Errorf("invalid certificate: not a valid X.509 certificate: %w", err)) - } - - // Verify it's a root certificate (self-signed) - if !cert.IsCA { - return errors.Join(db.ErrInvalidCertificate, errors.New("invalid certificate: must be a CA certificate (IsCA=true)")) - } - - // Check if it's self-signed by comparing issuer and subject - if cert.Issuer.String() != cert.Subject.String() { - return errors.Join(db.ErrInvalidCertificate, errors.New("invalid certificate: must be a root certificate (self-signed)")) - } - - // Verify the self-signed certificate signature - if err := cert.CheckSignatureFrom(cert); err != nil { - return errors.Join(db.ErrInvalidCertificate, fmt.Errorf("invalid certificate: signature verification failed: %w", err)) - } - - // Validate temporal properties (NotBefore and NotAfter) - now := time.Now() - if now.Before(cert.NotBefore) { - return errors.Join(db.ErrInvalidCertificate, fmt.Errorf("invalid certificate: not yet valid (NotBefore: %v, current time: %v)", cert.NotBefore, now)) - } - if now.After(cert.NotAfter) { - return errors.Join(db.ErrInvalidCertificate, fmt.Errorf("invalid certificate: expired (NotAfter: %v, current time: %v)", cert.NotAfter, now)) - } - - return nil -} - -// CreateCertificate imports the root certificate into the `certificates` table and returns policy.Certificate -func (c PolicyDBClient) CreateCertificate(ctx context.Context, pem string, metadata []byte) (*policy.Certificate, error) { - // Validate the certificate before storing - if err := validateRootCertificate(pem); err != nil { - return nil, err - } - - certID, err := c.queries.createCertificate(ctx, createCertificateParams{ - Pem: pem, - Metadata: metadata, - }) - if err != nil { - return nil, db.WrapIfKnownInvalidQueryErr(err) - } - - // Return the full certificate object - return c.GetCertificate(ctx, certID) -} - -// GetCertificate retrieves a certificate by its ID -func (c PolicyDBClient) GetCertificate(ctx context.Context, id string) (*policy.Certificate, error) { - cert, err := c.queries.getCertificate(ctx, id) - if err != nil { - return nil, db.WrapIfKnownInvalidQueryErr(err) - } - - metadata := &common.Metadata{} - if err = unmarshalMetadata(cert.Metadata, metadata); err != nil { - return nil, err - } - - return &policy.Certificate{ - Id: cert.ID, - Pem: cert.Pem, - Metadata: metadata, - }, nil -} - -// DeleteCertificate removes a certificate from the database -func (c PolicyDBClient) DeleteCertificate(ctx context.Context, id string) error { - count, err := c.queries.deleteCertificate(ctx, id) - if err != nil { - return db.WrapIfKnownInvalidQueryErr(err) - } - if count == 0 { - return db.ErrNotFound - } - return nil -} - -// resolveNamespaceID resolves a namespace identifier to its UUID -func (c PolicyDBClient) resolveNamespaceID(ctx context.Context, identifier *common.IdFqnIdentifier) (string, error) { - // If ID is provided, check if it's a valid UUID - if identifier.GetId() != "" { - id := identifier.GetId() - // Check if the ID is a valid UUID - uuid := pgtypeUUID(id) - if uuid.Valid { - // It's a valid UUID, use it directly - return id, nil - } - // Not a valid UUID, treat it as a namespace name and look it up - ns, err := c.GetNamespace(ctx, &namespaces.GetNamespaceRequest_Fqn{Fqn: id}) - if err != nil { - return "", err - } - return ns.GetId(), nil - } - - // If FQN is provided, look up the namespace by FQN to get its ID - if identifier.GetFqn() != "" { - ns, err := c.GetNamespace(ctx, &namespaces.GetNamespaceRequest_Fqn{Fqn: identifier.GetFqn()}) - if err != nil { - return "", err - } - return ns.GetId(), nil - } - return "", errors.Join(db.ErrUnknownSelectIdentifier, fmt.Errorf("type [%T] value [%v]", identifier, identifier)) -} - -// AssignCertificateToNamespace assigns a trusted root certificate to a namespace for trust validation -func (c PolicyDBClient) AssignCertificateToNamespace(ctx context.Context, namespaceIdentifier *common.IdFqnIdentifier, certificateID string) error { - namespaceID, err := c.resolveNamespaceID(ctx, namespaceIdentifier) - if err != nil { - return err - } - - _, err = c.queries.assignCertificateToNamespace(ctx, assignCertificateToNamespaceParams{ - NamespaceID: namespaceID, - CertificateID: certificateID, - }) - if err != nil { - return db.WrapIfKnownInvalidQueryErr(err) - } - return nil -} - -// CreateAndAssignCertificateToNamespace creates a certificate and assigns it to a namespace in a transaction -func (c PolicyDBClient) CreateAndAssignCertificateToNamespace(ctx context.Context, namespaceID *common.IdFqnIdentifier, pem string, metadata []byte) (string, error) { - var certID string - err := c.RunInTx(ctx, func(txClient *PolicyDBClient) error { - // Check if certificate with same PEM already exists (inside transaction to avoid race condition) - existingCert, err := txClient.queries.getCertificateByPEM(ctx, pem) - if err == nil { - // Certificate exists, just assign it to namespace - certID = existingCert.ID - err = txClient.AssignCertificateToNamespace(ctx, namespaceID, existingCert.ID) - if err != nil { - return err - } - return nil - } - - // Certificate doesn't exist, create it - cert, err := txClient.CreateCertificate(ctx, pem, metadata) - if err != nil { - return err - } - certID = cert.GetId() - - err = txClient.AssignCertificateToNamespace(ctx, namespaceID, certID) - if err != nil { - return err - } - - return nil - }) - if err != nil { - return "", err - } - return certID, nil -} - -// RemoveCertificateFromNamespace removes a certificate from a namespace and deletes the certificate if it's not used elsewhere -func (c PolicyDBClient) RemoveCertificateFromNamespace(ctx context.Context, namespaceIdentifier *common.IdFqnIdentifier, certificateID string) error { - namespaceID, err := c.resolveNamespaceID(ctx, namespaceIdentifier) - if err != nil { - return err - } - - count, err := c.queries.removeCertificateFromNamespace(ctx, removeCertificateFromNamespaceParams{ - NamespaceID: namespaceID, - CertificateID: certificateID, - }) - if err != nil { - return db.WrapIfKnownInvalidQueryErr(err) - } - if count == 0 { - return db.ErrNotFound - } - - // Check if the certificate is still assigned to any other namespaces - assignmentCount, err := c.queries.countCertificateNamespaceAssignments(ctx, certificateID) - if err != nil { - return db.WrapIfKnownInvalidQueryErr(err) - } - - // Only delete the certificate if it's not assigned to any other namespace - if assignmentCount == 0 { - err = c.DeleteCertificate(ctx, certificateID) - if err != nil { - return err - } - } - - return nil -} diff --git a/service/policy/db/namespaces.sql.go b/service/policy/db/namespaces.sql.go index b988a20140..7bb3937313 100644 --- a/service/policy/db/namespaces.sql.go +++ b/service/policy/db/namespaces.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: namespaces.sql package db @@ -11,29 +11,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const assignCertificateToNamespace = `-- name: assignCertificateToNamespace :one -INSERT INTO attribute_namespace_certificates (namespace_id, certificate_id) -VALUES ($1, $2) -RETURNING namespace_id, certificate_id -` - -type assignCertificateToNamespaceParams struct { - NamespaceID string `json:"namespace_id"` - CertificateID string `json:"certificate_id"` -} - -// assignCertificateToNamespace -// -// INSERT INTO attribute_namespace_certificates (namespace_id, certificate_id) -// VALUES ($1, $2) -// RETURNING namespace_id, certificate_id -func (q *Queries) assignCertificateToNamespace(ctx context.Context, arg assignCertificateToNamespaceParams) (AttributeNamespaceCertificate, error) { - row := q.db.QueryRow(ctx, assignCertificateToNamespace, arg.NamespaceID, arg.CertificateID) - var i AttributeNamespaceCertificate - err := row.Scan(&i.NamespaceID, &i.CertificateID) - return i, err -} - const assignPublicKeyToNamespace = `-- name: assignPublicKeyToNamespace :one INSERT INTO attribute_namespace_public_key_map (namespace_id, key_access_server_key_id) VALUES ($1, $2) @@ -57,48 +34,6 @@ func (q *Queries) assignPublicKeyToNamespace(ctx context.Context, arg assignPubl return i, err } -const countCertificateNamespaceAssignments = `-- name: countCertificateNamespaceAssignments :one -SELECT COUNT(*) FROM attribute_namespace_certificates -WHERE certificate_id = $1 -` - -// countCertificateNamespaceAssignments -// -// SELECT COUNT(*) FROM attribute_namespace_certificates -// WHERE certificate_id = $1 -func (q *Queries) countCertificateNamespaceAssignments(ctx context.Context, certificateID string) (int64, error) { - row := q.db.QueryRow(ctx, countCertificateNamespaceAssignments, certificateID) - var count int64 - err := row.Scan(&count) - return count, err -} - -const createCertificate = `-- name: createCertificate :one - -INSERT INTO certificates (pem, metadata) -VALUES ($1, $2) -RETURNING id -` - -type createCertificateParams struct { - Pem string `json:"pem"` - Metadata []byte `json:"metadata"` -} - -// -------------------------------------------------------------- -// CERTIFICATES -// -------------------------------------------------------------- -// -// INSERT INTO certificates (pem, metadata) -// VALUES ($1, $2) -// RETURNING id -func (q *Queries) createCertificate(ctx context.Context, arg createCertificateParams) (string, error) { - row := q.db.QueryRow(ctx, createCertificate, arg.Pem, arg.Metadata) - var id string - err := row.Scan(&id) - return id, err -} - const createNamespace = `-- name: createNamespace :one INSERT INTO attribute_namespaces (name, metadata) VALUES ($1, $2) @@ -122,21 +57,6 @@ func (q *Queries) createNamespace(ctx context.Context, arg createNamespaceParams return id, err } -const deleteCertificate = `-- name: deleteCertificate :execrows -DELETE FROM certificates WHERE id = $1 -` - -// deleteCertificate -// -// DELETE FROM certificates WHERE id = $1 -func (q *Queries) deleteCertificate(ctx context.Context, id string) (int64, error) { - result, err := q.db.Exec(ctx, deleteCertificate, id) - if err != nil { - return 0, err - } - return result.RowsAffected(), nil -} - const deleteNamespace = `-- name: deleteNamespace :execrows DELETE FROM attribute_namespaces WHERE id = $1 ` @@ -152,66 +72,6 @@ func (q *Queries) deleteNamespace(ctx context.Context, id string) (int64, error) return result.RowsAffected(), nil } -const getCertificate = `-- name: getCertificate :one -SELECT - id, - pem, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -FROM certificates -WHERE id = $1 -` - -type getCertificateRow struct { - ID string `json:"id"` - Pem string `json:"pem"` - Metadata []byte `json:"metadata"` -} - -// getCertificate -// -// SELECT -// id, -// pem, -// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -// FROM certificates -// WHERE id = $1 -func (q *Queries) getCertificate(ctx context.Context, id string) (getCertificateRow, error) { - row := q.db.QueryRow(ctx, getCertificate, id) - var i getCertificateRow - err := row.Scan(&i.ID, &i.Pem, &i.Metadata) - return i, err -} - -const getCertificateByPEM = `-- name: getCertificateByPEM :one -SELECT - id, - pem, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -FROM certificates -WHERE pem = $1 -` - -type getCertificateByPEMRow struct { - ID string `json:"id"` - Pem string `json:"pem"` - Metadata []byte `json:"metadata"` -} - -// getCertificateByPEM -// -// SELECT -// id, -// pem, -// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -// FROM certificates -// WHERE pem = $1 -func (q *Queries) getCertificateByPEM(ctx context.Context, pem string) (getCertificateByPEMRow, error) { - row := q.db.QueryRow(ctx, getCertificateByPEM, pem) - var i getCertificateByPEMRow - err := row.Scan(&i.ID, &i.Pem, &i.Metadata) - return i, err -} - const getNamespace = `-- name: getNamespace :one SELECT ns.id, @@ -225,8 +85,7 @@ SELECT 'name', kas.name, 'public_key', kas.public_key )) FILTER (WHERE kas_ns_grants.namespace_id IS NOT NULL) as grants, - nmp_keys.keys as keys, - nmp_certs.certs as certs + nmp_keys.keys as keys FROM attribute_namespaces ns LEFT JOIN attribute_namespace_key_access_grants kas_ns_grants ON kas_ns_grants.namespace_id = ns.id LEFT JOIN key_access_servers kas ON kas.id = kas_ns_grants.key_access_server_id @@ -250,23 +109,10 @@ LEFT JOIN ( INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id GROUP BY k.namespace_id ) nmp_keys ON ns.id = nmp_keys.namespace_id -LEFT JOIN ( - SELECT - c.namespace_id, - JSONB_AGG( - DISTINCT JSONB_BUILD_OBJECT( - 'id', cert.id, - 'pem', cert.pem - ) - ) FILTER (WHERE cert.id IS NOT NULL) AS certs - FROM attribute_namespace_certificates c - INNER JOIN certificates cert ON c.certificate_id = cert.id - GROUP BY c.namespace_id -) nmp_certs ON ns.id = nmp_certs.namespace_id WHERE fqns.attribute_id IS NULL AND fqns.value_id IS NULL AND ($1::uuid IS NULL OR ns.id = $1::uuid) - AND ($2::text IS NULL OR ns.name = REGEXP_REPLACE($2::text, '^https?://', '')) -GROUP BY ns.id, fqns.fqn, nmp_keys.keys, nmp_certs.certs + AND ($2::text IS NULL OR ns.name = REGEXP_REPLACE($2::text, '^https://', '')) +GROUP BY ns.id, fqns.fqn, nmp_keys.keys ` type getNamespaceParams struct { @@ -282,7 +128,6 @@ type getNamespaceRow struct { Metadata []byte `json:"metadata"` Grants []byte `json:"grants"` Keys []byte `json:"keys"` - Certs []byte `json:"certs"` } // getNamespace @@ -299,8 +144,7 @@ type getNamespaceRow struct { // 'name', kas.name, // 'public_key', kas.public_key // )) FILTER (WHERE kas_ns_grants.namespace_id IS NOT NULL) as grants, -// nmp_keys.keys as keys, -// nmp_certs.certs as certs +// nmp_keys.keys as keys // FROM attribute_namespaces ns // LEFT JOIN attribute_namespace_key_access_grants kas_ns_grants ON kas_ns_grants.namespace_id = ns.id // LEFT JOIN key_access_servers kas ON kas.id = kas_ns_grants.key_access_server_id @@ -324,23 +168,10 @@ type getNamespaceRow struct { // INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id // GROUP BY k.namespace_id // ) nmp_keys ON ns.id = nmp_keys.namespace_id -// LEFT JOIN ( -// SELECT -// c.namespace_id, -// JSONB_AGG( -// DISTINCT JSONB_BUILD_OBJECT( -// 'id', cert.id, -// 'pem', cert.pem -// ) -// ) FILTER (WHERE cert.id IS NOT NULL) AS certs -// FROM attribute_namespace_certificates c -// INNER JOIN certificates cert ON c.certificate_id = cert.id -// GROUP BY c.namespace_id -// ) nmp_certs ON ns.id = nmp_certs.namespace_id // WHERE fqns.attribute_id IS NULL AND fqns.value_id IS NULL // AND ($1::uuid IS NULL OR ns.id = $1::uuid) -// AND ($2::text IS NULL OR ns.name = REGEXP_REPLACE($2::text, '^https?://', '')) -// GROUP BY ns.id, fqns.fqn, nmp_keys.keys, nmp_certs.certs +// AND ($2::text IS NULL OR ns.name = REGEXP_REPLACE($2::text, '^https://', '')) +// GROUP BY ns.id, fqns.fqn, nmp_keys.keys func (q *Queries) getNamespace(ctx context.Context, arg getNamespaceParams) (getNamespaceRow, error) { row := q.db.QueryRow(ctx, getNamespace, arg.ID, arg.Name) var i getNamespaceRow @@ -352,13 +183,17 @@ func (q *Queries) getNamespace(ctx context.Context, arg getNamespaceParams) (get &i.Metadata, &i.Grants, &i.Keys, - &i.Certs, ) return i, err } const listNamespaces = `-- name: listNamespaces :many +WITH params AS ( + SELECT + COALESCE(NULLIF($4::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($5::text, ''), 'DESC') AS resolved_direction +) SELECT COUNT(*) OVER() AS total, ns.id, @@ -368,15 +203,28 @@ SELECT fqns.fqn FROM attribute_namespaces ns LEFT JOIN attribute_fqns fqns ON ns.id = fqns.namespace_id AND fqns.attribute_id IS NULL +CROSS JOIN params p WHERE ($1::BOOLEAN IS NULL OR ns.active = $1::BOOLEAN) +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ns.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ns.name END DESC, + CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'ASC' THEN fqns.fqn END ASC, + CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'DESC' THEN fqns.fqn END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ns.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ns.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ns.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ns.updated_at END DESC, + ns.id ASC LIMIT $3 OFFSET $2 ` type listNamespacesParams struct { - Active pgtype.Bool `json:"active"` - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + Active pgtype.Bool `json:"active"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` } type listNamespacesRow struct { @@ -392,6 +240,11 @@ type listNamespacesRow struct { // NAMESPACES // -------------------------------------------------------------- // +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($4::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($5::text, ''), 'DESC') AS resolved_direction +// ) // SELECT // COUNT(*) OVER() AS total, // ns.id, @@ -401,11 +254,28 @@ type listNamespacesRow struct { // fqns.fqn // FROM attribute_namespaces ns // LEFT JOIN attribute_fqns fqns ON ns.id = fqns.namespace_id AND fqns.attribute_id IS NULL +// CROSS JOIN params p // WHERE ($1::BOOLEAN IS NULL OR ns.active = $1::BOOLEAN) +// ORDER BY +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ns.name END ASC, +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ns.name END DESC, +// CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'ASC' THEN fqns.fqn END ASC, +// CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'DESC' THEN fqns.fqn END DESC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ns.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ns.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ns.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ns.updated_at END DESC, +// ns.id ASC // LIMIT $3 // OFFSET $2 func (q *Queries) listNamespaces(ctx context.Context, arg listNamespacesParams) ([]listNamespacesRow, error) { - rows, err := q.db.Query(ctx, listNamespaces, arg.Active, arg.Offset, arg.Limit) + rows, err := q.db.Query(ctx, listNamespaces, + arg.Active, + arg.Offset, + arg.Limit, + arg.SortField, + arg.SortDirection, + ) if err != nil { return nil, err } @@ -431,28 +301,6 @@ func (q *Queries) listNamespaces(ctx context.Context, arg listNamespacesParams) return items, nil } -const removeCertificateFromNamespace = `-- name: removeCertificateFromNamespace :execrows -DELETE FROM attribute_namespace_certificates -WHERE namespace_id = $1 AND certificate_id = $2 -` - -type removeCertificateFromNamespaceParams struct { - NamespaceID string `json:"namespace_id"` - CertificateID string `json:"certificate_id"` -} - -// removeCertificateFromNamespace -// -// DELETE FROM attribute_namespace_certificates -// WHERE namespace_id = $1 AND certificate_id = $2 -func (q *Queries) removeCertificateFromNamespace(ctx context.Context, arg removeCertificateFromNamespaceParams) (int64, error) { - result, err := q.db.Exec(ctx, removeCertificateFromNamespace, arg.NamespaceID, arg.CertificateID) - if err != nil { - return 0, err - } - return result.RowsAffected(), nil -} - const removeKeyAccessServerFromNamespace = `-- name: removeKeyAccessServerFromNamespace :execrows DELETE FROM attribute_namespace_key_access_grants WHERE namespace_id = $1 AND key_access_server_id = $2 diff --git a/service/policy/db/namespaces_test.go b/service/policy/db/namespaces_test.go deleted file mode 100644 index 9d974e3f4c..0000000000 --- a/service/policy/db/namespaces_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package db - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// Helper function to generate a valid self-signed root certificate -func generateValidRootCert(t *testing.T, notBefore, notAfter time.Time) string { - t.Helper() - - // Generate a new ECDSA private key - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Create certificate template - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - Organization: []string{"Test Org"}, - CommonName: "Test Root CA", - }, - NotBefore: notBefore, - NotAfter: notAfter, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - IsCA: true, - } - - // Self-sign the certificate - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) - require.NoError(t, err) - - // Encode to PEM format - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: certDER, - }) - - return string(certPEM) -} - -// Helper function to generate a non-CA certificate -func generateNonCACert(t *testing.T) string { - t.Helper() - - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - Organization: []string{"Test Org"}, - CommonName: "Test Non-CA Cert", - }, - NotBefore: time.Now().Add(-1 * time.Hour), - NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - IsCA: false, // Not a CA certificate - } - - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) - require.NoError(t, err) - - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: certDER, - }) - - return string(certPEM) -} - -// Helper function to generate a non-self-signed certificate -func generateNonSelfSignedCert(t *testing.T) string { - t.Helper() - - // Generate CA key - caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Create CA certificate - caTemplate := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - Organization: []string{"CA Org"}, - CommonName: "CA Root", - }, - NotBefore: time.Now().Add(-1 * time.Hour), - NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageCertSign, - BasicConstraintsValid: true, - IsCA: true, - } - - // Generate leaf certificate key - leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Create leaf certificate with different subject (signed by CA) - leafTemplate := x509.Certificate{ - SerialNumber: big.NewInt(2), - Subject: pkix.Name{ - Organization: []string{"Leaf Org"}, - CommonName: "Leaf Cert", - }, - NotBefore: time.Now().Add(-1 * time.Hour), - NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageCertSign, - BasicConstraintsValid: true, - IsCA: true, - } - - // Sign leaf certificate with CA (not self-signed) - certDER, err := x509.CreateCertificate(rand.Reader, &leafTemplate, &caTemplate, &leafKey.PublicKey, caKey) - require.NoError(t, err) - - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: certDER, - }) - - return string(certPEM) -} - -// Helper function to generate a certificate with corrupted signature -func generateCertWithInvalidSignature(t *testing.T) string { - t.Helper() - - // Start with a valid certificate - validCert := generateValidRootCert(t, time.Now().Add(-1*time.Hour), time.Now().Add(24*time.Hour)) - - // Decode the PEM - block, _ := pem.Decode([]byte(validCert)) - require.NotNil(t, block) - - // Corrupt the signature by modifying the last byte - block.Bytes[len(block.Bytes)-1] ^= 0xFF - - // Re-encode to PEM - corruptedPEM := pem.EncodeToMemory(block) - return string(corruptedPEM) -} - -func Test_validateRootCertificate(t *testing.T) { - tests := []struct { - name string - pemStr string - wantErr bool - errContains string - }{ - { - name: "Valid root certificate", - pemStr: generateValidRootCert(t, time.Now().Add(-1*time.Hour), time.Now().Add(24*time.Hour)), - wantErr: false, - }, - { - name: "Missing BEGIN CERTIFICATE marker", - pemStr: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\n-----END PUBLIC KEY-----", - wantErr: true, - errContains: "must contain BEGIN CERTIFICATE marker", - }, - { - name: "Missing newlines", - pemStr: "-----BEGIN CERTIFICATE-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA-----END CERTIFICATE-----", - wantErr: true, - errContains: "must contain newlines", - }, - { - name: "Invalid PEM format - failed decode", - pemStr: "-----BEGIN CERTIFICATE-----\ninvalid base64 content!!!\n-----END CERTIFICATE-----\n", - wantErr: true, - errContains: "failed to decode PEM block", - }, - { - name: "Wrong PEM type - not CERTIFICATE", - pemStr: `-----BEGIN CERTIFICATE REQUEST----- -MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNV -BAcMBFRlc3QxDTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxDTALBgNVBAMM -BFRlc3QxGzAZBgkqhkiG9w0BCQEWDHRlc3RAdGVzdC5jb20wggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQCxkSWk ------END CERTIFICATE REQUEST----- -`, - wantErr: true, - errContains: "expected CERTIFICATE", - }, - { - name: "Invalid X.509 certificate", - pemStr: `-----BEGIN CERTIFICATE----- -aW52YWxpZCBjZXJ0aWZpY2F0ZSBkYXRh ------END CERTIFICATE----- -`, - wantErr: true, - errContains: "not a valid X.509 certificate", - }, - { - name: "Non-CA certificate (IsCA=false)", - pemStr: generateNonCACert(t), - wantErr: true, - errContains: "must be a CA certificate", - }, - { - name: "Non-self-signed certificate", - pemStr: generateNonSelfSignedCert(t), - wantErr: true, - errContains: "must be a root certificate (self-signed)", - }, - { - name: "Invalid signature", - pemStr: generateCertWithInvalidSignature(t), - wantErr: true, - errContains: "signature verification failed", - }, - { - name: "Certificate not yet valid (NotBefore in future)", - pemStr: generateValidRootCert(t, time.Now().Add(24*time.Hour), time.Now().Add(48*time.Hour)), - wantErr: true, - errContains: "not yet valid", - }, - { - name: "Expired certificate (NotAfter in past)", - pemStr: generateValidRootCert(t, time.Now().Add(-48*time.Hour), time.Now().Add(-24*time.Hour)), - wantErr: true, - errContains: "expired", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateRootCertificate(tt.pemStr) - - if tt.wantErr { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errContains) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/service/policy/db/obligations.go b/service/policy/db/obligations.go index 2030fdf12c..5e57dbf583 100644 --- a/service/policy/db/obligations.go +++ b/service/policy/db/obligations.go @@ -9,6 +9,7 @@ import ( "github.com/opentdf/platform/lib/identifier" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/opentdf/platform/protocol/go/policy/obligations" "github.com/opentdf/platform/service/pkg/db" "google.golang.org/protobuf/types/known/timestamppb" @@ -22,6 +23,32 @@ func setOblValFQNs(values []*policy.ObligationValue, nsFQN, name string) []*poli return values } +func hydrateObligationTrigger(triggerJSON, metadataJSON []byte) (*policy.ObligationTrigger, error) { + trigger, err := unmarshalObligationTrigger(triggerJSON) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal obligation trigger: %w", err) + } + + metadata := &common.Metadata{} + if err := unmarshalMetadata(metadataJSON, metadata); err != nil { + return nil, fmt.Errorf("failed to unmarshal obligation trigger metadata: %w", err) + } + + if returnedOblVal := trigger.GetObligationValue(); returnedOblVal != nil { + if obligation := returnedOblVal.GetObligation(); obligation != nil && obligation.GetNamespace() != nil { + returnedOblVal.Fqn = identifier.BuildOblValFQN( + obligation.GetNamespace().GetFqn(), + obligation.GetName(), + returnedOblVal.GetValue(), + ) + } + } + + trigger.Metadata = metadata + + return trigger, nil +} + /// /// Obligation Definitions /// @@ -199,6 +226,8 @@ func (c PolicyDBClient) ListObligations(ctx context.Context, r *obligations.List parsedID := pgtypeUUID(namespaceID) idIsValid := parsedID.Valid + sortField, sortDirection := GetObligationsSortParams(r.GetSort()) + if useID && !idIsValid { return nil, nil, db.ErrUUIDInvalid } @@ -211,10 +240,12 @@ func (c PolicyDBClient) ListObligations(ctx context.Context, r *obligations.List } rows, err := c.queries.listObligations(ctx, listObligationsParams{ - NamespaceID: parsedID, - NamespaceFqn: pgtypeText(r.GetNamespaceFqn()), - Limit: limit, - Offset: offset, + NamespaceID: parsedID, + NamespaceFqn: pgtypeText(r.GetNamespaceFqn()), + Limit: limit, + Offset: offset, + SortField: sortField, + SortDirection: sortDirection, }) if err != nil { return nil, nil, db.WrapIfKnownInvalidQueryErr(err) @@ -642,13 +673,22 @@ func (c PolicyDBClient) DeleteObligationValue(ctx context.Context, r *obligation // ! Obligation Triggers // ******************************************** +func (c PolicyDBClient) GetObligationTrigger(ctx context.Context, r *obligations.GetObligationTriggerRequest) (*policy.ObligationTrigger, error) { + id := r.GetId() + row, err := c.queries.getObligationTrigger(ctx, id) + if err != nil { + return nil, db.WrapIfKnownInvalidQueryErr(err) + } + + return hydrateObligationTrigger(row.Trigger, row.Metadata) +} + func (c PolicyDBClient) CreateObligationTrigger(ctx context.Context, r *obligations.AddObligationTriggerRequest) (*policy.ObligationTrigger, error) { metadataJSON, _, err := db.MarshalCreateMetadata(r.GetMetadata()) if err != nil { return nil, err } - // Get obligation var oblValReq *obligations.GetObligationValueRequest if r.GetObligationValue().GetId() != "" { oblValReq = &obligations.GetObligationValueRequest{ @@ -664,11 +704,24 @@ func (c PolicyDBClient) CreateObligationTrigger(ctx context.Context, r *obligati if err != nil { return nil, fmt.Errorf("failed to get obligation value: %w", err) } + triggerNamespaceID, err := c.getAttributeValueNamespaceID(ctx, r.GetAttributeValue()) + if err != nil { + return nil, err + } + + actionID, err := c.resolveObligationTriggerActionID(ctx, r.GetAction(), triggerNamespaceID) + if err != nil { + return nil, err + } + + err = c.validateObligationTriggerSourceNamespace(ctx, triggerNamespaceID, actionID) + if err != nil { + return nil, err + } params := createObligationTriggerParams{ ObligationValueID: pgtypeUUID(oblVal.GetId()), - ActionName: pgtypeText(r.GetAction().GetName()), - ActionID: pgtypeUUID(r.GetAction().GetId()), + ActionID: pgtypeUUID(actionID), AttributeValueID: pgtypeUUID(r.GetAttributeValue().GetId()), AttributeValueFqn: pgtypeText(r.GetAttributeValue().GetFqn()), ClientID: pgtypeText(r.GetContext().GetPep().GetClientId()), @@ -683,23 +736,84 @@ func (c PolicyDBClient) CreateObligationTrigger(ctx context.Context, r *obligati return nil, wrappedErr } - metadata := &common.Metadata{} - if err := unmarshalMetadata(row.Metadata, metadata); err != nil { - return nil, err + return hydrateObligationTrigger(row.Trigger, row.Metadata) +} + +func (c PolicyDBClient) resolveObligationTriggerActionID(ctx context.Context, action *common.IdNameIdentifier, actionNamespaceID string) (string, error) { + actionID := action.GetId() + if actionID != "" { + return actionID, nil + } + + actionName := strings.ToLower(action.GetName()) + if actionName == "" { + // this shouldnt happen due to proto validation, but just in case + return "", errors.Join( + db.ErrMissingValue, + errors.New("action identifier must include either id or name"), + ) } - trigger, err := unmarshalObligationTrigger(row.Trigger) + createdOrListedActions, err := c.queries.createOrListActionsByNameInNamespace(ctx, createOrListActionsByNameInNamespaceParams{ + ActionNames: []string{actionName}, + NamespaceID: actionNamespaceID, + }) if err != nil { - return nil, err + return "", db.WrapIfKnownInvalidQueryErr( + errors.Join(db.ErrMissingValue, fmt.Errorf("failed to create or list action names [%v]: %w", actionName, err)), + ) + } + if len(createdOrListedActions) == 0 { + return "", db.WrapIfKnownInvalidQueryErr( + errors.Join(db.ErrMissingValue, fmt.Errorf("failed to create or list action names [%v]", actionName)), + ) } - if returnedOblVal := trigger.GetObligationValue(); returnedOblVal != nil { - returnedOblVal.Fqn = oblVal.GetFqn() + return createdOrListedActions[0].ID, nil +} + +func (c PolicyDBClient) getAttributeValueNamespaceID(ctx context.Context, attributeValue *common.IdFqnIdentifier) (string, error) { + var attributeValueIdentifier any + if attributeValue.GetId() != "" { + attributeValueIdentifier = &attributes.GetAttributeValueRequest_ValueId{ValueId: attributeValue.GetId()} + } else { + attributeValueIdentifier = &attributes.GetAttributeValueRequest_Fqn{Fqn: attributeValue.GetFqn()} } - trigger.Metadata = metadata + av, err := c.GetAttributeValue(ctx, attributeValueIdentifier) + if err != nil { + return "", db.WrapIfKnownInvalidQueryErr(err) + } + attr, err := c.GetAttribute(ctx, av.GetAttribute().GetId()) + if err != nil { + return "", db.WrapIfKnownInvalidQueryErr(err) + } - return trigger, nil + return attr.GetNamespace().GetId(), nil +} + +// validateObligationTriggerSourceNamespace ensures that the action belongs to the +// same namespace as the attribute value that anchors the trigger. +func (c PolicyDBClient) validateObligationTriggerSourceNamespace( + ctx context.Context, + triggerNamespaceID string, + actionID string, +) error { + actionRows, err := c.queries.getActionsByIDs(ctx, []string{actionID}) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + if len(actionRows) == 0 { + return errors.Join(db.ErrNotFound, fmt.Errorf("action [%s] was not found", actionID)) + } + a := actionRows[0] + actionNsID := UUIDToString(a.NamespaceID) + if actionNsID != triggerNamespaceID { + return errors.Join(db.ErrNamespaceMismatch, + fmt.Errorf("action [%s] namespace [%s] does not match the attribute value namespace [%s]", a.ID, actionNsID, triggerNamespaceID)) + } + + return nil } func (c PolicyDBClient) DeleteObligationTrigger(ctx context.Context, r *obligations.RemoveObligationTriggerRequest) (*policy.ObligationTrigger, error) { @@ -736,20 +850,11 @@ func (c PolicyDBClient) ListObligationTriggers(ctx context.Context, r *obligatio var result []*policy.ObligationTrigger for _, row := range rows { - metadata := &common.Metadata{} - if err := unmarshalMetadata(row.Metadata, metadata); err != nil { - return nil, nil, err - } - - obligationTrigger, err := unmarshalObligationTrigger(row.Trigger) + obligationTrigger, err := hydrateObligationTrigger(row.Trigger, row.Metadata) if err != nil { return nil, nil, err } - if returnedOblVal := obligationTrigger.GetObligationValue(); returnedOblVal != nil { - returnedOblVal.Fqn = identifier.BuildOblValFQN(returnedOblVal.GetObligation().GetNamespace().GetFqn(), returnedOblVal.GetObligation().GetName(), returnedOblVal.GetValue()) - } - obligationTrigger.Metadata = metadata result = append(result, obligationTrigger) } diff --git a/service/policy/db/obligations.sql.go b/service/policy/db/obligations.sql.go index c2e99de122..893dcea627 100644 --- a/service/policy/db/obligations.sql.go +++ b/service/policy/db/obligations.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: obligations.sql package db @@ -147,31 +147,24 @@ func (q *Queries) createObligation(ctx context.Context, arg createObligationPara } const createObligationTrigger = `-- name: createObligationTrigger :one - WITH ov_id AS ( - SELECT ov.id, od.namespace_id + SELECT ov.id FROM obligation_values_standard ov - JOIN obligation_definitions od ON ov.obligation_definition_id = od.id WHERE $1::uuid IS NOT NULL AND ov.id = $1::uuid ), a_id AS ( SELECT a.id FROM actions a - WHERE - ($2::uuid IS NOT NULL AND a.id = $2::uuid) - OR - ($3::text IS NOT NULL AND a.name = $3::text) + WHERE ($2::uuid IS NOT NULL AND a.id = $2::uuid) ), av_id AS ( SELECT av.id FROM attribute_values av - JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id LEFT JOIN attribute_fqns fqns ON fqns.value_id = av.id WHERE - (($4::uuid IS NOT NULL AND av.id = $4::uuid) + (($3::uuid IS NOT NULL AND av.id = $3::uuid) OR - ($5::text IS NOT NULL AND fqns.fqn = $5::text)) - AND ad.namespace_id = (SELECT namespace_id FROM ov_id) + ($4::text IS NOT NULL AND fqns.fqn = $4::text)) ), inserted AS ( INSERT INTO obligation_triggers (obligation_value_id, action_id, attribute_value_id, metadata, client_id) @@ -179,8 +172,8 @@ inserted AS ( (SELECT id FROM ov_id), (SELECT id FROM a_id), (SELECT id FROM av_id), - $6, - $7::text + $5, + $6::text RETURNING id, obligation_value_id, action_id, attribute_value_id, metadata, created_at, updated_at, client_id ) SELECT @@ -216,6 +209,11 @@ SELECT 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN i.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -234,13 +232,14 @@ JOIN attribute_namespaces n ON od.namespace_id = n.id LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL JOIN actions a ON i.action_id = a.id JOIN attribute_values av ON i.attribute_value_id = av.id +JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id ` type createObligationTriggerParams struct { ObligationValueID pgtype.UUID `json:"obligation_value_id"` ActionID pgtype.UUID `json:"action_id"` - ActionName pgtype.Text `json:"action_name"` AttributeValueID pgtype.UUID `json:"attribute_value_id"` AttributeValueFqn pgtype.Text `json:"attribute_value_fqn"` Metadata []byte `json:"metadata"` @@ -252,35 +251,27 @@ type createObligationTriggerRow struct { Trigger []byte `json:"trigger"` } -// -------------------------------------------------------------- -// OBLIGATION TRIGGERS -// -------------------------------------------------------------- -// Gets the attribute value, but also ensures that the attribute value belongs to the same namespace as the obligation, to which the obligation value belongs +// Attribute value lookup is intentionally namespace-agnostic here; the caller +// validates that the action and attribute value share the trigger's source namespace. // // WITH ov_id AS ( -// SELECT ov.id, od.namespace_id +// SELECT ov.id // FROM obligation_values_standard ov -// JOIN obligation_definitions od ON ov.obligation_definition_id = od.id // WHERE $1::uuid IS NOT NULL AND ov.id = $1::uuid // ), // a_id AS ( // SELECT a.id // FROM actions a -// WHERE -// ($2::uuid IS NOT NULL AND a.id = $2::uuid) -// OR -// ($3::text IS NOT NULL AND a.name = $3::text) +// WHERE ($2::uuid IS NOT NULL AND a.id = $2::uuid) // ), // av_id AS ( // SELECT av.id // FROM attribute_values av -// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id // LEFT JOIN attribute_fqns fqns ON fqns.value_id = av.id // WHERE -// (($4::uuid IS NOT NULL AND av.id = $4::uuid) +// (($3::uuid IS NOT NULL AND av.id = $3::uuid) // OR -// ($5::text IS NOT NULL AND fqns.fqn = $5::text)) -// AND ad.namespace_id = (SELECT namespace_id FROM ov_id) +// ($4::text IS NOT NULL AND fqns.fqn = $4::text)) // ), // inserted AS ( // INSERT INTO obligation_triggers (obligation_value_id, action_id, attribute_value_id, metadata, client_id) @@ -288,8 +279,8 @@ type createObligationTriggerRow struct { // (SELECT id FROM ov_id), // (SELECT id FROM a_id), // (SELECT id FROM av_id), -// $6, -// $7::text +// $5, +// $6::text // RETURNING id, obligation_value_id, action_id, attribute_value_id, metadata, created_at, updated_at, client_id // ) // SELECT @@ -325,6 +316,11 @@ type createObligationTriggerRow struct { // 'value', av.value, // 'fqn', COALESCE(av_fqns.fqn, '') // ), +// 'namespace', JSON_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), // 'context', CASE // WHEN i.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( // JSON_BUILD_OBJECT( @@ -343,12 +339,13 @@ type createObligationTriggerRow struct { // LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL // JOIN actions a ON i.action_id = a.id // JOIN attribute_values av ON i.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id // LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id func (q *Queries) createObligationTrigger(ctx context.Context, arg createObligationTriggerParams) (createObligationTriggerRow, error) { row := q.db.QueryRow(ctx, createObligationTrigger, arg.ObligationValueID, arg.ActionID, - arg.ActionName, arg.AttributeValueID, arg.AttributeValueFqn, arg.Metadata, @@ -560,8 +557,9 @@ RETURNING id // RETURNING id func (q *Queries) deleteObligationTrigger(ctx context.Context, id string) (string, error) { row := q.db.QueryRow(ctx, deleteObligationTrigger, id) - err := row.Scan(&id) - return id, err + var id_2 string + err := row.Scan(&id_2) + return id_2, err } const deleteObligationValue = `-- name: deleteObligationValue :one @@ -642,6 +640,11 @@ WITH obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -657,6 +660,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -725,6 +730,11 @@ type getObligationRow struct { // 'value', av.value, // 'fqn', COALESCE(av_fqns.fqn, '') // ), +// 'namespace', JSON_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), // 'context', CASE // WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( // JSON_BUILD_OBJECT( @@ -740,6 +750,8 @@ type getObligationRow struct { // FROM obligation_triggers ot // JOIN actions a ON ot.action_id = a.id // JOIN attribute_values av ON ot.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id // LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id // GROUP BY ot.obligation_value_id // ) @@ -788,6 +800,148 @@ func (q *Queries) getObligation(ctx context.Context, arg getObligationParams) (g return i, err } +const getObligationTrigger = `-- name: getObligationTrigger :one + +SELECT + JSON_STRIP_NULLS( + JSON_BUILD_OBJECT( + 'id', ot.id, + 'obligation_value', JSON_BUILD_OBJECT( + 'id', ov.id, + 'value', ov.value, + 'obligation', JSON_BUILD_OBJECT( + 'id', od.id, + 'name', od.name, + 'namespace', JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', COALESCE(ns_fqns.fqn, '') + ) + ) + ), + 'action', JSON_BUILD_OBJECT( + 'id', a.id, + 'name', a.name + ), + 'attribute_value', JSON_BUILD_OBJECT( + 'id', av.id, + 'value', av.value, + 'fqn', COALESCE(av_fqns.fqn, '') + ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), + 'context', CASE + WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( + JSON_BUILD_OBJECT( + 'pep', JSON_BUILD_OBJECT( + 'client_id', ot.client_id + ) + ) + ) + ELSE '[]'::JSON + END + ) + ) as trigger, + JSON_STRIP_NULLS( + JSON_BUILD_OBJECT( + 'labels', ot.metadata -> 'labels', + 'created_at', ot.created_at, + 'updated_at', ot.updated_at + ) + ) as metadata +FROM obligation_triggers ot +JOIN obligation_values_standard ov ON ot.obligation_value_id = ov.id +JOIN obligation_definitions od ON ov.obligation_definition_id = od.id +JOIN attribute_namespaces n ON od.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +JOIN actions a ON ot.action_id = a.id +JOIN attribute_values av ON ot.attribute_value_id = av.id +JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id +LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id +WHERE ot.id = $1 +` + +type getObligationTriggerRow struct { + Trigger []byte `json:"trigger"` + Metadata []byte `json:"metadata"` +} + +// -------------------------------------------------------------- +// OBLIGATION TRIGGERS +// -------------------------------------------------------------- +// +// SELECT +// JSON_STRIP_NULLS( +// JSON_BUILD_OBJECT( +// 'id', ot.id, +// 'obligation_value', JSON_BUILD_OBJECT( +// 'id', ov.id, +// 'value', ov.value, +// 'obligation', JSON_BUILD_OBJECT( +// 'id', od.id, +// 'name', od.name, +// 'namespace', JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', COALESCE(ns_fqns.fqn, '') +// ) +// ) +// ), +// 'action', JSON_BUILD_OBJECT( +// 'id', a.id, +// 'name', a.name +// ), +// 'attribute_value', JSON_BUILD_OBJECT( +// 'id', av.id, +// 'value', av.value, +// 'fqn', COALESCE(av_fqns.fqn, '') +// ), +// 'namespace', JSON_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), +// 'context', CASE +// WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( +// JSON_BUILD_OBJECT( +// 'pep', JSON_BUILD_OBJECT( +// 'client_id', ot.client_id +// ) +// ) +// ) +// ELSE '[]'::JSON +// END +// ) +// ) as trigger, +// JSON_STRIP_NULLS( +// JSON_BUILD_OBJECT( +// 'labels', ot.metadata -> 'labels', +// 'created_at', ot.created_at, +// 'updated_at', ot.updated_at +// ) +// ) as metadata +// FROM obligation_triggers ot +// JOIN obligation_values_standard ov ON ot.obligation_value_id = ov.id +// JOIN obligation_definitions od ON ov.obligation_definition_id = od.id +// JOIN attribute_namespaces n ON od.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +// JOIN actions a ON ot.action_id = a.id +// JOIN attribute_values av ON ot.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id +// LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id +// WHERE ot.id = $1 +func (q *Queries) getObligationTrigger(ctx context.Context, id string) (getObligationTriggerRow, error) { + row := q.db.QueryRow(ctx, getObligationTrigger, id) + var i getObligationTriggerRow + err := row.Scan(&i.Trigger, &i.Metadata) + return i, err +} + const getObligationValue = `-- name: getObligationValue :one WITH obligation_triggers_agg AS ( SELECT @@ -804,6 +958,11 @@ WITH obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -819,6 +978,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -885,6 +1046,11 @@ type getObligationValueRow struct { // 'value', av.value, // 'fqn', COALESCE(av_fqns.fqn, '') // ), +// 'namespace', JSON_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), // 'context', CASE // WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( // JSON_BUILD_OBJECT( @@ -900,6 +1066,8 @@ type getObligationValueRow struct { // FROM obligation_triggers ot // JOIN actions a ON ot.action_id = a.id // JOIN attribute_values av ON ot.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id // LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id // GROUP BY ot.obligation_value_id // ) @@ -966,6 +1134,11 @@ WITH obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -981,6 +1154,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -1045,6 +1220,11 @@ type getObligationValuesByFQNsRow struct { // 'value', av.value, // 'fqn', COALESCE(av_fqns.fqn, '') // ), +// 'namespace', JSON_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), // 'context', CASE // WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( // JSON_BUILD_OBJECT( @@ -1060,6 +1240,8 @@ type getObligationValuesByFQNsRow struct { // FROM obligation_triggers ot // JOIN actions a ON ot.action_id = a.id // JOIN attribute_values av ON ot.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id // LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id // GROUP BY ot.obligation_value_id // ) @@ -1133,6 +1315,11 @@ WITH obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -1148,6 +1335,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -1218,6 +1407,11 @@ type getObligationsByFQNsRow struct { // 'value', av.value, // 'fqn', COALESCE(av_fqns.fqn, '') // ), +// 'namespace', JSON_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), // 'context', CASE // WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( // JSON_BUILD_OBJECT( @@ -1233,6 +1427,8 @@ type getObligationsByFQNsRow struct { // FROM obligation_triggers ot // JOIN actions a ON ot.action_id = a.id // JOIN attribute_values av ON ot.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id // LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id // GROUP BY ot.obligation_value_id // ) @@ -1324,6 +1520,11 @@ SELECT 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -1351,10 +1552,13 @@ JOIN attribute_namespaces n ON od.namespace_id = n.id LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id +JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id +LEFT JOIN attribute_fqns trigger_ns_fqns ON trigger_ns_fqns.namespace_id = trigger_ns.id AND trigger_ns_fqns.attribute_id IS NULL AND trigger_ns_fqns.value_id IS NULL LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id WHERE - ($1::uuid IS NULL OR od.namespace_id = $1::uuid) AND - ($2::text IS NULL OR ns_fqns.fqn = $2::text) + ($1::uuid IS NULL OR trigger_ns.id = $1::uuid) AND + ($2::text IS NULL OR trigger_ns_fqns.fqn = $2::text) ORDER BY ot.created_at DESC LIMIT $4 OFFSET $3 @@ -1401,6 +1605,11 @@ type listObligationTriggersRow struct { // 'value', av.value, // 'fqn', COALESCE(av_fqns.fqn, '') // ), +// 'namespace', JSON_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), // 'context', CASE // WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( // JSON_BUILD_OBJECT( @@ -1428,10 +1637,13 @@ type listObligationTriggersRow struct { // LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL // JOIN actions a ON ot.action_id = a.id // JOIN attribute_values av ON ot.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id +// LEFT JOIN attribute_fqns trigger_ns_fqns ON trigger_ns_fqns.namespace_id = trigger_ns.id AND trigger_ns_fqns.attribute_id IS NULL AND trigger_ns_fqns.value_id IS NULL // LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id // WHERE -// ($1::uuid IS NULL OR od.namespace_id = $1::uuid) AND -// ($2::text IS NULL OR ns_fqns.fqn = $2::text) +// ($1::uuid IS NULL OR trigger_ns.id = $1::uuid) AND +// ($2::text IS NULL OR trigger_ns_fqns.fqn = $2::text) // ORDER BY ot.created_at DESC // LIMIT $4 // OFFSET $3 @@ -1461,7 +1673,12 @@ func (q *Queries) listObligationTriggers(ctx context.Context, arg listObligation } const listObligations = `-- name: listObligations :many -WITH counted AS ( +WITH params AS ( + SELECT + COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +), +counted AS ( SELECT COUNT(od.id) AS total FROM obligation_definitions od LEFT JOIN attribute_namespaces n ON od.namespace_id = n.id @@ -1485,6 +1702,11 @@ obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -1500,6 +1722,8 @@ obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -1524,21 +1748,34 @@ FROM obligation_definitions od JOIN attribute_namespaces n on od.namespace_id = n.id LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL CROSS JOIN counted +CROSS JOIN params p LEFT JOIN obligation_values_standard ov on od.id = ov.obligation_definition_id LEFT JOIN obligation_triggers_agg ota on ov.id = ota.obligation_value_id WHERE ($1::uuid IS NULL OR od.namespace_id = $1::uuid) AND ($2::text IS NULL OR fqns.fqn = $2::text) -GROUP BY od.id, n.id, fqns.fqn, counted.total +GROUP BY od.id, n.id, fqns.fqn, counted.total, p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN od.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN od.name END DESC, + CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'ASC' THEN fqns.fqn || LOWER(od.name) END ASC, + CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'DESC' THEN fqns.fqn || LOWER(od.name) END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN od.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN od.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN od.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN od.updated_at END DESC, + od.id ASC LIMIT $4 OFFSET $3 ` type listObligationsParams struct { - NamespaceID pgtype.UUID `json:"namespace_id"` - NamespaceFqn pgtype.Text `json:"namespace_fqn"` - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` } type listObligationsRow struct { @@ -1552,7 +1789,12 @@ type listObligationsRow struct { // listObligations // -// WITH counted AS ( +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +// ), +// counted AS ( // SELECT COUNT(od.id) AS total // FROM obligation_definitions od // LEFT JOIN attribute_namespaces n ON od.namespace_id = n.id @@ -1576,6 +1818,11 @@ type listObligationsRow struct { // 'value', av.value, // 'fqn', COALESCE(av_fqns.fqn, '') // ), +// 'namespace', JSON_BUILD_OBJECT( +// 'id', trigger_ns.id, +// 'name', trigger_ns.name, +// 'fqn', CONCAT('https://', trigger_ns.name) +// ), // 'context', CASE // WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( // JSON_BUILD_OBJECT( @@ -1591,6 +1838,8 @@ type listObligationsRow struct { // FROM obligation_triggers ot // JOIN actions a ON ot.action_id = a.id // JOIN attribute_values av ON ot.attribute_value_id = av.id +// JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +// JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id // LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id // GROUP BY ot.obligation_value_id // ) @@ -1615,12 +1864,23 @@ type listObligationsRow struct { // JOIN attribute_namespaces n on od.namespace_id = n.id // LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL // CROSS JOIN counted +// CROSS JOIN params p // LEFT JOIN obligation_values_standard ov on od.id = ov.obligation_definition_id // LEFT JOIN obligation_triggers_agg ota on ov.id = ota.obligation_value_id // WHERE // ($1::uuid IS NULL OR od.namespace_id = $1::uuid) AND // ($2::text IS NULL OR fqns.fqn = $2::text) -// GROUP BY od.id, n.id, fqns.fqn, counted.total +// GROUP BY od.id, n.id, fqns.fqn, counted.total, p.resolved_field, p.resolved_direction +// ORDER BY +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN od.name END ASC, +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN od.name END DESC, +// CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'ASC' THEN fqns.fqn || LOWER(od.name) END ASC, +// CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'DESC' THEN fqns.fqn || LOWER(od.name) END DESC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN od.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN od.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN od.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN od.updated_at END DESC, +// od.id ASC // LIMIT $4 // OFFSET $3 func (q *Queries) listObligations(ctx context.Context, arg listObligationsParams) ([]listObligationsRow, error) { @@ -1629,6 +1889,8 @@ func (q *Queries) listObligations(ctx context.Context, arg listObligationsParams arg.NamespaceFqn, arg.Offset, arg.Limit, + arg.SortField, + arg.SortDirection, ) if err != nil { return nil, err diff --git a/service/policy/db/queries/actions.sql b/service/policy/db/queries/actions.sql index 64d410bf26..4d9c3bd451 100644 --- a/service/policy/db/queries/actions.sql +++ b/service/policy/db/queries/actions.sql @@ -3,8 +3,18 @@ ---------------------------------------------------------------- -- name: listActions :many -WITH counted AS ( - SELECT COUNT(id) AS total FROM actions +WITH resolved_namespace AS ( + SELECT + n.id, + n.name, + fqns.fqn + FROM attribute_namespaces n + LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL + WHERE + (sqlc.narg('namespace_id')::uuid IS NOT NULL AND n.id = sqlc.narg('namespace_id')::uuid) + OR + (sqlc.narg('namespace_fqn')::text IS NOT NULL AND fqns.fqn = sqlc.narg('namespace_fqn')::text) + LIMIT 1 ) SELECT a.id, @@ -15,37 +25,111 @@ SELECT 'updated_at', a.updated_at )) as metadata, a.is_standard, - counted.total + CASE + WHEN a.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + END AS namespace, + COUNT(*) OVER() as total FROM actions a -CROSS JOIN counted +LEFT JOIN resolved_namespace rn ON TRUE +LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +WHERE + ( + sqlc.narg('namespace_id')::uuid IS NULL + AND sqlc.narg('namespace_fqn')::text IS NULL + ) + OR ( + a.namespace_id = rn.id + OR ( + rn.id IS NOT NULL + AND a.is_standard = TRUE + AND a.namespace_id IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM actions ax + WHERE ax.name = a.name + AND ax.namespace_id = rn.id + ) + ) + ) +ORDER BY a.created_at DESC LIMIT @limit_ OFFSET @offset_; -- name: getAction :one +WITH resolved_namespace AS ( + SELECT + n.id, + n.name, + fqns.fqn + FROM attribute_namespaces n + LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL + WHERE + (sqlc.narg('namespace_id')::uuid IS NOT NULL AND n.id = sqlc.narg('namespace_id')::uuid) + OR + (sqlc.narg('namespace_fqn')::text IS NOT NULL AND fqns.fqn = sqlc.narg('namespace_fqn')::text) + LIMIT 1 +) SELECT a.id, a.name, a.is_standard, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata + JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata, + CASE + WHEN a.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + END AS namespace FROM actions a +LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +LEFT JOIN resolved_namespace rn ON TRUE WHERE - (sqlc.narg('id')::uuid IS NULL OR a.id = sqlc.narg('id')::uuid) - AND (sqlc.narg('name')::text IS NULL OR a.name = sqlc.narg('name')::text); + ( + (sqlc.narg('id')::uuid IS NOT NULL AND a.id = sqlc.narg('id')::uuid) + OR + ( + sqlc.narg('name')::text IS NOT NULL + AND a.name = sqlc.narg('name')::text + AND ( + (rn.id IS NOT NULL AND a.namespace_id = rn.id) + OR + (rn.id IS NULL AND a.namespace_id IS NULL) + ) + ) + ) +ORDER BY + CASE + WHEN a.namespace_id = rn.id THEN 0 + WHEN a.is_standard = TRUE THEN 1 + ELSE 2 + END, + a.created_at DESC +LIMIT 1; -- name: createOrListActionsByName :many WITH input_actions AS ( SELECT unnest(sqlc.arg('action_names')::text[]) AS name ), new_actions AS ( - INSERT INTO actions (name, is_standard) + INSERT INTO actions (name, is_standard, namespace_id) SELECT input.name, - FALSE -- custom actions + FALSE, -- custom actions + NULL FROM input_actions input WHERE NOT EXISTS ( - SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) + SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) AND a.namespace_id IS NULL ) - ON CONFLICT (name) DO NOTHING + ON CONFLICT (name) WHERE namespace_id IS NULL DO NOTHING RETURNING id, name, is_standard, created_at ), all_actions AS ( @@ -54,6 +138,7 @@ all_actions AS ( TRUE AS pre_existing FROM actions a JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) + WHERE a.namespace_id IS NULL UNION ALL @@ -71,11 +156,68 @@ SELECT FROM all_actions ORDER BY name; +-- name: createOrListActionsByNameInNamespace :many +WITH resolved_namespace AS ( + SELECT n.id + FROM attribute_namespaces n + WHERE n.id = sqlc.arg('namespace_id')::uuid + LIMIT 1 +), +input_actions AS ( + SELECT unnest(sqlc.arg('action_names')::text[]) AS name +), +existing_actions AS ( + SELECT a.id, a.name, a.is_standard, a.created_at + FROM actions a + JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) + WHERE a.namespace_id = (SELECT id FROM resolved_namespace) +), +new_actions AS ( + INSERT INTO actions (name, is_standard, namespace_id) + SELECT input.name, FALSE, (SELECT id FROM resolved_namespace) + FROM input_actions input + WHERE NOT EXISTS ( + SELECT 1 FROM existing_actions ea WHERE LOWER(ea.name) = LOWER(input.name) + ) + ON CONFLICT (namespace_id, name) WHERE namespace_id IS NOT NULL DO NOTHING + RETURNING id, name, is_standard, created_at +) +SELECT id, name, is_standard, created_at FROM existing_actions +UNION ALL +SELECT id, name, is_standard, created_at FROM new_actions +ORDER BY name; + -- name: createCustomAction :one -INSERT INTO actions (name, metadata, is_standard) -VALUES ($1, $2, FALSE) +WITH ns AS ( + SELECT + sqlc.narg('namespace_id')::uuid AS id, + sqlc.narg('namespace_fqn')::text AS fqn +) +INSERT INTO actions (name, metadata, is_standard, namespace_id) +SELECT + @name, + @metadata, + FALSE, + COALESCE(ns.id, fqns.namespace_id) +FROM ns +LEFT JOIN attribute_fqns fqns ON fqns.fqn = ns.fqn AND ns.id IS NULL +WHERE + (ns.id IS NULL AND ns.fqn IS NULL) + OR + (ns.id IS NOT NULL) + OR + (ns.fqn IS NOT NULL AND fqns.namespace_id IS NOT NULL) RETURNING id; +-- name: seedStandardActionsForNamespace :execrows +INSERT INTO actions (name, is_standard, namespace_id) +VALUES + ('create', TRUE, $1), + ('read', TRUE, $1), + ('update', TRUE, $1), + ('delete', TRUE, $1) +ON CONFLICT (namespace_id, name) WHERE namespace_id IS NOT NULL DO NOTHING; + -- name: updateCustomAction :execrows UPDATE actions SET @@ -84,6 +226,15 @@ SET WHERE id = $1 AND is_standard = FALSE; +-- name: getActionsByIDs :many +SELECT + a.id, + a.is_standard, + a.namespace_id +FROM actions + a +WHERE a.id = ANY(@ids::uuid[]); + -- name: deleteCustomAction :execrows DELETE FROM actions WHERE id = $1 diff --git a/service/policy/db/queries/attribute_values.sql b/service/policy/db/queries/attribute_values.sql index bd908a9c5b..30247b4099 100644 --- a/service/policy/db/queries/attribute_values.sql +++ b/service/policy/db/queries/attribute_values.sql @@ -2,24 +2,6 @@ -- ATTRIBUTE VALUES ---------------------------------------------------------------- --- name: listAttributeValues :many -SELECT - COUNT(*) OVER() AS total, - av.id, - av.value, - av.active, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', av.metadata -> 'labels', 'created_at', av.created_at, 'updated_at', av.updated_at)) as metadata, - av.attribute_definition_id, - fqns.fqn -FROM attribute_values av -LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id -WHERE ( - (sqlc.narg('active')::BOOLEAN IS NULL OR av.active = sqlc.narg('active')) AND - (sqlc.narg('attribute_definition_id')::uuid IS NULL OR av.attribute_definition_id = sqlc.narg('attribute_definition_id')::uuid) -) -LIMIT @limit_ -OFFSET @offset_; - -- name: getAttributeValue :one WITH obligation_triggers_agg AS ( SELECT @@ -35,6 +17,11 @@ WITH obligation_triggers_agg AS ( 'id', av.id, 'fqn', av_fqns.fqn ), + 'namespace', JSONB_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSONB_BUILD_ARRAY( JSONB_BUILD_OBJECT( @@ -50,6 +37,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av.id = av_fqns.value_id GROUP BY ot.obligation_value_id ), @@ -175,3 +164,9 @@ UPDATE attribute_value_public_key_map SET key_access_server_key_id = sqlc.arg('new_key_id')::uuid WHERE (key_access_server_key_id = sqlc.arg('old_key_id')::uuid) RETURNING value_id; + +-- name: getAttributeValueNamespaceIDs :many +SELECT av.id AS attribute_value_id, ad.namespace_id +FROM attribute_values av +JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +WHERE av.id = ANY(sqlc.arg('ids')::uuid[]); diff --git a/service/policy/db/queries/attributes.sql b/service/policy/db/queries/attributes.sql index ac1fad989f..bfd5e613f8 100644 --- a/service/policy/db/queries/attributes.sql +++ b/service/policy/db/queries/attributes.sql @@ -3,10 +3,16 @@ ---------------------------------------------------------------- -- name: listAttributesDetail :many +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction +) SELECT ad.id, ad.name as attribute_name, ad.rule, + ad.allow_traversal, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, ad.namespace_id, ad.active, @@ -18,7 +24,7 @@ SELECT 'active', avt.active, 'fqn', CONCAT(fqns.fqn, '/value/', avt.value) ) ORDER BY ARRAY_POSITION(ad.values_order, avt.id) - ) AS values, + ) FILTER (WHERE avt.id IS NOT NULL) AS values, fqns.fqn, COUNT(*) OVER() AS total FROM attribute_definitions ad @@ -33,19 +39,34 @@ LEFT JOIN ( GROUP BY av.id ) avt ON avt.attribute_definition_id = ad.id LEFT JOIN attribute_fqns fqns ON fqns.attribute_id = ad.id AND fqns.value_id IS NULL +CROSS JOIN params p WHERE (sqlc.narg('active')::BOOLEAN IS NULL OR ad.active = sqlc.narg('active')) AND - (sqlc.narg('namespace_id')::uuid IS NULL OR ad.namespace_id = sqlc.narg('namespace_id')::uuid) AND - (sqlc.narg('namespace_name')::text IS NULL OR n.name = sqlc.narg('namespace_name')::text) -GROUP BY ad.id, n.name, fqns.fqn -LIMIT @limit_ -OFFSET @offset_; + (sqlc.narg('namespace_id')::uuid IS NULL OR ad.namespace_id = sqlc.narg('namespace_id')::uuid) AND + (sqlc.narg('namespace_name')::text IS NULL OR n.name = sqlc.narg('namespace_name')::text) +GROUP BY ad.id, n.name, fqns.fqn, p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ad.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ad.name END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ad.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ad.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ad.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ad.updated_at END DESC, + ad.id ASC +LIMIT @limit_ +OFFSET @offset_; -- name: listAttributesSummary :many +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction +) SELECT ad.id, ad.name as attribute_name, ad.rule, + ad.allow_traversal, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, ad.namespace_id, ad.active, @@ -53,10 +74,19 @@ SELECT COUNT(*) OVER() AS total FROM attribute_definitions ad LEFT JOIN attribute_namespaces n ON n.id = ad.namespace_id +CROSS JOIN params p WHERE ad.namespace_id = $1 -GROUP BY ad.id, n.name -LIMIT @limit_ -OFFSET @offset_; +GROUP BY ad.id, n.name, p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ad.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ad.name END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ad.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ad.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ad.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ad.updated_at END DESC, + ad.id ASC +LIMIT @limit_ +OFFSET @offset_; -- name: listAttributesByDefOrValueFqns :many -- get the attribute definition for the provided value or definition fqn @@ -66,8 +96,10 @@ WITH target_definition AS ( ad.namespace_id, ad.name, ad.rule, + ad.allow_traversal, ad.active, ad.values_order, + ad.created_at, JSONB_AGG( DISTINCT JSONB_BUILD_OBJECT( 'id', kas.id, @@ -102,7 +134,7 @@ WITH target_definition AS ( ) defk ON ad.id = defk.definition_id WHERE fqns.fqn = ANY(@fqns::TEXT[]) AND ad.active = TRUE - GROUP BY ad.id, defk.keys + GROUP BY ad.id, ad.created_at, defk.keys ), namespaces AS ( SELECT @@ -263,13 +295,14 @@ values AS ( INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id GROUP BY k.value_id ) value_keys ON av.id = value_keys.value_id - WHERE av.active = TRUE + WHERE (av.active = TRUE OR sqlc.arg('include_inactive_values')::BOOLEAN = TRUE) GROUP BY av.attribute_definition_id ) SELECT td.id, td.name, td.rule, + td.allow_traversal, td.active, n.namespace, fqns.fqn, @@ -280,13 +313,15 @@ FROM target_definition td INNER JOIN attribute_fqns fqns ON td.id = fqns.attribute_id INNER JOIN namespaces n ON td.namespace_id = n.id LEFT JOIN values ON td.id = values.attribute_definition_id -WHERE fqns.value_id IS NULL; +WHERE fqns.value_id IS NULL +ORDER BY td.created_at DESC; -- name: getAttribute :one SELECT ad.id, ad.name as attribute_name, ad.rule, + ad.allow_traversal, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', ad.metadata -> 'labels', 'created_at', ad.created_at, 'updated_at', ad.updated_at)) AS metadata, ad.namespace_id, ad.active, @@ -298,7 +333,7 @@ SELECT 'active', avt.active, 'fqn', CONCAT(fqns.fqn, '/value/', avt.value) ) ORDER BY ARRAY_POSITION(ad.values_order, avt.id) - ) AS values, + ) FILTER (WHERE avt.id IS NOT NULL) AS values, JSONB_AGG( DISTINCT JSONB_BUILD_OBJECT( 'id', kas.id, @@ -347,8 +382,8 @@ WHERE (sqlc.narg('id')::uuid IS NULL OR ad.id = sqlc.narg('id')::uuid) GROUP BY ad.id, n.name, fqns.fqn, defk.keys; -- name: createAttribute :one -INSERT INTO attribute_definitions (namespace_id, name, rule, metadata) -VALUES (@namespace_id, @name, @rule, @metadata) +INSERT INTO attribute_definitions (namespace_id, name, rule, metadata, allow_traversal) +VALUES (@namespace_id, @name, @rule, @metadata, @allow_traversal) RETURNING id; -- updateAttribute: Unsafe and Safe Updates both @@ -359,7 +394,8 @@ SET rule = COALESCE(sqlc.narg('rule'), rule), values_order = COALESCE(sqlc.narg('values_order'), values_order), metadata = COALESCE(sqlc.narg('metadata'), metadata), - active = COALESCE(sqlc.narg('active'), active) + active = COALESCE(sqlc.narg('active'), active), + allow_traversal = COALESCE(sqlc.narg('allow_traversal'), allow_traversal) WHERE id = $1; -- name: deleteAttribute :execrows diff --git a/service/policy/db/queries/key_access_server_registry.sql b/service/policy/db/queries/key_access_server_registry.sql index c699b6e4e8..a8317ea555 100644 --- a/service/policy/db/queries/key_access_server_registry.sql +++ b/service/policy/db/queries/key_access_server_registry.sql @@ -68,7 +68,12 @@ LIMIT @limit_ OFFSET @offset_; -- name: listKeyAccessServers :many -WITH counted AS ( +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction +), +counted AS ( SELECT COUNT(kas.id) AS total FROM key_access_servers AS kas ) @@ -82,6 +87,7 @@ SELECT kas.id, counted.total FROM key_access_servers AS kas CROSS JOIN counted +CROSS JOIN params p LEFT JOIN ( SELECT kask.key_access_server_id, @@ -100,8 +106,18 @@ LEFT JOIN ( INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id GROUP BY kask.key_access_server_id ) kask_keys ON kas.id = kask_keys.key_access_server_id -LIMIT @limit_ -OFFSET @offset_; +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN kas.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN kas.name END DESC, + CASE WHEN p.resolved_field = 'uri' AND p.resolved_direction = 'ASC' THEN kas.uri END ASC, + CASE WHEN p.resolved_field = 'uri' AND p.resolved_direction = 'DESC' THEN kas.uri END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN kas.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN kas.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN kas.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN kas.updated_at END DESC, + kas.id ASC +LIMIT @limit_ +OFFSET @offset_; -- name: getKeyAccessServer :one SELECT @@ -315,7 +331,7 @@ CROSS JOIN keys_with_mappings_count kwmc LEFT JOIN namespace_mappings nm ON fk.id = nm.key_id LEFT JOIN definition_mappings dm ON fk.id = dm.key_id LEFT JOIN value_mappings vm ON fk.id = vm.key_id -ORDER BY fk.created_at +ORDER BY fk.created_at DESC LIMIT @limit_ OFFSET @offset_; @@ -326,8 +342,22 @@ SET metadata = COALESCE(sqlc.narg('metadata'), metadata) WHERE id = $1; +-- name: keyAccessServerExists :one +SELECT EXISTS ( + SELECT 1 + FROM key_access_servers AS kas + WHERE (sqlc.narg('kas_id')::uuid IS NULL OR kas.id = sqlc.narg('kas_id')::uuid) + AND (sqlc.narg('kas_name')::text IS NULL OR kas.name = sqlc.narg('kas_name')::text) + AND (sqlc.narg('kas_uri')::text IS NULL OR kas.uri = sqlc.narg('kas_uri')::text) +); + -- name: listKeys :many -WITH listed AS ( +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction +), +listed AS ( SELECT kas.id AS kas_id, kas.uri AS kas_uri @@ -362,13 +392,21 @@ SELECT FROM key_access_server_keys AS kask INNER JOIN listed ON kask.key_access_server_id = listed.kas_id -LEFT JOIN +CROSS JOIN params p +LEFT JOIN provider_config as pc ON kask.provider_config_id = pc.id WHERE (sqlc.narg('key_algorithm')::integer IS NULL OR kask.key_algorithm = sqlc.narg('key_algorithm')::integer) AND (sqlc.narg('legacy')::boolean IS NULL OR kask.legacy = sqlc.narg('legacy')::boolean) -ORDER BY kask.created_at DESC -LIMIT @limit_ +ORDER BY + CASE WHEN p.resolved_field = 'key_id' AND p.resolved_direction = 'ASC' THEN kask.key_id END ASC, + CASE WHEN p.resolved_field = 'key_id' AND p.resolved_direction = 'DESC' THEN kask.key_id END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN kask.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN kask.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN kask.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN kask.updated_at END DESC, + kask.id ASC +LIMIT @limit_ OFFSET @offset_; -- name: deleteKey :execrows diff --git a/service/policy/db/queries/key_management.sql b/service/policy/db/queries/key_management.sql index 4ef8012781..2b63cec022 100644 --- a/service/policy/db/queries/key_management.sql +++ b/service/policy/db/queries/key_management.sql @@ -49,6 +49,7 @@ SELECT counted.total FROM provider_config AS pc CROSS JOIN counted +ORDER BY pc.created_at DESC LIMIT @limit_ OFFSET @offset_; diff --git a/service/policy/db/queries/namespaces.sql b/service/policy/db/queries/namespaces.sql index 1ab6336326..c62c363c46 100644 --- a/service/policy/db/queries/namespaces.sql +++ b/service/policy/db/queries/namespaces.sql @@ -3,6 +3,11 @@ ---------------------------------------------------------------- -- name: listNamespaces :many +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction +) SELECT COUNT(*) OVER() AS total, ns.id, @@ -12,7 +17,18 @@ SELECT fqns.fqn FROM attribute_namespaces ns LEFT JOIN attribute_fqns fqns ON ns.id = fqns.namespace_id AND fqns.attribute_id IS NULL +CROSS JOIN params p WHERE (sqlc.narg('active')::BOOLEAN IS NULL OR ns.active = sqlc.narg('active')::BOOLEAN) +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN ns.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN ns.name END DESC, + CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'ASC' THEN fqns.fqn END ASC, + CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'DESC' THEN fqns.fqn END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN ns.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN ns.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN ns.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN ns.updated_at END DESC, + ns.id ASC LIMIT @limit_ OFFSET @offset_; @@ -29,8 +45,7 @@ SELECT 'name', kas.name, 'public_key', kas.public_key )) FILTER (WHERE kas_ns_grants.namespace_id IS NOT NULL) as grants, - nmp_keys.keys as keys, - nmp_certs.certs as certs + nmp_keys.keys as keys FROM attribute_namespaces ns LEFT JOIN attribute_namespace_key_access_grants kas_ns_grants ON kas_ns_grants.namespace_id = ns.id LEFT JOIN key_access_servers kas ON kas.id = kas_ns_grants.key_access_server_id @@ -54,23 +69,10 @@ LEFT JOIN ( INNER JOIN key_access_servers kas ON kask.key_access_server_id = kas.id GROUP BY k.namespace_id ) nmp_keys ON ns.id = nmp_keys.namespace_id -LEFT JOIN ( - SELECT - c.namespace_id, - JSONB_AGG( - DISTINCT JSONB_BUILD_OBJECT( - 'id', cert.id, - 'pem', cert.pem - ) - ) FILTER (WHERE cert.id IS NOT NULL) AS certs - FROM attribute_namespace_certificates c - INNER JOIN certificates cert ON c.certificate_id = cert.id - GROUP BY c.namespace_id -) nmp_certs ON ns.id = nmp_certs.namespace_id WHERE fqns.attribute_id IS NULL AND fqns.value_id IS NULL AND (sqlc.narg('id')::uuid IS NULL OR ns.id = sqlc.narg('id')::uuid) AND (sqlc.narg('name')::text IS NULL OR ns.name = REGEXP_REPLACE(sqlc.narg('name')::text, '^https://', '')) -GROUP BY ns.id, fqns.fqn, nmp_keys.keys, nmp_certs.certs; +GROUP BY ns.id, fqns.fqn, nmp_keys.keys; -- name: createNamespace :one INSERT INTO attribute_namespaces (name, metadata) @@ -107,44 +109,3 @@ UPDATE attribute_namespace_public_key_map SET key_access_server_key_id = sqlc.arg('new_key_id')::uuid WHERE (key_access_server_key_id = sqlc.arg('old_key_id')::uuid) RETURNING namespace_id; - ----------------------------------------------------------------- --- CERTIFICATES ----------------------------------------------------------------- - --- name: createCertificate :one -INSERT INTO certificates (pem, metadata) -VALUES (sqlc.arg('pem'), sqlc.arg('metadata')) -RETURNING id; - --- name: getCertificate :one -SELECT - id, - pem, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -FROM certificates -WHERE id = $1; - --- name: getCertificateByPEM :one -SELECT - id, - pem, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -FROM certificates -WHERE pem = $1; - --- name: deleteCertificate :execrows -DELETE FROM certificates WHERE id = $1; - --- name: assignCertificateToNamespace :one -INSERT INTO attribute_namespace_certificates (namespace_id, certificate_id) -VALUES ($1, $2) -RETURNING namespace_id, certificate_id; - --- name: removeCertificateFromNamespace :execrows -DELETE FROM attribute_namespace_certificates -WHERE namespace_id = $1 AND certificate_id = $2; - --- name: countCertificateNamespaceAssignments :one -SELECT COUNT(*) FROM attribute_namespace_certificates -WHERE certificate_id = $1; diff --git a/service/policy/db/queries/obligations.sql b/service/policy/db/queries/obligations.sql index 1f1be1ca98..54f636bf14 100644 --- a/service/policy/db/queries/obligations.sql +++ b/service/policy/db/queries/obligations.sql @@ -66,6 +66,11 @@ WITH obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -81,6 +86,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -118,7 +125,12 @@ WHERE GROUP BY od.id, n.id, fqns.fqn; -- name: listObligations :many -WITH counted AS ( +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction +), +counted AS ( SELECT COUNT(od.id) AS total FROM obligation_definitions od LEFT JOIN attribute_namespaces n ON od.namespace_id = n.id @@ -142,6 +154,11 @@ obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -157,6 +174,8 @@ obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -181,12 +200,23 @@ FROM obligation_definitions od JOIN attribute_namespaces n on od.namespace_id = n.id LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL CROSS JOIN counted +CROSS JOIN params p LEFT JOIN obligation_values_standard ov on od.id = ov.obligation_definition_id LEFT JOIN obligation_triggers_agg ota on ov.id = ota.obligation_value_id WHERE (sqlc.narg('namespace_id')::uuid IS NULL OR od.namespace_id = sqlc.narg('namespace_id')::uuid) AND (sqlc.narg('namespace_fqn')::text IS NULL OR fqns.fqn = sqlc.narg('namespace_fqn')::text) -GROUP BY od.id, n.id, fqns.fqn, counted.total +GROUP BY od.id, n.id, fqns.fqn, counted.total, p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN od.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN od.name END DESC, + CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'ASC' THEN fqns.fqn || LOWER(od.name) END ASC, + CASE WHEN p.resolved_field = 'fqn' AND p.resolved_direction = 'DESC' THEN fqns.fqn || LOWER(od.name) END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN od.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN od.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN od.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN od.updated_at END DESC, + od.id ASC LIMIT @limit_ OFFSET @offset_; @@ -233,6 +263,11 @@ WITH obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -248,6 +283,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -346,6 +383,11 @@ WITH obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -361,6 +403,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -415,6 +459,11 @@ WITH obligation_triggers_agg AS ( 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -430,6 +479,8 @@ WITH obligation_triggers_agg AS ( FROM obligation_triggers ot JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id + JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id + JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id GROUP BY ot.obligation_value_id ) @@ -485,32 +536,90 @@ RETURNING id; -- OBLIGATION TRIGGERS ---------------------------------------------------------------- +-- name: getObligationTrigger :one +SELECT + JSON_STRIP_NULLS( + JSON_BUILD_OBJECT( + 'id', ot.id, + 'obligation_value', JSON_BUILD_OBJECT( + 'id', ov.id, + 'value', ov.value, + 'obligation', JSON_BUILD_OBJECT( + 'id', od.id, + 'name', od.name, + 'namespace', JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', COALESCE(ns_fqns.fqn, '') + ) + ) + ), + 'action', JSON_BUILD_OBJECT( + 'id', a.id, + 'name', a.name + ), + 'attribute_value', JSON_BUILD_OBJECT( + 'id', av.id, + 'value', av.value, + 'fqn', COALESCE(av_fqns.fqn, '') + ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), + 'context', CASE + WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( + JSON_BUILD_OBJECT( + 'pep', JSON_BUILD_OBJECT( + 'client_id', ot.client_id + ) + ) + ) + ELSE '[]'::JSON + END + ) + ) as trigger, + JSON_STRIP_NULLS( + JSON_BUILD_OBJECT( + 'labels', ot.metadata -> 'labels', + 'created_at', ot.created_at, + 'updated_at', ot.updated_at + ) + ) as metadata +FROM obligation_triggers ot +JOIN obligation_values_standard ov ON ot.obligation_value_id = ov.id +JOIN obligation_definitions od ON ov.obligation_definition_id = od.id +JOIN attribute_namespaces n ON od.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +JOIN actions a ON ot.action_id = a.id +JOIN attribute_values av ON ot.attribute_value_id = av.id +JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id +LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id +WHERE ot.id = $1; + -- name: createObligationTrigger :one WITH ov_id AS ( - SELECT ov.id, od.namespace_id + SELECT ov.id FROM obligation_values_standard ov - JOIN obligation_definitions od ON ov.obligation_definition_id = od.id WHERE sqlc.narg('obligation_value_id')::uuid IS NOT NULL AND ov.id = sqlc.narg('obligation_value_id')::uuid ), a_id AS ( SELECT a.id FROM actions a - WHERE - (sqlc.narg('action_id')::uuid IS NOT NULL AND a.id = sqlc.narg('action_id')::uuid) - OR - (sqlc.narg('action_name')::text IS NOT NULL AND a.name = sqlc.narg('action_name')::text) + WHERE (sqlc.narg('action_id')::uuid IS NOT NULL AND a.id = sqlc.narg('action_id')::uuid) ), --- Gets the attribute value, but also ensures that the attribute value belongs to the same namespace as the obligation, to which the obligation value belongs +-- Attribute value lookup is intentionally namespace-agnostic here; the caller +-- validates that the action and attribute value share the trigger's source namespace. av_id AS ( SELECT av.id FROM attribute_values av - JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id LEFT JOIN attribute_fqns fqns ON fqns.value_id = av.id WHERE ((sqlc.narg('attribute_value_id')::uuid IS NOT NULL AND av.id = sqlc.narg('attribute_value_id')::uuid) OR (sqlc.narg('attribute_value_fqn')::text IS NOT NULL AND fqns.fqn = sqlc.narg('attribute_value_fqn')::text)) - AND ad.namespace_id = (SELECT namespace_id FROM ov_id) ), inserted AS ( INSERT INTO obligation_triggers (obligation_value_id, action_id, attribute_value_id, metadata, client_id) @@ -555,6 +664,11 @@ SELECT 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN i.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -573,6 +687,8 @@ JOIN attribute_namespaces n ON od.namespace_id = n.id LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL JOIN actions a ON i.action_id = a.id JOIN attribute_values av ON i.attribute_value_id = av.id +JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id; -- name: deleteAllObligationTriggersForValue :execrows @@ -612,6 +728,11 @@ SELECT 'value', av.value, 'fqn', COALESCE(av_fqns.fqn, '') ), + 'namespace', JSON_BUILD_OBJECT( + 'id', trigger_ns.id, + 'name', trigger_ns.name, + 'fqn', CONCAT('https://', trigger_ns.name) + ), 'context', CASE WHEN ot.client_id IS NOT NULL THEN JSON_BUILD_ARRAY( JSON_BUILD_OBJECT( @@ -639,11 +760,13 @@ JOIN attribute_namespaces n ON od.namespace_id = n.id LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL JOIN actions a ON ot.action_id = a.id JOIN attribute_values av ON ot.attribute_value_id = av.id +JOIN attribute_definitions ad ON av.attribute_definition_id = ad.id +JOIN attribute_namespaces trigger_ns ON ad.namespace_id = trigger_ns.id +LEFT JOIN attribute_fqns trigger_ns_fqns ON trigger_ns_fqns.namespace_id = trigger_ns.id AND trigger_ns_fqns.attribute_id IS NULL AND trigger_ns_fqns.value_id IS NULL LEFT JOIN attribute_fqns av_fqns ON av_fqns.value_id = av.id WHERE - (sqlc.narg('namespace_id')::uuid IS NULL OR od.namespace_id = sqlc.narg('namespace_id')::uuid) AND - (sqlc.narg('namespace_fqn')::text IS NULL OR ns_fqns.fqn = sqlc.narg('namespace_fqn')::text) + (sqlc.narg('namespace_id')::uuid IS NULL OR trigger_ns.id = sqlc.narg('namespace_id')::uuid) AND + (sqlc.narg('namespace_fqn')::text IS NULL OR trigger_ns_fqns.fqn = sqlc.narg('namespace_fqn')::text) ORDER BY ot.created_at DESC LIMIT @limit_ OFFSET @offset_; - diff --git a/service/policy/db/queries/registered_resources.sql b/service/policy/db/queries/registered_resources.sql index c46c4dbc93..3118b63642 100644 --- a/service/policy/db/queries/registered_resources.sql +++ b/service/policy/db/queries/registered_resources.sql @@ -3,37 +3,128 @@ ---------------------------------------------------------------- -- name: createRegisteredResource :one -INSERT INTO registered_resources (name, metadata) -VALUES ($1, $2) -RETURNING id; +WITH inserted AS ( + INSERT INTO registered_resources (namespace_id, name, metadata) + SELECT + COALESCE(sqlc.narg('namespace_id')::uuid, fqns.namespace_id), + @name, + @metadata + FROM ( + SELECT + sqlc.narg('namespace_id')::uuid as direct_namespace_id + ) direct + LEFT JOIN attribute_fqns fqns ON fqns.fqn = sqlc.narg('namespace_fqn')::text AND sqlc.narg('namespace_id')::text IS NULL + WHERE + (sqlc.narg('namespace_id')::text IS NOT NULL AND direct.direct_namespace_id IS NOT NULL) OR + (sqlc.narg('namespace_fqn')::text IS NOT NULL AND fqns.namespace_id IS NOT NULL) OR + (sqlc.narg('namespace_id')::text IS NULL AND sqlc.narg('namespace_fqn')::text IS NULL) + RETURNING id, namespace_id, name, metadata +) +SELECT + i.id, + i.name, + i.metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', fqns.fqn + ) + ELSE NULL END as namespace +FROM inserted i +LEFT JOIN attribute_namespaces n ON i.namespace_id = n.id +LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL; -- name: getRegisteredResource :one SELECT r.id, r.name, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', r.metadata -> 'labels', 'created_at', r.created_at, 'updated_at', r.updated_at)) as metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + ELSE NULL END as namespace, JSON_AGG( JSON_BUILD_OBJECT( 'id', v.id, - 'value', v.value + 'value', v.value, + 'action_attribute_values', action_attrs.values ) ) FILTER (WHERE v.id IS NOT NULL) as values FROM registered_resources r +LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL LEFT JOIN registered_resource_values v ON v.registered_resource_id = r.id +-- Build a JSON array of action/attribute pairs for each resource value +LEFT JOIN LATERAL ( + -- COALESCE so a value with no mappings yields '[]' rather than SQL NULL, + -- giving consumers a consistent JSON array shape for action_attribute_values. + SELECT COALESCE(JSON_AGG( + JSON_BUILD_OBJECT( + 'action', JSON_BUILD_OBJECT( + 'id', a.id, + 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + -- Namespace FQN is deterministic from the name, so build it inline + -- instead of joining attribute_fqns for it. + ELSE JSON_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', CONCAT('https://', ans.name)) + END + ), + 'attribute_value', JSON_BUILD_OBJECT( + 'id', av.id, + 'value', av.value, + 'fqn', fqns.fqn + ) + ) + ), '[]'::json) AS values + -- Join to get all action-attribute relationships for this resource value + FROM registered_resource_action_attribute_values rav + LEFT JOIN actions a on rav.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_values av on rav.attribute_value_id = av.id + LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id + -- Correlate to the outer query's resource value + WHERE rav.registered_resource_value_id = v.id +) action_attrs ON true -- required syntax for LATERAL joins WHERE (sqlc.narg('id')::uuid IS NULL OR r.id = sqlc.narg('id')::uuid) AND - (sqlc.narg('name')::text IS NULL OR r.name = sqlc.narg('name')::text) -GROUP BY r.id; + (sqlc.narg('name')::text IS NULL OR r.name = sqlc.narg('name')::text) AND + (sqlc.narg('namespace_id')::uuid IS NULL OR r.namespace_id = sqlc.narg('namespace_id')::uuid) AND + (sqlc.narg('namespace_fqn')::text IS NULL OR ns_fqns.fqn = sqlc.narg('namespace_fqn')::text) +GROUP BY r.id, n.id, ns_fqns.fqn +-- prefer non-namespaced over namespaced results (to support legacy behavior) +ORDER BY r.namespace_id NULLS FIRST +LIMIT 1; -- name: listRegisteredResources :many -WITH counted AS ( - SELECT COUNT(id) AS total - FROM registered_resources +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction +), +counted AS ( + SELECT COUNT(r.id) AS total + FROM registered_resources r + LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id + LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL + WHERE + (sqlc.narg('namespace_id')::uuid IS NULL OR r.namespace_id = sqlc.narg('namespace_id')::uuid) AND + (sqlc.narg('namespace_fqn')::text IS NULL OR ns_fqns.fqn = sqlc.narg('namespace_fqn')::text) ) SELECT r.id, r.name, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', r.metadata -> 'labels', 'created_at', r.created_at, 'updated_at', r.updated_at)) as metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + ELSE NULL END as namespace, -- Aggregate all values for this resource into a JSON array, filtering NULL entries JSON_AGG( JSON_BUILD_OBJECT( @@ -44,15 +135,25 @@ SELECT ) FILTER (WHERE v.id IS NOT NULL) as values, counted.total FROM registered_resources r +LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL CROSS JOIN counted +CROSS JOIN params p LEFT JOIN registered_resource_values v ON v.registered_resource_id = r.id -- Build a JSON array of action/attribute pairs for each resource value LEFT JOIN LATERAL ( - SELECT JSON_AGG( + -- COALESCE so a value with no mappings yields '[]' rather than SQL NULL, + -- giving consumers a consistent JSON array shape for action_attribute_values. + SELECT COALESCE(JSON_AGG( JSON_BUILD_OBJECT( 'action', JSON_BUILD_OBJECT( 'id', a.id, - 'name', a.name + 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + -- Namespace FQN is deterministic from the name, so build it inline + -- instead of joining attribute_fqns for it. + ELSE JSON_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', CONCAT('https://', ans.name)) + END ), 'attribute_value', JSON_BUILD_OBJECT( 'id', av.id, @@ -60,17 +161,29 @@ LEFT JOIN LATERAL ( 'fqn', fqns.fqn ) ) - ) AS values + ), '[]'::json) AS values -- Join to get all action-attribute relationships for this resource value FROM registered_resource_action_attribute_values rav LEFT JOIN actions a on rav.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id LEFT JOIN attribute_values av on rav.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id -- Correlate to the outer query's resource value WHERE rav.registered_resource_value_id = v.id ) action_attrs ON true -- required syntax for LATERAL joins -GROUP BY r.id, counted.total -LIMIT @limit_ +WHERE + (sqlc.narg('namespace_id')::uuid IS NULL OR r.namespace_id = sqlc.narg('namespace_id')::uuid) AND + (sqlc.narg('namespace_fqn')::text IS NULL OR ns_fqns.fqn = sqlc.narg('namespace_fqn')::text) +GROUP BY r.id, n.id, ns_fqns.fqn, counted.total, p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN r.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN r.name END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN r.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN r.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN r.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN r.updated_at END DESC, + r.id ASC +LIMIT @limit_ OFFSET @offset_; -- name: updateRegisteredResource :execrows @@ -99,6 +212,14 @@ SELECT v.registered_resource_id, v.value, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', v.metadata -> 'labels', 'created_at', v.created_at, 'updated_at', v.updated_at)) as metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + ELSE NULL END as namespace, + r.name as resource_name, JSON_AGG( JSON_BUILD_OBJECT( 'action', JSON_BUILD_OBJECT( @@ -114,6 +235,8 @@ SELECT ) FILTER (WHERE rav.id IS NOT NULL) as action_attribute_values FROM registered_resource_values v JOIN registered_resources r ON v.registered_resource_id = r.id +LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL LEFT JOIN registered_resource_action_attribute_values rav ON v.id = rav.registered_resource_value_id LEFT JOIN actions a on rav.action_id = a.id LEFT JOIN attribute_values av on rav.attribute_value_id = av.id @@ -121,8 +244,9 @@ LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id WHERE (sqlc.narg('id')::uuid IS NULL OR v.id = sqlc.narg('id')::uuid) AND (sqlc.narg('name')::text IS NULL OR r.name = sqlc.narg('name')::text) AND - (sqlc.narg('value')::text IS NULL OR v.value = sqlc.narg('value')::text) -GROUP BY v.id; + (sqlc.narg('value')::text IS NULL OR v.value = sqlc.narg('value')::text) AND + (sqlc.narg('namespace_fqn')::text IS NULL OR ns_fqns.fqn = sqlc.narg('namespace_fqn')::text) +GROUP BY v.id, r.name, n.id, ns_fqns.fqn; -- name: listRegisteredResourceValues :many WITH counted AS ( @@ -135,6 +259,14 @@ SELECT v.registered_resource_id, v.value, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', v.metadata -> 'labels', 'created_at', v.created_at, 'updated_at', v.updated_at)) as metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + ELSE NULL END as namespace, + r.name as resource_name, JSON_AGG( JSON_BUILD_OBJECT( 'action', JSON_BUILD_OBJECT( @@ -151,14 +283,17 @@ SELECT counted.total FROM registered_resource_values v JOIN registered_resources r ON v.registered_resource_id = r.id +LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL LEFT JOIN registered_resource_action_attribute_values rav ON v.id = rav.registered_resource_value_id LEFT JOIN actions a on rav.action_id = a.id LEFT JOIN attribute_values av on rav.attribute_value_id = av.id -LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id +LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id CROSS JOIN counted WHERE sqlc.narg('registered_resource_id')::uuid IS NULL OR v.registered_resource_id = sqlc.narg('registered_resource_id')::uuid -GROUP BY v.id, counted.total +GROUP BY v.id, r.name, n.id, ns_fqns.fqn, counted.total +ORDER BY v.created_at DESC LIMIT @limit_ OFFSET @offset_; @@ -172,14 +307,20 @@ WHERE id = $1; -- name: deleteRegisteredResourceValue :execrows DELETE FROM registered_resource_values WHERE id = $1; ----------------------------------------------------------------- +---------------------------------------------------------------- -- Registered Resource Action Attribute Values ---------------------------------------------------------------- +-- name: getRegisteredResourceNamespaceIDByValueID :one +SELECT rr.namespace_id +FROM registered_resources rr +JOIN registered_resource_values rrv ON rrv.registered_resource_id = rr.id +WHERE rrv.id = $1; + -- name: createRegisteredResourceActionAttributeValues :copyfrom INSERT INTO registered_resource_action_attribute_values (registered_resource_value_id, action_id, attribute_value_id) VALUES ($1, $2, $3); -- name: deleteRegisteredResourceActionAttributeValues :execrows DELETE FROM registered_resource_action_attribute_values -WHERE registered_resource_value_id = $1; \ No newline at end of file +WHERE registered_resource_value_id = $1; diff --git a/service/policy/db/queries/resource_mapping.sql b/service/policy/db/queries/resource_mapping.sql index 02052f8ed4..92611c909a 100644 --- a/service/policy/db/queries/resource_mapping.sql +++ b/service/policy/db/queries/resource_mapping.sql @@ -6,18 +6,25 @@ SELECT rmg.id, rmg.namespace_id, rmg.name, + CONCAT('https://', ns.name, '/resm/', rmg.name)::TEXT AS fqn, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', rmg.metadata -> 'labels', 'created_at', rmg.created_at, 'updated_at', rmg.updated_at)) as metadata, COUNT(*) OVER() AS total FROM resource_mapping_groups rmg +JOIN attribute_namespaces ns ON rmg.namespace_id = ns.id WHERE (sqlc.narg('namespace_id')::uuid IS NULL OR rmg.namespace_id = sqlc.narg('namespace_id')::uuid) +ORDER BY rmg.created_at DESC LIMIT @limit_ OFFSET @offset_; -- name: getResourceMappingGroup :one -SELECT id, namespace_id, name, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -FROM resource_mapping_groups -WHERE id = $1; +SELECT rmg.id, + rmg.namespace_id, + rmg.name, + CONCAT('https://', ns.name, '/resm/', rmg.name)::TEXT AS fqn, + JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', rmg.metadata -> 'labels', 'created_at', rmg.created_at, 'updated_at', rmg.updated_at)) as metadata +FROM resource_mapping_groups rmg +JOIN attribute_namespaces ns ON rmg.namespace_id = ns.id +WHERE rmg.id = $1; -- name: createResourceMappingGroup :one INSERT INTO resource_mapping_groups (namespace_id, name, metadata) @@ -45,20 +52,24 @@ SELECT JSON_BUILD_OBJECT('id', av.id, 'value', av.value, 'fqn', fqns.fqn) as attribute_value, m.terms, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', m.metadata -> 'labels', 'created_at', m.created_at, 'updated_at', m.updated_at)) as metadata, - JSON_STRIP_NULLS( - JSON_BUILD_OBJECT( - 'id', rmg.id, - 'name', rmg.name, - 'namespace_id', rmg.namespace_id - ) - ) AS group, + (CASE + WHEN rmg.id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', rmg.id, + 'name', rmg.name, + 'namespace_id', rmg.namespace_id, + 'fqn', CONCAT('https://', rmg_ns.name, '/resm/', rmg.name)::TEXT + ) + END)::JSON AS group, COUNT(*) OVER() AS total FROM resource_mappings m LEFT JOIN attribute_values av on m.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id LEFT JOIN resource_mapping_groups rmg ON m.group_id = rmg.id +LEFT JOIN attribute_namespaces rmg_ns ON rmg.namespace_id = rmg_ns.id WHERE (sqlc.narg('group_id')::uuid IS NULL OR m.group_id = sqlc.narg('group_id')::uuid) -GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id +GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id, rmg_ns.name +ORDER BY m.created_at DESC LIMIT @limit_ OFFSET @offset_; @@ -71,6 +82,7 @@ WITH groups_cte AS ( 'id', g.id, 'namespace_id', g.namespace_id, 'name', g.name, + 'fqn', CONCAT('https://', ns.name, '/resm/', g.name)::TEXT, 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT( 'labels', g.metadata -> 'labels', 'created_at', g.created_at, @@ -90,7 +102,8 @@ SELECT FROM resource_mappings m JOIN groups_cte g ON m.group_id = g.id JOIN attribute_values av on m.attribute_value_id = av.id -JOIN attribute_fqns fqns on av.id = fqns.value_id; +JOIN attribute_fqns fqns on av.id = fqns.value_id +ORDER BY m.created_at DESC; -- name: getResourceMapping :one SELECT @@ -98,12 +111,22 @@ SELECT JSON_BUILD_OBJECT('id', av.id, 'value', av.value, 'fqn', fqns.fqn) as attribute_value, m.terms, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', m.metadata -> 'labels', 'created_at', m.created_at, 'updated_at', m.updated_at)) as metadata, - COALESCE(m.group_id::TEXT, '')::TEXT as group_id + (CASE + WHEN rmg.id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', rmg.id, + 'name', rmg.name, + 'namespace_id', rmg.namespace_id, + 'fqn', CONCAT('https://', rmg_ns.name, '/resm/', rmg.name)::TEXT + ) + END)::JSON AS group FROM resource_mappings m LEFT JOIN attribute_values av on m.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id +LEFT JOIN resource_mapping_groups rmg ON m.group_id = rmg.id +LEFT JOIN attribute_namespaces rmg_ns ON rmg.namespace_id = rmg_ns.id WHERE m.id = $1 -GROUP BY av.id, m.id, fqns.fqn; +GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id, rmg_ns.name; -- name: createResourceMapping :one INSERT INTO resource_mappings (attribute_value_id, terms, metadata, group_id) diff --git a/service/policy/db/queries/subject_mappings.sql b/service/policy/db/queries/subject_mappings.sql index 9c3d38ddc4..3b75417519 100644 --- a/service/policy/db/queries/subject_mappings.sql +++ b/service/policy/db/queries/subject_mappings.sql @@ -1,33 +1,60 @@ ----------------------------------------------------------------- +---------------------------------------------------------------- -- SUBJECT CONDITION SETS ---------------------------------------------------------------- -- name: listSubjectConditionSets :many -WITH counted AS ( - SELECT COUNT(scs.id) AS total - FROM subject_condition_set scs +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction ) SELECT scs.id, scs.condition, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)) as metadata, - counted.total + CASE + WHEN scs.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', n.id, 'name', n.name, 'fqn', ns_fqns.fqn) + END AS namespace, + COUNT(*) OVER() as total FROM subject_condition_set scs -CROSS JOIN counted -LIMIT @limit_ -OFFSET @offset_; +LEFT JOIN attribute_namespaces n ON n.id = scs.namespace_id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +CROSS JOIN params p +WHERE + (sqlc.narg('namespace_id')::uuid IS NULL AND sqlc.narg('namespace_fqn')::text IS NULL) + OR scs.namespace_id = sqlc.narg('namespace_id')::uuid + OR ns_fqns.fqn = sqlc.narg('namespace_fqn')::text +ORDER BY + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN scs.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN scs.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN scs.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN scs.updated_at END DESC, + scs.id ASC +LIMIT @limit_ +OFFSET @offset_; -- name: getSubjectConditionSet :one SELECT - id, - condition, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -FROM subject_condition_set -WHERE id = $1; + scs.id, + scs.condition, + JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)) as metadata, + CASE + WHEN scs.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', n.id, 'name', n.name, 'fqn', ns_fqns.fqn) + END AS namespace +FROM subject_condition_set scs +LEFT JOIN attribute_namespaces n ON n.id = scs.namespace_id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +WHERE scs.id = $1; -- name: createSubjectConditionSet :one -INSERT INTO subject_condition_set (condition, metadata) -VALUES ($1, $2) +INSERT INTO subject_condition_set (condition, metadata, namespace_id) +VALUES ( + @condition, + @metadata, + sqlc.narg('namespace_id')::uuid +) RETURNING id; -- name: updateSubjectConditionSet :execrows @@ -45,28 +72,49 @@ DELETE FROM subject_condition_set WHERE id NOT IN (SELECT DISTINCT sm.subject_condition_set_id FROM subject_mappings sm) RETURNING id; ----------------------------------------------------------------- +---------------------------------------------------------------- -- SUBJECT MAPPINGS ---------------------------------------------------------------- -- name: listSubjectMappings :many -WITH subject_actions AS ( +WITH params AS ( + SELECT + COALESCE(NULLIF(@sort_field::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF(@sort_direction::text, ''), 'DESC') AS resolved_direction +), +subject_actions AS ( SELECT sma.subject_mapping_id, COALESCE( - JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = TRUE), + JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FILTER (WHERE a.is_standard = TRUE), '[]'::JSONB ) AS standard_actions, COALESCE( - JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = FALSE), + JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FILTER (WHERE a.is_standard = FALSE), '[]'::JSONB ) AS custom_actions FROM subject_mapping_actions sma JOIN actions a ON sma.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL GROUP BY sma.subject_mapping_id ), counted AS ( SELECT COUNT(sm.id) AS total FROM subject_mappings sm + LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id + LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL + WHERE + (sqlc.narg('namespace_id')::uuid IS NULL AND sqlc.narg('namespace_fqn')::text IS NULL) + OR sm.namespace_id = sqlc.narg('namespace_id')::uuid + OR sm_ns_fqns.fqn = sqlc.narg('namespace_fqn')::text ) SELECT sm.id, @@ -76,7 +124,11 @@ SELECT JSON_BUILD_OBJECT( 'id', scs.id, 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata->'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)), - 'subject_sets', scs.condition + 'subject_sets', scs.condition, + 'namespace', CASE + WHEN scs.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', scs_ns.id, 'name', scs_ns.name, 'fqn', scs_ns_fqns.fqn) + END ) AS subject_condition_set, JSON_BUILD_OBJECT( 'id', av.id, @@ -84,22 +136,44 @@ SELECT 'active', av.active, 'fqn', fqns.fqn ) AS attribute_value, + CASE + WHEN sm.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', sm_ns.id, 'name', sm_ns.name, 'fqn', sm_ns_fqns.fqn) + END AS namespace, counted.total FROM subject_mappings sm CROSS JOIN counted +CROSS JOIN params p LEFT JOIN subject_actions sa ON sm.id = sa.subject_mapping_id LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id +LEFT JOIN attribute_namespaces scs_ns ON scs_ns.id = scs.namespace_id +LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL +LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id +LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL +WHERE + (sqlc.narg('namespace_id')::uuid IS NULL AND sqlc.narg('namespace_fqn')::text IS NULL) + OR sm.namespace_id = sqlc.narg('namespace_id')::uuid + OR sm_ns_fqns.fqn = sqlc.narg('namespace_fqn')::text GROUP BY sm.id, sa.standard_actions, sa.custom_actions, - sm.metadata, sm.created_at, sm.updated_at, -- for metadata object - scs.id, scs.metadata, scs.created_at, scs.updated_at, scs.condition, -- for subject_condition_set object - av.id, av.value, av.active, -- for attribute_value object + sm.metadata, sm.created_at, sm.updated_at, + scs.id, scs.metadata, scs.created_at, scs.updated_at, scs.condition, scs.namespace_id, + scs_ns.id, scs_ns.name, scs_ns_fqns.fqn, + sm_ns.id, sm_ns.name, sm_ns_fqns.fqn, + av.id, av.value, av.active, fqns.fqn, - counted.total + counted.total, + p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN sm.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN sm.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN sm.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN sm.updated_at END DESC, + sm.id ASC LIMIT @limit_ OFFSET @offset_; @@ -107,44 +181,78 @@ OFFSET @offset_; SELECT sm.id, ( - SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) + SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FROM actions a JOIN subject_mapping_actions sma ON sma.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL WHERE sma.subject_mapping_id = sm.id AND a.is_standard = TRUE ) AS standard_actions, ( - SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) + SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FROM actions a JOIN subject_mapping_actions sma ON sma.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL WHERE sma.subject_mapping_id = sm.id AND a.is_standard = FALSE ) AS custom_actions, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', sm.metadata -> 'labels', 'created_at', sm.created_at, 'updated_at', sm.updated_at)) AS metadata, JSON_BUILD_OBJECT( 'id', scs.id, 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)), - 'subject_sets', scs.condition + 'subject_sets', scs.condition, + 'namespace', CASE + WHEN scs.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', scs_ns.id, 'name', scs_ns.name, 'fqn', scs_ns_fqns.fqn) + END ) AS subject_condition_set, - JSON_BUILD_OBJECT('id', av.id,'value', av.value,'active', av.active) AS attribute_value + JSON_BUILD_OBJECT('id', av.id,'value', av.value,'active', av.active) AS attribute_value, + CASE + WHEN sm.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', sm_ns.id, 'name', sm_ns.name, 'fqn', sm_ns_fqns.fqn) + END AS namespace FROM subject_mappings sm LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id +LEFT JOIN attribute_namespaces scs_ns ON scs_ns.id = scs.namespace_id +LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL +LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id +LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL WHERE sm.id = $1 -GROUP BY av.id, sm.id, scs.id; +GROUP BY av.id, sm.id, scs.id, scs.namespace_id, scs_ns.id, scs_ns.name, scs_ns_fqns.fqn, sm_ns.id, sm_ns.name, sm_ns_fqns.fqn; -- name: matchSubjectMappings :many WITH subject_actions AS ( SELECT sma.subject_mapping_id, COALESCE( - JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = TRUE), + JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FILTER (WHERE a.is_standard = TRUE), '[]'::JSONB ) AS standard_actions, COALESCE( - JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = FALSE), + JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FILTER (WHERE a.is_standard = FALSE), '[]'::JSONB ) AS custom_actions FROM subject_mapping_actions sma JOIN actions a ON sma.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL GROUP BY sma.subject_mapping_id ) SELECT @@ -185,14 +293,20 @@ WITH inserted_mapping AS ( INSERT INTO subject_mappings ( attribute_value_id, metadata, - subject_condition_set_id + subject_condition_set_id, + namespace_id + ) + VALUES ( + @attribute_value_id, + @metadata, + @subject_condition_set_id, + sqlc.narg('namespace_id')::uuid ) - VALUES ($1, $2, $3) RETURNING id ), inserted_actions AS ( INSERT INTO subject_mapping_actions (subject_mapping_id, action_id) - SELECT + SELECT (SELECT id FROM inserted_mapping), unnest(sqlc.arg('action_ids')::uuid[]) ) diff --git a/service/policy/db/registered_resources.go b/service/policy/db/registered_resources.go index 73bfe1e579..a24c25564f 100644 --- a/service/policy/db/registered_resources.go +++ b/service/policy/db/registered_resources.go @@ -3,10 +3,12 @@ package db import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "strings" + "github.com/jackc/pgx/v5/pgtype" "github.com/opentdf/platform/lib/identifier" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" @@ -57,28 +59,94 @@ func unmarshalRegisteredResourceActionAttributeValuesProto(actionAttrValuesJSON return nil } +// hydrateNamespaceFromInterface converts a nullable namespace interface{} (from CASE WHEN SQL) +// to a *policy.Namespace. Returns nil if the namespace is NULL (legacy RRs without namespace). +func hydrateNamespaceFromInterface(nsRaw interface{}) (*policy.Namespace, error) { + if nsRaw == nil { + return nil, nil //nolint:nilnil // nil namespace is valid for legacy RRs without namespace + } + + var nsBytes []byte + switch v := nsRaw.(type) { + case []byte: + nsBytes = v + case map[string]interface{}: + var err error + nsBytes, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("failed to marshal namespace map: %w", err) + } + default: + return nil, fmt.Errorf("unexpected namespace type: %T", nsRaw) + } + + ns := &policy.Namespace{} + if err := unmarshalNamespace(nsBytes, ns); err != nil { + return nil, fmt.Errorf("failed to unmarshal registered resource namespace: %w", err) + } + return ns, nil +} + +func registeredResourceValueFQN(namespace *policy.Namespace, resourceName, value string) string { + if namespace == nil { + // Legacy registered resources do not have a namespace; identifier.FQN preserves that as https://reg_res//value/. + return (&identifier.FullyQualifiedRegisteredResourceValue{ + Name: resourceName, + Value: value, + }).FQN() + } + + return (&identifier.FullyQualifiedRegisteredResourceValue{ + Namespace: namespace.GetName(), + Name: resourceName, + Value: value, + }).FQN() +} + +func hydrateRegisteredResourceValueFQNs(values []*policy.RegisteredResourceValue, namespace *policy.Namespace, resourceName string) { + for _, value := range values { + value.Fqn = registeredResourceValueFQN(namespace, resourceName, value.GetValue()) + } +} + /// /// Registered Resources /// func (c PolicyDBClient) CreateRegisteredResource(ctx context.Context, r *registeredresources.CreateRegisteredResourceRequest) (*policy.RegisteredResource, error) { name := strings.ToLower(r.GetName()) + namespaceID := r.GetNamespaceId() + namespaceFqn := r.GetNamespaceFqn() + + useID := len(namespaceID) > 0 + parsedID := pgtypeUUID(namespaceID) + if useID && !parsedID.Valid { + return nil, db.ErrUUIDInvalid + } + metadataJSON, _, err := db.MarshalCreateMetadata(r.GetMetadata()) if err != nil { return nil, err } - createdID, err := c.queries.createRegisteredResource(ctx, createRegisteredResourceParams{ - Name: name, - Metadata: metadataJSON, + row, err := c.queries.createRegisteredResource(ctx, createRegisteredResourceParams{ + NamespaceID: pgtypeUUID(namespaceID), + NamespaceFqn: pgtypeText(namespaceFqn), + Name: name, + Metadata: metadataJSON, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) } + // Validate namespace JSON is parseable (actual namespace retrieved via GetRegisteredResource below) + if _, err := hydrateNamespaceFromInterface(row.Namespace); err != nil { + return nil, err + } + for _, v := range r.GetValues() { req := ®isteredresources.CreateRegisteredResourceValueRequest{ - ResourceId: createdID, + ResourceId: row.ID, Value: v, } _, err := c.CreateRegisteredResourceValue(ctx, req) @@ -89,7 +157,7 @@ func (c PolicyDBClient) CreateRegisteredResource(ctx context.Context, r *registe return c.GetRegisteredResource(ctx, ®isteredresources.GetRegisteredResourceRequest{ Identifier: ®isteredresources.GetRegisteredResourceRequest_Id{ - Id: createdID, + Id: row.ID, }, }) } @@ -102,6 +170,16 @@ func (c PolicyDBClient) GetRegisteredResource(ctx context.Context, r *registered params.ID = pgtypeUUID(r.GetId()) case r.GetName() != "": params.Name = pgtypeText(strings.ToLower(r.GetName())) + namespaceID := r.GetNamespaceId() + if len(namespaceID) > 0 { + parsedID := pgtypeUUID(namespaceID) + if !parsedID.Valid { + return nil, db.ErrUUIDInvalid + } + params.NamespaceID = parsedID + } else if r.GetNamespaceFqn() != "" { + params.NamespaceFqn = pgtypeText(r.GetNamespaceFqn()) + } default: return nil, db.ErrSelectIdentifierInvalid } @@ -116,20 +194,34 @@ func (c PolicyDBClient) GetRegisteredResource(ctx context.Context, r *registered return nil, err } + namespace, err := hydrateNamespaceFromInterface(rr.Namespace) + if err != nil { + return nil, err + } + values := []*policy.RegisteredResourceValue{} if err = unmarshalRegisteredResourceValuesProto(rr.Values, &values); err != nil { return nil, err } + hydrateRegisteredResourceValueFQNs(values, namespace, rr.Name) return &policy.RegisteredResource{ - Id: rr.ID, - Name: rr.Name, - Metadata: metadata, - Values: values, + Id: rr.ID, + Name: rr.Name, + Metadata: metadata, + Namespace: namespace, + Values: values, }, nil } func (c PolicyDBClient) ListRegisteredResources(ctx context.Context, r *registeredresources.ListRegisteredResourcesRequest) (*registeredresources.ListRegisteredResourcesResponse, error) { + namespaceID := r.GetNamespaceId() + useID := len(namespaceID) > 0 + parsedID := pgtypeUUID(namespaceID) + if useID && !parsedID.Valid { + return nil, db.ErrUUIDInvalid + } + limit, offset := c.getRequestedLimitOffset(r.GetPagination()) maxLimit := c.listCfg.limitMax @@ -137,9 +229,15 @@ func (c PolicyDBClient) ListRegisteredResources(ctx context.Context, r *register return nil, db.ErrListLimitTooLarge } + sortField, sortDirection := GetRegisteredResourcesSortParams(r.GetSort()) + list, err := c.queries.listRegisteredResources(ctx, listRegisteredResourcesParams{ - Limit: limit, - Offset: offset, + NamespaceID: parsedID, + NamespaceFqn: pgtypeText(r.GetNamespaceFqn()), + Limit: limit, + Offset: offset, + SortField: sortField, + SortDirection: sortDirection, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -153,16 +251,23 @@ func (c PolicyDBClient) ListRegisteredResources(ctx context.Context, r *register return nil, err } + namespace, err := hydrateNamespaceFromInterface(r.Namespace) + if err != nil { + return nil, err + } + values := []*policy.RegisteredResourceValue{} if err = unmarshalRegisteredResourceValuesProto(r.Values, &values); err != nil { return nil, err } + hydrateRegisteredResourceValueFQNs(values, namespace, r.Name) rrList[i] = &policy.RegisteredResource{ - Id: r.ID, - Name: r.Name, - Metadata: metadata, - Values: values, + Id: r.ID, + Name: r.Name, + Metadata: metadata, + Namespace: namespace, + Values: values, } } @@ -281,6 +386,9 @@ func (c PolicyDBClient) GetRegisteredResourceValue(ctx context.Context, r *regis } params.Name = pgtypeText(parsed.Name) params.Value = pgtypeText(parsed.Value) + if parsed.Namespace != "" { + params.NamespaceFqn = pgtypeText("https://" + parsed.Namespace) + } default: // unexpected type return nil, db.ErrSelectIdentifierInvalid @@ -296,6 +404,11 @@ func (c PolicyDBClient) GetRegisteredResourceValue(ctx context.Context, r *regis return nil, err } + namespace, err := hydrateNamespaceFromInterface(rv.Namespace) + if err != nil { + return nil, err + } + actionAttrValues := []*policy.RegisteredResourceValue_ActionAttributeValue{} if err = unmarshalRegisteredResourceActionAttributeValuesProto(rv.ActionAttributeValues, &actionAttrValues); err != nil { return nil, err @@ -304,9 +417,12 @@ func (c PolicyDBClient) GetRegisteredResourceValue(ctx context.Context, r *regis return &policy.RegisteredResourceValue{ Id: rv.ID, Value: rv.Value, + Fqn: registeredResourceValueFQN(namespace, rv.ResourceName, rv.Value), Metadata: metadata, Resource: &policy.RegisteredResource{ - Id: rv.RegisteredResourceID, + Id: rv.RegisteredResourceID, + Name: rv.ResourceName, + Namespace: namespace, }, ActionAttributeValues: actionAttrValues, }, nil @@ -371,6 +487,11 @@ func (c PolicyDBClient) ListRegisteredResourceValues(ctx context.Context, r *reg return nil, err } + namespace, err := hydrateNamespaceFromInterface(r.Namespace) + if err != nil { + return nil, err + } + actionAttrValues := []*policy.RegisteredResourceValue_ActionAttributeValue{} if err = unmarshalRegisteredResourceActionAttributeValuesProto(r.ActionAttributeValues, &actionAttrValues); err != nil { return nil, err @@ -379,9 +500,12 @@ func (c PolicyDBClient) ListRegisteredResourceValues(ctx context.Context, r *reg rvList[i] = &policy.RegisteredResourceValue{ Id: r.ID, Value: r.Value, + Fqn: registeredResourceValueFQN(namespace, r.ResourceName, r.Value), Metadata: metadata, Resource: &policy.RegisteredResource{ - Id: r.RegisteredResourceID, + Id: r.RegisteredResourceID, + Name: r.ResourceName, + Namespace: namespace, }, ActionAttributeValues: actionAttrValues, } @@ -456,6 +580,15 @@ func (c PolicyDBClient) UpdateRegisteredResourceValue(ctx context.Context, r *re } func (c PolicyDBClient) DeleteRegisteredResourceValue(ctx context.Context, id string) (*policy.RegisteredResourceValue, error) { + deleted, err := c.GetRegisteredResourceValue(ctx, ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_Id{ + Id: id, + }, + }) + if err != nil { + return nil, err + } + count, err := c.queries.deleteRegisteredResourceValue(ctx, id) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -464,9 +597,7 @@ func (c PolicyDBClient) DeleteRegisteredResourceValue(ctx context.Context, id st return nil, db.ErrNotFound } - return &policy.RegisteredResourceValue{ - Id: id, - }, nil + return deleted, nil } /// @@ -478,37 +609,22 @@ func (c PolicyDBClient) createRegisteredResourceActionAttributeValues(ctx contex return nil } + // Look up the namespace_id of the registered resource for same-namespace enforcement + nsUUID, err := c.queries.getRegisteredResourceNamespaceIDByValueID(ctx, registeredResourceValueID) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + resourceNamespaceID := UUIDToString(nsUUID) + createActionAttributeValueParams := make([]createRegisteredResourceActionAttributeValuesParams, len(actionAttrValues)) - var actionID, attributeValueID string for i, aav := range actionAttrValues { - switch identifier := aav.GetActionIdentifier().(type) { - case *registeredresources.ActionAttributeValue_ActionId: - actionID = identifier.ActionId - case *registeredresources.ActionAttributeValue_ActionName: - a, err := c.queries.getAction(ctx, getActionParams{ - Name: pgtypeText(strings.ToLower(identifier.ActionName)), - }) - if err != nil { - return db.WrapIfKnownInvalidQueryErr(err) - } - actionID = a.ID - default: - return db.ErrSelectIdentifierInvalid - } - - switch identifier := aav.GetAttributeValueIdentifier().(type) { - case *registeredresources.ActionAttributeValue_AttributeValueId: - attributeValueID = identifier.AttributeValueId - case *registeredresources.ActionAttributeValue_AttributeValueFqn: - av, err := c.queries.getAttributeValue(ctx, getAttributeValueParams{ - Fqn: pgtypeText(strings.ToLower(identifier.AttributeValueFqn)), - }) - if err != nil { - return db.WrapIfKnownInvalidQueryErr(err) - } - attributeValueID = av.ID - default: - return db.ErrSelectIdentifierInvalid + actionID, attributeValueID, err := c.resolveRegResAAV(ctx, aav, nsUUID) + if err != nil { + return err + } + err = c.validateRRAAVNamespaceConsistency(ctx, resourceNamespaceID, attributeValueID, actionID) + if err != nil { + return err } createActionAttributeValueParams[i] = createRegisteredResourceActionAttributeValuesParams{ @@ -518,6 +634,24 @@ func (c PolicyDBClient) createRegisteredResourceActionAttributeValues(ctx contex } } + // Same-namespace enforcement (batch): all attribute values must belong to the same namespace as the registered resource + if resourceNamespaceID != "" { + avIDs := make([]string, len(createActionAttributeValueParams)) + for i, p := range createActionAttributeValueParams { + avIDs[i] = p.AttributeValueID + } + rows, err := c.queries.getAttributeValueNamespaceIDs(ctx, avIDs) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + for _, row := range rows { + if row.NamespaceID != resourceNamespaceID { + return fmt.Errorf("attribute value %s belongs to namespace %s, but registered resource belongs to namespace %s: %w", + row.AttributeValueID, row.NamespaceID, resourceNamespaceID, db.ErrForeignKeyViolation) + } + } + } + count, err := c.queries.createRegisteredResourceActionAttributeValues(ctx, createActionAttributeValueParams) if err != nil { return db.WrapIfKnownInvalidQueryErr(err) @@ -528,3 +662,88 @@ func (c PolicyDBClient) createRegisteredResourceActionAttributeValues(ctx contex return nil } + +// resolveRegResAAV parses the action from a createRegisteredResourceActionAttributeValues input +// resolving actions by name within the given namespace and collecting existing action ID. +func (c PolicyDBClient) resolveRegResAAV(ctx context.Context, aav *registeredresources.ActionAttributeValue, parsedNamespaceID pgtype.UUID) (string, string, error) { + var actionID, attributeValueID string + switch ident := aav.GetActionIdentifier().(type) { + case *registeredresources.ActionAttributeValue_ActionId: + actionID = ident.ActionId + case *registeredresources.ActionAttributeValue_ActionName: + ids, err := c.resolveActionNameIDs(ctx, []string{strings.ToLower(ident.ActionName)}, parsedNamespaceID) + if err != nil { + return "", "", err + } + if len(ids) == 0 { + return "", "", db.ErrMissingValue + } + actionID = ids[0] + default: + return "", "", db.ErrSelectIdentifierInvalid + } + + switch ident := aav.GetAttributeValueIdentifier().(type) { + case *registeredresources.ActionAttributeValue_AttributeValueId: + attributeValueID = ident.AttributeValueId + case *registeredresources.ActionAttributeValue_AttributeValueFqn: + av, err := c.queries.getAttributeValue(ctx, getAttributeValueParams{ + Fqn: pgtypeText(strings.ToLower(ident.AttributeValueFqn)), + }) + if err != nil { + return "", "", db.WrapIfKnownInvalidQueryErr(err) + } + attributeValueID = av.ID + default: + return "", "", db.ErrSelectIdentifierInvalid + } + + return actionID, attributeValueID, nil +} + +// validateRRAAVNamespaceConsistency ensures that action +// belongs to the same namespace as the RR val being created. When the RR is namespaced, +// the attribute value must also be in that namespace. When unnamespaced, the attribute value +// may have any namespace, but actions must be unnamespaced. +func (c PolicyDBClient) validateRRAAVNamespaceConsistency( + ctx context.Context, + targetNsID string, + attributeValueID string, + actionID string, +) error { + // Attribute value namespace check only applies when the RR is namespaced + if targetNsID != "" { + av, err := c.GetAttributeValue(ctx, attributeValueID) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + attr, err := c.GetAttribute(ctx, av.GetAttribute().GetId()) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + if attr.GetNamespace().GetId() != targetNsID { + return errors.Join(db.ErrNamespaceMismatch, + fmt.Errorf("attribute value namespace [%s] does not match the specified registered resource namespace [%s]", attr.GetNamespace().GetId(), targetNsID)) + } + } + + // Action namespace must match the RR namespace (or be unnamespaced when RR is unnamespaced). + if actionID != "" { + actionRows, err := c.queries.getActionsByIDs(ctx, []string{actionID}) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + if len(actionRows) == 0 { + return errors.Join(db.ErrMissingValue, fmt.Errorf("action [%s] was not found", actionID)) + } + for _, a := range actionRows { + actionNsID := UUIDToString(a.NamespaceID) + if actionNsID != targetNsID { + return errors.Join(db.ErrNamespaceMismatch, + fmt.Errorf("action [%s] namespace [%s] does not match the specified registered resource namespace [%s]", a.ID, actionNsID, targetNsID)) + } + } + } + + return nil +} diff --git a/service/policy/db/registered_resources.sql.go b/service/policy/db/registered_resources.sql.go index c3a2275667..f2d85ee96b 100644 --- a/service/policy/db/registered_resources.sql.go +++ b/service/policy/db/registered_resources.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: registered_resources.sql package db @@ -13,28 +13,103 @@ import ( const createRegisteredResource = `-- name: createRegisteredResource :one -INSERT INTO registered_resources (name, metadata) -VALUES ($1, $2) -RETURNING id +WITH inserted AS ( + INSERT INTO registered_resources (namespace_id, name, metadata) + SELECT + COALESCE($1::uuid, fqns.namespace_id), + $2, + $3 + FROM ( + SELECT + $1::uuid as direct_namespace_id + ) direct + LEFT JOIN attribute_fqns fqns ON fqns.fqn = $4::text AND $1::text IS NULL + WHERE + ($1::text IS NOT NULL AND direct.direct_namespace_id IS NOT NULL) OR + ($4::text IS NOT NULL AND fqns.namespace_id IS NOT NULL) OR + ($1::text IS NULL AND $4::text IS NULL) + RETURNING id, namespace_id, name, metadata +) +SELECT + i.id, + i.name, + i.metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', fqns.fqn + ) + ELSE NULL END as namespace +FROM inserted i +LEFT JOIN attribute_namespaces n ON i.namespace_id = n.id +LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL ` type createRegisteredResourceParams struct { - Name string `json:"name"` - Metadata []byte `json:"metadata"` + NamespaceID pgtype.UUID `json:"namespace_id"` + Name string `json:"name"` + Metadata []byte `json:"metadata"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` +} + +type createRegisteredResourceRow struct { + ID string `json:"id"` + Name string `json:"name"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` } // -------------------------------------------------------------- // REGISTERED RESOURCES // -------------------------------------------------------------- // -// INSERT INTO registered_resources (name, metadata) -// VALUES ($1, $2) -// RETURNING id -func (q *Queries) createRegisteredResource(ctx context.Context, arg createRegisteredResourceParams) (string, error) { - row := q.db.QueryRow(ctx, createRegisteredResource, arg.Name, arg.Metadata) - var id string - err := row.Scan(&id) - return id, err +// WITH inserted AS ( +// INSERT INTO registered_resources (namespace_id, name, metadata) +// SELECT +// COALESCE($1::uuid, fqns.namespace_id), +// $2, +// $3 +// FROM ( +// SELECT +// $1::uuid as direct_namespace_id +// ) direct +// LEFT JOIN attribute_fqns fqns ON fqns.fqn = $4::text AND $1::text IS NULL +// WHERE +// ($1::text IS NOT NULL AND direct.direct_namespace_id IS NOT NULL) OR +// ($4::text IS NOT NULL AND fqns.namespace_id IS NOT NULL) OR +// ($1::text IS NULL AND $4::text IS NULL) +// RETURNING id, namespace_id, name, metadata +// ) +// SELECT +// i.id, +// i.name, +// i.metadata, +// CASE WHEN n.id IS NOT NULL THEN +// JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', fqns.fqn +// ) +// ELSE NULL END as namespace +// FROM inserted i +// LEFT JOIN attribute_namespaces n ON i.namespace_id = n.id +// LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL +func (q *Queries) createRegisteredResource(ctx context.Context, arg createRegisteredResourceParams) (createRegisteredResourceRow, error) { + row := q.db.QueryRow(ctx, createRegisteredResource, + arg.NamespaceID, + arg.Name, + arg.Metadata, + arg.NamespaceFqn, + ) + var i createRegisteredResourceRow + err := row.Scan( + &i.ID, + &i.Name, + &i.Metadata, + &i.Namespace, + ) + return i, err } type createRegisteredResourceActionAttributeValuesParams struct { @@ -122,68 +197,197 @@ SELECT r.id, r.name, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', r.metadata -> 'labels', 'created_at', r.created_at, 'updated_at', r.updated_at)) as metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + ELSE NULL END as namespace, JSON_AGG( JSON_BUILD_OBJECT( 'id', v.id, - 'value', v.value + 'value', v.value, + 'action_attribute_values', action_attrs.values ) ) FILTER (WHERE v.id IS NOT NULL) as values FROM registered_resources r +LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL LEFT JOIN registered_resource_values v ON v.registered_resource_id = r.id +LEFT JOIN LATERAL ( + -- COALESCE so a value with no mappings yields '[]' rather than SQL NULL, + -- giving consumers a consistent JSON array shape for action_attribute_values. + SELECT COALESCE(JSON_AGG( + JSON_BUILD_OBJECT( + 'action', JSON_BUILD_OBJECT( + 'id', a.id, + 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + -- Namespace FQN is deterministic from the name, so build it inline + -- instead of joining attribute_fqns for it. + ELSE JSON_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', CONCAT('https://', ans.name)) + END + ), + 'attribute_value', JSON_BUILD_OBJECT( + 'id', av.id, + 'value', av.value, + 'fqn', fqns.fqn + ) + ) + ), '[]'::json) AS values + -- Join to get all action-attribute relationships for this resource value + FROM registered_resource_action_attribute_values rav + LEFT JOIN actions a on rav.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_values av on rav.attribute_value_id = av.id + LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id + -- Correlate to the outer query's resource value + WHERE rav.registered_resource_value_id = v.id +) action_attrs ON true -- required syntax for LATERAL joins WHERE ($1::uuid IS NULL OR r.id = $1::uuid) AND - ($2::text IS NULL OR r.name = $2::text) -GROUP BY r.id + ($2::text IS NULL OR r.name = $2::text) AND + ($3::uuid IS NULL OR r.namespace_id = $3::uuid) AND + ($4::text IS NULL OR ns_fqns.fqn = $4::text) +GROUP BY r.id, n.id, ns_fqns.fqn +ORDER BY r.namespace_id NULLS FIRST +LIMIT 1 ` type getRegisteredResourceParams struct { - ID pgtype.UUID `json:"id"` - Name pgtype.Text `json:"name"` + ID pgtype.UUID `json:"id"` + Name pgtype.Text `json:"name"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` } type getRegisteredResourceRow struct { - ID string `json:"id"` - Name string `json:"name"` - Metadata []byte `json:"metadata"` - Values []byte `json:"values"` + ID string `json:"id"` + Name string `json:"name"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` + Values []byte `json:"values"` } -// getRegisteredResource +// Build a JSON array of action/attribute pairs for each resource value +// prefer non-namespaced over namespaced results (to support legacy behavior) // // SELECT // r.id, // r.name, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', r.metadata -> 'labels', 'created_at', r.created_at, 'updated_at', r.updated_at)) as metadata, +// CASE WHEN n.id IS NOT NULL THEN +// JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', ns_fqns.fqn +// ) +// ELSE NULL END as namespace, // JSON_AGG( // JSON_BUILD_OBJECT( // 'id', v.id, -// 'value', v.value +// 'value', v.value, +// 'action_attribute_values', action_attrs.values // ) // ) FILTER (WHERE v.id IS NOT NULL) as values // FROM registered_resources r +// LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL // LEFT JOIN registered_resource_values v ON v.registered_resource_id = r.id +// LEFT JOIN LATERAL ( +// -- COALESCE so a value with no mappings yields '[]' rather than SQL NULL, +// -- giving consumers a consistent JSON array shape for action_attribute_values. +// SELECT COALESCE(JSON_AGG( +// JSON_BUILD_OBJECT( +// 'action', JSON_BUILD_OBJECT( +// 'id', a.id, +// 'name', a.name, +// 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL +// -- Namespace FQN is deterministic from the name, so build it inline +// -- instead of joining attribute_fqns for it. +// ELSE JSON_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', CONCAT('https://', ans.name)) +// END +// ), +// 'attribute_value', JSON_BUILD_OBJECT( +// 'id', av.id, +// 'value', av.value, +// 'fqn', fqns.fqn +// ) +// ) +// ), '[]'::json) AS values +// -- Join to get all action-attribute relationships for this resource value +// FROM registered_resource_action_attribute_values rav +// LEFT JOIN actions a on rav.action_id = a.id +// LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id +// LEFT JOIN attribute_values av on rav.attribute_value_id = av.id +// LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id +// -- Correlate to the outer query's resource value +// WHERE rav.registered_resource_value_id = v.id +// ) action_attrs ON true -- required syntax for LATERAL joins // WHERE // ($1::uuid IS NULL OR r.id = $1::uuid) AND -// ($2::text IS NULL OR r.name = $2::text) -// GROUP BY r.id +// ($2::text IS NULL OR r.name = $2::text) AND +// ($3::uuid IS NULL OR r.namespace_id = $3::uuid) AND +// ($4::text IS NULL OR ns_fqns.fqn = $4::text) +// GROUP BY r.id, n.id, ns_fqns.fqn +// ORDER BY r.namespace_id NULLS FIRST +// LIMIT 1 func (q *Queries) getRegisteredResource(ctx context.Context, arg getRegisteredResourceParams) (getRegisteredResourceRow, error) { - row := q.db.QueryRow(ctx, getRegisteredResource, arg.ID, arg.Name) + row := q.db.QueryRow(ctx, getRegisteredResource, + arg.ID, + arg.Name, + arg.NamespaceID, + arg.NamespaceFqn, + ) var i getRegisteredResourceRow err := row.Scan( &i.ID, &i.Name, &i.Metadata, + &i.Namespace, &i.Values, ) return i, err } +const getRegisteredResourceNamespaceIDByValueID = `-- name: getRegisteredResourceNamespaceIDByValueID :one + +SELECT rr.namespace_id +FROM registered_resources rr +JOIN registered_resource_values rrv ON rrv.registered_resource_id = rr.id +WHERE rrv.id = $1 +` + +// -------------------------------------------------------------- +// Registered Resource Action Attribute Values +// -------------------------------------------------------------- +// +// SELECT rr.namespace_id +// FROM registered_resources rr +// JOIN registered_resource_values rrv ON rrv.registered_resource_id = rr.id +// WHERE rrv.id = $1 +func (q *Queries) getRegisteredResourceNamespaceIDByValueID(ctx context.Context, id string) (pgtype.UUID, error) { + row := q.db.QueryRow(ctx, getRegisteredResourceNamespaceIDByValueID, id) + var namespace_id pgtype.UUID + err := row.Scan(&namespace_id) + return namespace_id, err +} + const getRegisteredResourceValue = `-- name: getRegisteredResourceValue :one SELECT v.id, v.registered_resource_id, v.value, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', v.metadata -> 'labels', 'created_at', v.created_at, 'updated_at', v.updated_at)) as metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + ELSE NULL END as namespace, + r.name as resource_name, JSON_AGG( JSON_BUILD_OBJECT( 'action', JSON_BUILD_OBJECT( @@ -199,6 +403,8 @@ SELECT ) FILTER (WHERE rav.id IS NOT NULL) as action_attribute_values FROM registered_resource_values v JOIN registered_resources r ON v.registered_resource_id = r.id +LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL LEFT JOIN registered_resource_action_attribute_values rav ON v.id = rav.registered_resource_value_id LEFT JOIN actions a on rav.action_id = a.id LEFT JOIN attribute_values av on rav.attribute_value_id = av.id @@ -206,22 +412,26 @@ LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id WHERE ($1::uuid IS NULL OR v.id = $1::uuid) AND ($2::text IS NULL OR r.name = $2::text) AND - ($3::text IS NULL OR v.value = $3::text) -GROUP BY v.id + ($3::text IS NULL OR v.value = $3::text) AND + ($4::text IS NULL OR ns_fqns.fqn = $4::text) +GROUP BY v.id, r.name, n.id, ns_fqns.fqn ` type getRegisteredResourceValueParams struct { - ID pgtype.UUID `json:"id"` - Name pgtype.Text `json:"name"` - Value pgtype.Text `json:"value"` + ID pgtype.UUID `json:"id"` + Name pgtype.Text `json:"name"` + Value pgtype.Text `json:"value"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` } type getRegisteredResourceValueRow struct { - ID string `json:"id"` - RegisteredResourceID string `json:"registered_resource_id"` - Value string `json:"value"` - Metadata []byte `json:"metadata"` - ActionAttributeValues []byte `json:"action_attribute_values"` + ID string `json:"id"` + RegisteredResourceID string `json:"registered_resource_id"` + Value string `json:"value"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` + ResourceName string `json:"resource_name"` + ActionAttributeValues []byte `json:"action_attribute_values"` } // getRegisteredResourceValue @@ -231,6 +441,14 @@ type getRegisteredResourceValueRow struct { // v.registered_resource_id, // v.value, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', v.metadata -> 'labels', 'created_at', v.created_at, 'updated_at', v.updated_at)) as metadata, +// CASE WHEN n.id IS NOT NULL THEN +// JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', ns_fqns.fqn +// ) +// ELSE NULL END as namespace, +// r.name as resource_name, // JSON_AGG( // JSON_BUILD_OBJECT( // 'action', JSON_BUILD_OBJECT( @@ -246,6 +464,8 @@ type getRegisteredResourceValueRow struct { // ) FILTER (WHERE rav.id IS NOT NULL) as action_attribute_values // FROM registered_resource_values v // JOIN registered_resources r ON v.registered_resource_id = r.id +// LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL // LEFT JOIN registered_resource_action_attribute_values rav ON v.id = rav.registered_resource_value_id // LEFT JOIN actions a on rav.action_id = a.id // LEFT JOIN attribute_values av on rav.attribute_value_id = av.id @@ -253,16 +473,24 @@ type getRegisteredResourceValueRow struct { // WHERE // ($1::uuid IS NULL OR v.id = $1::uuid) AND // ($2::text IS NULL OR r.name = $2::text) AND -// ($3::text IS NULL OR v.value = $3::text) -// GROUP BY v.id +// ($3::text IS NULL OR v.value = $3::text) AND +// ($4::text IS NULL OR ns_fqns.fqn = $4::text) +// GROUP BY v.id, r.name, n.id, ns_fqns.fqn func (q *Queries) getRegisteredResourceValue(ctx context.Context, arg getRegisteredResourceValueParams) (getRegisteredResourceValueRow, error) { - row := q.db.QueryRow(ctx, getRegisteredResourceValue, arg.ID, arg.Name, arg.Value) + row := q.db.QueryRow(ctx, getRegisteredResourceValue, + arg.ID, + arg.Name, + arg.Value, + arg.NamespaceFqn, + ) var i getRegisteredResourceValueRow err := row.Scan( &i.ID, &i.RegisteredResourceID, &i.Value, &i.Metadata, + &i.Namespace, + &i.ResourceName, &i.ActionAttributeValues, ) return i, err @@ -279,6 +507,14 @@ SELECT v.registered_resource_id, v.value, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', v.metadata -> 'labels', 'created_at', v.created_at, 'updated_at', v.updated_at)) as metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + ELSE NULL END as namespace, + r.name as resource_name, JSON_AGG( JSON_BUILD_OBJECT( 'action', JSON_BUILD_OBJECT( @@ -295,14 +531,17 @@ SELECT counted.total FROM registered_resource_values v JOIN registered_resources r ON v.registered_resource_id = r.id +LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL LEFT JOIN registered_resource_action_attribute_values rav ON v.id = rav.registered_resource_value_id LEFT JOIN actions a on rav.action_id = a.id LEFT JOIN attribute_values av on rav.attribute_value_id = av.id -LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id +LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id CROSS JOIN counted WHERE $1::uuid IS NULL OR v.registered_resource_id = $1::uuid -GROUP BY v.id, counted.total +GROUP BY v.id, r.name, n.id, ns_fqns.fqn, counted.total +ORDER BY v.created_at DESC LIMIT $3 OFFSET $2 ` @@ -314,12 +553,14 @@ type listRegisteredResourceValuesParams struct { } type listRegisteredResourceValuesRow struct { - ID string `json:"id"` - RegisteredResourceID string `json:"registered_resource_id"` - Value string `json:"value"` - Metadata []byte `json:"metadata"` - ActionAttributeValues []byte `json:"action_attribute_values"` - Total int64 `json:"total"` + ID string `json:"id"` + RegisteredResourceID string `json:"registered_resource_id"` + Value string `json:"value"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` + ResourceName string `json:"resource_name"` + ActionAttributeValues []byte `json:"action_attribute_values"` + Total int64 `json:"total"` } // listRegisteredResourceValues @@ -334,6 +575,14 @@ type listRegisteredResourceValuesRow struct { // v.registered_resource_id, // v.value, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', v.metadata -> 'labels', 'created_at', v.created_at, 'updated_at', v.updated_at)) as metadata, +// CASE WHEN n.id IS NOT NULL THEN +// JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', ns_fqns.fqn +// ) +// ELSE NULL END as namespace, +// r.name as resource_name, // JSON_AGG( // JSON_BUILD_OBJECT( // 'action', JSON_BUILD_OBJECT( @@ -350,6 +599,8 @@ type listRegisteredResourceValuesRow struct { // counted.total // FROM registered_resource_values v // JOIN registered_resources r ON v.registered_resource_id = r.id +// LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL // LEFT JOIN registered_resource_action_attribute_values rav ON v.id = rav.registered_resource_value_id // LEFT JOIN actions a on rav.action_id = a.id // LEFT JOIN attribute_values av on rav.attribute_value_id = av.id @@ -357,7 +608,8 @@ type listRegisteredResourceValuesRow struct { // CROSS JOIN counted // WHERE // $1::uuid IS NULL OR v.registered_resource_id = $1::uuid -// GROUP BY v.id, counted.total +// GROUP BY v.id, r.name, n.id, ns_fqns.fqn, counted.total +// ORDER BY v.created_at DESC // LIMIT $3 // OFFSET $2 func (q *Queries) listRegisteredResourceValues(ctx context.Context, arg listRegisteredResourceValuesParams) ([]listRegisteredResourceValuesRow, error) { @@ -374,6 +626,8 @@ func (q *Queries) listRegisteredResourceValues(ctx context.Context, arg listRegi &i.RegisteredResourceID, &i.Value, &i.Metadata, + &i.Namespace, + &i.ResourceName, &i.ActionAttributeValues, &i.Total, ); err != nil { @@ -388,14 +642,31 @@ func (q *Queries) listRegisteredResourceValues(ctx context.Context, arg listRegi } const listRegisteredResources = `-- name: listRegisteredResources :many -WITH counted AS ( - SELECT COUNT(id) AS total - FROM registered_resources +WITH params AS ( + SELECT + COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +), +counted AS ( + SELECT COUNT(r.id) AS total + FROM registered_resources r + LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id + LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL + WHERE + ($1::uuid IS NULL OR r.namespace_id = $1::uuid) AND + ($2::text IS NULL OR ns_fqns.fqn = $2::text) ) SELECT r.id, r.name, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', r.metadata -> 'labels', 'created_at', r.created_at, 'updated_at', r.updated_at)) as metadata, + CASE WHEN n.id IS NOT NULL THEN + JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + ELSE NULL END as namespace, -- Aggregate all values for this resource into a JSON array, filtering NULL entries JSON_AGG( JSON_BUILD_OBJECT( @@ -406,14 +677,24 @@ SELECT ) FILTER (WHERE v.id IS NOT NULL) as values, counted.total FROM registered_resources r +LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL CROSS JOIN counted +CROSS JOIN params p LEFT JOIN registered_resource_values v ON v.registered_resource_id = r.id LEFT JOIN LATERAL ( - SELECT JSON_AGG( + -- COALESCE so a value with no mappings yields '[]' rather than SQL NULL, + -- giving consumers a consistent JSON array shape for action_attribute_values. + SELECT COALESCE(JSON_AGG( JSON_BUILD_OBJECT( 'action', JSON_BUILD_OBJECT( 'id', a.id, - 'name', a.name + 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + -- Namespace FQN is deterministic from the name, so build it inline + -- instead of joining attribute_fqns for it. + ELSE JSON_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', CONCAT('https://', ans.name)) + END ), 'attribute_value', JSON_BUILD_OBJECT( 'id', av.id, @@ -421,43 +702,77 @@ LEFT JOIN LATERAL ( 'fqn', fqns.fqn ) ) - ) AS values + ), '[]'::json) AS values -- Join to get all action-attribute relationships for this resource value FROM registered_resource_action_attribute_values rav LEFT JOIN actions a on rav.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id LEFT JOIN attribute_values av on rav.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id -- Correlate to the outer query's resource value WHERE rav.registered_resource_value_id = v.id ) action_attrs ON true -- required syntax for LATERAL joins -GROUP BY r.id, counted.total -LIMIT $2 -OFFSET $1 +WHERE + ($1::uuid IS NULL OR r.namespace_id = $1::uuid) AND + ($2::text IS NULL OR ns_fqns.fqn = $2::text) +GROUP BY r.id, n.id, ns_fqns.fqn, counted.total, p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN r.name END ASC, + CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN r.name END DESC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN r.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN r.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN r.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN r.updated_at END DESC, + r.id ASC +LIMIT $4 +OFFSET $3 ` type listRegisteredResourcesParams struct { - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` } type listRegisteredResourcesRow struct { - ID string `json:"id"` - Name string `json:"name"` - Metadata []byte `json:"metadata"` - Values []byte `json:"values"` - Total int64 `json:"total"` + ID string `json:"id"` + Name string `json:"name"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` + Values []byte `json:"values"` + Total int64 `json:"total"` } // Build a JSON array of action/attribute pairs for each resource value // -// WITH counted AS ( -// SELECT COUNT(id) AS total -// FROM registered_resources +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +// ), +// counted AS ( +// SELECT COUNT(r.id) AS total +// FROM registered_resources r +// LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +// WHERE +// ($1::uuid IS NULL OR r.namespace_id = $1::uuid) AND +// ($2::text IS NULL OR ns_fqns.fqn = $2::text) // ) // SELECT // r.id, // r.name, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', r.metadata -> 'labels', 'created_at', r.created_at, 'updated_at', r.updated_at)) as metadata, +// CASE WHEN n.id IS NOT NULL THEN +// JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', ns_fqns.fqn +// ) +// ELSE NULL END as namespace, // -- Aggregate all values for this resource into a JSON array, filtering NULL entries // JSON_AGG( // JSON_BUILD_OBJECT( @@ -468,14 +783,24 @@ type listRegisteredResourcesRow struct { // ) FILTER (WHERE v.id IS NOT NULL) as values, // counted.total // FROM registered_resources r +// LEFT JOIN attribute_namespaces n ON r.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL // CROSS JOIN counted +// CROSS JOIN params p // LEFT JOIN registered_resource_values v ON v.registered_resource_id = r.id // LEFT JOIN LATERAL ( -// SELECT JSON_AGG( +// -- COALESCE so a value with no mappings yields '[]' rather than SQL NULL, +// -- giving consumers a consistent JSON array shape for action_attribute_values. +// SELECT COALESCE(JSON_AGG( // JSON_BUILD_OBJECT( // 'action', JSON_BUILD_OBJECT( // 'id', a.id, -// 'name', a.name +// 'name', a.name, +// 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL +// -- Namespace FQN is deterministic from the name, so build it inline +// -- instead of joining attribute_fqns for it. +// ELSE JSON_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', CONCAT('https://', ans.name)) +// END // ), // 'attribute_value', JSON_BUILD_OBJECT( // 'id', av.id, @@ -483,20 +808,39 @@ type listRegisteredResourcesRow struct { // 'fqn', fqns.fqn // ) // ) -// ) AS values +// ), '[]'::json) AS values // -- Join to get all action-attribute relationships for this resource value // FROM registered_resource_action_attribute_values rav // LEFT JOIN actions a on rav.action_id = a.id +// LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id // LEFT JOIN attribute_values av on rav.attribute_value_id = av.id // LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id // -- Correlate to the outer query's resource value // WHERE rav.registered_resource_value_id = v.id // ) action_attrs ON true -- required syntax for LATERAL joins -// GROUP BY r.id, counted.total -// LIMIT $2 -// OFFSET $1 +// WHERE +// ($1::uuid IS NULL OR r.namespace_id = $1::uuid) AND +// ($2::text IS NULL OR ns_fqns.fqn = $2::text) +// GROUP BY r.id, n.id, ns_fqns.fqn, counted.total, p.resolved_field, p.resolved_direction +// ORDER BY +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'ASC' THEN r.name END ASC, +// CASE WHEN p.resolved_field = 'name' AND p.resolved_direction = 'DESC' THEN r.name END DESC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN r.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN r.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN r.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN r.updated_at END DESC, +// r.id ASC +// LIMIT $4 +// OFFSET $3 func (q *Queries) listRegisteredResources(ctx context.Context, arg listRegisteredResourcesParams) ([]listRegisteredResourcesRow, error) { - rows, err := q.db.Query(ctx, listRegisteredResources, arg.Offset, arg.Limit) + rows, err := q.db.Query(ctx, listRegisteredResources, + arg.NamespaceID, + arg.NamespaceFqn, + arg.Offset, + arg.Limit, + arg.SortField, + arg.SortDirection, + ) if err != nil { return nil, err } @@ -508,6 +852,7 @@ func (q *Queries) listRegisteredResources(ctx context.Context, arg listRegistere &i.ID, &i.Name, &i.Metadata, + &i.Namespace, &i.Values, &i.Total, ); err != nil { diff --git a/service/policy/db/resource_mapping.go b/service/policy/db/resource_mapping.go index 7bc238bd50..beda72cd01 100644 --- a/service/policy/db/resource_mapping.go +++ b/service/policy/db/resource_mapping.go @@ -46,6 +46,7 @@ func (c PolicyDBClient) ListResourceMappingGroups(ctx context.Context, r *resour Id: rmGroup.ID, NamespaceId: rmGroup.NamespaceID, Name: rmGroup.Name, + Fqn: rmGroup.Fqn, Metadata: metadata, } } @@ -82,6 +83,7 @@ func (c PolicyDBClient) GetResourceMappingGroup(ctx context.Context, id string) Id: rmGroup.ID, NamespaceId: rmGroup.NamespaceID, Name: rmGroup.Name, + Fqn: rmGroup.Fqn, Metadata: metadata, }, nil } @@ -90,7 +92,7 @@ func (c PolicyDBClient) CreateResourceMappingGroup(ctx context.Context, r *resou namespaceID := r.GetNamespaceId() name := strings.ToLower(r.GetName()) - metadataJSON, metadata, err := db.MarshalCreateMetadata(r.GetMetadata()) + metadataJSON, _, err := db.MarshalCreateMetadata(r.GetMetadata()) if err != nil { return nil, err } @@ -104,19 +106,14 @@ func (c PolicyDBClient) CreateResourceMappingGroup(ctx context.Context, r *resou return nil, db.WrapIfKnownInvalidQueryErr(err) } - return &policy.ResourceMappingGroup{ - Id: createdID, - NamespaceId: namespaceID, - Name: name, - Metadata: metadata, - }, nil + return c.GetResourceMappingGroup(ctx, createdID) } func (c PolicyDBClient) UpdateResourceMappingGroup(ctx context.Context, id string, r *resourcemapping.UpdateResourceMappingGroupRequest) (*policy.ResourceMappingGroup, error) { namespaceID := r.GetNamespaceId() name := strings.ToLower(r.GetName()) - metadataJSON, metadata, err := db.MarshalUpdateMetadata(r.GetMetadata(), r.GetMetadataUpdateBehavior(), func() (*common.Metadata, error) { + metadataJSON, _, err := db.MarshalUpdateMetadata(r.GetMetadata(), r.GetMetadataUpdateBehavior(), func() (*common.Metadata, error) { rmGroup, err := c.GetResourceMappingGroup(ctx, id) if err != nil { return nil, err @@ -140,15 +137,15 @@ func (c PolicyDBClient) UpdateResourceMappingGroup(ctx context.Context, id strin return nil, db.ErrNotFound } - return &policy.ResourceMappingGroup{ - Id: id, - NamespaceId: namespaceID, - Name: name, - Metadata: metadata, - }, nil + return c.GetResourceMappingGroup(ctx, id) } func (c PolicyDBClient) DeleteResourceMappingGroup(ctx context.Context, id string) (*policy.ResourceMappingGroup, error) { + rmGroup, err := c.GetResourceMappingGroup(ctx, id) + if err != nil { + return nil, err + } + count, err := c.queries.deleteResourceMappingGroup(ctx, id) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -157,9 +154,7 @@ func (c PolicyDBClient) DeleteResourceMappingGroup(ctx context.Context, id strin return nil, db.ErrNotFound } - return &policy.ResourceMappingGroup{ - Id: id, - }, nil + return rmGroup, nil } /* @@ -187,9 +182,8 @@ func (c PolicyDBClient) ListResourceMappings(ctx context.Context, r *resourcemap for i, rm := range list { var ( - metadata = new(common.Metadata) - attributeValue = new(policy.Value) - resourceMappingGroup = new(policy.ResourceMappingGroup) + metadata = new(common.Metadata) + attributeValue = new(policy.Value) ) if err = unmarshalMetadata(rm.Metadata, metadata); err != nil { @@ -200,11 +194,12 @@ func (c PolicyDBClient) ListResourceMappings(ctx context.Context, r *resourcemap return nil, err } - if err = unmarshalResourceMappingGroup(rm.Group, resourceMappingGroup); err != nil { - return nil, err - } - if resourceMappingGroup.GetId() == "" { - resourceMappingGroup = nil + var resourceMappingGroup *policy.ResourceMappingGroup + if rm.Group != nil { + resourceMappingGroup = new(policy.ResourceMappingGroup) + if err = unmarshalResourceMappingGroup(rm.Group, resourceMappingGroup); err != nil { + return nil, err + } } mapping := &policy.ResourceMapping{ @@ -322,15 +317,20 @@ func (c PolicyDBClient) GetResourceMapping(ctx context.Context, id string) (*pol return nil, err } + var resourceMappingGroup *policy.ResourceMappingGroup + if rm.Group != nil { + resourceMappingGroup = new(policy.ResourceMappingGroup) + if err = unmarshalResourceMappingGroup(rm.Group, resourceMappingGroup); err != nil { + return nil, err + } + } + policyRM := &policy.ResourceMapping{ Id: rm.ID, AttributeValue: attributeValue, Terms: rm.Terms, Metadata: metadata, - } - - if rm.GroupID != "" { - policyRM.Group = &policy.ResourceMappingGroup{Id: rm.GroupID} + Group: resourceMappingGroup, } return policyRM, nil @@ -340,7 +340,7 @@ func (c PolicyDBClient) CreateResourceMapping(ctx context.Context, r *resourcema attributeValueID := r.GetAttributeValueId() terms := r.GetTerms() groupID := r.GetGroupId() - metadataJSON, metadata, err := db.MarshalCreateMetadata(r.GetMetadata()) + metadataJSON, _, err := db.MarshalCreateMetadata(r.GetMetadata()) if err != nil { return nil, err } @@ -374,25 +374,14 @@ func (c PolicyDBClient) CreateResourceMapping(ctx context.Context, r *resourcema return nil, db.WrapIfKnownInvalidQueryErr(err) } - rm := &policy.ResourceMapping{ - Id: createdID, - AttributeValue: &policy.Value{Id: attributeValueID}, - Terms: terms, - Metadata: metadata, - } - - if groupID != "" { - rm.Group = &policy.ResourceMappingGroup{Id: groupID} - } - - return rm, nil + return c.GetResourceMapping(ctx, createdID) } func (c PolicyDBClient) UpdateResourceMapping(ctx context.Context, id string, r *resourcemapping.UpdateResourceMappingRequest) (*policy.ResourceMapping, error) { attributeValueID := r.GetAttributeValueId() terms := r.GetTerms() groupID := r.GetGroupId() - metadataJSON, metadata, err := db.MarshalUpdateMetadata(r.GetMetadata(), r.GetMetadataUpdateBehavior(), func() (*common.Metadata, error) { + metadataJSON, _, err := db.MarshalUpdateMetadata(r.GetMetadata(), r.GetMetadataUpdateBehavior(), func() (*common.Metadata, error) { rm, err := c.GetResourceMapping(ctx, id) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -436,21 +425,7 @@ func (c PolicyDBClient) UpdateResourceMapping(ctx context.Context, id string, r return nil, db.ErrNotFound } - rm := &policy.ResourceMapping{ - Id: id, - Terms: terms, - Metadata: metadata, - } - - if attributeValueID != "" { - rm.AttributeValue = &policy.Value{Id: attributeValueID} - } - - if groupID != "" { - rm.Group = &policy.ResourceMappingGroup{Id: groupID} - } - - return rm, nil + return c.GetResourceMapping(ctx, id) } func (c PolicyDBClient) DeleteResourceMapping(ctx context.Context, id string) (*policy.ResourceMapping, error) { diff --git a/service/policy/db/resource_mapping.sql.go b/service/policy/db/resource_mapping.sql.go index 6dd5ba18b2..06313547d9 100644 --- a/service/policy/db/resource_mapping.sql.go +++ b/service/policy/db/resource_mapping.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: resource_mapping.sql package db @@ -101,12 +101,22 @@ SELECT JSON_BUILD_OBJECT('id', av.id, 'value', av.value, 'fqn', fqns.fqn) as attribute_value, m.terms, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', m.metadata -> 'labels', 'created_at', m.created_at, 'updated_at', m.updated_at)) as metadata, - COALESCE(m.group_id::TEXT, '')::TEXT as group_id + (CASE + WHEN rmg.id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', rmg.id, + 'name', rmg.name, + 'namespace_id', rmg.namespace_id, + 'fqn', CONCAT('https://', rmg_ns.name, '/resm/', rmg.name)::TEXT + ) + END)::JSON AS group FROM resource_mappings m LEFT JOIN attribute_values av on m.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id +LEFT JOIN resource_mapping_groups rmg ON m.group_id = rmg.id +LEFT JOIN attribute_namespaces rmg_ns ON rmg.namespace_id = rmg_ns.id WHERE m.id = $1 -GROUP BY av.id, m.id, fqns.fqn +GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id, rmg_ns.name ` type getResourceMappingRow struct { @@ -114,7 +124,7 @@ type getResourceMappingRow struct { AttributeValue []byte `json:"attribute_value"` Terms []string `json:"terms"` Metadata []byte `json:"metadata"` - GroupID string `json:"group_id"` + Group []byte `json:"group"` } // getResourceMapping @@ -124,12 +134,22 @@ type getResourceMappingRow struct { // JSON_BUILD_OBJECT('id', av.id, 'value', av.value, 'fqn', fqns.fqn) as attribute_value, // m.terms, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', m.metadata -> 'labels', 'created_at', m.created_at, 'updated_at', m.updated_at)) as metadata, -// COALESCE(m.group_id::TEXT, '')::TEXT as group_id +// (CASE +// WHEN rmg.id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT( +// 'id', rmg.id, +// 'name', rmg.name, +// 'namespace_id', rmg.namespace_id, +// 'fqn', CONCAT('https://', rmg_ns.name, '/resm/', rmg.name)::TEXT +// ) +// END)::JSON AS group // FROM resource_mappings m // LEFT JOIN attribute_values av on m.attribute_value_id = av.id // LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id +// LEFT JOIN resource_mapping_groups rmg ON m.group_id = rmg.id +// LEFT JOIN attribute_namespaces rmg_ns ON rmg.namespace_id = rmg_ns.id // WHERE m.id = $1 -// GROUP BY av.id, m.id, fqns.fqn +// GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id, rmg_ns.name func (q *Queries) getResourceMapping(ctx context.Context, id string) (getResourceMappingRow, error) { row := q.db.QueryRow(ctx, getResourceMapping, id) var i getResourceMappingRow @@ -138,31 +158,40 @@ func (q *Queries) getResourceMapping(ctx context.Context, id string) (getResourc &i.AttributeValue, &i.Terms, &i.Metadata, - &i.GroupID, + &i.Group, ) return i, err } const getResourceMappingGroup = `-- name: getResourceMappingGroup :one -SELECT id, namespace_id, name, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -FROM resource_mapping_groups -WHERE id = $1 +SELECT rmg.id, + rmg.namespace_id, + rmg.name, + CONCAT('https://', ns.name, '/resm/', rmg.name)::TEXT AS fqn, + JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', rmg.metadata -> 'labels', 'created_at', rmg.created_at, 'updated_at', rmg.updated_at)) as metadata +FROM resource_mapping_groups rmg +JOIN attribute_namespaces ns ON rmg.namespace_id = ns.id +WHERE rmg.id = $1 ` type getResourceMappingGroupRow struct { ID string `json:"id"` NamespaceID string `json:"namespace_id"` Name string `json:"name"` + Fqn string `json:"fqn"` Metadata []byte `json:"metadata"` } // getResourceMappingGroup // -// SELECT id, namespace_id, name, -// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -// FROM resource_mapping_groups -// WHERE id = $1 +// SELECT rmg.id, +// rmg.namespace_id, +// rmg.name, +// CONCAT('https://', ns.name, '/resm/', rmg.name)::TEXT AS fqn, +// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', rmg.metadata -> 'labels', 'created_at', rmg.created_at, 'updated_at', rmg.updated_at)) as metadata +// FROM resource_mapping_groups rmg +// JOIN attribute_namespaces ns ON rmg.namespace_id = ns.id +// WHERE rmg.id = $1 func (q *Queries) getResourceMappingGroup(ctx context.Context, id string) (getResourceMappingGroupRow, error) { row := q.db.QueryRow(ctx, getResourceMappingGroup, id) var i getResourceMappingGroupRow @@ -170,6 +199,7 @@ func (q *Queries) getResourceMappingGroup(ctx context.Context, id string) (getRe &i.ID, &i.NamespaceID, &i.Name, + &i.Fqn, &i.Metadata, ) return i, err @@ -180,10 +210,13 @@ const listResourceMappingGroups = `-- name: listResourceMappingGroups :many SELECT rmg.id, rmg.namespace_id, rmg.name, + CONCAT('https://', ns.name, '/resm/', rmg.name)::TEXT AS fqn, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', rmg.metadata -> 'labels', 'created_at', rmg.created_at, 'updated_at', rmg.updated_at)) as metadata, COUNT(*) OVER() AS total FROM resource_mapping_groups rmg +JOIN attribute_namespaces ns ON rmg.namespace_id = ns.id WHERE ($1::uuid IS NULL OR rmg.namespace_id = $1::uuid) +ORDER BY rmg.created_at DESC LIMIT $3 OFFSET $2 ` @@ -198,6 +231,7 @@ type listResourceMappingGroupsRow struct { ID string `json:"id"` NamespaceID string `json:"namespace_id"` Name string `json:"name"` + Fqn string `json:"fqn"` Metadata []byte `json:"metadata"` Total int64 `json:"total"` } @@ -209,10 +243,13 @@ type listResourceMappingGroupsRow struct { // SELECT rmg.id, // rmg.namespace_id, // rmg.name, +// CONCAT('https://', ns.name, '/resm/', rmg.name)::TEXT AS fqn, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', rmg.metadata -> 'labels', 'created_at', rmg.created_at, 'updated_at', rmg.updated_at)) as metadata, // COUNT(*) OVER() AS total // FROM resource_mapping_groups rmg +// JOIN attribute_namespaces ns ON rmg.namespace_id = ns.id // WHERE ($1::uuid IS NULL OR rmg.namespace_id = $1::uuid) +// ORDER BY rmg.created_at DESC // LIMIT $3 // OFFSET $2 func (q *Queries) listResourceMappingGroups(ctx context.Context, arg listResourceMappingGroupsParams) ([]listResourceMappingGroupsRow, error) { @@ -228,6 +265,7 @@ func (q *Queries) listResourceMappingGroups(ctx context.Context, arg listResourc &i.ID, &i.NamespaceID, &i.Name, + &i.Fqn, &i.Metadata, &i.Total, ); err != nil { @@ -248,20 +286,24 @@ SELECT JSON_BUILD_OBJECT('id', av.id, 'value', av.value, 'fqn', fqns.fqn) as attribute_value, m.terms, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', m.metadata -> 'labels', 'created_at', m.created_at, 'updated_at', m.updated_at)) as metadata, - JSON_STRIP_NULLS( - JSON_BUILD_OBJECT( - 'id', rmg.id, - 'name', rmg.name, - 'namespace_id', rmg.namespace_id - ) - ) AS group, + (CASE + WHEN rmg.id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', rmg.id, + 'name', rmg.name, + 'namespace_id', rmg.namespace_id, + 'fqn', CONCAT('https://', rmg_ns.name, '/resm/', rmg.name)::TEXT + ) + END)::JSON AS group, COUNT(*) OVER() AS total FROM resource_mappings m LEFT JOIN attribute_values av on m.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id LEFT JOIN resource_mapping_groups rmg ON m.group_id = rmg.id +LEFT JOIN attribute_namespaces rmg_ns ON rmg.namespace_id = rmg_ns.id WHERE ($1::uuid IS NULL OR m.group_id = $1::uuid) -GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id +GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id, rmg_ns.name +ORDER BY m.created_at DESC LIMIT $3 OFFSET $2 ` @@ -290,20 +332,24 @@ type listResourceMappingsRow struct { // JSON_BUILD_OBJECT('id', av.id, 'value', av.value, 'fqn', fqns.fqn) as attribute_value, // m.terms, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', m.metadata -> 'labels', 'created_at', m.created_at, 'updated_at', m.updated_at)) as metadata, -// JSON_STRIP_NULLS( -// JSON_BUILD_OBJECT( -// 'id', rmg.id, -// 'name', rmg.name, -// 'namespace_id', rmg.namespace_id -// ) -// ) AS group, +// (CASE +// WHEN rmg.id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT( +// 'id', rmg.id, +// 'name', rmg.name, +// 'namespace_id', rmg.namespace_id, +// 'fqn', CONCAT('https://', rmg_ns.name, '/resm/', rmg.name)::TEXT +// ) +// END)::JSON AS group, // COUNT(*) OVER() AS total // FROM resource_mappings m // LEFT JOIN attribute_values av on m.attribute_value_id = av.id // LEFT JOIN attribute_fqns fqns on av.id = fqns.value_id // LEFT JOIN resource_mapping_groups rmg ON m.group_id = rmg.id +// LEFT JOIN attribute_namespaces rmg_ns ON rmg.namespace_id = rmg_ns.id // WHERE ($1::uuid IS NULL OR m.group_id = $1::uuid) -// GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id +// GROUP BY av.id, m.id, fqns.fqn, rmg.id, rmg.name, rmg.namespace_id, rmg_ns.name +// ORDER BY m.created_at DESC // LIMIT $3 // OFFSET $2 func (q *Queries) listResourceMappings(ctx context.Context, arg listResourceMappingsParams) ([]listResourceMappingsRow, error) { @@ -341,6 +387,7 @@ WITH groups_cte AS ( 'id', g.id, 'namespace_id', g.namespace_id, 'name', g.name, + 'fqn', CONCAT('https://', ns.name, '/resm/', g.name)::TEXT, 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT( 'labels', g.metadata -> 'labels', 'created_at', g.created_at, @@ -361,6 +408,7 @@ FROM resource_mappings m JOIN groups_cte g ON m.group_id = g.id JOIN attribute_values av on m.attribute_value_id = av.id JOIN attribute_fqns fqns on av.id = fqns.value_id +ORDER BY m.created_at DESC ` type listResourceMappingsByFullyQualifiedGroupParams struct { @@ -385,6 +433,7 @@ type listResourceMappingsByFullyQualifiedGroupRow struct { // 'id', g.id, // 'namespace_id', g.namespace_id, // 'name', g.name, +// 'fqn', CONCAT('https://', ns.name, '/resm/', g.name)::TEXT, // 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT( // 'labels', g.metadata -> 'labels', // 'created_at', g.created_at, @@ -405,6 +454,7 @@ type listResourceMappingsByFullyQualifiedGroupRow struct { // JOIN groups_cte g ON m.group_id = g.id // JOIN attribute_values av on m.attribute_value_id = av.id // JOIN attribute_fqns fqns on av.id = fqns.value_id +// ORDER BY m.created_at DESC func (q *Queries) listResourceMappingsByFullyQualifiedGroup(ctx context.Context, arg listResourceMappingsByFullyQualifiedGroupParams) ([]listResourceMappingsByFullyQualifiedGroupRow, error) { rows, err := q.db.Query(ctx, listResourceMappingsByFullyQualifiedGroup, arg.NamespaceName, arg.GroupName) if err != nil { diff --git a/service/policy/db/schema_erd.md b/service/policy/db/schema_erd.md index 69351c89a0..9e43e4fe29 100644 --- a/service/policy/db/schema_erd.md +++ b/service/policy/db/schema_erd.md @@ -5,7 +5,8 @@ erDiagram uuid id PK "Unique identifier for the action" boolean is_standard "Whether the action is standard (proto-enum) or custom (user-defined)." jsonb metadata "Metadata for the action (see protos for structure)" - character_varying name UK "Unique name of the action, e.g. read, write, etc." + character_varying name "Unique name of the action, e.g. read, write, etc." + uuid namespace_id FK timestamp_with_time_zone updated_at } @@ -36,6 +37,7 @@ erDiagram attribute_definitions { boolean active "Active/Inactive state" + boolean allow_traversal "Whether or not to allow platform to return the definition key when encrypting, if the value specified is missing." timestamp_with_time_zone created_at uuid id PK "Primary key for the table" jsonb metadata "Metadata for the attribute definition (see protos for structure)" @@ -54,11 +56,6 @@ erDiagram uuid value_id FK,UK "Foreign key to the attribute value" } - attribute_namespace_certificates { - uuid certificate_id PK,FK "Foreign key to the certificate" - uuid namespace_id PK,FK "Foreign key to the namespace" - } - attribute_namespace_key_access_grants { uuid key_access_server_id PK,FK "Foreign key to the KAS registration" uuid namespace_id PK,FK "Foreign key to the namespace of the KAS grant" @@ -103,14 +100,6 @@ erDiagram uuid key_access_server_key_id FK } - certificates { - timestamp_with_time_zone created_at "Timestamp when the certificate was created" - uuid id PK "Unique identifier for the certificate" - jsonb metadata "Optional metadata for the certificate" - text pem "PEM format - Base64-encoded DER certificate (not PEM; no headers/footers)" - timestamp_with_time_zone updated_at "Timestamp when the certificate was last updated" - } - goose_db_version { integer id PK boolean is_applied @@ -165,13 +154,13 @@ erDiagram } obligation_triggers { - uuid action_id FK,UK - uuid attribute_value_id FK,UK + uuid action_id FK + uuid attribute_value_id FK text client_id "Holds the client_id associated with this trigger." timestamp_with_time_zone created_at uuid id PK jsonb metadata - uuid obligation_value_id FK,UK + uuid obligation_value_id FK timestamp_with_time_zone updated_at } @@ -216,7 +205,8 @@ erDiagram timestamp_with_time_zone created_at "Timestamp when the record was created" uuid id PK "Primary key for the table" jsonb metadata "Metadata for the registered resource (see protos for structure)" - character_varying name UK "Name for the registered resource" + character_varying name "Name for the registered resource" + uuid namespace_id FK timestamp_with_time_zone updated_at "Timestamp when the record was last updated" } @@ -244,6 +234,7 @@ erDiagram timestamp_with_time_zone created_at uuid id PK "Primary key for the table" jsonb metadata "Metadata for the condition set (see protos for structure)" + uuid namespace_id FK ARRAY selector_values "Array of cached selector values extracted from the condition JSONB and maintained via trigger." timestamp_with_time_zone updated_at } @@ -259,6 +250,7 @@ erDiagram timestamp_with_time_zone created_at uuid id PK "Primary key for the table" jsonb metadata "Metadata for the subject mapping (see protos for structure)" + uuid namespace_id FK uuid subject_condition_set_id FK "Foreign key to the condition set that entitles the subject entity to the attribute value" timestamp_with_time_zone updated_at } @@ -276,6 +268,7 @@ erDiagram timestamp_with_time_zone updated_at "Timestamp when the key was last updated" } + actions }o--|| attribute_namespaces : "namespace_id" obligation_triggers }o--|| actions : "action_id" registered_resource_action_attribute_values }o--|| actions : "action_id" subject_mapping_actions }o--|| actions : "action_id" @@ -289,14 +282,15 @@ erDiagram attribute_values }o--|| attribute_definitions : "attribute_definition_id" attribute_fqns }o--|| attribute_namespaces : "namespace_id" attribute_fqns }o--|| attribute_values : "value_id" - attribute_namespace_certificates }o--|| attribute_namespaces : "namespace_id" - attribute_namespace_certificates }o--|| certificates : "certificate_id" attribute_namespace_key_access_grants }o--|| attribute_namespaces : "namespace_id" attribute_namespace_key_access_grants }o--|| key_access_servers : "key_access_server_id" attribute_namespace_public_key_map }o--|| attribute_namespaces : "namespace_id" attribute_namespace_public_key_map }o--|| key_access_server_keys : "key_access_server_key_id" obligation_definitions }o--|| attribute_namespaces : "namespace_id" + registered_resources }o--|| attribute_namespaces : "namespace_id" resource_mapping_groups }o--|| attribute_namespaces : "namespace_id" + subject_condition_set }o--|| attribute_namespaces : "namespace_id" + subject_mappings }o--|| attribute_namespaces : "namespace_id" attribute_value_key_access_grants }o--|| attribute_values : "attribute_value_id" attribute_value_key_access_grants }o--|| key_access_servers : "key_access_server_id" attribute_value_public_key_map }o--|| attribute_values : "value_id" diff --git a/service/policy/db/subject_mappings.go b/service/policy/db/subject_mappings.go index b58cc38693..6153629e5a 100644 --- a/service/policy/db/subject_mappings.go +++ b/service/policy/db/subject_mappings.go @@ -7,8 +7,10 @@ import ( "fmt" "strings" + "github.com/jackc/pgx/v5/pgtype" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" + policynamespaces "github.com/opentdf/platform/protocol/go/policy/namespaces" "github.com/opentdf/platform/protocol/go/policy/subjectmapping" "github.com/opentdf/platform/service/pkg/db" "google.golang.org/protobuf/encoding/protojson" @@ -53,32 +55,35 @@ func unmarshalSubjectSetsProto(conditionJSON []byte) ([]*policy.SubjectSet, erro Subject Condition Sets */ -// Creates a new subject condition set and returns it -func (c PolicyDBClient) CreateSubjectConditionSet(ctx context.Context, s *subjectmapping.SubjectConditionSetCreate) (*policy.SubjectConditionSet, error) { +// Creates a new subject condition set and returns it. +// The namespaceID and namespaceFQN parameters are optional; if provided, the SCS is placed in that namespace. +func (c PolicyDBClient) CreateSubjectConditionSet(ctx context.Context, s *subjectmapping.SubjectConditionSetCreate, namespaceID, namespaceFQN string) (*policy.SubjectConditionSet, error) { subjectSets := s.GetSubjectSets() conditionJSON, err := marshalSubjectSetsProto(subjectSets) if err != nil { return nil, err } - metadataJSON, metadata, err := db.MarshalCreateMetadata(s.GetMetadata()) + metadataJSON, _, err := db.MarshalCreateMetadata(s.GetMetadata()) + if err != nil { + return nil, err + } + + resolvedNamespaceID, err := c.resolveNamespace(ctx, namespaceID, namespaceFQN) if err != nil { return nil, err } createdID, err := c.queries.createSubjectConditionSet(ctx, createSubjectConditionSetParams{ - Condition: conditionJSON, - Metadata: metadataJSON, + Condition: conditionJSON, + Metadata: metadataJSON, + NamespaceID: pgtypeUUID(resolvedNamespaceID), }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) } - return &policy.SubjectConditionSet{ - Id: createdID, - SubjectSets: subjectSets, - Metadata: metadata, - }, nil + return c.GetSubjectConditionSet(ctx, createdID) } func (c PolicyDBClient) GetSubjectConditionSet(ctx context.Context, id string) (*policy.SubjectConditionSet, error) { @@ -97,10 +102,16 @@ func (c PolicyDBClient) GetSubjectConditionSet(ctx context.Context, id string) ( return nil, err } + namespace, err := hydrateNamespaceFromInterface(cs.Namespace) + if err != nil { + return nil, err + } + return &policy.SubjectConditionSet{ Id: id, SubjectSets: sets, Metadata: metadata, + Namespace: namespace, }, nil } @@ -112,9 +123,15 @@ func (c PolicyDBClient) ListSubjectConditionSets(ctx context.Context, r *subject return nil, db.ErrListLimitTooLarge } + sortField, sortDirection := GetSubjectConditionSetsSortParams(r.GetSort()) + list, err := c.queries.listSubjectConditionSets(ctx, listSubjectConditionSetsParams{ - Limit: limit, - Offset: offset, + NamespaceID: pgtypeUUID(r.GetNamespaceId()), + NamespaceFqn: pgtypeText(r.GetNamespaceFqn()), + Limit: limit, + Offset: offset, + SortField: sortField, + SortDirection: sortDirection, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -132,10 +149,16 @@ func (c PolicyDBClient) ListSubjectConditionSets(ctx context.Context, r *subject return nil, err } + namespace, err := hydrateNamespaceFromInterface(set.Namespace) + if err != nil { + return nil, err + } + setList[i] = &policy.SubjectConditionSet{ Id: set.ID, SubjectSets: sets, Metadata: metadata, + Namespace: namespace, } } @@ -240,65 +263,30 @@ func (c PolicyDBClient) DeleteAllUnmappedSubjectConditionSets(ctx context.Contex // Creates a new subject mapping and returns it. If an existing subject condition set id is provided, it will be used. // If a new subject condition set is provided, it will be created. The existing subject condition set id takes precedence. func (c PolicyDBClient) CreateSubjectMapping(ctx context.Context, s *subjectmapping.CreateSubjectMappingRequest) (*policy.SubjectMapping, error) { - actions := s.GetActions() attributeValueID := s.GetAttributeValueId() - var ( - err error - scs *policy.SubjectConditionSet - ) - - // Actions are required on Subject Mappings - if len(actions) == 0 { - return nil, db.WrapIfKnownInvalidQueryErr( - errors.Join(db.ErrMissingValue, errors.New("actions are required when creating a subject mapping")), - ) + resolvedNamespaceID, err := c.resolveNamespace(ctx, s.GetNamespaceId(), s.GetNamespaceFqn()) + if err != nil { + return nil, err } - actionIDs := make([]string, 0) - actionNames := make([]string, 0) - // Check for provided existing Action IDs and existing/new Action Names - for idx, a := range actions { - switch { - case a.GetId() != "": - actionIDs = append(actionIDs, a.GetId()) - case a.GetName() != "": - actionNames = append(actionNames, strings.ToLower(a.GetName())) - default: - return nil, db.WrapIfKnownInvalidQueryErr( - errors.Join(db.ErrMissingValue, fmt.Errorf("action at index %d missing required 'id' or 'name' when creating a subject mapping; action details: %+v", idx, a)), - ) - } + parsedNamespaceID := pgtypeUUID(resolvedNamespaceID) + + // Resolve action IDs from the request (by id or by name) + actionIDs, err := c.resolveSubjectMappingActions(ctx, s.GetActions(), parsedNamespaceID) + if err != nil { + return nil, err } - // Create or list Actions for those provided by name - if len(actionNames) > 0 { - createdOrListedActions, err := c.queries.createOrListActionsByName(ctx, actionNames) - if err != nil { - return nil, db.WrapIfKnownInvalidQueryErr( - errors.Join(db.ErrMissingValue, fmt.Errorf("failed to create or list action names [%v]: %w", actionNames, err)), - ) - } - for _, a := range createdOrListedActions { - actionIDs = append(actionIDs, a.ID) - } + + // Resolve or create the subject condition set + scs, err := c.resolveSubjectConditionSet(ctx, s, resolvedNamespaceID) + if err != nil { + return nil, err } - // Subject Condition Sets may be existing or new, and protos document preference for existing SCS IDs when both provided - switch { - case s.GetExistingSubjectConditionSetId() != "": - scs, err = c.GetSubjectConditionSet(ctx, s.GetExistingSubjectConditionSetId()) - if err != nil { - return nil, db.WrapIfKnownInvalidQueryErr(err) - } - case s.GetNewSubjectConditionSet() != nil: - // create the new subject condition set - scs, err = c.CreateSubjectConditionSet(ctx, s.GetNewSubjectConditionSet()) - if err != nil { - return nil, db.WrapIfKnownInvalidQueryErr(err) - } - default: - return nil, db.WrapIfKnownInvalidQueryErr(errors.Join(db.ErrMissingValue, errors.New("either an existing Subject Condition Set ID or a new Subject Condition Set is required when creating a subject mapping"))) + if err := c.validateSubjectMappingNamespaceConsistency(ctx, resolvedNamespaceID, attributeValueID, actionIDs, scs); err != nil { + return nil, err } - metadataJSON, metadata, err := db.MarshalCreateMetadata(s.GetMetadata()) + metadataJSON, _, err := db.MarshalCreateMetadata(s.GetMetadata()) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) } @@ -308,20 +296,13 @@ func (c PolicyDBClient) CreateSubjectMapping(ctx context.Context, s *subjectmapp ActionIds: actionIDs, Metadata: metadataJSON, SubjectConditionSetID: pgtypeUUID(scs.GetId()), + NamespaceID: parsedNamespaceID, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) } - return &policy.SubjectMapping{ - Id: createdID, - AttributeValue: &policy.Value{ - Id: attributeValueID, - }, - SubjectConditionSet: scs, - Actions: actions, - Metadata: metadata, - }, nil + return c.GetSubjectMapping(ctx, createdID) } func (c PolicyDBClient) GetSubjectMapping(ctx context.Context, id string) (*policy.SubjectMapping, error) { @@ -354,12 +335,18 @@ func (c PolicyDBClient) GetSubjectMapping(ctx context.Context, id string) (*poli return nil, err } + namespace, err := hydrateNamespaceFromInterface(sm.Namespace) + if err != nil { + return nil, err + } + return &policy.SubjectMapping{ Id: id, Metadata: metadata, AttributeValue: av, SubjectConditionSet: &scs, Actions: a, + Namespace: namespace, }, nil } @@ -371,9 +358,15 @@ func (c PolicyDBClient) ListSubjectMappings(ctx context.Context, r *subjectmappi return nil, db.ErrListLimitTooLarge } + sortField, sortDirection := GetSubjectMappingsSortParams(r.GetSort()) + list, err := c.queries.listSubjectMappings(ctx, listSubjectMappingsParams{ - Limit: limit, - Offset: offset, + NamespaceID: pgtypeUUID(r.GetNamespaceId()), + NamespaceFqn: pgtypeText(r.GetNamespaceFqn()), + Limit: limit, + Offset: offset, + SortField: sortField, + SortDirection: sortDirection, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -413,12 +406,18 @@ func (c PolicyDBClient) ListSubjectMappings(ctx context.Context, r *subjectmappi return nil, err } + namespace, err := hydrateNamespaceFromInterface(sm.Namespace) + if err != nil { + return nil, err + } + mappings[i] = &policy.SubjectMapping{ Id: sm.ID, Metadata: metadata, AttributeValue: av, SubjectConditionSet: &scs, Actions: a, + Namespace: namespace, } } @@ -580,3 +579,166 @@ func (c PolicyDBClient) GetMatchedSubjectMappings(ctx context.Context, propertie return mappings, nil } + +// resolveSubjectMappingActions parses the action list from a CreateSubjectMappingRequest, +// resolving actions by name within the given namespace and collecting existing action IDs. +func (c PolicyDBClient) resolveSubjectMappingActions(ctx context.Context, actions []*policy.Action, parsedNamespaceID pgtype.UUID) ([]string, error) { + if len(actions) == 0 { + return nil, db.WrapIfKnownInvalidQueryErr( + errors.Join(db.ErrMissingValue, errors.New("actions are required when creating a subject mapping")), + ) + } + + var actionIDs, actionNames []string + for idx, a := range actions { + switch { + case a.GetId() != "": + actionIDs = append(actionIDs, a.GetId()) + case a.GetName() != "": + actionNames = append(actionNames, strings.ToLower(a.GetName())) + default: + return nil, db.WrapIfKnownInvalidQueryErr( + errors.Join(db.ErrMissingValue, fmt.Errorf("action at index %d missing required 'id' or 'name'; action details: %+v", idx, a)), + ) + } + } + + if len(actionNames) > 0 { + ids, err := c.resolveActionNameIDs(ctx, actionNames, parsedNamespaceID) + if err != nil { + return nil, err + } + actionIDs = append(actionIDs, ids...) + } + + return actionIDs, nil +} + +// resolveSubjectConditionSet fetches an existing SCS or creates a new one for the subject mapping. +func (c PolicyDBClient) resolveSubjectConditionSet(ctx context.Context, s *subjectmapping.CreateSubjectMappingRequest, namespaceID string) (*policy.SubjectConditionSet, error) { + switch { + case s.GetExistingSubjectConditionSetId() != "": + scs, err := c.GetSubjectConditionSet(ctx, s.GetExistingSubjectConditionSetId()) + if err != nil { + return nil, db.WrapIfKnownInvalidQueryErr(err) + } + return scs, nil + case s.GetNewSubjectConditionSet() != nil: + scs, err := c.CreateSubjectConditionSet(ctx, s.GetNewSubjectConditionSet(), namespaceID, "") + if err != nil { + return nil, db.WrapIfKnownInvalidQueryErr(err) + } + return scs, nil + default: + return nil, db.WrapIfKnownInvalidQueryErr( + errors.Join(db.ErrMissingValue, errors.New("either an existing Subject Condition Set ID or a new Subject Condition Set is required when creating a subject mapping")), + ) + } +} + +func (c PolicyDBClient) resolveNamespace(ctx context.Context, namespaceID, namespaceFQN string) (string, error) { + if namespaceID != "" { + parsedNamespaceID := pgtypeUUID(namespaceID) + if !parsedNamespaceID.Valid { + return "", db.ErrUUIDInvalid + } + return namespaceID, nil + } + + if namespaceFQN == "" { + return "", nil + } + + ns, err := c.GetNamespace(ctx, &policynamespaces.GetNamespaceRequest_Fqn{Fqn: namespaceFQN}) + if err != nil { + return "", db.WrapIfKnownInvalidQueryErr(err) + } + + return ns.GetId(), nil +} + +// validateSubjectMappingNamespaceConsistency ensures that actions and subject condition set +// belong to the same namespace as the subject mapping being created. When the SM is namespaced, +// the attribute value must also be in that namespace. When unnamespaced, the attribute value +// may have any namespace, but actions and SCS must be unnamespaced. +func (c PolicyDBClient) validateSubjectMappingNamespaceConsistency( + ctx context.Context, + targetNsID string, + attributeValueID string, + actionIDs []string, + scs *policy.SubjectConditionSet, +) error { + // Attribute value namespace check only applies when the SM is namespaced + if targetNsID != "" { + av, err := c.GetAttributeValue(ctx, attributeValueID) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + attr, err := c.GetAttribute(ctx, av.GetAttribute().GetId()) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + if attr.GetNamespace().GetId() != targetNsID { + return errors.Join(db.ErrNamespaceMismatch, + fmt.Errorf("attribute value namespace [%s] does not match the specified subject mapping namespace [%s]", attr.GetNamespace().GetId(), targetNsID)) + } + } + + // All actions must be in the same namespace + if len(actionIDs) > 0 { + actionRows, err := c.queries.getActionsByIDs(ctx, actionIDs) + if err != nil { + return db.WrapIfKnownInvalidQueryErr(err) + } + for _, a := range actionRows { + actionNsID := UUIDToString(a.NamespaceID) + if actionNsID != targetNsID { + return errors.Join(db.ErrNamespaceMismatch, + fmt.Errorf("action [%s] namespace [%s] does not match the specified subject mapping namespace [%s]", a.ID, actionNsID, targetNsID)) + } + } + } + + // Subject condition set namespace + if scs.GetNamespace().GetId() != targetNsID { + return errors.Join(db.ErrNamespaceMismatch, + fmt.Errorf("subject condition set [%s] namespace [%s] does not match the specified subject mapping namespace [%s]", scs.GetId(), scs.GetNamespace().GetId(), targetNsID)) + } + + return nil +} + +// resolveActionNameIDs creates or fetches action IDs for the given action names. +// When namespaced is true, actions are created/fetched within the given namespace; +// otherwise the legacy global (unnamespaced) path is used. +func (c PolicyDBClient) resolveActionNameIDs(ctx context.Context, actionNames []string, namespaceID pgtype.UUID) ([]string, error) { + if namespaceID.Valid { + rows, err := c.queries.createOrListActionsByNameInNamespace(ctx, createOrListActionsByNameInNamespaceParams{ + ActionNames: actionNames, + NamespaceID: UUIDToString(namespaceID), + }) + if err != nil { + return nil, db.WrapIfKnownInvalidQueryErr( + errors.Join(db.ErrMissingValue, fmt.Errorf("failed to create or list action names [%v] in namespace: %w", actionNames, err)), + ) + } + ids := make([]string, len(rows)) + for i, a := range rows { + ids[i] = a.ID + } + return ids, nil + } + + // No namespace: use global unnamespaced actions (legacy behavior) + rows, err := c.queries.createOrListActionsByName(ctx, actionNames) + if err != nil { + return nil, db.WrapIfKnownInvalidQueryErr( + errors.Join(db.ErrMissingValue, fmt.Errorf("failed to create or list action names [%v]: %w", actionNames, err)), + ) + } + ids := make([]string, len(rows)) + for i, a := range rows { + ids[i] = a.ID + } + return ids, nil +} diff --git a/service/policy/db/subject_mappings.sql.go b/service/policy/db/subject_mappings.sql.go index 18a1a66e57..259e84114b 100644 --- a/service/policy/db/subject_mappings.sql.go +++ b/service/policy/db/subject_mappings.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.0 // source: subject_mappings.sql package db @@ -12,23 +12,32 @@ import ( ) const createSubjectConditionSet = `-- name: createSubjectConditionSet :one -INSERT INTO subject_condition_set (condition, metadata) -VALUES ($1, $2) +INSERT INTO subject_condition_set (condition, metadata, namespace_id) +VALUES ( + $1, + $2, + $3::uuid +) RETURNING id ` type createSubjectConditionSetParams struct { - Condition []byte `json:"condition"` - Metadata []byte `json:"metadata"` + Condition []byte `json:"condition"` + Metadata []byte `json:"metadata"` + NamespaceID pgtype.UUID `json:"namespace_id"` } // createSubjectConditionSet // -// INSERT INTO subject_condition_set (condition, metadata) -// VALUES ($1, $2) +// INSERT INTO subject_condition_set (condition, metadata, namespace_id) +// VALUES ( +// $1, +// $2, +// $3::uuid +// ) // RETURNING id func (q *Queries) createSubjectConditionSet(ctx context.Context, arg createSubjectConditionSetParams) (string, error) { - row := q.db.QueryRow(ctx, createSubjectConditionSet, arg.Condition, arg.Metadata) + row := q.db.QueryRow(ctx, createSubjectConditionSet, arg.Condition, arg.Metadata, arg.NamespaceID) var id string err := row.Scan(&id) return id, err @@ -39,16 +48,22 @@ WITH inserted_mapping AS ( INSERT INTO subject_mappings ( attribute_value_id, metadata, - subject_condition_set_id + subject_condition_set_id, + namespace_id + ) + VALUES ( + $1, + $2, + $3, + $4::uuid ) - VALUES ($1, $2, $3) RETURNING id ), inserted_actions AS ( INSERT INTO subject_mapping_actions (subject_mapping_id, action_id) - SELECT + SELECT (SELECT id FROM inserted_mapping), - unnest($4::uuid[]) + unnest($5::uuid[]) ) SELECT id FROM inserted_mapping ` @@ -57,6 +72,7 @@ type createSubjectMappingParams struct { AttributeValueID string `json:"attribute_value_id"` Metadata []byte `json:"metadata"` SubjectConditionSetID pgtype.UUID `json:"subject_condition_set_id"` + NamespaceID pgtype.UUID `json:"namespace_id"` ActionIds []string `json:"action_ids"` } @@ -66,16 +82,22 @@ type createSubjectMappingParams struct { // INSERT INTO subject_mappings ( // attribute_value_id, // metadata, -// subject_condition_set_id +// subject_condition_set_id, +// namespace_id +// ) +// VALUES ( +// $1, +// $2, +// $3, +// $4::uuid // ) -// VALUES ($1, $2, $3) // RETURNING id // ), // inserted_actions AS ( // INSERT INTO subject_mapping_actions (subject_mapping_id, action_id) // SELECT // (SELECT id FROM inserted_mapping), -// unnest($4::uuid[]) +// unnest($5::uuid[]) // ) // SELECT id FROM inserted_mapping func (q *Queries) createSubjectMapping(ctx context.Context, arg createSubjectMappingParams) (string, error) { @@ -83,6 +105,7 @@ func (q *Queries) createSubjectMapping(ctx context.Context, arg createSubjectMap arg.AttributeValueID, arg.Metadata, arg.SubjectConditionSetID, + arg.NamespaceID, arg.ActionIds, ) var id string @@ -153,31 +176,49 @@ func (q *Queries) deleteSubjectMapping(ctx context.Context, id string) (int64, e const getSubjectConditionSet = `-- name: getSubjectConditionSet :one SELECT - id, - condition, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -FROM subject_condition_set -WHERE id = $1 + scs.id, + scs.condition, + JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)) as metadata, + CASE + WHEN scs.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', n.id, 'name', n.name, 'fqn', ns_fqns.fqn) + END AS namespace +FROM subject_condition_set scs +LEFT JOIN attribute_namespaces n ON n.id = scs.namespace_id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +WHERE scs.id = $1 ` type getSubjectConditionSetRow struct { - ID string `json:"id"` - Condition []byte `json:"condition"` - Metadata []byte `json:"metadata"` + ID string `json:"id"` + Condition []byte `json:"condition"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` } // getSubjectConditionSet // // SELECT -// id, -// condition, -// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', metadata -> 'labels', 'created_at', created_at, 'updated_at', updated_at)) as metadata -// FROM subject_condition_set -// WHERE id = $1 +// scs.id, +// scs.condition, +// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)) as metadata, +// CASE +// WHEN scs.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT('id', n.id, 'name', n.name, 'fqn', ns_fqns.fqn) +// END AS namespace +// FROM subject_condition_set scs +// LEFT JOIN attribute_namespaces n ON n.id = scs.namespace_id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +// WHERE scs.id = $1 func (q *Queries) getSubjectConditionSet(ctx context.Context, id string) (getSubjectConditionSetRow, error) { row := q.db.QueryRow(ctx, getSubjectConditionSet, id) var i getSubjectConditionSetRow - err := row.Scan(&i.ID, &i.Condition, &i.Metadata) + err := row.Scan( + &i.ID, + &i.Condition, + &i.Metadata, + &i.Namespace, + ) return i, err } @@ -185,38 +226,63 @@ const getSubjectMapping = `-- name: getSubjectMapping :one SELECT sm.id, ( - SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) + SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FROM actions a JOIN subject_mapping_actions sma ON sma.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL WHERE sma.subject_mapping_id = sm.id AND a.is_standard = TRUE ) AS standard_actions, ( - SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) + SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FROM actions a JOIN subject_mapping_actions sma ON sma.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL WHERE sma.subject_mapping_id = sm.id AND a.is_standard = FALSE ) AS custom_actions, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', sm.metadata -> 'labels', 'created_at', sm.created_at, 'updated_at', sm.updated_at)) AS metadata, JSON_BUILD_OBJECT( 'id', scs.id, 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)), - 'subject_sets', scs.condition + 'subject_sets', scs.condition, + 'namespace', CASE + WHEN scs.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', scs_ns.id, 'name', scs_ns.name, 'fqn', scs_ns_fqns.fqn) + END ) AS subject_condition_set, - JSON_BUILD_OBJECT('id', av.id,'value', av.value,'active', av.active) AS attribute_value + JSON_BUILD_OBJECT('id', av.id,'value', av.value,'active', av.active) AS attribute_value, + CASE + WHEN sm.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', sm_ns.id, 'name', sm_ns.name, 'fqn', sm_ns_fqns.fqn) + END AS namespace FROM subject_mappings sm LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id +LEFT JOIN attribute_namespaces scs_ns ON scs_ns.id = scs.namespace_id +LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL +LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id +LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL WHERE sm.id = $1 -GROUP BY av.id, sm.id, scs.id +GROUP BY av.id, sm.id, scs.id, scs.namespace_id, scs_ns.id, scs_ns.name, scs_ns_fqns.fqn, sm_ns.id, sm_ns.name, sm_ns_fqns.fqn ` type getSubjectMappingRow struct { - ID string `json:"id"` - StandardActions []byte `json:"standard_actions"` - CustomActions []byte `json:"custom_actions"` - Metadata []byte `json:"metadata"` - SubjectConditionSet []byte `json:"subject_condition_set"` - AttributeValue []byte `json:"attribute_value"` + ID string `json:"id"` + StandardActions []byte `json:"standard_actions"` + CustomActions []byte `json:"custom_actions"` + Metadata []byte `json:"metadata"` + SubjectConditionSet []byte `json:"subject_condition_set"` + AttributeValue []byte `json:"attribute_value"` + Namespace interface{} `json:"namespace"` } // getSubjectMapping @@ -224,29 +290,53 @@ type getSubjectMappingRow struct { // SELECT // sm.id, // ( -// SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) +// SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, +// 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) +// END +// )) // FROM actions a // JOIN subject_mapping_actions sma ON sma.action_id = a.id +// LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id +// LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL // WHERE sma.subject_mapping_id = sm.id AND a.is_standard = TRUE // ) AS standard_actions, // ( -// SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) +// SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, +// 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) +// END +// )) // FROM actions a // JOIN subject_mapping_actions sma ON sma.action_id = a.id +// LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id +// LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL // WHERE sma.subject_mapping_id = sm.id AND a.is_standard = FALSE // ) AS custom_actions, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', sm.metadata -> 'labels', 'created_at', sm.created_at, 'updated_at', sm.updated_at)) AS metadata, // JSON_BUILD_OBJECT( // 'id', scs.id, // 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)), -// 'subject_sets', scs.condition +// 'subject_sets', scs.condition, +// 'namespace', CASE +// WHEN scs.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT('id', scs_ns.id, 'name', scs_ns.name, 'fqn', scs_ns_fqns.fqn) +// END // ) AS subject_condition_set, -// JSON_BUILD_OBJECT('id', av.id,'value', av.value,'active', av.active) AS attribute_value +// JSON_BUILD_OBJECT('id', av.id,'value', av.value,'active', av.active) AS attribute_value, +// CASE +// WHEN sm.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT('id', sm_ns.id, 'name', sm_ns.name, 'fqn', sm_ns_fqns.fqn) +// END AS namespace // FROM subject_mappings sm // LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id // LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id +// LEFT JOIN attribute_namespaces scs_ns ON scs_ns.id = scs.namespace_id +// LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL +// LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id +// LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL // WHERE sm.id = $1 -// GROUP BY av.id, sm.id, scs.id +// GROUP BY av.id, sm.id, scs.id, scs.namespace_id, scs_ns.id, scs_ns.name, scs_ns_fqns.fqn, sm_ns.id, sm_ns.name, sm_ns_fqns.fqn func (q *Queries) getSubjectMapping(ctx context.Context, id string) (getSubjectMappingRow, error) { row := q.db.QueryRow(ctx, getSubjectMapping, id) var i getSubjectMappingRow @@ -257,58 +347,105 @@ func (q *Queries) getSubjectMapping(ctx context.Context, id string) (getSubjectM &i.Metadata, &i.SubjectConditionSet, &i.AttributeValue, + &i.Namespace, ) return i, err } const listSubjectConditionSets = `-- name: listSubjectConditionSets :many -WITH counted AS ( - SELECT COUNT(scs.id) AS total - FROM subject_condition_set scs +WITH params AS ( + SELECT + COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction ) SELECT scs.id, scs.condition, JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)) as metadata, - counted.total + CASE + WHEN scs.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', n.id, 'name', n.name, 'fqn', ns_fqns.fqn) + END AS namespace, + COUNT(*) OVER() as total FROM subject_condition_set scs -CROSS JOIN counted -LIMIT $2 -OFFSET $1 +LEFT JOIN attribute_namespaces n ON n.id = scs.namespace_id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +CROSS JOIN params p +WHERE + ($1::uuid IS NULL AND $2::text IS NULL) + OR scs.namespace_id = $1::uuid + OR ns_fqns.fqn = $2::text +ORDER BY + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN scs.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN scs.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN scs.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN scs.updated_at END DESC, + scs.id ASC +LIMIT $4 +OFFSET $3 ` type listSubjectConditionSetsParams struct { - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` } type listSubjectConditionSetsRow struct { - ID string `json:"id"` - Condition []byte `json:"condition"` - Metadata []byte `json:"metadata"` - Total int64 `json:"total"` + ID string `json:"id"` + Condition []byte `json:"condition"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` + Total int64 `json:"total"` } // -------------------------------------------------------------- // SUBJECT CONDITION SETS // -------------------------------------------------------------- // -// WITH counted AS ( -// SELECT COUNT(scs.id) AS total -// FROM subject_condition_set scs +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction // ) // SELECT // scs.id, // scs.condition, // JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata -> 'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)) as metadata, -// counted.total +// CASE +// WHEN scs.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT('id', n.id, 'name', n.name, 'fqn', ns_fqns.fqn) +// END AS namespace, +// COUNT(*) OVER() as total // FROM subject_condition_set scs -// CROSS JOIN counted -// LIMIT $2 -// OFFSET $1 +// LEFT JOIN attribute_namespaces n ON n.id = scs.namespace_id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +// CROSS JOIN params p +// WHERE +// ($1::uuid IS NULL AND $2::text IS NULL) +// OR scs.namespace_id = $1::uuid +// OR ns_fqns.fqn = $2::text +// ORDER BY +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN scs.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN scs.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN scs.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN scs.updated_at END DESC, +// scs.id ASC +// LIMIT $4 +// OFFSET $3 func (q *Queries) listSubjectConditionSets(ctx context.Context, arg listSubjectConditionSetsParams) ([]listSubjectConditionSetsRow, error) { - rows, err := q.db.Query(ctx, listSubjectConditionSets, arg.Offset, arg.Limit) + rows, err := q.db.Query(ctx, listSubjectConditionSets, + arg.NamespaceID, + arg.NamespaceFqn, + arg.Offset, + arg.Limit, + arg.SortField, + arg.SortDirection, + ) if err != nil { return nil, err } @@ -320,6 +457,7 @@ func (q *Queries) listSubjectConditionSets(ctx context.Context, arg listSubjectC &i.ID, &i.Condition, &i.Metadata, + &i.Namespace, &i.Total, ); err != nil { return nil, err @@ -334,23 +472,44 @@ func (q *Queries) listSubjectConditionSets(ctx context.Context, arg listSubjectC const listSubjectMappings = `-- name: listSubjectMappings :many -WITH subject_actions AS ( +WITH params AS ( + SELECT + COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +), +subject_actions AS ( SELECT sma.subject_mapping_id, COALESCE( - JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = TRUE), + JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FILTER (WHERE a.is_standard = TRUE), '[]'::JSONB ) AS standard_actions, COALESCE( - JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = FALSE), + JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FILTER (WHERE a.is_standard = FALSE), '[]'::JSONB ) AS custom_actions FROM subject_mapping_actions sma JOIN actions a ON sma.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL GROUP BY sma.subject_mapping_id ), counted AS ( SELECT COUNT(sm.id) AS total FROM subject_mappings sm + LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id + LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL + WHERE + ($1::uuid IS NULL AND $2::text IS NULL) + OR sm.namespace_id = $1::uuid + OR sm_ns_fqns.fqn = $2::text ) SELECT sm.id, @@ -360,7 +519,11 @@ SELECT JSON_BUILD_OBJECT( 'id', scs.id, 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata->'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)), - 'subject_sets', scs.condition + 'subject_sets', scs.condition, + 'namespace', CASE + WHEN scs.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', scs_ns.id, 'name', scs_ns.name, 'fqn', scs_ns_fqns.fqn) + END ) AS subject_condition_set, JSON_BUILD_OBJECT( 'id', av.id, @@ -368,29 +531,55 @@ SELECT 'active', av.active, 'fqn', fqns.fqn ) AS attribute_value, + CASE + WHEN sm.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT('id', sm_ns.id, 'name', sm_ns.name, 'fqn', sm_ns_fqns.fqn) + END AS namespace, counted.total FROM subject_mappings sm CROSS JOIN counted +CROSS JOIN params p LEFT JOIN subject_actions sa ON sm.id = sa.subject_mapping_id LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id +LEFT JOIN attribute_namespaces scs_ns ON scs_ns.id = scs.namespace_id +LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL +LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id +LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL +WHERE + ($1::uuid IS NULL AND $2::text IS NULL) + OR sm.namespace_id = $1::uuid + OR sm_ns_fqns.fqn = $2::text GROUP BY sm.id, sa.standard_actions, sa.custom_actions, - sm.metadata, sm.created_at, sm.updated_at, -- for metadata object - scs.id, scs.metadata, scs.created_at, scs.updated_at, scs.condition, -- for subject_condition_set object - av.id, av.value, av.active, -- for attribute_value object + sm.metadata, sm.created_at, sm.updated_at, + scs.id, scs.metadata, scs.created_at, scs.updated_at, scs.condition, scs.namespace_id, + scs_ns.id, scs_ns.name, scs_ns_fqns.fqn, + sm_ns.id, sm_ns.name, sm_ns_fqns.fqn, + av.id, av.value, av.active, fqns.fqn, - counted.total -LIMIT $2 -OFFSET $1 + counted.total, + p.resolved_field, p.resolved_direction +ORDER BY + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN sm.created_at END ASC, + CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN sm.created_at END DESC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN sm.updated_at END ASC, + CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN sm.updated_at END DESC, + sm.id ASC +LIMIT $4 +OFFSET $3 ` type listSubjectMappingsParams struct { - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + SortField string `json:"sort_field"` + SortDirection string `json:"sort_direction"` } type listSubjectMappingsRow struct { @@ -400,6 +589,7 @@ type listSubjectMappingsRow struct { Metadata []byte `json:"metadata"` SubjectConditionSet []byte `json:"subject_condition_set"` AttributeValue []byte `json:"attribute_value"` + Namespace interface{} `json:"namespace"` Total int64 `json:"total"` } @@ -407,23 +597,44 @@ type listSubjectMappingsRow struct { // SUBJECT MAPPINGS // -------------------------------------------------------------- // -// WITH subject_actions AS ( +// WITH params AS ( +// SELECT +// COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +// ), +// subject_actions AS ( // SELECT // sma.subject_mapping_id, // COALESCE( -// JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = TRUE), +// JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, +// 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) +// END +// )) FILTER (WHERE a.is_standard = TRUE), // '[]'::JSONB // ) AS standard_actions, // COALESCE( -// JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = FALSE), +// JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, +// 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) +// END +// )) FILTER (WHERE a.is_standard = FALSE), // '[]'::JSONB // ) AS custom_actions // FROM subject_mapping_actions sma // JOIN actions a ON sma.action_id = a.id +// LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id +// LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL // GROUP BY sma.subject_mapping_id // ), counted AS ( // SELECT COUNT(sm.id) AS total // FROM subject_mappings sm +// LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id +// LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL +// WHERE +// ($1::uuid IS NULL AND $2::text IS NULL) +// OR sm.namespace_id = $1::uuid +// OR sm_ns_fqns.fqn = $2::text // ) // SELECT // sm.id, @@ -433,7 +644,11 @@ type listSubjectMappingsRow struct { // JSON_BUILD_OBJECT( // 'id', scs.id, // 'metadata', JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', scs.metadata->'labels', 'created_at', scs.created_at, 'updated_at', scs.updated_at)), -// 'subject_sets', scs.condition +// 'subject_sets', scs.condition, +// 'namespace', CASE +// WHEN scs.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT('id', scs_ns.id, 'name', scs_ns.name, 'fqn', scs_ns_fqns.fqn) +// END // ) AS subject_condition_set, // JSON_BUILD_OBJECT( // 'id', av.id, @@ -441,26 +656,55 @@ type listSubjectMappingsRow struct { // 'active', av.active, // 'fqn', fqns.fqn // ) AS attribute_value, +// CASE +// WHEN sm.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT('id', sm_ns.id, 'name', sm_ns.name, 'fqn', sm_ns_fqns.fqn) +// END AS namespace, // counted.total // FROM subject_mappings sm // CROSS JOIN counted +// CROSS JOIN params p // LEFT JOIN subject_actions sa ON sm.id = sa.subject_mapping_id // LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id // LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id // LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id +// LEFT JOIN attribute_namespaces scs_ns ON scs_ns.id = scs.namespace_id +// LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL +// LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id +// LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL +// WHERE +// ($1::uuid IS NULL AND $2::text IS NULL) +// OR sm.namespace_id = $1::uuid +// OR sm_ns_fqns.fqn = $2::text // GROUP BY // sm.id, // sa.standard_actions, // sa.custom_actions, -// sm.metadata, sm.created_at, sm.updated_at, -- for metadata object -// scs.id, scs.metadata, scs.created_at, scs.updated_at, scs.condition, -- for subject_condition_set object -// av.id, av.value, av.active, -- for attribute_value object +// sm.metadata, sm.created_at, sm.updated_at, +// scs.id, scs.metadata, scs.created_at, scs.updated_at, scs.condition, scs.namespace_id, +// scs_ns.id, scs_ns.name, scs_ns_fqns.fqn, +// sm_ns.id, sm_ns.name, sm_ns_fqns.fqn, +// av.id, av.value, av.active, // fqns.fqn, -// counted.total -// LIMIT $2 -// OFFSET $1 +// counted.total, +// p.resolved_field, p.resolved_direction +// ORDER BY +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'ASC' THEN sm.created_at END ASC, +// CASE WHEN p.resolved_field = 'created_at' AND p.resolved_direction = 'DESC' THEN sm.created_at END DESC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN sm.updated_at END ASC, +// CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN sm.updated_at END DESC, +// sm.id ASC +// LIMIT $4 +// OFFSET $3 func (q *Queries) listSubjectMappings(ctx context.Context, arg listSubjectMappingsParams) ([]listSubjectMappingsRow, error) { - rows, err := q.db.Query(ctx, listSubjectMappings, arg.Offset, arg.Limit) + rows, err := q.db.Query(ctx, listSubjectMappings, + arg.NamespaceID, + arg.NamespaceFqn, + arg.Offset, + arg.Limit, + arg.SortField, + arg.SortDirection, + ) if err != nil { return nil, err } @@ -475,6 +719,7 @@ func (q *Queries) listSubjectMappings(ctx context.Context, arg listSubjectMappin &i.Metadata, &i.SubjectConditionSet, &i.AttributeValue, + &i.Namespace, &i.Total, ); err != nil { return nil, err @@ -492,15 +737,25 @@ WITH subject_actions AS ( SELECT sma.subject_mapping_id, COALESCE( - JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = TRUE), + JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FILTER (WHERE a.is_standard = TRUE), '[]'::JSONB ) AS standard_actions, COALESCE( - JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = FALSE), + JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, + 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL + ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) + END + )) FILTER (WHERE a.is_standard = FALSE), '[]'::JSONB ) AS custom_actions FROM subject_mapping_actions sma JOIN actions a ON sma.action_id = a.id + LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id + LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL GROUP BY sma.subject_mapping_id ) SELECT @@ -551,15 +806,25 @@ type matchSubjectMappingsRow struct { // SELECT // sma.subject_mapping_id, // COALESCE( -// JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = TRUE), +// JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, +// 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) +// END +// )) FILTER (WHERE a.is_standard = TRUE), // '[]'::JSONB // ) AS standard_actions, // COALESCE( -// JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name)) FILTER (WHERE a.is_standard = FALSE), +// JSONB_AGG(JSONB_BUILD_OBJECT('id', a.id, 'name', a.name, +// 'namespace', CASE WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSONB_BUILD_OBJECT('id', ans.id, 'name', ans.name, 'fqn', ans_fqns.fqn) +// END +// )) FILTER (WHERE a.is_standard = FALSE), // '[]'::JSONB // ) AS custom_actions // FROM subject_mapping_actions sma // JOIN actions a ON sma.action_id = a.id +// LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id +// LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL // GROUP BY sma.subject_mapping_id // ) // SELECT diff --git a/service/policy/db/utils.go b/service/policy/db/utils.go index 8dbe7b85ce..7e43fd63cd 100644 --- a/service/policy/db/utils.go +++ b/service/policy/db/utils.go @@ -9,10 +9,26 @@ import ( "github.com/jackc/pgx/v5/pgtype" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/obligations" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" "github.com/opentdf/platform/service/pkg/db" "google.golang.org/protobuf/encoding/protojson" ) +// Sort field constants shared across all List endpoint sort helpers. +const ( + sortFieldName = "name" + sortFieldCreatedAt = "created_at" + sortFieldUpdatedAt = "updated_at" + sortFieldFQN = "fqn" + sortFieldURI = "uri" + sortFieldKeyID = "key_id" +) + // Gathers request pagination limit/offset or configured default func (c PolicyDBClient) getRequestedLimitOffset(page *policy.PageRequest) (int32, int32) { return getListLimit(page.GetLimit(), c.listCfg.limitDefault), page.GetOffset() @@ -25,6 +41,127 @@ func getListLimit(limit int32, fallback int32) int32 { return fallback } +// getSortDirection maps the direction enum to a SQL string. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getSortDirection(direction policy.SortDirection) string { + switch direction { + case policy.SortDirection_SORT_DIRECTION_DESC: + return "DESC" + case policy.SortDirection_SORT_DIRECTION_ASC: + return "ASC" + case policy.SortDirection_SORT_DIRECTION_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// getNamespacesSortField maps the field enum to a SQL column name. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getNamespacesSortField(field namespaces.SortNamespacesType) string { + switch field { + case namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_NAME: + return sortFieldName + case namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_FQN: + return sortFieldFQN + case namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT: + return sortFieldCreatedAt + case namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UPDATED_AT: + return sortFieldUpdatedAt + case namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// GetNamespacesSortParams resolves sort field and direction independently, +// returning SQL-compatible strings. Empty strings delegate defaults to SQL. +func GetNamespacesSortParams(sort []*namespaces.NamespacesSort) (string, string) { + if len(sort) == 0 { + return "", "" + } + return getNamespacesSortField(sort[0].GetField()), getSortDirection(sort[0].GetDirection()) +} + +// getSubjectConditionSetsSortField maps the field enum to a SQL column name. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getSubjectConditionSetsSortField(field subjectmapping.SortSubjectConditionSetsType) string { + switch field { + case subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT: + return sortFieldCreatedAt + case subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT: + return sortFieldUpdatedAt + case subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// GetSubjectConditionSetsSortParams resolves sort field and direction independently, +// returning SQL-compatible strings. Empty strings delegate defaults to SQL. +func GetSubjectConditionSetsSortParams(sort []*subjectmapping.SubjectConditionSetsSort) (string, string) { + if len(sort) == 0 { + return "", "" + } + return getSubjectConditionSetsSortField(sort[0].GetField()), getSortDirection(sort[0].GetDirection()) +} + +// getObligationsSortField maps the field enum to a SQL column name. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getObligationsSortField(field obligations.SortObligationsType) string { + switch field { + case obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME: + return sortFieldName + case obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN: + return sortFieldFQN + case obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT: + return sortFieldCreatedAt + case obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UPDATED_AT: + return sortFieldUpdatedAt + case obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// GetObligationsSortParams resolves sort field and direction independently, +// returning SQL-compatible strings. Empty strings delegate defaults to SQL. +func GetObligationsSortParams(sort []*obligations.ObligationsSort) (string, string) { + if len(sort) == 0 { + return "", "" + } + return getObligationsSortField(sort[0].GetField()), getSortDirection(sort[0].GetDirection()) +} + +// getRegisteredResourcesSortField maps the field enum to a SQL column name. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getRegisteredResourcesSortField(field registeredresources.SortRegisteredResourcesType) string { + switch field { + case registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME: + return sortFieldName + case registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT: + return sortFieldCreatedAt + case registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT: + return sortFieldUpdatedAt + case registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// GetRegisteredResourcesSortParams resolves sort field and direction independently, +// returning SQL-compatible strings. Empty strings delegate defaults to SQL. +func GetRegisteredResourcesSortParams(sort []*registeredresources.RegisteredResourcesSort) (string, string) { + if len(sort) == 0 { + return "", "" + } + return getRegisteredResourcesSortField(sort[0].GetField()), getSortDirection(sort[0].GetDirection()) +} + // Returns next page's offset if has not yet reached total, or else returns 0 func getNextOffset(currentOffset, limit, total int32) int32 { next := currentOffset + limit @@ -243,6 +380,30 @@ func pgtypeInt4(i int32, valid bool) pgtype.Int4 { } } +// getSubjectMappingsSortField maps the field enum to a SQL column name. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getSubjectMappingsSortField(field subjectmapping.SortSubjectMappingsType) string { + switch field { + case subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT: + return sortFieldCreatedAt + case subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT: + return sortFieldUpdatedAt + case subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// GetSubjectMappingsSortParams resolves sort field and direction independently, +// returning SQL-compatible strings. Empty strings delegate defaults to SQL. +func GetSubjectMappingsSortParams(sort []*subjectmapping.SubjectMappingsSort) (string, string) { + if len(sort) == 0 { + return "", "" + } + return getSubjectMappingsSortField(sort[0].GetField()), getSortDirection(sort[0].GetDirection()) +} + func UUIDToString(uuid pgtype.UUID) string { if !uuid.Valid { return "" @@ -256,3 +417,83 @@ func UUIDToString(uuid pgtype.UUID) string { uuid.Bytes[10:16], ) } + +// getAttributesSortField maps the field enum to a SQL column name. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getAttributesSortField(field attributes.SortAttributesType) string { + switch field { + case attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME: + return sortFieldName + case attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT: + return sortFieldCreatedAt + case attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UPDATED_AT: + return sortFieldUpdatedAt + case attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// GetAttributesSortParams resolves sort field and direction independently, +// returning SQL-compatible strings. Empty strings delegate defaults to SQL. +func GetAttributesSortParams(sort []*attributes.AttributesSort) (string, string) { + if len(sort) == 0 { + return "", "" + } + return getAttributesSortField(sort[0].GetField()), getSortDirection(sort[0].GetDirection()) +} + +// getKasKeysSortField maps the field enum to a SQL column name. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getKasKeysSortField(field kasregistry.SortKasKeysType) string { + switch field { + case kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID: + return sortFieldKeyID + case kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT: + return sortFieldCreatedAt + case kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UPDATED_AT: + return sortFieldUpdatedAt + case kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// GetKasKeysSortParams resolves sort field and direction independently, +// returning SQL-compatible strings. Empty strings delegate defaults to SQL. +func GetKasKeysSortParams(sort []*kasregistry.KasKeysSort) (string, string) { + if len(sort) == 0 { + return "", "" + } + return getKasKeysSortField(sort[0].GetField()), getSortDirection(sort[0].GetDirection()) +} + +// getKeyAccessServersSortField maps the field enum to a SQL column name. +// UNSPECIFIED returns empty so SQL can apply its per-query default. +func getKeyAccessServersSortField(field kasregistry.SortKeyAccessServersType) string { + switch field { + case kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME: + return sortFieldName + case kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_URI: + return sortFieldURI + case kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT: + return sortFieldCreatedAt + case kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT: + return sortFieldUpdatedAt + case kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED: + fallthrough + default: + return "" + } +} + +// GetKeyAccessServersSortParams resolves sort field and direction independently, +// returning SQL-compatible strings. Empty strings delegate defaults to SQL. +func GetKeyAccessServersSortParams(sort []*kasregistry.KeyAccessServersSort) (string, string) { + if len(sort) == 0 { + return "", "" + } + return getKeyAccessServersSortField(sort[0].GetField()), getSortDirection(sort[0].GetDirection()) +} diff --git a/service/policy/db/utils_test.go b/service/policy/db/utils_test.go index df306c55c7..4adfbfe5be 100644 --- a/service/policy/db/utils_test.go +++ b/service/policy/db/utils_test.go @@ -4,6 +4,12 @@ import ( "testing" "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/obligations" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -93,6 +99,206 @@ func Test_GetNextOffset(t *testing.T) { } } +func Test_GetNamespacesSortParams(t *testing.T) { + cases := []struct { + name string + sort []*namespaces.NamespacesSort + expectedField string + expectedDir string + }{ + { + name: "nil sort returns empty strings", + sort: nil, + expectedField: "", + expectedDir: "", + }, + { + name: "empty slice returns empty strings", + sort: []*namespaces.NamespacesSort{}, + expectedField: "", + expectedDir: "", + }, + { + name: "nil element returns empty strings", + sort: []*namespaces.NamespacesSort{nil}, + expectedField: "", + expectedDir: "", + }, + { + name: "UNSPECIFIED field with ASC preserves direction", + sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "", + expectedDir: "ASC", + }, + { + name: "UNSPECIFIED field with DESC preserves direction", + sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "", + expectedDir: "DESC", + }, + { + name: "both UNSPECIFIED returns empty strings", + sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "", + expectedDir: "", + }, + { + name: "NAME with ASC", + sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "name", + expectedDir: "ASC", + }, + { + name: "NAME with DESC", + sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "name", + expectedDir: "DESC", + }, + { + name: "FQN with unspecified direction returns empty direction", + sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_FQN}, + }, + expectedField: "fqn", + expectedDir: "", + }, + { + name: "CREATED_AT with ASC", + sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "created_at", + expectedDir: "ASC", + }, + { + name: "UPDATED_AT with DESC", + sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "updated_at", + expectedDir: "DESC", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + field, dir := GetNamespacesSortParams(tc.sort) + assert.Equal(t, tc.expectedField, field) + assert.Equal(t, tc.expectedDir, dir) + }) + } +} + +func Test_GetSubjectMappingsSortParams(t *testing.T) { + cases := []struct { + name string + sort []*subjectmapping.SubjectMappingsSort + expectedField string + expectedDir string + }{ + { + name: "nil sort returns empty strings", + sort: nil, + expectedField: "", + expectedDir: "", + }, + { + name: "empty slice returns empty strings", + sort: []*subjectmapping.SubjectMappingsSort{}, + expectedField: "", + expectedDir: "", + }, + { + name: "nil element returns empty strings", + sort: []*subjectmapping.SubjectMappingsSort{nil}, + expectedField: "", + expectedDir: "", + }, + { + name: "UNSPECIFIED field with ASC preserves direction", + sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "", + expectedDir: "ASC", + }, + { + name: "UNSPECIFIED field with DESC preserves direction", + sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "", + expectedDir: "DESC", + }, + { + name: "both UNSPECIFIED returns empty strings", + sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "", + expectedDir: "", + }, + { + name: "CREATED_AT with ASC", + sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "created_at", + expectedDir: "ASC", + }, + { + name: "CREATED_AT with DESC", + sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "created_at", + expectedDir: "DESC", + }, + { + name: "CREATED_AT with unspecified direction returns empty direction", + sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT}, + }, + expectedField: "created_at", + expectedDir: "", + }, + { + name: "UPDATED_AT with ASC", + sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "updated_at", + expectedDir: "ASC", + }, + { + name: "UPDATED_AT with DESC", + sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "updated_at", + expectedDir: "DESC", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + field, dir := GetSubjectMappingsSortParams(tc.sort) + assert.Equal(t, tc.expectedField, field) + assert.Equal(t, tc.expectedDir, dir) + }) + } +} + func Test_UnmarshalAllActionsProto(t *testing.T) { tests := []struct { name string @@ -260,3 +466,691 @@ func Test_UnmarshalPrivatePublicKeyContext(t *testing.T) { }) } } + +func Test_GetAttributesSortParams(t *testing.T) { + cases := []struct { + name string + sort []*attributes.AttributesSort + expectedField string + expectedDir string + }{ + { + name: "nil sort returns empty strings", + sort: nil, + expectedField: "", + expectedDir: "", + }, + { + name: "empty slice returns empty strings", + sort: []*attributes.AttributesSort{}, + expectedField: "", + expectedDir: "", + }, + { + name: "nil element returns empty strings", + sort: []*attributes.AttributesSort{nil}, + expectedField: "", + expectedDir: "", + }, + { + name: "UNSPECIFIED field with ASC preserves direction", + sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "", + expectedDir: "ASC", + }, + { + name: "UNSPECIFIED field with DESC preserves direction", + sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "", + expectedDir: "DESC", + }, + { + name: "both UNSPECIFIED returns empty strings", + sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "", + expectedDir: "", + }, + { + name: "NAME with ASC", + sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "name", + expectedDir: "ASC", + }, + { + name: "NAME with DESC", + sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "name", + expectedDir: "DESC", + }, + { + name: "CREATED_AT with ASC", + sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "created_at", + expectedDir: "ASC", + }, + { + name: "CREATED_AT with unspecified direction returns empty direction", + sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT}, + }, + expectedField: "created_at", + expectedDir: "", + }, + { + name: "UPDATED_AT with DESC", + sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "updated_at", + expectedDir: "DESC", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + field, dir := GetAttributesSortParams(tc.sort) + assert.Equal(t, tc.expectedField, field) + assert.Equal(t, tc.expectedDir, dir) + }) + } +} + +func Test_GetSubjectConditionSetsSortParams(t *testing.T) { + cases := []struct { + name string + sort []*subjectmapping.SubjectConditionSetsSort + expectedField string + expectedDir string + }{ + { + name: "nil sort returns empty strings", + sort: nil, + expectedField: "", + expectedDir: "", + }, + { + name: "empty slice returns empty strings", + sort: []*subjectmapping.SubjectConditionSetsSort{}, + expectedField: "", + expectedDir: "", + }, + { + name: "nil element returns empty strings", + sort: []*subjectmapping.SubjectConditionSetsSort{nil}, + expectedField: "", + expectedDir: "", + }, + { + name: "UNSPECIFIED field with ASC preserves direction", + sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "", + expectedDir: "ASC", + }, + { + name: "UNSPECIFIED field with DESC preserves direction", + sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "", + expectedDir: "DESC", + }, + { + name: "both UNSPECIFIED returns empty strings", + sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "", + expectedDir: "", + }, + { + name: "CREATED_AT with ASC", + sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "created_at", + expectedDir: "ASC", + }, + { + name: "CREATED_AT with DESC", + sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "created_at", + expectedDir: "DESC", + }, + { + name: "CREATED_AT with unspecified direction returns empty direction", + sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT}, + }, + expectedField: "created_at", + expectedDir: "", + }, + { + name: "UPDATED_AT with DESC", + sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "updated_at", + expectedDir: "DESC", + }, + { + name: "UPDATED_AT with ASC", + sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "updated_at", + expectedDir: "ASC", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + field, dir := GetSubjectConditionSetsSortParams(tc.sort) + assert.Equal(t, tc.expectedField, field) + assert.Equal(t, tc.expectedDir, dir) + }) + } +} + +func Test_GetObligationsSortParams(t *testing.T) { + cases := []struct { + name string + sort []*obligations.ObligationsSort + expectedField string + expectedDir string + }{ + { + name: "nil sort returns empty strings", + sort: nil, + expectedField: "", + expectedDir: "", + }, + { + name: "empty slice returns empty strings", + sort: []*obligations.ObligationsSort{}, + expectedField: "", + expectedDir: "", + }, + { + name: "nil element returns empty strings", + sort: []*obligations.ObligationsSort{nil}, + expectedField: "", + expectedDir: "", + }, + { + name: "UNSPECIFIED field with ASC preserves direction", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "", + expectedDir: "ASC", + }, + { + name: "UNSPECIFIED field with DESC preserves direction", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "", + expectedDir: "DESC", + }, + { + name: "both UNSPECIFIED returns empty strings", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "", + expectedDir: "", + }, + { + name: "NAME with ASC", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "name", + expectedDir: "ASC", + }, + { + name: "NAME with DESC", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "name", + expectedDir: "DESC", + }, + { + name: "FQN with ASC", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "fqn", + expectedDir: "ASC", + }, + { + name: "FQN with DESC", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "fqn", + expectedDir: "DESC", + }, + { + name: "FQN with unspecified direction returns empty direction", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN}, + }, + expectedField: "fqn", + expectedDir: "", + }, + { + name: "CREATED_AT with ASC", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "created_at", + expectedDir: "ASC", + }, + { + name: "CREATED_AT with DESC", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "created_at", + expectedDir: "DESC", + }, + { + name: "UPDATED_AT with ASC", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "updated_at", + expectedDir: "ASC", + }, + { + name: "UPDATED_AT with DESC", + sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "updated_at", + expectedDir: "DESC", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + field, dir := GetObligationsSortParams(tc.sort) + assert.Equal(t, tc.expectedField, field) + assert.Equal(t, tc.expectedDir, dir) + }) + } +} + +func Test_GetKeyAccessServersSortParams(t *testing.T) { + tests := []struct { + name string + sort []*kasregistry.KeyAccessServersSort + expectedField string + expectedDirection string + }{ + { + name: "nil sort returns empty strings", + sort: nil, + expectedField: "", + expectedDirection: "", + }, + { + name: "empty slice returns empty strings", + sort: []*kasregistry.KeyAccessServersSort{}, + expectedField: "", + expectedDirection: "", + }, + { + name: "nil element returns empty strings", + sort: []*kasregistry.KeyAccessServersSort{nil}, + expectedField: "", + expectedDirection: "", + }, + { + name: "UNSPECIFIED field with ASC preserves direction", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "", + expectedDirection: "ASC", + }, + { + name: "UNSPECIFIED field with DESC preserves direction", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "", + expectedDirection: "DESC", + }, + { + name: "both UNSPECIFIED returns empty strings", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "", + expectedDirection: "", + }, + { + name: "NAME with ASC", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "name", + expectedDirection: "ASC", + }, + { + name: "NAME with DESC", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "name", + expectedDirection: "DESC", + }, + { + name: "URI with ASC", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_URI, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "uri", + expectedDirection: "ASC", + }, + { + name: "URI with DESC", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_URI, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "uri", + expectedDirection: "DESC", + }, + { + name: "CREATED_AT with ASC", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "created_at", + expectedDirection: "ASC", + }, + { + name: "CREATED_AT with DESC", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "created_at", + expectedDirection: "DESC", + }, + { + name: "UPDATED_AT with DESC", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "updated_at", + expectedDirection: "DESC", + }, + { + name: "UNSPECIFIED direction returns empty direction", + sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "created_at", + expectedDirection: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + field, direction := GetKeyAccessServersSortParams(tc.sort) + assert.Equal(t, tc.expectedField, field) + assert.Equal(t, tc.expectedDirection, direction) + }) + } +} + +func Test_GetRegisteredResourcesSortParams(t *testing.T) { + cases := []struct { + name string + sort []*registeredresources.RegisteredResourcesSort + expectedField string + expectedDir string + }{ + { + name: "nil sort returns empty strings", + sort: nil, + expectedField: "", + expectedDir: "", + }, + { + name: "empty slice returns empty strings", + sort: []*registeredresources.RegisteredResourcesSort{}, + expectedField: "", + expectedDir: "", + }, + { + name: "nil element returns empty strings", + sort: []*registeredresources.RegisteredResourcesSort{nil}, + expectedField: "", + expectedDir: "", + }, + { + name: "UNSPECIFIED field with ASC preserves direction", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "", + expectedDir: "ASC", + }, + { + name: "UNSPECIFIED field with DESC preserves direction", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "", + expectedDir: "DESC", + }, + { + name: "both UNSPECIFIED returns empty strings", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "", + expectedDir: "", + }, + { + name: "NAME with ASC", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "name", + expectedDir: "ASC", + }, + { + name: "NAME with DESC", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "name", + expectedDir: "DESC", + }, + { + name: "NAME with unspecified direction returns empty direction", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME}, + }, + expectedField: "name", + expectedDir: "", + }, + { + name: "CREATED_AT with ASC", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "created_at", + expectedDir: "ASC", + }, + { + name: "CREATED_AT with DESC", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "created_at", + expectedDir: "DESC", + }, + { + name: "UPDATED_AT with ASC", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "updated_at", + expectedDir: "ASC", + }, + { + name: "UPDATED_AT with DESC", + sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "updated_at", + expectedDir: "DESC", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + field, dir := GetRegisteredResourcesSortParams(tc.sort) + assert.Equal(t, tc.expectedField, field) + assert.Equal(t, tc.expectedDir, dir) + }) + } +} + +func Test_GetKasKeysSortParams(t *testing.T) { + tests := []struct { + name string + sort []*kasregistry.KasKeysSort + expectedField string + expectedDirection string + }{ + { + name: "nil sort returns empty strings", + sort: nil, + expectedField: "", + expectedDirection: "", + }, + { + name: "empty slice returns empty strings", + sort: []*kasregistry.KasKeysSort{}, + expectedField: "", + expectedDirection: "", + }, + { + name: "nil element returns empty strings", + sort: []*kasregistry.KasKeysSort{nil}, + expectedField: "", + expectedDirection: "", + }, + { + name: "UNSPECIFIED field with ASC preserves direction", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "", + expectedDirection: "ASC", + }, + { + name: "UNSPECIFIED field with DESC preserves direction", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "", + expectedDirection: "DESC", + }, + { + name: "both UNSPECIFIED returns empty strings", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UNSPECIFIED, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "", + expectedDirection: "", + }, + { + name: "KEY_ID with ASC", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "key_id", + expectedDirection: "ASC", + }, + { + name: "KEY_ID with DESC", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "key_id", + expectedDirection: "DESC", + }, + { + name: "CREATED_AT with ASC", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "created_at", + expectedDirection: "ASC", + }, + { + name: "CREATED_AT with DESC", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "created_at", + expectedDirection: "DESC", + }, + { + name: "UPDATED_AT with ASC", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + expectedField: "updated_at", + expectedDirection: "ASC", + }, + { + name: "UPDATED_AT with DESC", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UPDATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_DESC}, + }, + expectedField: "updated_at", + expectedDirection: "DESC", + }, + { + name: "UNSPECIFIED direction returns empty direction", + sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID, Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED}, + }, + expectedField: "key_id", + expectedDirection: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + field, direction := GetKasKeysSortParams(tc.sort) + assert.Equal(t, tc.expectedField, field) + assert.Equal(t, tc.expectedDirection, direction) + }) + } +} diff --git a/service/policy/kasregistry/key_access_server_registry.go b/service/policy/kasregistry/key_access_server_registry.go index 53d12a84c6..6c5000b700 100644 --- a/service/policy/kasregistry/key_access_server_registry.go +++ b/service/policy/kasregistry/key_access_server_registry.go @@ -58,12 +58,11 @@ func NewRegistration(ns string, dbRegister serviceregistry.DBRegister) *servicer return &serviceregistry.Service[kasregistryconnect.KeyAccessServerRegistryServiceHandler]{ Close: kasrSvc.Close, ServiceOptions: serviceregistry.ServiceOptions[kasregistryconnect.KeyAccessServerRegistryServiceHandler]{ - Namespace: ns, - DB: dbRegister, - ServiceDesc: &kasr.KeyAccessServerRegistryService_ServiceDesc, - ConnectRPCFunc: kasregistryconnect.NewKeyAccessServerRegistryServiceHandler, - GRPCGatewayFunc: kasr.RegisterKeyAccessServerRegistryServiceHandler, - OnConfigUpdate: onUpdateConfigHook, + Namespace: ns, + DB: dbRegister, + ServiceDesc: &kasr.KeyAccessServerRegistryService_ServiceDesc, + ConnectRPCFunc: kasregistryconnect.NewKeyAccessServerRegistryServiceHandler, + OnConfigUpdate: onUpdateConfigHook, RegisterFunc: func(srp serviceregistry.RegistrationParams) (kasregistryconnect.KeyAccessServerRegistryServiceHandler, serviceregistry.HandlerServer) { logger := srp.Logger cfg, err := policyconfig.GetSharedPolicyConfig(srp.Config) @@ -107,7 +106,7 @@ func (s KeyAccessServerRegistry) CreateKeyAccessServer(ctx context.Context, ks, err := s.dbClient.CreateKeyAccessServer(ctx, req.Msg) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("keyAccessServer", req.Msg.String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("key_access_server", req.Msg.String())) } auditParams.ObjectID = ks.GetId() @@ -175,7 +174,7 @@ func (s KeyAccessServerRegistry) UpdateKeyAccessServer(ctx context.Context, updated, err := s.dbClient.UpdateKeyAccessServer(ctx, kasID, req.Msg) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("id", kasID), slog.String("keyAccessServer", req.Msg.String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("id", kasID), slog.String("key_access_server", req.Msg.String())) } auditParams.Original = original @@ -216,8 +215,8 @@ func (s KeyAccessServerRegistry) DeleteKeyAccessServer(ctx context.Context, } func (s KeyAccessServerRegistry) ListKeyAccessServerGrants(ctx context.Context, - req *connect.Request[kasr.ListKeyAccessServerGrantsRequest], -) (*connect.Response[kasr.ListKeyAccessServerGrantsResponse], error) { + req *connect.Request[kasr.ListKeyAccessServerGrantsRequest], //nolint:staticcheck // Compatibility path for deprecated RPC. +) (*connect.Response[kasr.ListKeyAccessServerGrantsResponse], error) { //nolint:staticcheck // Compatibility path for deprecated RPC. rsp, err := s.dbClient.ListKeyAccessServerGrants(ctx, req.Msg) if err != nil { return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextListRetrievalFailed) @@ -267,7 +266,7 @@ func (s KeyAccessServerRegistry) CreateKey(ctx context.Context, r *connect.Reque return nil }) if err != nil { - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("keyAccessServer Keys", r.Msg.GetKasId()), slog.String("key id", r.Msg.GetKeyId())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("key_access_server_keys", r.Msg.GetKasId()), slog.String("key_id", r.Msg.GetKeyId())) } return connect.NewResponse(resp), nil @@ -288,7 +287,7 @@ func (s KeyAccessServerRegistry) UpdateKey(ctx context.Context, req *connect.Req }) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("keyAccessServer Keys", req.Msg.GetId())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("key_access_server_keys", req.Msg.GetId())) } err = s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { @@ -315,7 +314,7 @@ func (s KeyAccessServerRegistry) UpdateKey(ctx context.Context, req *connect.Req return nil }) if err != nil { - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("keyAccessServer Keys", req.Msg.GetId())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("key_access_server_keys", req.Msg.GetId())) } return connect.NewResponse(rsp), nil @@ -341,7 +340,7 @@ func (s KeyAccessServerRegistry) GetKey(ctx context.Context, r *connect.Request[ key, err := s.dbClient.GetKey(ctx, r.Msg.GetIdentifier()) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("keyAccessServer Keys", r.Msg.String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("key_access_server_keys", r.Msg.String())) } auditParams.ObjectID = key.GetKey().GetKeyId() @@ -356,7 +355,7 @@ func (s KeyAccessServerRegistry) ListKeys(ctx context.Context, r *connect.Reques s.logger.DebugContext(ctx, "listing KAS Keys") resp, err := s.dbClient.ListKeys(ctx, r.Msg) if err != nil { - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextListRetrievalFailed, slog.String("keyAccessServer Keys", r.Msg.String())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextListRetrievalFailed, slog.String("key_access_server_keys", r.Msg.String())) } return connect.NewResponse(resp), nil @@ -397,7 +396,7 @@ func (s KeyAccessServerRegistry) RotateKey(ctx context.Context, r *connect.Reque original, err := s.dbClient.GetKey(ctx, identifier) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("keyAccessServer Keys", objectID)) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("key_access_server_keys", objectID)) } auditParams.Original = &policy.KasKey{ @@ -441,7 +440,7 @@ func (s KeyAccessServerRegistry) RotateKey(ctx context.Context, r *connect.Reque return nil }) if err != nil { - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextKeyRotationFailed, slog.String("Active Key ID", objectID), slog.String("New Key ID", r.Msg.GetNewKey().GetKeyId())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextKeyRotationFailed, slog.String("active_key_id", objectID), slog.String("new_key_id", r.Msg.GetNewKey().GetKeyId())) } // Implementation for RotateKey @@ -485,7 +484,7 @@ func (s KeyAccessServerRegistry) SetBaseKey(ctx context.Context, r *connect.Requ return nil }) if err != nil { - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("SetDefaultKey", r.Msg.GetId())) + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("set_default_key", r.Msg.GetId())) } return connect.NewResponse(resp), nil diff --git a/service/policy/kasregistry/key_access_server_registry.proto b/service/policy/kasregistry/key_access_server_registry.proto index f7c4bdf7f8..5d0e805f7e 100644 --- a/service/policy/kasregistry/key_access_server_registry.proto +++ b/service/policy/kasregistry/key_access_server_registry.proto @@ -4,7 +4,6 @@ package policy.kasregistry; import "buf/validate/validate.proto"; import "common/common.proto"; -import "google/api/annotations.proto"; import "policy/objects.proto"; import "policy/selectors.proto"; @@ -43,9 +42,28 @@ message GetKeyAccessServerResponse { KeyAccessServer key_access_server = 1; } +enum SortKeyAccessServersType { + SORT_KEY_ACCESS_SERVERS_TYPE_UNSPECIFIED = 0; + SORT_KEY_ACCESS_SERVERS_TYPE_NAME = 1; + SORT_KEY_ACCESS_SERVERS_TYPE_URI = 2; + SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT = 3; + SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT = 4; +} + +message KeyAccessServersSort { + SortKeyAccessServersType field = 1 [(buf.validate.field).enum.defined_only = true]; + policy.SortDirection direction = 2 [(buf.validate.field).enum.defined_only = true]; +} + message ListKeyAccessServersRequest { // Optional policy.PageRequest pagination = 10; + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + repeated KeyAccessServersSort sort = 11 [(buf.validate.field).repeated.max_items = 1]; } message ListKeyAccessServersResponse { repeated KeyAccessServer key_access_servers = 1; @@ -53,6 +71,18 @@ message ListKeyAccessServersResponse { policy.PageResponse pagination = 10; } +enum SortKasKeysType { + SORT_KAS_KEYS_TYPE_UNSPECIFIED = 0; + SORT_KAS_KEYS_TYPE_KEY_ID = 1; + SORT_KAS_KEYS_TYPE_CREATED_AT = 2; + SORT_KAS_KEYS_TYPE_UPDATED_AT = 3; +} + +message KasKeysSort { + SortKasKeysType field = 1 [(buf.validate.field).enum.defined_only = true]; + policy.SortDirection direction = 2 [(buf.validate.field).enum.defined_only = true]; +} + // TODO: optional validation below should be through a custom validator, which // is too bleeding edge at present without full plugin support @@ -403,7 +433,7 @@ message CreateKeyRequest { Algorithm key_algorithm = 3 [(buf.validate.field).cel = { id: "key_algorithm_defined" message: "The key_algorithm must be one of the defined values." - expression: "this in [1, 2, 3, 4, 5]" // Allow ALGORITHM_RSA_2048, ALGORITHM_RSA_4096, ALGORITHM_EC_P256, ALGORITHM_EC_P384, ALGORITHM_EC_P521 + expression: "this in [1, 2, 3, 4, 5, 6, 7, 8]" // Allow ALGORITHM_RSA_2048, ALGORITHM_RSA_4096, ALGORITHM_EC_P256, ALGORITHM_EC_P384, ALGORITHM_EC_P521, ALGORITHM_HPQT_XWING, ALGORITHM_HPQT_SECP256R1_MLKEM768, ALGORITHM_HPQT_SECP384R1_MLKEM1024 }]; // The algorithm to be used for the key // Required KeyMode key_mode = 4 [(buf.validate.field).cel = { @@ -447,7 +477,7 @@ message ListKeysRequest { Algorithm key_algorithm = 1 [(buf.validate.field).cel = { id: "key_algorithm_defined" message: "The key_algorithm must be one of the defined values." - expression: "this in [0, 1, 2, 3, 4, 5]" // Allow unspecified and object.Algorithm values for currently supported RSA bit sizes and EC curve types + expression: "this in [0, 1, 2, 3, 4, 5, 6, 7, 8]" // Allow unspecified and all supported algorithm values }]; // Filter keys by algorithm oneof kas_filter { @@ -464,6 +494,13 @@ message ListKeysRequest { // Optional policy.PageRequest pagination = 10; // Pagination request for the list of keys + + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + repeated KasKeysSort sort = 11 [(buf.validate.field).repeated.max_items = 1]; } // Response to a ListKeysRequest, containing the list of asymmetric keys and pagination information @@ -550,7 +587,7 @@ message RotateKeyRequest { Algorithm algorithm = 2 [(buf.validate.field).cel = { id: "key_algorithm_defined" message: "The key_algorithm must be one of the defined values." - expression: "this in [1, 2, 3, 4, 5]" // Allow ALGORITHM_RSA_2048, ALGORITHM_RSA_4096, ALGORITHM_EC_P256, ALGORITHM_EC_P384, ALGORITHM_EC_P521 + expression: "this in [1, 2, 3, 4, 5, 6, 7, 8]" // Allow ALGORITHM_RSA_2048, ALGORITHM_RSA_4096, ALGORITHM_EC_P256, ALGORITHM_EC_P384, ALGORITHM_EC_P521, ALGORITHM_HPQT_XWING, ALGORITHM_HPQT_SECP256R1_MLKEM768, ALGORITHM_HPQT_SECP384R1_MLKEM1024 }]; // Required KeyMode key_mode = 3 [ @@ -653,7 +690,6 @@ message ListKeyMappingsResponse { service KeyAccessServerRegistryService { rpc ListKeyAccessServers(ListKeyAccessServersRequest) returns (ListKeyAccessServersResponse) { - option (google.api.http) = {get: "/key-access-servers"}; option idempotency_level = NO_SIDE_EFFECTS; } diff --git a/service/policy/kasregistry/key_access_server_registry_keys_test.go b/service/policy/kasregistry/key_access_server_registry_keys_test.go index 74fde617a5..b777fa50f5 100644 --- a/service/policy/kasregistry/key_access_server_registry_keys_test.go +++ b/service/policy/kasregistry/key_access_server_registry_keys_test.go @@ -1423,7 +1423,7 @@ func Test_SetDefault_Keys(t *testing.T) { errorMessage: errMessageRequired, }, { - name: "Valid Request (nano)", + name: "Valid Request (ec)", req: &kasregistry.SetBaseKeyRequest{ ActiveKey: &kasregistry.SetBaseKeyRequest_Id{ Id: validUUID, @@ -1432,7 +1432,7 @@ func Test_SetDefault_Keys(t *testing.T) { expectError: false, }, { - name: "Valid Request (ztdf)", + name: "Valid Request (rsa)", req: &kasregistry.SetBaseKeyRequest{ ActiveKey: &kasregistry.SetBaseKeyRequest_Id{ Id: validUUID, diff --git a/service/policy/kasregistry/key_access_server_registry_test.go b/service/policy/kasregistry/key_access_server_registry_test.go index 5df15134d4..a32338b73d 100644 --- a/service/policy/kasregistry/key_access_server_registry_test.go +++ b/service/policy/kasregistry/key_access_server_registry_test.go @@ -1096,3 +1096,75 @@ func Test_ActivatePublicKey_Validation(t *testing.T) { }) } } + +func Test_ListKeyAccessServersRequest_Sort(t *testing.T) { + v := getValidator() + + // no sort — valid + req := &kasregistry.ListKeyAccessServersRequest{} + require.NoError(t, v.Validate(req)) + + // one sort item — valid + req = &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + { + Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + } + require.NoError(t, v.Validate(req)) + + // two sort items — exceeds max_items = 1 + req = &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + { + Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + { + Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME, + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + } + err := v.Validate(req) + require.Error(t, err) + require.Contains(t, err.Error(), "sort") +} + +func Test_ListKeysRequest_Sort(t *testing.T) { + v := getValidator() + + // no sort — valid + req := &kasregistry.ListKeysRequest{} + require.NoError(t, v.Validate(req)) + + // one sort item — valid + req = &kasregistry.ListKeysRequest{ + Sort: []*kasregistry.KasKeysSort{ + { + Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + } + require.NoError(t, v.Validate(req)) + + // two sort items — exceeds max_items = 1 + req = &kasregistry.ListKeysRequest{ + Sort: []*kasregistry.KasKeysSort{ + { + Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + { + Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID, + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + } + err := v.Validate(req) + require.Error(t, err) + require.Contains(t, err.Error(), "sort") +} diff --git a/service/policy/namespaces/namespaces.go b/service/policy/namespaces/namespaces.go index 231630399a..4fa03a0865 100644 --- a/service/policy/namespaces/namespaces.go +++ b/service/policy/namespaces/namespaces.go @@ -222,11 +222,11 @@ func (ns NamespacesService) DeactivateNamespace(ctx context.Context, req *connec return connect.NewResponse(rsp), nil } -func (ns NamespacesService) AssignKeyAccessServerToNamespace(_ context.Context, _ *connect.Request[namespaces.AssignKeyAccessServerToNamespaceRequest]) (*connect.Response[namespaces.AssignKeyAccessServerToNamespaceResponse], error) { +func (ns NamespacesService) AssignKeyAccessServerToNamespace(_ context.Context, _ *connect.Request[namespaces.AssignKeyAccessServerToNamespaceRequest]) (*connect.Response[namespaces.AssignKeyAccessServerToNamespaceResponse], error) { //nolint:staticcheck // Compatibility stub for deprecated RPC. return nil, connect.NewError(connect.CodeUnimplemented, errors.New("this compatibility stub will be removed entirely in the following release")) } -func (ns NamespacesService) RemoveKeyAccessServerFromNamespace(ctx context.Context, req *connect.Request[namespaces.RemoveKeyAccessServerFromNamespaceRequest]) (*connect.Response[namespaces.RemoveKeyAccessServerFromNamespaceResponse], error) { +func (ns NamespacesService) RemoveKeyAccessServerFromNamespace(ctx context.Context, req *connect.Request[namespaces.RemoveKeyAccessServerFromNamespaceRequest]) (*connect.Response[namespaces.RemoveKeyAccessServerFromNamespaceResponse], error) { //nolint:staticcheck // Compatibility path for deprecated RPC. rsp := &namespaces.RemoveKeyAccessServerFromNamespaceResponse{} grant := req.Msg.GetNamespaceKeyAccessServer() @@ -289,81 +289,3 @@ func (ns NamespacesService) RemovePublicKeyFromNamespace(ctx context.Context, r return connect.NewResponse(rsp), nil } - -func (ns NamespacesService) AssignCertificateToNamespace(ctx context.Context, r *connect.Request[namespaces.AssignCertificateToNamespaceRequest]) (*connect.Response[namespaces.AssignCertificateToNamespaceResponse], error) { - rsp := &namespaces.AssignCertificateToNamespaceResponse{} - - namespaceIdentifier := r.Msg.GetNamespace() - pem := r.Msg.GetPem() - metadata := r.Msg.GetMetadata() - - // Get string representation for audit log (either ID or FQN) - auditObjectID := namespaceIdentifier.GetId() - if auditObjectID == "" { - auditObjectID = namespaceIdentifier.GetFqn() - } - - auditParams := audit.PolicyEventParams{ - ActionType: audit.ActionTypeCreate, - ObjectType: audit.ObjectTypeNamespaceCertificate, - ObjectID: auditObjectID, - } - - // Create the certificate metadata - metadataJSON, _, err := db.MarshalCreateMetadata(metadata) - if err != nil { - ns.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, ns.logger, err, "Failed to marshal metadata") - } - - // Create and assign certificate in a transaction - // This ensures that if assignment fails, certificate creation is rolled back - certID, err := ns.dbClient.CreateAndAssignCertificateToNamespace(ctx, namespaceIdentifier, pem, metadataJSON) - if err != nil { - ns.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, ns.logger, err, "Failed to create and assign certificate") - } - - ns.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) - - rsp.NamespaceCertificate = &namespaces.NamespaceCertificate{ - Namespace: namespaceIdentifier, - CertificateId: certID, - } - rsp.Certificate = &policy.Certificate{ - Id: certID, - Pem: pem, - } - - return connect.NewResponse(rsp), nil -} - -func (ns NamespacesService) RemoveCertificateFromNamespace(ctx context.Context, r *connect.Request[namespaces.RemoveCertificateFromNamespaceRequest]) (*connect.Response[namespaces.RemoveCertificateFromNamespaceResponse], error) { - rsp := &namespaces.RemoveCertificateFromNamespaceResponse{} - - cert := r.Msg.GetNamespaceCertificate() - namespaceIdentifier := cert.GetNamespace() - - // Get string representation for audit log (either ID or FQN) - auditNamespaceID := namespaceIdentifier.GetId() - if auditNamespaceID == "" { - auditNamespaceID = namespaceIdentifier.GetFqn() - } - - auditParams := audit.PolicyEventParams{ - ActionType: audit.ActionTypeDelete, - ObjectType: audit.ObjectTypeNamespaceCertificate, - ObjectID: fmt.Sprintf("%s:%s", auditNamespaceID, cert.GetCertificateId()), - } - - err := ns.dbClient.RemoveCertificateFromNamespace(ctx, namespaceIdentifier, cert.GetCertificateId()) - if err != nil { - ns.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, ns.logger, err, "Failed to remove certificate from namespace") - } - ns.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) - - rsp.NamespaceCertificate = cert - - return connect.NewResponse(rsp), nil -} diff --git a/service/policy/namespaces/namespaces.proto b/service/policy/namespaces/namespaces.proto index 163af22ff8..3093ba13b9 100644 --- a/service/policy/namespaces/namespaces.proto +++ b/service/policy/namespaces/namespaces.proto @@ -78,6 +78,19 @@ message GetNamespaceResponse { policy.Namespace namespace = 1; } +enum SortNamespacesType { + SORT_NAMESPACES_TYPE_UNSPECIFIED = 0; + SORT_NAMESPACES_TYPE_NAME = 1; + SORT_NAMESPACES_TYPE_FQN = 2; + SORT_NAMESPACES_TYPE_CREATED_AT = 3; + SORT_NAMESPACES_TYPE_UPDATED_AT = 4; +} + +message NamespacesSort { + SortNamespacesType field = 1 [(buf.validate.field).enum.defined_only = true]; + policy.SortDirection direction = 2 [(buf.validate.field).enum.defined_only = true]; +} + message ListNamespacesRequest { // Optional // ACTIVE by default when not specified @@ -85,6 +98,13 @@ message ListNamespacesRequest { // Optional policy.PageRequest pagination = 10; + + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + repeated NamespacesSort sort = 11 [(buf.validate.field).repeated.max_items = 1]; } message ListNamespacesResponse { repeated policy.Namespace namespaces = 1; @@ -172,46 +192,6 @@ message RemovePublicKeyFromNamespaceResponse { NamespaceKey namespace_key = 1; } -/* - Certificates -*/ - -// Maps a namespace to a certificate (similar to NamespaceKey pattern) -message NamespaceCertificate { - // Required - namespace identifier (id or fqn) - common.IdFqnIdentifier namespace = 1 [(buf.validate.field).required = true]; - // Required (The id from the Certificate object) - string certificate_id = 2 [ - (buf.validate.field).string.uuid = true, - (buf.validate.field).required = true - ]; -} - -message AssignCertificateToNamespaceRequest { - // Required - namespace identifier (id or fqn) - common.IdFqnIdentifier namespace = 1 [(buf.validate.field).required = true]; - // Required - PEM format certificate - string pem = 2 [(buf.validate.field).required = true]; - // Optional - common.MetadataMutable metadata = 100; -} - -message AssignCertificateToNamespaceResponse { - // The mapping of the namespace to the certificate. - NamespaceCertificate namespace_certificate = 1; - policy.Certificate certificate = 2; // Return the full certificate object for convenience -} - -message RemoveCertificateFromNamespaceRequest { - // The namespace and certificate to unassign. - NamespaceCertificate namespace_certificate = 1 [(buf.validate.field).required = true]; -} - -message RemoveCertificateFromNamespaceResponse { - // The unassigned namespace and certificate. - NamespaceCertificate namespace_certificate = 1; -} - service NamespaceService { rpc GetNamespace(GetNamespaceRequest) returns (GetNamespaceResponse) { option idempotency_level = NO_SIDE_EFFECTS; @@ -243,8 +223,4 @@ service NamespaceService { *---------------------------------------*/ rpc AssignPublicKeyToNamespace(AssignPublicKeyToNamespaceRequest) returns (AssignPublicKeyToNamespaceResponse) {} rpc RemovePublicKeyFromNamespace(RemovePublicKeyFromNamespaceRequest) returns (RemovePublicKeyFromNamespaceResponse) {} - - // Namespace <> Certificate RPCs - rpc AssignCertificateToNamespace(AssignCertificateToNamespaceRequest) returns (AssignCertificateToNamespaceResponse) {} - rpc RemoveCertificateFromNamespace(RemoveCertificateFromNamespaceRequest) returns (RemoveCertificateFromNamespaceResponse) {} } diff --git a/service/policy/namespaces/namespaces_test.go b/service/policy/namespaces/namespaces_test.go index 8938b03573..40c018c3db 100644 --- a/service/policy/namespaces/namespaces_test.go +++ b/service/policy/namespaces/namespaces_test.go @@ -5,6 +5,7 @@ import ( "buf.build/go/protovalidate" "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/namespaces" "github.com/stretchr/testify/require" ) @@ -339,6 +340,42 @@ func Test_AssignPublicKeyToNamespace(t *testing.T) { } } +func Test_ListNamespacesRequest_Sort(t *testing.T) { + v := getValidator() + + // no sort — valid + req := &namespaces.ListNamespacesRequest{} + require.NoError(t, v.Validate(req)) + + // one sort item — valid + req = &namespaces.ListNamespacesRequest{ + Sort: []*namespaces.NamespacesSort{ + { + Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + } + require.NoError(t, v.Validate(req)) + + // two sort items — exceeds max_items = 1 + req = &namespaces.ListNamespacesRequest{ + Sort: []*namespaces.NamespacesSort{ + { + Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + { + Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_NAME, + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + } + err := v.Validate(req) + require.Error(t, err) + require.Contains(t, err.Error(), "sort") +} + func Test_RemovePublicKeyFromNamespace(t *testing.T) { testCases := []struct { name string @@ -398,190 +435,3 @@ func Test_RemovePublicKeyFromNamespace(t *testing.T) { }) } } - -func Test_AssignCertificateToNamespace(t *testing.T) { - const ( - // Valid PEM certificate - validPem = `-----BEGIN CERTIFICATE----- -MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC -VVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRkwFwYDVQQK -DBBvcGVudGRmLm9yZyBJbmMxDTALBgNVBAsMBFRlc3QxHjAcBgNVBAMMFW9wZW50 -ZGYub3JnIFRlc3QgQ0EwHhcNMjMwMTA0MTcwMDAwWhcNMzMwMTA0MTcwMDAwWjB8 -MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz -Y28xGTAXBgNVBAoMEG9wZW50ZGYub3JnIEluYzENMAsGA1UECwwEVGVzdDEeMBwG -A1UEAwwVb3BlbnRkZi5vcmcgVGVzdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH -A0IABJxnFtjHhP+oVPXm/hj/mZzzsKfKlF0vCL0eMR0K+Pp4OqEWVe0KN6FZPDGz -7zKcrmqU5TXnNJ9YI9U6d0hJDyCjUzBRMB0GA1UdDgQWBBQVFzPXe9XHOD+UGpnL -8N6m7w7fYDAfBgNVHSMEGDAWgBQVFzPXe9XHOD+UGpnL8N6m7w7fYDAPBgNVHRMB -Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIFBEa8VPY9xJfMPNDGR8g7mFPHvx -NKCNUZk8ooLjkVsVAiBZONcH5dDCr+fRGUnXjqWN0v+ZCVEoQr8vMrZBPf3KOQ== ------END CERTIFICATE-----` - ) - - testCases := []struct { - name string - req *namespaces.AssignCertificateToNamespaceRequest - expectError bool - errorMessage string - }{ - { - name: "Invalid - Empty Request", - req: &namespaces.AssignCertificateToNamespaceRequest{}, - expectError: true, - errorMessage: "namespace", - }, - { - name: "Invalid - Missing pem", - req: &namespaces.AssignCertificateToNamespaceRequest{ - Namespace: &common.IdFqnIdentifier{Id: validUUID}, - }, - expectError: true, - errorMessage: "pem", - }, - { - name: "Invalid - Missing namespace ID", - req: &namespaces.AssignCertificateToNamespaceRequest{ - Pem: validPem, - }, - expectError: true, - errorMessage: "namespace", - }, - { - name: "Invalid - Bad namespace UUID", - req: &namespaces.AssignCertificateToNamespaceRequest{ - Namespace: &common.IdFqnIdentifier{Id: "not-a-uuid"}, - Pem: validPem, - }, - expectError: true, - errorMessage: errMessageUUID, - }, - { - name: "Valid - All fields present", - req: &namespaces.AssignCertificateToNamespaceRequest{ - Namespace: &common.IdFqnIdentifier{Id: validUUID}, - Pem: validPem, - }, - expectError: false, - }, - { - name: "Valid - With metadata", - req: &namespaces.AssignCertificateToNamespaceRequest{ - Namespace: &common.IdFqnIdentifier{Id: validUUID}, - Pem: validPem, - Metadata: &common.MetadataMutable{ - Labels: map[string]string{"source": "test"}, - }, - }, - expectError: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := getValidator().Validate(tc.req) - if tc.expectError { - require.Error(t, err, "Expected error for test case: %s", tc.name) - if tc.errorMessage != "" { - require.Contains(t, err.Error(), tc.errorMessage, "Expected error message to contain '%s' for test case: %s", tc.errorMessage, tc.name) - } - } else { - require.NoError(t, err, "Expected no error for test case: %s", tc.name) - } - }) - } -} - -func Test_RemoveCertificateFromNamespace(t *testing.T) { - const ( - errMessageNamespaceCert = "namespace_certificate" - errMessageCertID = "certificate_id" - ) - - testCases := []struct { - name string - req *namespaces.RemoveCertificateFromNamespaceRequest - expectError bool - errorMessage string - }{ - { - name: "Invalid - Empty Request", - req: &namespaces.RemoveCertificateFromNamespaceRequest{}, - expectError: true, - errorMessage: errMessageNamespaceCert, - }, - { - name: "Invalid - Empty NamespaceCertificate", - req: &namespaces.RemoveCertificateFromNamespaceRequest{ - NamespaceCertificate: &namespaces.NamespaceCertificate{}, - }, - expectError: true, - errorMessage: "namespace", - }, - { - name: "Invalid - Missing certificate ID", - req: &namespaces.RemoveCertificateFromNamespaceRequest{ - NamespaceCertificate: &namespaces.NamespaceCertificate{ - Namespace: &common.IdFqnIdentifier{Id: validUUID}, - }, - }, - expectError: true, - errorMessage: errMessageCertID, - }, - { - name: "Invalid - Missing namespace ID", - req: &namespaces.RemoveCertificateFromNamespaceRequest{ - NamespaceCertificate: &namespaces.NamespaceCertificate{ - CertificateId: validUUID, - }, - }, - expectError: true, - errorMessage: "namespace", - }, - { - name: "Invalid - Bad namespace UUID", - req: &namespaces.RemoveCertificateFromNamespaceRequest{ - NamespaceCertificate: &namespaces.NamespaceCertificate{ - Namespace: &common.IdFqnIdentifier{Id: "not-a-uuid"}, - CertificateId: validUUID, - }, - }, - expectError: true, - errorMessage: errMessageUUID, - }, - { - name: "Invalid - Bad certificate UUID", - req: &namespaces.RemoveCertificateFromNamespaceRequest{ - NamespaceCertificate: &namespaces.NamespaceCertificate{ - Namespace: &common.IdFqnIdentifier{Id: validUUID}, - CertificateId: "not-a-uuid", - }, - }, - expectError: true, - errorMessage: errMessageUUID, - }, - { - name: "Valid - All fields present", - req: &namespaces.RemoveCertificateFromNamespaceRequest{ - NamespaceCertificate: &namespaces.NamespaceCertificate{ - Namespace: &common.IdFqnIdentifier{Id: validUUID}, - CertificateId: validUUID, - }, - }, - expectError: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := getValidator().Validate(tc.req) - if tc.expectError { - require.Error(t, err, "Expected error for test case: %s", tc.name) - if tc.errorMessage != "" { - require.Contains(t, err.Error(), tc.errorMessage, "Expected error message to contain '%s' for test case: %s", tc.errorMessage, tc.name) - } - } else { - require.NoError(t, err, "Expected no error for test case: %s", tc.name) - } - }) - } -} diff --git a/service/policy/objects.proto b/service/policy/objects.proto index 35b7cfb408..3e6ee4d794 100644 --- a/service/policy/objects.proto +++ b/service/policy/objects.proto @@ -48,18 +48,6 @@ message Namespace { // Keys for the namespace repeated SimpleKasKey kas_keys = 7; - - // Root certificates for chain of trust - repeated Certificate root_certs = 8; -} - -message Certificate { - // generated uuid in database - string id = 1; - // PEM format certificate - string pem = 2; - // Optional metadata. - common.Metadata metadata = 3; } message Attribute { @@ -90,6 +78,10 @@ message Attribute { //Keys associated with the attribute repeated SimpleKasKey kas_keys = 9; + // Whether or not we will use the attribute definition during encryption + // if the attribute value is missing. + google.protobuf.BoolValue allow_traversal = 10; + // Common metadata common.Metadata metadata = 100; } @@ -160,6 +152,9 @@ message Action { string name = 4; + // Namespace context for this action + Namespace namespace = 5; + common.Metadata metadata = 100; } @@ -201,6 +196,11 @@ message SubjectMapping { // The actions permitted by subjects in this mapping repeated Action actions = 4; + // the namespace containing this subject mapping + // possible this is empty. If so that means + // the Subject Mapping has not been migrated to a namespace. + Namespace namespace = 5; + common.Metadata metadata = 100; } @@ -252,6 +252,11 @@ message SubjectSet { message SubjectConditionSet { string id = 1; + // the namespace containing this subject condition set + // possible this is empty in the case a subject condition set + // has not been migrated to a namespace. + Namespace namespace = 2; + repeated SubjectSet subject_sets = 3 [(buf.validate.field).repeated.min_items = 1]; common.Metadata metadata = 100; @@ -292,6 +297,9 @@ message ResourceMappingGroup { // per namespace string name = 3 [(buf.validate.field).required = true]; + // the fully qualified name of the resource mapping group + string fqn = 4; + // Common metadata common.Metadata metadata = 100; } @@ -383,6 +391,9 @@ enum KasPublicKeyAlgEnum { KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 = 5; KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 = 6; KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 = 7; + KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING = 10; + KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 = 11; + KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 = 12; } // Deprecated @@ -401,8 +412,8 @@ message KasPublicKey { }]; // A known algorithm type with any additional parameters encoded. - // To start, these may be `rsa:2048` for encrypting ZTDF files and - // `ec:secp256r1` for nanoTDF, but more formats may be added as needed. + // To start, these may be `rsa:2048` for RSA-based wrapping and + // `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. KasPublicKeyAlgEnum alg = 3 [(buf.validate.field).enum = { defined_only: true not_in: [0] @@ -448,6 +459,8 @@ message RegisteredResource { repeated RegisteredResourceValue values = 3; + Namespace namespace = 4; + // Common metadata common.Metadata metadata = 100; } @@ -471,6 +484,8 @@ message RegisteredResourceValue { repeated ActionAttributeValue action_attribute_values = 4; + string fqn = 5; + // Common metadata common.Metadata metadata = 100; } @@ -525,6 +540,9 @@ message ObligationTrigger { repeated RequestContext context = 5; + // The source namespace for this trigger, derived from the attribute value and action. + Namespace namespace = 11; + common.Metadata metadata = 100; } @@ -546,6 +564,9 @@ enum Algorithm { ALGORITHM_EC_P256 = 3; ALGORITHM_EC_P384 = 4; ALGORITHM_EC_P521 = 5; + ALGORITHM_HPQT_XWING = 6; + ALGORITHM_HPQT_SECP256R1_MLKEM768 = 7; + ALGORITHM_HPQT_SECP384R1_MLKEM1024 = 8; } // The status of the key diff --git a/service/policy/obligations/obligations.go b/service/policy/obligations/obligations.go index efae842d2c..871fc3815c 100644 --- a/service/policy/obligations/obligations.go +++ b/service/policy/obligations/obligations.go @@ -339,6 +339,19 @@ func (s *Service) DeleteObligationValue(ctx context.Context, req *connect.Reques return connect.NewResponse(rsp), nil } +func (s *Service) GetObligationTrigger(ctx context.Context, req *connect.Request[obligations.GetObligationTriggerRequest]) (*connect.Response[obligations.GetObligationTriggerResponse], error) { + id := req.Msg.GetId() + s.logger.DebugContext(ctx, "getting obligation trigger", slog.String("id", id)) + + trigger, err := s.dbClient.GetObligationTrigger(ctx, req.Msg) + if err != nil { + return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("id", id)) + } + + rsp := &obligations.GetObligationTriggerResponse{Trigger: trigger} + return connect.NewResponse(rsp), nil +} + func (s *Service) AddObligationTrigger(ctx context.Context, req *connect.Request[obligations.AddObligationTriggerRequest]) (*connect.Response[obligations.AddObligationTriggerResponse], error) { rsp := &obligations.AddObligationTriggerResponse{} diff --git a/service/policy/obligations/obligations.proto b/service/policy/obligations/obligations.proto index a2812b3d61..d6ad3a9763 100644 --- a/service/policy/obligations/obligations.proto +++ b/service/policy/obligations/obligations.proto @@ -12,6 +12,20 @@ import "buf/validate/validate.proto"; /// // Definitions + +enum SortObligationsType { + SORT_OBLIGATIONS_TYPE_UNSPECIFIED = 0; + SORT_OBLIGATIONS_TYPE_NAME = 1; + SORT_OBLIGATIONS_TYPE_FQN = 2; + SORT_OBLIGATIONS_TYPE_CREATED_AT = 3; + SORT_OBLIGATIONS_TYPE_UPDATED_AT = 4; +} + +message ObligationsSort { + SortObligationsType field = 1 [(buf.validate.field).enum.defined_only = true]; + policy.SortDirection direction = 2 [(buf.validate.field).enum.defined_only = true]; +} + message GetObligationRequest { option (buf.validate.message).oneof = { fields: ["id", "fqn"], required: true }; string id = 1 [(buf.validate.field).string.uuid = true]; @@ -150,6 +164,13 @@ message ListObligationsRequest { // Optional policy.PageRequest pagination = 10; + + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + repeated ObligationsSort sort = 11 [(buf.validate.field).repeated.max_items = 1]; } message ListObligationsResponse { @@ -272,6 +293,18 @@ message DeleteObligationValueResponse { } // Triggers +message GetObligationTriggerRequest { + // Required + string id = 1 [(buf.validate.field).string.uuid = true]; +} + +message GetObligationTriggerResponse { + policy.ObligationTrigger trigger = 1; +} + +// Obligation Triggers are owned by the namespace that owns the action and attribute value, which must +// be the same. In this way, a trigger can intentionally cross namespace boundaries: associating +// obligation values of a different namespace than the one that owns the action being taken or the attribute value. message AddObligationTriggerRequest { // Required common.IdFqnIdentifier obligation_value = 1 [(buf.validate.field).required = true]; @@ -393,6 +426,10 @@ service Service { /*--------------------------------------* * Trigger RPCs *--------------------------------------*/ + + rpc GetObligationTrigger(GetObligationTriggerRequest) returns (GetObligationTriggerResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } rpc AddObligationTrigger(AddObligationTriggerRequest) returns (AddObligationTriggerResponse) {} @@ -409,4 +446,4 @@ service Service { // rpc AddObligationFulfiller(AddObligationFulfillerRequest) returns (AddObligationFulfillerResponse) {} // rpc RemoveObligationFulfiller(RemoveObligationFulfillerRequest) returns (RemoveObligationFulfillerResponse) {} -} \ No newline at end of file +} diff --git a/service/policy/obligations/obligations_test.go b/service/policy/obligations/obligations_test.go index d37d4f3d3b..585e529c60 100644 --- a/service/policy/obligations/obligations_test.go +++ b/service/policy/obligations/obligations_test.go @@ -22,6 +22,7 @@ const ( invalidName = "invalid name" invalidFQN = "invalid-fqn" errMessageUUID = "string.uuid" + errMessageUUIDEmpty = "string.uuid_empty" errMessageURI = "string.uri" errMessageMinItems = "repeated.min_items" errMessageUnique = "repeated.unique" @@ -746,6 +747,52 @@ func Test_AddObligationTrigger_Request(t *testing.T) { } } +func Test_GetObligationTrigger_Request(t *testing.T) { + validUUID := uuid.NewString() + testCases := []struct { + name string + req *obligations.GetObligationTriggerRequest + expectError bool + errorMessage string + }{ + { + name: "valid", + req: &obligations.GetObligationTriggerRequest{ + Id: validUUID, + }, + expectError: false, + }, + { + name: "invalid id", + req: &obligations.GetObligationTriggerRequest{ + Id: invalidUUID, + }, + expectError: true, + errorMessage: errMessageUUID, + }, + { + name: "missing id", + req: &obligations.GetObligationTriggerRequest{}, + expectError: true, + errorMessage: errMessageUUIDEmpty, + }, + } + + v := getValidator() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := v.Validate(tc.req) + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errorMessage) + } else { + require.NoError(t, err) + } + }) + } +} + func Test_RemoveObligationTrigger_Request(t *testing.T) { validUUID := uuid.NewString() testCases := []struct { @@ -1246,3 +1293,39 @@ func Test_ListObligationTriggers_Request(t *testing.T) { }) } } + +func Test_ListObligationsRequest_Sort(t *testing.T) { + v := getValidator() + + // no sort (valid) + req := &obligations.ListObligationsRequest{} + require.NoError(t, v.Validate(req)) + + // one sorted item (valid) + req = &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + { + Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + } + require.NoError(t, v.Validate(req)) + + // two items sorted (invalid, exceeds max_items = 1) + req = &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + { + Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + { + Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME, + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + } + err := v.Validate(req) + require.Error(t, err) + require.Contains(t, err.Error(), "sort") +} diff --git a/service/policy/registeredresources/registered_resources.go b/service/policy/registeredresources/registered_resources.go index a3448bfd35..6fc98fbd8b 100644 --- a/service/policy/registeredresources/registered_resources.go +++ b/service/policy/registeredresources/registered_resources.go @@ -2,10 +2,12 @@ package registeredresources import ( "context" + "errors" "fmt" "log/slog" "connectrpc.com/connect" + "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/registeredresources" "github.com/opentdf/platform/protocol/go/policy/registeredresources/registeredresourcesconnect" "github.com/opentdf/platform/service/logger" @@ -94,6 +96,12 @@ func (s *RegisteredResourcesService) CreateRegisteredResource(ctx context.Contex s.logger.DebugContext(ctx, "creating registered resource", slog.String("name", req.Msg.GetName())) + // --- BEGIN namespace enforcement (remove when enforce_namespace flag is phased out) --- + if s.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("namespace is required: provide either namespace_id or namespace_fqn")) + } + // --- END namespace enforcement --- + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { resource, err := txClient.CreateRegisteredResource(ctx, req.Msg) if err != nil { @@ -340,7 +348,12 @@ func (s *RegisteredResourcesService) DeleteRegisteredResourceValue(ctx context.C s.logger.DebugContext(ctx, "deleting registered resource value", slog.String("id", valueID)) - deleted, err := s.dbClient.DeleteRegisteredResourceValue(ctx, valueID) + var deleted *policy.RegisteredResourceValue + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + var err error + deleted, err = txClient.DeleteRegisteredResourceValue(ctx, valueID) + return err + }) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextDeletionFailed, slog.String("registered resource value", req.Msg.String())) diff --git a/service/policy/registeredresources/registered_resources.proto b/service/policy/registeredresources/registered_resources.proto index c69401971f..f12a1f4ac3 100644 --- a/service/policy/registeredresources/registered_resources.proto +++ b/service/policy/registeredresources/registered_resources.proto @@ -24,7 +24,7 @@ message CreateRegisteredResourceRequest { } ]; - // Optional + // Optional // Registered Resource Values (when provided) must be alphanumeric strings, allowing hyphens and underscores but not as the first or last character. // The stored value will be normalized to lower case. repeated string values = 2 [ @@ -32,7 +32,7 @@ message CreateRegisteredResourceRequest { min_items: 0, unique: true, items: { - string: + string: { max_len: 253, pattern: "^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$" @@ -41,6 +41,16 @@ message CreateRegisteredResourceRequest { } ]; + // Optional (yet to be enforced) + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + string namespace_id = 3 [(buf.validate.field).string.uuid = true]; + string namespace_fqn = 4 [ + (buf.validate.field).string = { + min_len : 1 + uri : true + } + ]; + // Optional // Common metadata common.MetadataMutable metadata = 100; @@ -67,14 +77,54 @@ message GetRegisteredResourceRequest { } ]; } + + // Optional - namespace context for name-based lookups (since names are not globally unique for namespaced RRs) + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + string namespace_fqn = 3 [ + (buf.validate.field).string = { + min_len : 1 + uri : true + } + ]; + string namespace_id = 4 [(buf.validate.field).string.uuid = true]; } message GetRegisteredResourceResponse { policy.RegisteredResource resource = 1; } +enum SortRegisteredResourcesType { + SORT_REGISTERED_RESOURCES_TYPE_UNSPECIFIED = 0; + SORT_REGISTERED_RESOURCES_TYPE_NAME = 1; + SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT = 2; + SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT = 3; +} + +message RegisteredResourcesSort { + SortRegisteredResourcesType field = 1 [(buf.validate.field).enum.defined_only = true]; + policy.SortDirection direction = 2 [(buf.validate.field).enum.defined_only = true]; +} + message ListRegisteredResourcesRequest { + // Optional + // Namespace ID or FQN + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + string namespace_id = 1 [(buf.validate.field).string.uuid = true]; + string namespace_fqn = 2 [ + (buf.validate.field).string = { + min_len : 1 + uri : true + } + ]; + // Optional policy.PageRequest pagination = 10; + + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + repeated RegisteredResourcesSort sort = 11 [(buf.validate.field).repeated.max_items = 1]; } message ListRegisteredResourcesResponse { repeated policy.RegisteredResource resources = 1; diff --git a/service/policy/registeredresources/registered_resources_test.go b/service/policy/registeredresources/registered_resources_test.go index 2ca82dcaf5..b8d311e4e8 100644 --- a/service/policy/registeredresources/registered_resources_test.go +++ b/service/policy/registeredresources/registered_resources_test.go @@ -5,6 +5,7 @@ import ( "testing" "buf.build/go/protovalidate" + "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/registeredresources" "github.com/stretchr/testify/suite" ) @@ -63,20 +64,37 @@ func (s *RegisteredResourcesSuite) TestCreateRegisteredResource_Valid_Succeeds() req *registeredresources.CreateRegisteredResourceRequest }{ { - name: "Name Only", + name: "Name with Namespace ID", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: validName, + Name: validName, + NamespaceId: validUUID, }, }, { - name: "Name with Values", + name: "Name with Namespace FQN", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: validName, + Name: validName, + NamespaceFqn: validURI, + }, + }, + { + name: "Name with Values and Namespace ID", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: validName, + NamespaceId: validUUID, Values: []string{ validValue, }, }, }, + // --- BEGIN namespace-optional (remove when enforce_namespace flag is phased out) --- + { + name: "Name without Namespace", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: validName, + }, + }, + // --- END namespace-optional --- } for _, tc := range testCases { @@ -95,70 +113,95 @@ func (s *RegisteredResourcesSuite) TestCreateRegisteredResource_Invalid_Fails() errMsg string }{ { - name: "Missing Name", + name: "Missing Name and Namespace", req: ®isteredresources.CreateRegisteredResourceRequest{}, errMsg: errMsgRequired, }, + { + name: "Invalid Namespace ID", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: validName, + NamespaceId: invalidUUID, + }, + errMsg: errMsgUUID, + }, + { + name: "Invalid Namespace FQN", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: validName, + NamespaceFqn: invalidURI, + }, + errMsg: errMsgURI, + }, { name: "Invalid Name (space)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: " ", + Name: " ", + NamespaceId: validUUID, }, errMsg: errMsgNameFormat, }, { name: "Invalid Name (too long)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: strings.Repeat("a", 254), + Name: strings.Repeat("a", 254), + NamespaceId: validUUID, }, errMsg: errMsgStringMaxLen, }, { name: "Invalid Name (text with spaces)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: "invalid name", + Name: "invalid name", + NamespaceId: validUUID, }, errMsg: errMsgNameFormat, }, { name: "Invalid Name (text with special chars)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: "invalid@name", + Name: "invalid@name", + NamespaceId: validUUID, }, errMsg: errMsgNameFormat, }, { name: "Invalid Name (leading underscore)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: "_invalid_name", + Name: "_invalid_name", + NamespaceId: validUUID, }, errMsg: errMsgNameFormat, }, { name: "Invalid Name (trailing underscore)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: "invalid_name_", + Name: "invalid_name_", + NamespaceId: validUUID, }, errMsg: errMsgNameFormat, }, { name: "Invalid Name (leading hyphen)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: "-invalid-name", + Name: "-invalid-name", + NamespaceId: validUUID, }, errMsg: errMsgNameFormat, }, { name: "Invalid Name (trailing hyphen)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: "invalid-name-", + Name: "invalid-name-", + NamespaceId: validUUID, }, errMsg: errMsgNameFormat, }, { name: "Invalid Name (invalid values)", req: ®isteredresources.CreateRegisteredResourceRequest{ - Name: validName, + Name: validName, + NamespaceId: validUUID, Values: []string{ "invalid value", }, @@ -200,6 +243,24 @@ func (s *RegisteredResourcesSuite) TestGetRegisteredResource_Valid_Succeeds() { }, }, }, + { + name: "Name with Namespace ID", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: validName, + }, + NamespaceId: validUUID, + }, + }, + { + name: "Name with Namespace FQN", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: validName, + }, + NamespaceFqn: validURI, + }, + }, } for _, tc := range testCases { @@ -240,6 +301,37 @@ func (s *RegisteredResourcesSuite) TestGetRegisteredResource_Invalid_Fails() { }, errMsg: errMsgNameFormat, }, + { + name: "Invalid Namespace ID (non-UUID)", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: validName, + }, + NamespaceId: invalidUUID, + }, + errMsg: errMsgUUID, + }, + { + name: "Invalid Namespace FQN (non-URI)", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: validName, + }, + NamespaceFqn: invalidURI, + }, + errMsg: errMsgURI, + }, + { + name: "Both Namespace ID and FQN provided (oneof violation)", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: validName, + }, + NamespaceId: validUUID, + NamespaceFqn: validURI, + }, + errMsg: "oneof", + }, } for _, tc := range testCases { @@ -1034,3 +1126,37 @@ func (s *RegisteredResourcesSuite) TestDeleteRegisteredResourceValue_Invalid_Fai }) } } + +func (s *RegisteredResourcesSuite) TestListRegisteredResourcesRequest_Sort() { + // no sort — valid + req := ®isteredresources.ListRegisteredResourcesRequest{} + s.Require().NoError(s.v.Validate(req)) + + // one sort item — valid + req = ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + { + Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + } + s.Require().NoError(s.v.Validate(req)) + + // two sort items — exceeds max_items = 1 + req = ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + { + Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + { + Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME, + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + } + err := s.v.Validate(req) + s.Require().Error(err) + s.Require().ErrorContains(err, "sort") +} diff --git a/service/policy/resourcemapping/resource_mapping.go b/service/policy/resourcemapping/resource_mapping.go index 1426dfd673..af87df416f 100644 --- a/service/policy/resourcemapping/resource_mapping.go +++ b/service/policy/resourcemapping/resource_mapping.go @@ -108,7 +108,16 @@ func (s ResourceMappingService) CreateResourceMappingGroup(ctx context.Context, ObjectType: audit.ObjectTypeResourceMappingGroup, } - rmGroup, err := s.dbClient.CreateResourceMappingGroup(ctx, req.Msg) + var rmGroup *policy.ResourceMappingGroup + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + var err error + rmGroup, err = txClient.CreateResourceMappingGroup(ctx, req.Msg) + if err != nil { + return err + } + + return nil + }) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("resourceMappingGroup", req.Msg.String())) @@ -134,13 +143,22 @@ func (s ResourceMappingService) UpdateResourceMappingGroup(ctx context.Context, ObjectID: id, } - originalRmGroup, err := s.dbClient.GetResourceMappingGroup(ctx, id) - if err != nil { - s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("id", id)) - } + var originalRmGroup *policy.ResourceMappingGroup + var updatedRmGroup *policy.ResourceMappingGroup + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + var err error + originalRmGroup, err = txClient.GetResourceMappingGroup(ctx, id) + if err != nil { + return err + } - updatedRmGroup, err := s.dbClient.UpdateResourceMappingGroup(ctx, id, req.Msg) + updatedRmGroup, err = txClient.UpdateResourceMappingGroup(ctx, id, req.Msg) + if err != nil { + return err + } + + return nil + }) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("id", id)) @@ -148,12 +166,9 @@ func (s ResourceMappingService) UpdateResourceMappingGroup(ctx context.Context, auditParams.Original = originalRmGroup auditParams.Updated = updatedRmGroup - s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) - rsp.ResourceMappingGroup = &policy.ResourceMappingGroup{ - Id: id, - } + rsp.ResourceMappingGroup = updatedRmGroup return connect.NewResponse(rsp), nil } @@ -169,7 +184,16 @@ func (s ResourceMappingService) DeleteResourceMappingGroup(ctx context.Context, ObjectID: id, } - _, err := s.dbClient.DeleteResourceMappingGroup(ctx, id) + var deletedRmGroup *policy.ResourceMappingGroup + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + var err error + deletedRmGroup, err = txClient.DeleteResourceMappingGroup(ctx, id) + if err != nil { + return err + } + + return nil + }) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextDeletionFailed, slog.String("id", id)) @@ -177,9 +201,7 @@ func (s ResourceMappingService) DeleteResourceMappingGroup(ctx context.Context, s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) - rsp.ResourceMappingGroup = &policy.ResourceMappingGroup{ - Id: id, - } + rsp.ResourceMappingGroup = deletedRmGroup return connect.NewResponse(rsp), nil } @@ -241,7 +263,16 @@ func (s ResourceMappingService) CreateResourceMapping(ctx context.Context, ObjectType: audit.ObjectTypeResourceMapping, } - rm, err := s.dbClient.CreateResourceMapping(ctx, req.Msg) + var rm *policy.ResourceMapping + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + var err error + rm, err = txClient.CreateResourceMapping(ctx, req.Msg) + if err != nil { + return err + } + + return nil + }) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("resourceMapping", req.Msg.String())) @@ -269,13 +300,22 @@ func (s ResourceMappingService) UpdateResourceMapping(ctx context.Context, ObjectID: resourceMappingID, } - originalRM, err := s.dbClient.GetResourceMapping(ctx, resourceMappingID) - if err != nil { - s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) - return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextListRetrievalFailed) - } + var originalRM *policy.ResourceMapping + var updatedRM *policy.ResourceMapping + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + var err error + originalRM, err = txClient.GetResourceMapping(ctx, resourceMappingID) + if err != nil { + return err + } + + updatedRM, err = txClient.UpdateResourceMapping(ctx, resourceMappingID, req.Msg) + if err != nil { + return err + } - updatedRM, err := s.dbClient.UpdateResourceMapping(ctx, resourceMappingID, req.Msg) + return nil + }) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) return nil, db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, @@ -288,9 +328,7 @@ func (s ResourceMappingService) UpdateResourceMapping(ctx context.Context, auditParams.Updated = updatedRM s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) - rsp.ResourceMapping = &policy.ResourceMapping{ - Id: resourceMappingID, - } + rsp.ResourceMapping = updatedRM return connect.NewResponse(rsp), nil } diff --git a/service/policy/selectors.proto b/service/policy/selectors.proto index 704ac97053..8ad27001fc 100644 --- a/service/policy/selectors.proto +++ b/service/policy/selectors.proto @@ -52,6 +52,16 @@ message AttributeValueSelector { AttributeSelector with_attribute = 10; } +// Sorting direction shared across list APIs. +// When the 'sort' field is omitted or the chosen sort 'field' is UNSPECIFIED, +// the endpoint's request message defines the default ordering; see the +// specific List* request docs. +enum SortDirection { + SORT_DIRECTION_UNSPECIFIED = 0; + SORT_DIRECTION_ASC = 1; + SORT_DIRECTION_DESC = 2; +} + message PageRequest { // Optional // Set to configured default limit if not provided diff --git a/service/policy/subjectmapping/subject_condition_set_test.go b/service/policy/subjectmapping/subject_condition_set_test.go index 3044870639..9c689f2e97 100644 --- a/service/policy/subjectmapping/subject_condition_set_test.go +++ b/service/policy/subjectmapping/subject_condition_set_test.go @@ -29,6 +29,7 @@ func Test_CreateSubjectConditionSetRequest_InvalidSubjectConditionSet_Fails(t *t conditionSet := &subjectmapping.SubjectConditionSetCreate{} return &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: conditionSet, + NamespaceId: fakeID, } }, expectedError: errLessThanMinItems, @@ -42,6 +43,7 @@ func Test_CreateSubjectConditionSetRequest_InvalidSubjectConditionSet_Fails(t *t } return &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: conditionSet, + NamespaceId: fakeID, } }, expectedError: errLessThanMinItems, @@ -59,6 +61,7 @@ func Test_CreateSubjectConditionSetRequest_InvalidSubjectConditionSet_Fails(t *t } return &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: conditionSet, + NamespaceId: fakeID, } }, expectedError: errLessThanMinItems, @@ -76,6 +79,7 @@ func Test_CreateSubjectConditionSetRequest_InvalidSubjectConditionSet_Fails(t *t } return &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: conditionSet, + NamespaceId: fakeID, } }, expectedError: errLessThanMinItems, @@ -102,6 +106,7 @@ func Test_CreateSubjectConditionSetRequest_InvalidSubjectConditionSet_Fails(t *t } return &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: conditionSet, + NamespaceId: fakeID, } }, expectedError: "operator", @@ -127,6 +132,7 @@ func Test_CreateSubjectConditionSetRequest_InvalidSubjectConditionSet_Fails(t *t } return &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: conditionSet, + NamespaceId: fakeID, } }, expectedError: "subject_external_selector_value", @@ -153,6 +159,7 @@ func Test_CreateSubjectConditionSetRequest_InvalidSubjectConditionSet_Fails(t *t } return &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: conditionSet, + NamespaceId: fakeID, } }, expectedError: "subject_external_values", @@ -197,8 +204,241 @@ func Test_CreateSubjectConditionSetRequest_ValidSubjectConditionSet_Succeeds(t * } req := &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: conditionSet, + NamespaceId: fakeID, } err := getValidator().Validate(req) require.NoError(t, err) + + req = &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: conditionSet, + NamespaceFqn: validNamespaceFQN, + } + err = getValidator().Validate(req) + require.NoError(t, err) + + req = &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: conditionSet, + } + err = getValidator().Validate(req) + require.NoError(t, err) +} + +func Test_CreateSubjectConditionSetRequest_MissingNamespace_Succeeds(t *testing.T) { + conditionSet := &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalSelectorValue: ".some_field", + SubjectExternalValues: []string{"some_value"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR, + }, + }, + }, + }, + } + req := &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: conditionSet, + } + + err := getValidator().Validate(req) + require.NoError(t, err) + + req = &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: conditionSet, + NamespaceFqn: validNamespaceFQN, + } + err = getValidator().Validate(req) + require.NoError(t, err) +} + +func Test_CreateSubjectConditionSetRequest_InvalidNamespace_Fails(t *testing.T) { + conditionSet := &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalSelectorValue: ".some_field", + SubjectExternalValues: []string{"some_value"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR, + }, + }, + }, + }, + } + + testCases := []struct { + name string + req *subjectmapping.CreateSubjectConditionSetRequest + expectedError string + }{ + { + name: "invalid namespace id", + req: &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: conditionSet, + NamespaceId: "bad-namespace-id", + }, + expectedError: errMessageUUID, + }, + { + name: "invalid namespace fqn", + req: &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: conditionSet, + NamespaceFqn: "not-a-uri", + }, + expectedError: errMessageURI, + }, + { + name: "both namespace id and fqn", + req: &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: conditionSet, + NamespaceId: fakeID, + NamespaceFqn: validNamespaceFQN, + }, + expectedError: errMessageOneof, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := getValidator().Validate(tc.req) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedError) + }) + } +} + +func Test_ListSubjectConditionSetsRequest_Succeeds(t *testing.T) { + testCases := []struct { + name string + req *subjectmapping.ListSubjectConditionSetsRequest + }{ + { + name: "no filters", + req: &subjectmapping.ListSubjectConditionSetsRequest{}, + }, + { + name: "namespace id only", + req: &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceId: fakeID, + }, + }, + { + name: "namespace fqn only", + req: &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceFqn: validNamespaceFQN, + }, + }, + { + name: "pagination only", + req: &subjectmapping.ListSubjectConditionSetsRequest{ + Pagination: &policy.PageRequest{ + Limit: 10, + Offset: 5, + }, + }, + }, + { + name: "namespace filter with pagination", + req: &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceFqn: validNamespaceFQN, + Pagination: &policy.PageRequest{ + Limit: 20, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := getValidator().Validate(tc.req) + require.NoError(t, err) + }) + } +} + +func Test_ListSubjectConditionSetsRequest_Fails(t *testing.T) { + testCases := []struct { + name string + req *subjectmapping.ListSubjectConditionSetsRequest + expectedError string + }{ + { + name: "invalid namespace id", + req: &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceId: "bad-namespace-id", + }, + expectedError: errMessageUUID, + }, + { + name: "invalid namespace fqn", + req: &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceFqn: "not-a-uri", + }, + expectedError: errMessageURI, + }, + { + name: "both namespace id and fqn", + req: &subjectmapping.ListSubjectConditionSetsRequest{ + NamespaceId: fakeID, + NamespaceFqn: validNamespaceFQN, + }, + expectedError: errMessageOneof, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := getValidator().Validate(tc.req) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedError) + }) + } +} + +func Test_ListSubjectConditionSetsRequest_Sort(t *testing.T) { + v := getValidator() + + // no sort — valid + req := &subjectmapping.ListSubjectConditionSetsRequest{} + require.NoError(t, v.Validate(req)) + + // one sort item — valid + req = &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + { + Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + } + require.NoError(t, v.Validate(req)) + + // two sort items — exceeds max_items = 1 + req = &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + { + Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + { + Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + } + err := v.Validate(req) + require.Error(t, err) + require.Contains(t, err.Error(), "sort") } diff --git a/service/policy/subjectmapping/subject_mapping.go b/service/policy/subjectmapping/subject_mapping.go index 806cac865d..554a3c3ab3 100644 --- a/service/policy/subjectmapping/subject_mapping.go +++ b/service/policy/subjectmapping/subject_mapping.go @@ -2,6 +2,7 @@ package subjectmapping import ( "context" + "errors" "fmt" "log/slog" @@ -83,6 +84,9 @@ func (s SubjectMappingService) CreateSubjectMapping(ctx context.Context, ) (*connect.Response[sm.CreateSubjectMappingResponse], error) { rsp := &sm.CreateSubjectMappingResponse{} s.logger.DebugContext(ctx, "creating subject mapping") + if s.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("either namespace_id or namespace_fqn must be provided")) + } auditParams := audit.PolicyEventParams{ ActionType: audit.ActionTypeCreate, @@ -261,6 +265,9 @@ func (s SubjectMappingService) CreateSubjectConditionSet(ctx context.Context, ) (*connect.Response[sm.CreateSubjectConditionSetResponse], error) { rsp := &sm.CreateSubjectConditionSetResponse{} s.logger.DebugContext(ctx, "creating subject condition set", slog.Any("subject_condition_set", req.Msg)) + if s.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("either namespace_id or namespace_fqn must be provided")) + } auditParams := audit.PolicyEventParams{ ActionType: audit.ActionTypeCreate, @@ -269,7 +276,7 @@ func (s SubjectMappingService) CreateSubjectConditionSet(ctx context.Context, var conditionSet *policy.SubjectConditionSet err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { - cs, err := txClient.CreateSubjectConditionSet(ctx, req.Msg.GetSubjectConditionSet()) + cs, err := txClient.CreateSubjectConditionSet(ctx, req.Msg.GetSubjectConditionSet(), req.Msg.GetNamespaceId(), req.Msg.GetNamespaceFqn()) if err != nil { s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) return db.StatusifyError(ctx, s.logger, err, db.ErrTextCreationFailed, slog.String("subjectConditionSet", req.Msg.String())) diff --git a/service/policy/subjectmapping/subject_mapping.proto b/service/policy/subjectmapping/subject_mapping.proto index f725cc2729..af6da858ba 100644 --- a/service/policy/subjectmapping/subject_mapping.proto +++ b/service/policy/subjectmapping/subject_mapping.proto @@ -29,9 +29,38 @@ message GetSubjectMappingResponse { policy.SubjectMapping subject_mapping = 1; } +enum SortSubjectMappingsType { + SORT_SUBJECT_MAPPINGS_TYPE_UNSPECIFIED = 0; + SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT = 1; + SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT = 2; +} + +message SubjectMappingsSort { + SortSubjectMappingsType field = 1 [(buf.validate.field).enum.defined_only = true]; + policy.SortDirection direction = 2 [(buf.validate.field).enum.defined_only = true]; +} + message ListSubjectMappingsRequest { + // Optional + // Namespace ID or FQN + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + string namespace_id = 1 [(buf.validate.field).string.uuid = true]; + string namespace_fqn = 2 [ + (buf.validate.field).string = { + min_len: 1 + uri: true + } + ]; + // Optional policy.PageRequest pagination = 10; + + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + repeated SubjectMappingsSort sort = 11 [(buf.validate.field).repeated.max_items = 1]; } message ListSubjectMappingsResponse { repeated policy.SubjectMapping subject_mappings = 1; @@ -40,6 +69,9 @@ message ListSubjectMappingsResponse { } message CreateSubjectMappingRequest { + // Optional + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + // Required // Attribute Value to be mapped to string attribute_value_id = 1 [(buf.validate.field).string.uuid = true]; @@ -64,6 +96,16 @@ message CreateSubjectMappingRequest { // Create new SubjectConditionSet (NOTE: ignored if existing_subject_condition_set_id is provided) SubjectConditionSetCreate new_subject_condition_set = 4; + // Optional + // Namespace ID or FQN for the subject mapping + string namespace_id = 5 [(buf.validate.field).string.uuid = true]; + string namespace_fqn = 6 [ + (buf.validate.field).string = { + min_len: 1 + uri: true + } + ]; + // Optional common.MetadataMutable metadata = 100; } @@ -122,9 +164,37 @@ message GetSubjectConditionSetResponse { repeated policy.SubjectMapping associated_subject_mappings = 2; } +enum SortSubjectConditionSetsType { + SORT_SUBJECT_CONDITION_SETS_TYPE_UNSPECIFIED = 0; + SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT = 1; + SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT = 2; +} + +message SubjectConditionSetsSort { + SortSubjectConditionSetsType field = 1 [(buf.validate.field).enum.defined_only = true]; + policy.SortDirection direction = 2 [(buf.validate.field).enum.defined_only = true]; +} + message ListSubjectConditionSetsRequest { + // Optional + // Namespace ID or FQN + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + string namespace_id = 1 [(buf.validate.field).string.uuid = true]; + string namespace_fqn = 2 [ + (buf.validate.field).string = { + min_len: 1 + uri: true + } + ]; + // Optional policy.PageRequest pagination = 10; + // Optional - CONSTRAINT: max 1 item + // Sort defaults: + // - direction UNSPECIFIED defaults to DESC for the specified field + // - field UNSPECIFIED defaults to created_at with the specified direction + // - both UNSPECIFIED or sort omitted defaults to created_at DESC + repeated SubjectConditionSetsSort sort = 11 [(buf.validate.field).repeated.max_items = 1]; } message ListSubjectConditionSetsResponse { repeated policy.SubjectConditionSet subject_condition_sets = 1; @@ -141,7 +211,17 @@ message SubjectConditionSetCreate { common.MetadataMutable metadata = 100; } message CreateSubjectConditionSetRequest { + // Optional + option (buf.validate.message).oneof = { fields: ["namespace_id", "namespace_fqn"], required: false }; + SubjectConditionSetCreate subject_condition_set = 1 [(buf.validate.field).required = true]; + string namespace_id = 2 [(buf.validate.field).string.uuid = true]; + string namespace_fqn = 3 [ + (buf.validate.field).string = { + min_len: 1 + uri: true + } + ]; } message CreateSubjectConditionSetResponse { SubjectConditionSet subject_condition_set = 1; diff --git a/service/policy/subjectmapping/subject_mapping_test.go b/service/policy/subjectmapping/subject_mapping_test.go index 478492936e..5740d93f43 100644 --- a/service/policy/subjectmapping/subject_mapping_test.go +++ b/service/policy/subjectmapping/subject_mapping_test.go @@ -21,7 +21,10 @@ const ( errMessageUUID = "string.uuid" errLessThanMinItems = "repeated.min_items" errMessageOptionalUUID = "optional_uuid_format" + errMessageOneof = "message.oneof" + errMessageURI = "string.uri" fakeID = "cf75540a-cd58-4c6c-a502-7108be7a6edd" + validNamespaceFQN = "https://example.com" ) var validActions = []*policy.Action{ @@ -48,6 +51,7 @@ func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testin AttributeValueId: fakeID, NewSubjectConditionSet: conditionSet, Actions: validActions, + NamespaceId: fakeID, } }, expectedError: errLessThanMinItems, @@ -63,6 +67,7 @@ func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testin AttributeValueId: fakeID, NewSubjectConditionSet: conditionSet, Actions: validActions, + NamespaceId: fakeID, } }, expectedError: errLessThanMinItems, @@ -82,6 +87,7 @@ func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testin AttributeValueId: fakeID, NewSubjectConditionSet: conditionSet, Actions: validActions, + NamespaceId: fakeID, } }, expectedError: errLessThanMinItems, @@ -101,6 +107,7 @@ func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testin AttributeValueId: fakeID, NewSubjectConditionSet: conditionSet, Actions: validActions, + NamespaceId: fakeID, } }, expectedError: errLessThanMinItems, @@ -129,6 +136,7 @@ func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testin AttributeValueId: fakeID, NewSubjectConditionSet: conditionSet, Actions: validActions, + NamespaceId: fakeID, } }, expectedError: "operator", @@ -157,6 +165,7 @@ func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testin AttributeValueId: fakeID, NewSubjectConditionSet: conditionSet, Actions: validActions, + NamespaceId: fakeID, } }, expectedError: "subject_external_selector_value", @@ -185,6 +194,7 @@ func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testin AttributeValueId: fakeID, NewSubjectConditionSet: conditionSet, Actions: validActions, + NamespaceId: fakeID, } }, expectedError: "subject_external_values", @@ -207,6 +217,7 @@ func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testin func Test_CreateSubjectMappingRequest_NilActionsArray_Fails(t *testing.T) { req := &subjectmapping.CreateSubjectMappingRequest{ AttributeValueId: fakeID, + NamespaceId: fakeID, } err := getValidator().Validate(req) @@ -217,6 +228,7 @@ func Test_CreateSubjectMappingRequest_EmptyActionsArray_Fails(t *testing.T) { req := &subjectmapping.CreateSubjectMappingRequest{ AttributeValueId: fakeID, Actions: []*policy.Action{}, + NamespaceId: fakeID, } err := getValidator().Validate(req) @@ -233,6 +245,7 @@ func Test_CreateSubjectMappingRequest_NoActionNameProvided_Fails(t *testing.T) { }, }, }, + NamespaceId: fakeID, } err := getValidator().Validate(req) @@ -247,6 +260,7 @@ func Test_CreateSubjectMappingRequest_PopulatedArray_BadValueID_Fails(t *testing Name: "read", }, }, + NamespaceId: fakeID, } err := getValidator().Validate(req) @@ -263,6 +277,7 @@ func Test_CreateSubjectMappingRequest_PopulatedArray_Succeeds(t *testing.T) { Name: "create", }, }, + NamespaceId: fakeID, } err := getValidator().Validate(req) require.NoError(t, err) @@ -274,6 +289,30 @@ func Test_CreateSubjectMappingRequest_PopulatedArray_Succeeds(t *testing.T) { Id: fakeID, }, }, + NamespaceId: fakeID, + } + err = getValidator().Validate(req) + require.NoError(t, err) + + req = &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fakeID, + Actions: []*policy.Action{ + { + Name: "read", + }, + }, + NamespaceFqn: validNamespaceFQN, + } + err = getValidator().Validate(req) + require.NoError(t, err) + + req = &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fakeID, + Actions: []*policy.Action{ + { + Name: "read", + }, + }, } err = getValidator().Validate(req) require.NoError(t, err) @@ -289,6 +328,7 @@ func Test_CreateSubjectMappingRequest_WithExistingSubjectConditionSetID_Succeeds }, }, ExistingSubjectConditionSetId: fakeID, + NamespaceId: fakeID, } err := v.Validate(req) @@ -300,6 +340,201 @@ func Test_CreateSubjectMappingRequest_WithExistingSubjectConditionSetID_Succeeds require.Contains(t, err.Error(), errMessageOptionalUUID) } +func Test_CreateSubjectMappingRequest_MissingNamespace_Succeeds(t *testing.T) { + req := &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fakeID, + Actions: []*policy.Action{ + { + Name: "read", + }, + }, + } + + err := getValidator().Validate(req) + require.NoError(t, err) +} + +func Test_CreateSubjectMappingRequest_InvalidNamespace_Fails(t *testing.T) { + testCases := []struct { + name string + req *subjectmapping.CreateSubjectMappingRequest + expectedError string + }{ + { + name: "invalid namespace id", + req: &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fakeID, + Actions: []*policy.Action{ + { + Name: "read", + }, + }, + NamespaceId: "bad-namespace-id", + }, + expectedError: errMessageUUID, + }, + { + name: "invalid namespace fqn", + req: &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fakeID, + Actions: []*policy.Action{ + { + Name: "read", + }, + }, + NamespaceFqn: "not-a-uri", + }, + expectedError: errMessageURI, + }, + { + name: "both namespace id and fqn", + req: &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fakeID, + Actions: []*policy.Action{ + { + Name: "read", + }, + }, + NamespaceId: fakeID, + NamespaceFqn: validNamespaceFQN, + }, + expectedError: errMessageOneof, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := getValidator().Validate(tc.req) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedError) + }) + } +} + +func Test_ListSubjectMappingsRequest_Succeeds(t *testing.T) { + testCases := []struct { + name string + req *subjectmapping.ListSubjectMappingsRequest + }{ + { + name: "no filters", + req: &subjectmapping.ListSubjectMappingsRequest{}, + }, + { + name: "namespace id only", + req: &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: fakeID, + }, + }, + { + name: "namespace fqn only", + req: &subjectmapping.ListSubjectMappingsRequest{ + NamespaceFqn: validNamespaceFQN, + }, + }, + { + name: "pagination only", + req: &subjectmapping.ListSubjectMappingsRequest{ + Pagination: &policy.PageRequest{ + Limit: 10, + Offset: 5, + }, + }, + }, + { + name: "namespace filter with pagination", + req: &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: fakeID, + Pagination: &policy.PageRequest{ + Limit: 20, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := getValidator().Validate(tc.req) + require.NoError(t, err) + }) + } +} + +func Test_ListSubjectMappingsRequest_Fails(t *testing.T) { + testCases := []struct { + name string + req *subjectmapping.ListSubjectMappingsRequest + expectedError string + }{ + { + name: "invalid namespace id", + req: &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: "bad-namespace-id", + }, + expectedError: errMessageUUID, + }, + { + name: "invalid namespace fqn", + req: &subjectmapping.ListSubjectMappingsRequest{ + NamespaceFqn: "not-a-uri", + }, + expectedError: errMessageURI, + }, + { + name: "both namespace id and fqn", + req: &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: fakeID, + NamespaceFqn: validNamespaceFQN, + }, + expectedError: errMessageOneof, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := getValidator().Validate(tc.req) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedError) + }) + } +} + +func Test_ListSubjectMappingsRequest_Sort(t *testing.T) { + v := getValidator() + + // no sort — valid + req := &subjectmapping.ListSubjectMappingsRequest{} + require.NoError(t, v.Validate(req)) + + // one sort item — valid + req = &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + { + Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + } + require.NoError(t, v.Validate(req)) + + // two sort items — exceeds max_items = 1 + req = &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + { + Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + { + Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT, + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + } + err := v.Validate(req) + require.Error(t, err) + require.Contains(t, err.Error(), "sort") +} + func Test_UpdateSubjectMappingRequest_Succeeds(t *testing.T) { v := getValidator() req := &subjectmapping.UpdateSubjectMappingRequest{} diff --git a/service/policy/unsafe/unsafe.proto b/service/policy/unsafe/unsafe.proto index 5f9f9062a3..86f8ab955b 100644 --- a/service/policy/unsafe/unsafe.proto +++ b/service/policy/unsafe/unsafe.proto @@ -4,6 +4,7 @@ package policy.unsafe; import "buf/validate/validate.proto"; +import "google/protobuf/wrappers.proto"; import "policy/objects.proto"; // Namespaces Unsafe RPCs @@ -81,6 +82,12 @@ message UnsafeUpdateAttributeRequest { AttributeRuleTypeEnum rule = 3 [(buf.validate.field).enum.defined_only = true]; // Optional // WARNING!! + // Updating allow_traversal allows TDF creation to be front-loaded, meaning a customer + // can create encrypted content with an attribute definitions key mapping before + // creating the attribute values needed to decrypt. + google.protobuf.BoolValue allow_traversal = 5; + // Optional + // WARNING!! // Unsafe reordering requires the full list of values in the new order they should be stored. Updating the order of values in a HIERARCHY-rule Attribute Definition // will retroactively alter access to existing TDFs containing those values. Replacing values on an attribute in place is not supported; values can be unsafely deleted // deleted, created, and unsafely re-ordered as necessary. diff --git a/service/tracing/connect_interceptor.go b/service/tracing/connect_interceptor.go new file mode 100644 index 0000000000..6168965990 --- /dev/null +++ b/service/tracing/connect_interceptor.go @@ -0,0 +1,29 @@ +package tracing + +import ( + "connectrpc.com/connect" + "connectrpc.com/otelconnect" +) + +// ConnectClientTraceInterceptor returns a Connect interceptor backed by +// otelconnect that injects OpenTelemetry trace context into outbound requests +// and creates per-RPC spans and metrics. +func ConnectClientTraceInterceptor() (connect.Interceptor, error) { + return otelconnect.NewInterceptor( + otelconnect.WithoutTraceEvents(), + ) +} + +// ConnectServerTraceInterceptor returns a Connect interceptor backed by +// otelconnect that extracts OpenTelemetry trace context from incoming requests +// and creates per-RPC spans and metrics. +// +// WithTrustRemote makes server spans children of the incoming trace rather +// than linked root spans. WithoutServerPeerAttributes reduces cardinality. +func ConnectServerTraceInterceptor() (connect.Interceptor, error) { + return otelconnect.NewInterceptor( + otelconnect.WithTrustRemote(), + otelconnect.WithoutServerPeerAttributes(), + otelconnect.WithoutTraceEvents(), + ) +} diff --git a/service/tracing/connect_interceptor_test.go b/service/tracing/connect_interceptor_test.go new file mode 100644 index 0000000000..7f95ee7442 --- /dev/null +++ b/service/tracing/connect_interceptor_test.go @@ -0,0 +1,235 @@ +package tracing_test + +import ( + "context" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "connectrpc.com/connect" + "github.com/opentdf/platform/service/tracing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/emptypb" +) + +// setupOTel configures an in-memory tracer provider and W3C trace propagator, +// returning the provider and a cleanup function that restores prior globals. +func setupOTel(t *testing.T) *sdktrace.TracerProvider { + t.Helper() + + exporter := tracetest.NewInMemoryExporter() + tp := sdktrace.NewTracerProvider( + sdktrace.WithSyncer(exporter), + sdktrace.WithSampler(sdktrace.AlwaysSample()), + ) + + prevTP := otel.GetTracerProvider() + prevProp := otel.GetTextMapPropagator() + t.Cleanup(func() { + _ = tp.Shutdown(context.Background()) + otel.SetTracerProvider(prevTP) + otel.SetTextMapPropagator(prevProp) + }) + + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + return tp +} + +// TestTraceContextPropagation_Unary verifies that the client interceptor +// injects traceparent/tracestate headers and the server interceptor extracts them, +// resulting in both sides sharing the same trace ID for unary RPCs. +func TestTraceContextPropagation_Unary(t *testing.T) { + tp := setupOTel(t) + + serverInt, err := tracing.ConnectServerTraceInterceptor() + require.NoError(t, err) + clientInt, err := tracing.ConnectClientTraceInterceptor() + require.NoError(t, err) + + var ( + mu sync.Mutex + serverTraceID trace.TraceID + ) + + mux := http.NewServeMux() + handler := connect.NewUnaryHandler( + "/test.v1.TestService/Ping", + func(ctx context.Context, _ *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { + sc := trace.SpanContextFromContext(ctx) + mu.Lock() + serverTraceID = sc.TraceID() + mu.Unlock() + return connect.NewResponse(&emptypb.Empty{}), nil + }, + connect.WithInterceptors(serverInt), + ) + mux.Handle("/test.v1.TestService/", handler) + + srv := httptest.NewServer(mux) + defer srv.Close() + + client := connect.NewClient[emptypb.Empty, emptypb.Empty]( + srv.Client(), + srv.URL+"/test.v1.TestService/Ping", + connect.WithInterceptors(clientInt), + ) + + ctx, span := tp.Tracer("test").Start(context.Background(), "client-call") + clientTraceID := span.SpanContext().TraceID() + + _, err = client.CallUnary(ctx, connect.NewRequest(&emptypb.Empty{})) + span.End() + require.NoError(t, err) + + mu.Lock() + defer mu.Unlock() + + assert.True(t, clientTraceID.IsValid(), "client trace ID should be valid") + assert.True(t, serverTraceID.IsValid(), "server trace ID should be valid") + assert.Equal(t, clientTraceID, serverTraceID, + "server must see the same trace ID as the client") + + t.Logf("client trace: %s", clientTraceID) + t.Logf("server trace: %s", serverTraceID) +} + +// TestTraceContextPropagation_ServerStream verifies trace context propagation +// for server-streaming RPCs, exercising WrapStreamingClient on the client side +// and WrapStreamingHandler on the server side. +func TestTraceContextPropagation_ServerStream(t *testing.T) { + tp := setupOTel(t) + + serverInt, err := tracing.ConnectServerTraceInterceptor() + require.NoError(t, err) + clientInt, err := tracing.ConnectClientTraceInterceptor() + require.NoError(t, err) + + var ( + mu sync.Mutex + serverTraceID trace.TraceID + ) + + mux := http.NewServeMux() + handler := connect.NewServerStreamHandler( + "/test.v1.TestService/StreamPing", + func(ctx context.Context, _ *connect.Request[emptypb.Empty], stream *connect.ServerStream[emptypb.Empty]) error { + sc := trace.SpanContextFromContext(ctx) + mu.Lock() + serverTraceID = sc.TraceID() + mu.Unlock() + return stream.Send(&emptypb.Empty{}) + }, + connect.WithInterceptors(serverInt), + ) + mux.Handle("/test.v1.TestService/", handler) + + srv := httptest.NewServer(mux) + defer srv.Close() + + client := connect.NewClient[emptypb.Empty, emptypb.Empty]( + srv.Client(), + srv.URL+"/test.v1.TestService/StreamPing", + connect.WithInterceptors(clientInt), + ) + + ctx, span := tp.Tracer("test").Start(context.Background(), "client-stream-call") + clientTraceID := span.SpanContext().TraceID() + + stream, err := client.CallServerStream(ctx, connect.NewRequest(&emptypb.Empty{})) + require.NoError(t, err) + for stream.Receive() { + } + require.NoError(t, stream.Err()) + require.NoError(t, stream.Close()) + span.End() + + mu.Lock() + defer mu.Unlock() + + assert.True(t, clientTraceID.IsValid(), "client trace ID should be valid") + assert.True(t, serverTraceID.IsValid(), "server trace ID should be valid") + assert.Equal(t, clientTraceID, serverTraceID, + "server must see the same trace ID as the client (streaming)") + + t.Logf("client trace: %s", clientTraceID) + t.Logf("server trace: %s", serverTraceID) +} + +// TestTraceContextPropagation_NoTraceContext verifies that a no-op propagator +// prevents trace context from reaching the server, even when the client has +// an active span. This proves the interceptor respects the propagator config. +func TestTraceContextPropagation_NoTraceContext(t *testing.T) { + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + defer func() { _ = tp.Shutdown(context.Background()) }() + + prevTP := otel.GetTracerProvider() + prevProp := otel.GetTextMapPropagator() + defer func() { + otel.SetTracerProvider(prevTP) + otel.SetTextMapPropagator(prevProp) + }() + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator()) + + serverInt, err := tracing.ConnectServerTraceInterceptor() + require.NoError(t, err) + clientInt, err := tracing.ConnectClientTraceInterceptor() + require.NoError(t, err) + + var ( + mu sync.Mutex + serverTraceID trace.TraceID + ) + + mux := http.NewServeMux() + handler := connect.NewUnaryHandler( + "/test.v1.TestService/Ping", + func(ctx context.Context, _ *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { + mu.Lock() + serverTraceID = trace.SpanContextFromContext(ctx).TraceID() + mu.Unlock() + return connect.NewResponse(&emptypb.Empty{}), nil + }, + connect.WithInterceptors(serverInt), + ) + mux.Handle("/test.v1.TestService/", handler) + + srv := httptest.NewServer(mux) + defer srv.Close() + + client := connect.NewClient[emptypb.Empty, emptypb.Empty]( + srv.Client(), + srv.URL+"/test.v1.TestService/Ping", + connect.WithInterceptors(clientInt), + ) + + ctx, span := tp.Tracer("test").Start(context.Background(), "client-call") + clientTraceID := span.SpanContext().TraceID() + require.True(t, clientTraceID.IsValid(), "client must have a valid trace ID for this test") + + _, err = client.CallUnary(ctx, connect.NewRequest(&emptypb.Empty{})) + span.End() + require.NoError(t, err) + + mu.Lock() + defer mu.Unlock() + + // otelconnect still creates a server span, so serverTraceID must be valid. + // But with a no-op propagator, the client's trace context is not injected + // into headers — the server starts a new independent trace. + require.True(t, serverTraceID.IsValid(), "server span should still be created") + assert.NotEqual(t, clientTraceID, serverTraceID, + "server should have a different trace ID when no propagator is configured") +} diff --git a/service/tracing/otel.go b/service/tracing/otel.go index 0856df5ceb..bfd66e3fac 100644 --- a/service/tracing/otel.go +++ b/service/tracing/otel.go @@ -20,7 +20,7 @@ import ( "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" "go.opentelemetry.io/otel/trace/noop" "google.golang.org/grpc/metadata" "gopkg.in/natefinch/lumberjack.v2" @@ -125,7 +125,7 @@ func InitTracer(ctx context.Context, cfg Config) (func(), error) { // 3. Create Resource: Combine attributes from explicit config, defaults, and environment. baseRes := resource.NewWithAttributes( - semconv.SchemaURL, // Required by NewWithAttributes + semconv.SchemaURL, semconv.ServiceNameKey.String(ServiceName), // Add other static resource attributes here if needed ) diff --git a/service/trust/delegating_key_service.go b/service/trust/delegating_key_service.go index 7eada4cd81..031d900a9c 100644 --- a/service/trust/delegating_key_service.go +++ b/service/trust/delegating_key_service.go @@ -43,13 +43,22 @@ type KeyManagerFactory func(opts *KeyManagerFactoryOptions) (KeyManager, error) // KeyManagerFactoryCtx defines the signature for functions that can create KeyManager instances. type KeyManagerFactoryCtx func(ctx context.Context, opts *KeyManagerFactoryOptions) (KeyManager, error) +// registeredFactory bundles a KeyManager factory with the static set of +// algorithms the manager declares it can serve. The algorithm list is the +// source of truth for SupportedAlgorithms; the factory is invoked lazily on +// the request path only. +type registeredFactory struct { + factory KeyManagerFactoryCtx + supportedAlgorithms []ocrypto.KeyType +} + // DelegatingKeyService is a key service that multiplexes between key managers based on the key's mode. type DelegatingKeyService struct { // Lookup key manager by mode for a given key identifier index KeyIndex // Lazily create key managers based on their manager - managerFactories map[string]KeyManagerFactoryCtx + managerFactories map[string]registeredFactory // Cache of key managers to avoid creating them multiple times managers map[keyManagerDesignation]loadedManager @@ -70,17 +79,30 @@ type DelegatingKeyService struct { func NewDelegatingKeyService(index KeyIndex, l *logger.Logger, c *cache.Cache) *DelegatingKeyService { return &DelegatingKeyService{ index: index, - managerFactories: make(map[string]KeyManagerFactoryCtx), + managerFactories: make(map[string]registeredFactory), managers: make(map[keyManagerDesignation]loadedManager), l: l, c: c, } } +// RegisterKeyManagerCtx registers a key manager factory without advertising any +// algorithms. Use RegisterKeyManagerCtxWithAlgorithms when the manager should +// contribute to capability listings. func (d *DelegatingKeyService) RegisterKeyManagerCtx(name string, factory KeyManagerFactoryCtx) { + d.RegisterKeyManagerCtxWithAlgorithms(name, factory, nil) +} + +// RegisterKeyManagerCtxWithAlgorithms registers a key manager factory and the +// static set of algorithms the manager can serve. The algorithm list is copied +// defensively so the caller may reuse or mutate its slice afterward. +func (d *DelegatingKeyService) RegisterKeyManagerCtxWithAlgorithms(name string, factory KeyManagerFactoryCtx, algs []ocrypto.KeyType) { d.mutex.Lock() defer d.mutex.Unlock() - d.managerFactories[name] = factory + d.managerFactories[name] = registeredFactory{ + factory: factory, + supportedAlgorithms: slices.Clone(algs), + } } func (d *DelegatingKeyService) SetDefaultMode(manager, name string, cfg []byte) { @@ -107,6 +129,31 @@ func (d *DelegatingKeyService) ListKeysWith(ctx context.Context, opts ListKeyOpt return d.index.ListKeysWith(ctx, opts) } +// SupportedAlgorithms returns the deduplicated, sorted union of algorithm +// identifiers declared by each registered key-manager factory. Registrations +// that did not declare any algorithms contribute nothing. Factories are not +// invoked — capability listing reads only the static metadata captured at +// registration, so it never constructs a manager. +func (d *DelegatingKeyService) SupportedAlgorithms(_ context.Context) []ocrypto.KeyType { + d.mutex.Lock() + algSets := make([][]ocrypto.KeyType, 0, len(d.managerFactories)) + for _, reg := range d.managerFactories { + algSets = append(algSets, reg.supportedAlgorithms) + } + d.mutex.Unlock() + + seen := make(map[ocrypto.KeyType]struct{}) + for _, algs := range algSets { + for _, alg := range algs { + seen[alg] = struct{}{} + } + } + + out := slices.Collect(maps.Keys(seen)) + slices.Sort(out) + return out +} + // Implementing KeyManager methods func (d *DelegatingKeyService) Name() string { return "DelegatingKeyService" @@ -219,7 +266,7 @@ func (d *DelegatingKeyService) getKeyManager(ctx context.Context, cfg *policy.Ke d.mutex.Unlock() return manager.KeyManager, nil } - factory, factoryExists := d.managerFactories[designation.Manager] + reg, factoryExists := d.managerFactories[designation.Manager] allManagers := slices.Collect(maps.Keys(d.managerFactories)) d.mutex.Unlock() @@ -229,7 +276,7 @@ func (d *DelegatingKeyService) getKeyManager(ctx context.Context, cfg *policy.Ke Cache: d.c, Config: cfg, } - managerFromFactory, err := factory(ctx, options) + managerFromFactory, err := reg.factory(ctx, options) if err != nil { return nil, fmt.Errorf("factory for key manager '%s' failed: %w", designation, err) } @@ -247,7 +294,8 @@ func (d *DelegatingKeyService) getKeyManager(ctx context.Context, cfg *policy.Ke // Factory for 'name' not found. // If 'name' was the defaultMode, _defKM will error if its factory is also missing. // If 'name' was not the defaultMode, we fall back to the default manager. - d.l.Debug("key manager factory not found for name, attempting to use/load default", + d.l.Debug( + "key manager factory not found for name, attempting to use/load default", slog.Any("key_managers", allManagers), slog.Any("requested_name", designation), ) diff --git a/service/trust/delegating_key_service_test.go b/service/trust/delegating_key_service_test.go index 5a62344492..e4ea9540da 100644 --- a/service/trust/delegating_key_service_test.go +++ b/service/trust/delegating_key_service_test.go @@ -4,12 +4,15 @@ import ( "context" "crypto/elliptic" "log/slog" + "sync/atomic" "testing" "github.com/opentdf/platform/lib/ocrypto" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/service/logger" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -321,3 +324,106 @@ func (suite *DelegatingKeyServiceTestSuite) TestGenerateECSessionKey() { func TestDelegatingKeyServiceTestSuite(t *testing.T) { suite.Run(t, new(DelegatingKeyServiceTestSuite)) } + +// failIfInvokedFactory returns a factory that fails the test if the +// DelegatingKeyService ever constructs a manager from it. SupportedAlgorithms +// must answer from registered metadata alone, never by invoking factories. +func failIfInvokedFactory(t *testing.T) KeyManagerFactoryCtx { + t.Helper() + return func(_ context.Context, _ *KeyManagerFactoryOptions) (KeyManager, error) { + t.Fatalf("factory must not be invoked by SupportedAlgorithms") + return nil, nil + } +} + +func TestDelegatingKeyService_SupportedAlgorithms(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + register func(t *testing.T, d *DelegatingKeyService) + want []ocrypto.KeyType + }{ + { + name: "no registrations returns empty", + register: func(_ *testing.T, _ *DelegatingKeyService) {}, + want: []ocrypto.KeyType{}, + }, + { + name: "single registration", + register: func(t *testing.T, d *DelegatingKeyService) { + d.RegisterKeyManagerCtxWithAlgorithms("a", failIfInvokedFactory(t), []ocrypto.KeyType{"rsa:2048", "ec:secp256r1"}) + }, + want: []ocrypto.KeyType{"ec:secp256r1", "rsa:2048"}, + }, + { + name: "two registrations, deduped and sorted", + register: func(t *testing.T, d *DelegatingKeyService) { + d.RegisterKeyManagerCtxWithAlgorithms("a", failIfInvokedFactory(t), []ocrypto.KeyType{"rsa:2048", "hpqt:xwing"}) + d.RegisterKeyManagerCtxWithAlgorithms("b", failIfInvokedFactory(t), []ocrypto.KeyType{"rsa:2048", "ec:secp256r1"}) + }, + want: []ocrypto.KeyType{"ec:secp256r1", "hpqt:xwing", "rsa:2048"}, + }, + { + name: "registration without algorithms contributes nothing", + register: func(t *testing.T, d *DelegatingKeyService) { + d.RegisterKeyManagerCtxWithAlgorithms("a", failIfInvokedFactory(t), []ocrypto.KeyType{"rsa:2048"}) + d.RegisterKeyManagerCtx("b", failIfInvokedFactory(t)) + }, + want: []ocrypto.KeyType{"rsa:2048"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + d := NewDelegatingKeyService(&MockKeyIndex{}, logger.CreateTestLogger(), nil) + tc.register(t, d) + got := d.SupportedAlgorithms(context.Background()) + if got == nil { + got = []ocrypto.KeyType{} + } + assert.Equal(t, tc.want, got) + // Probing must never instantiate a manager — the cache stays empty. + assert.Empty(t, d.managers, "SupportedAlgorithms must not populate the manager cache") + }) + } +} + +// TestDelegatingKeyService_SupportedAlgorithms_DoesNotInvokeFactories asserts +// the contract directly with a counter (in case the failIfInvokedFactory +// fast-path is ever bypassed via t.Run nesting). +func TestDelegatingKeyService_SupportedAlgorithms_DoesNotInvokeFactories(t *testing.T) { + t.Parallel() + + var invocations atomic.Int32 + counting := func(_ context.Context, _ *KeyManagerFactoryOptions) (KeyManager, error) { + invocations.Add(1) + return &MockKeyManager{}, nil + } + + d := NewDelegatingKeyService(&MockKeyIndex{}, logger.CreateTestLogger(), nil) + d.RegisterKeyManagerCtxWithAlgorithms("a", counting, []ocrypto.KeyType{"rsa:2048"}) + d.RegisterKeyManagerCtxWithAlgorithms("b", counting, []ocrypto.KeyType{"ec:secp256r1"}) + + _ = d.SupportedAlgorithms(context.Background()) + + assert.Zero(t, invocations.Load(), "SupportedAlgorithms must not invoke any registered factory") + assert.Empty(t, d.managers, "SupportedAlgorithms must not populate the manager cache") +} + +// TestDelegatingKeyService_RegisterKeyManagerCtxWithAlgorithms_CopiesSlice +// guards against callers retaining/mutating the slice they pass at registration. +func TestDelegatingKeyService_RegisterKeyManagerCtxWithAlgorithms_CopiesSlice(t *testing.T) { + t.Parallel() + + d := NewDelegatingKeyService(&MockKeyIndex{}, logger.CreateTestLogger(), nil) + algs := []ocrypto.KeyType{"rsa:2048", "ec:secp256r1"} + d.RegisterKeyManagerCtxWithAlgorithms("a", failIfInvokedFactory(t), algs) + + algs[0] = "tampered" + + got := d.SupportedAlgorithms(context.Background()) + want := []ocrypto.KeyType{"ec:secp256r1", "rsa:2048"} + require.Equal(t, want, got, "registration must copy the algorithm slice") +} diff --git a/service/trust/key_manager.go b/service/trust/key_manager.go index 0049db2252..33261b1df9 100644 --- a/service/trust/key_manager.go +++ b/service/trust/key_manager.go @@ -28,7 +28,7 @@ type KeyManager interface { // Returns an UnwrappedKeyData interface for further operations Decrypt(ctx context.Context, key KeyDetails, ciphertext []byte, ephemeralPublicKey []byte) (ProtectedKey, error) - // DeriveKey computes an agreed upon secret key, which NanoTDF may directly as the DEK or a key split + // DeriveKey computes an agreed upon secret key derived from an ECDH exchange. DeriveKey(ctx context.Context, key KeyDetails, ephemeralPublicKeyBytes []byte, curve elliptic.Curve) (ProtectedKey, error) // GenerateECSessionKey generates a private session key, for use with a client-provided ephemeral public key @@ -51,8 +51,12 @@ type NamedKeyManagerFactory struct { Factory KeyManagerFactory } -// NamedKeyManagerCtxFactory pairs a KeyManagerFactoryCtx with its intended registration name. +// NamedKeyManagerCtxFactory pairs a KeyManagerFactoryCtx with its intended +// registration name and the static set of algorithms the manager can serve +// when a corresponding key is provisioned. SupportedAlgorithms is optional; +// when empty, the manager contributes nothing to capability listings. type NamedKeyManagerCtxFactory struct { - Name string - Factory KeyManagerFactoryCtx + Name string + Factory KeyManagerFactoryCtx + SupportedAlgorithms []ocrypto.KeyType } diff --git a/service/wellknownconfiguration/wellknown_configuration.go b/service/wellknownconfiguration/wellknown_configuration.go index 98fd831ee0..49083890a2 100644 --- a/service/wellknownconfiguration/wellknown_configuration.go +++ b/service/wellknownconfiguration/wellknown_configuration.go @@ -2,9 +2,11 @@ package wellknownconfiguration import ( "context" + "encoding/json" "errors" "fmt" "log/slog" + "net/http" "sync" "connectrpc.com/connect" @@ -44,13 +46,28 @@ func UpdateConfigurationBaseKey(config any) { func NewRegistration() *serviceregistry.Service[wellknownconfigurationconnect.WellKnownServiceHandler] { return &serviceregistry.Service[wellknownconfigurationconnect.WellKnownServiceHandler]{ ServiceOptions: serviceregistry.ServiceOptions[wellknownconfigurationconnect.WellKnownServiceHandler]{ - Namespace: "wellknown", - ServiceDesc: &wellknown.WellKnownService_ServiceDesc, - ConnectRPCFunc: wellknownconfigurationconnect.NewWellKnownServiceHandler, - GRPCGatewayFunc: wellknown.RegisterWellKnownServiceHandler, + Namespace: "wellknown", + ServiceDesc: &wellknown.WellKnownService_ServiceDesc, + ConnectRPCFunc: wellknownconfigurationconnect.NewWellKnownServiceHandler, RegisterFunc: func(srp serviceregistry.RegistrationParams) (wellknownconfigurationconnect.WellKnownServiceHandler, serviceregistry.HandlerServer) { wk := &WellKnownService{logger: srp.Logger} - return wk, nil + return wk, func(_ context.Context, mux *http.ServeMux) error { + mux.HandleFunc("GET /.well-known/opentdf-configuration", func(w http.ResponseWriter, _ *http.Request) { + rwMutex.RLock() + cfg, err := structpb.NewStruct(wellKnownConfiguration) + rwMutex.RUnlock() + if err != nil { + srp.Logger.Error("failed to create struct for wellknown configuration", slog.String("error", err.Error())) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cfg.AsMap()); err != nil { + srp.Logger.Error("failed to encode wellknown configuration", slog.String("error", err.Error())) + } + }) + return nil + } }, }, } diff --git a/service/wellknownconfiguration/wellknown_configuration.proto b/service/wellknownconfiguration/wellknown_configuration.proto index b6d2c22569..5ee3cd8334 100644 --- a/service/wellknownconfiguration/wellknown_configuration.proto +++ b/service/wellknownconfiguration/wellknown_configuration.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package wellknownconfiguration; -import "google/api/annotations.proto"; import "google/protobuf/struct.proto"; message WellKnownConfig { @@ -17,7 +16,6 @@ message GetWellKnownConfigurationResponse { service WellKnownService { rpc GetWellKnownConfiguration(GetWellKnownConfigurationRequest) returns (GetWellKnownConfigurationResponse) { - option (google.api.http) = {get: "/.well-known/opentdf-configuration"}; option idempotency_level = NO_SIDE_EFFECTS; } } diff --git a/test/integration/go.mod b/test/integration/go.mod new file mode 100644 index 0000000000..1ad2914d7b --- /dev/null +++ b/test/integration/go.mod @@ -0,0 +1,87 @@ +module github.com/opentdf/platform/test/integration + +go 1.25.0 + +replace ( + github.com/opentdf/platform/lib/fixtures => ../../lib/fixtures + github.com/opentdf/platform/lib/ocrypto => ../../lib/ocrypto + github.com/opentdf/platform/protocol/go => ../../protocol/go + github.com/opentdf/platform/sdk => ../../sdk +) + +require ( + github.com/lestrrat-go/jwx/v2 v2.1.6 + github.com/opentdf/platform/lib/fixtures v0.0.0-00010101000000-000000000000 + github.com/opentdf/platform/sdk v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.42.0 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Nerzal/gocloak/v13 v13.9.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-resty/resty/v2 v2.12.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/segmentio/ksuid v1.0.4 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sys v0.42.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/test/integration/go.sum b/test/integration/go.sum new file mode 100644 index 0000000000..986e35f7ab --- /dev/null +++ b/test/integration/go.sum @@ -0,0 +1,220 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nerzal/gocloak/v13 v13.9.0 h1:YWsJsdM5b0yhM2Ba3MLydiOlujkBry4TtdzfIzSVZhw= +github.com/Nerzal/gocloak/v13 v13.9.0/go.mod h1:YYuDcXZ7K2zKECyVP7pPqjKxx2AzYSpKDj8d6GuyM10= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= +github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= +github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/test/integration/oauth/oauth_test.go b/test/integration/oauth/oauth_test.go new file mode 100644 index 0000000000..81db49ec9a --- /dev/null +++ b/test/integration/oauth/oauth_test.go @@ -0,0 +1,578 @@ +package oauth_test + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + _ "embed" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "slices" + "testing" + "time" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/opentdf/platform/lib/fixtures" + "github.com/opentdf/platform/sdk/auth/oauth" + "github.com/opentdf/platform/sdk/httputil" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + tc "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +type OAuthSuite struct { + suite.Suite + dpopJWK jwk.Key + keycloakContainer tc.Container + keycloakEndpoint string + keycloakHTTPSEndpoint string +} + +func TestOAuthTestSuite(t *testing.T) { + suite.Run(t, new(OAuthSuite)) +} + +func (s *OAuthSuite) SetupSuite() { + // Generate RSA Key to use for DPoP + dpopKey, err := rsa.GenerateKey(rand.Reader, 4096) + s.Require().NoError(err, "failed to generate dpop key") + + dpopJWK, err := jwk.FromRaw(dpopKey) + s.Require().NoError(err) + s.Require().NoError(dpopJWK.Set("use", "sig")) + s.Require().NoError(dpopJWK.Set("alg", jwa.RS256.String())) + + s.dpopJWK = dpopJWK + ctx := context.Background() + + keycloak, idpEndpoint, idpHTTPSEndpoint := setupKeycloak(ctx, s.T()) + s.keycloakContainer = keycloak + s.keycloakEndpoint = idpEndpoint + s.keycloakHTTPSEndpoint = idpHTTPSEndpoint +} + +func (s *OAuthSuite) TearDownSuite() { + _ = s.keycloakContainer.Terminate(context.Background()) +} + +//go:embed testdata/new-ca.crt +var ca []byte + +func (s *OAuthSuite) TestCertExchangeFromKeycloak() { + clientCredentials := oauth.ClientCredentials{ + ClientID: "opentdf-sdk", + ClientAuth: "secret", + } + cert, err := tls.LoadX509KeyPair("testdata/sampleuser.crt", "testdata/sampleuser.key") + s.Require().NoError(err) + rootCAs, err := x509.SystemCertPool() + s.Require().NoError(err) + rootCAs.AppendCertsFromPEM(ca) + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{cert}, + RootCAs: rootCAs, + } + exhcangeInfo := oauth.CertExchangeInfo{ + HTTPClient: httputil.SafeHTTPClientWithTLSConfig(tlsConfig), + Audience: []string{"opentdf-sdk"}, + } + + tok, err := oauth.DoCertExchange( + context.Background(), + s.keycloakHTTPSEndpoint, + exhcangeInfo, + clientCredentials, + s.dpopJWK) + s.Require().NoError(err) + + tokenDetails, err := jwt.ParseString(tok.AccessToken, jwt.WithVerify(false)) + s.Require().NoError(err) + + cnfClaim, ok := tokenDetails.Get("cnf") + s.Require().True(ok) + cnfClaimsMap, ok := cnfClaim.(map[string]interface{}) + s.Require().True(ok) + idpKeyFingerprint, ok := cnfClaimsMap["jkt"].(string) + s.Require().True(ok) + s.Require().NotEmpty(idpKeyFingerprint) + pk, err := s.dpopJWK.PublicKey() + s.Require().NoError(err) + hash, err := pk.Thumbprint(crypto.SHA256) + s.Require().NoError(err) + + expectedThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) + s.Equal(expectedThumbprint, idpKeyFingerprint, "didn't get expected fingerprint") + s.Positivef(tok.ExpiresIn, "invalid expiration is before current time: %v", tok) + s.Falsef(tok.Expired(), "got a token that is currently expired: %v", tok) + + name, ok := tokenDetails.Get("name") + s.Require().True(ok) + s.Equal("sample user", name, "got unexpected name") +} + +func (s *OAuthSuite) TestGettingAccessTokenFromKeycloak() { + clientCredentials := oauth.ClientCredentials{ + ClientID: "opentdf-sdk", + ClientAuth: "secret", + } + + tok, err := oauth.GetAccessToken( + http.DefaultClient, + s.keycloakEndpoint, + []string{"testscope"}, + clientCredentials, + s.dpopJWK) + + s.Require().NoError(err) + + tokenDetails, err := jwt.ParseString(tok.AccessToken, jwt.WithVerify(false)) + s.Require().NoError(err) + + cnfClaim, ok := tokenDetails.Get("cnf") + s.Require().True(ok) + cnfClaimsMap, ok := cnfClaim.(map[string]interface{}) + s.Require().True(ok) + idpKeyFingerprint, ok := cnfClaimsMap["jkt"].(string) + s.Require().True(ok) + s.Require().NotEmpty(idpKeyFingerprint) + pk, err := s.dpopJWK.PublicKey() + s.Require().NoError(err) + hash, err := pk.Thumbprint(crypto.SHA256) + s.Require().NoError(err) + + scope, ok := tokenDetails.Get("scope") + s.Require().True(ok) + scopeString, ok := scope.(string) + s.Require().True(ok) + s.Require().Contains(scopeString, "testscope") + + expectedThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) + s.Equal(expectedThumbprint, idpKeyFingerprint, "didn't get expected fingerprint") + s.Positivef(tok.ExpiresIn, "invalid expiration is before current time: %v", tok) + s.Falsef(tok.Expired(), "got a token that is currently expired: %v", tok) + + // verify that we got a token that has the opentdf-standard role, which only the sdk client has + ra, ok := tokenDetails.Get("realm_access") + s.Require().True(ok) + raMap, ok := ra.(map[string]interface{}) + s.Require().True(ok) + roles, ok := raMap["roles"] + s.Require().True(ok) + rolesList, ok := roles.([]interface{}) + s.Require().True(ok) + s.Require().True(slices.Contains(rolesList, "opentdf-standard"), "missing the `opentdf-standard` role") +} + +func (s *OAuthSuite) TestDoingTokenExchangeWithKeycloak() { + ctx := context.Background() + + clientCredentials := oauth.ClientCredentials{ + ClientID: "opentdf-sdk", + ClientAuth: "secret", + } + + subjectToken, err := oauth.GetAccessToken( + http.DefaultClient, + s.keycloakEndpoint, + []string{"testscope"}, + clientCredentials, + s.dpopJWK) + s.Require().NoError(err) + + exchangeCredentials := oauth.ClientCredentials{ + ClientID: "opentdf", + ClientAuth: "secret", + } + + tokenExchange := oauth.TokenExchangeInfo{ + SubjectToken: subjectToken.AccessToken, + Audience: []string{"opentdf-sdk"}, + } + + exchangedTok, err := oauth.DoTokenExchange(ctx, http.DefaultClient, s.keycloakEndpoint, []string{}, exchangeCredentials, tokenExchange, s.dpopJWK) + s.Require().NoError(err) + + tokenDetails, err := jwt.ParseString(exchangedTok.AccessToken, jwt.WithVerify(false)) + s.Require().NoError(err) + + cnfClaim, ok := tokenDetails.Get("cnf") + s.Require().True(ok) + cnfClaimsMap, ok := cnfClaim.(map[string]interface{}) + s.Require().True(ok) + idpKeyFingerprint, ok := cnfClaimsMap["jkt"].(string) + s.Require().True(ok) + s.Require().NotEmpty(idpKeyFingerprint) + pk, err := s.dpopJWK.PublicKey() + s.Require().NoError(err) + hash, err := pk.Thumbprint(crypto.SHA256) + s.Require().NoError(err) + + expectedThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) + s.Equal(expectedThumbprint, idpKeyFingerprint, "didn't get expected fingerprint") + s.Positivef(subjectToken.ExpiresIn, "invalid expiration is before current time: %v", subjectToken) + s.Falsef(subjectToken.Expired(), "got a token that is currently expired: %v", subjectToken) + + // verify that we got a token that has the opentdf-standard role, which only the sdk client has + ra, ok := tokenDetails.Get("realm_access") + s.Require().True(ok) + raMap, ok := ra.(map[string]interface{}) + s.Require().True(ok) + roles, ok := raMap["roles"] + s.Require().True(ok) + rolesList, ok := roles.([]interface{}) + s.Require().True(ok) + s.Require().True(slices.Contains(rolesList, "opentdf-standard"), "missing the `opentdf-standard` role") + + // verify that the calling client is the authorized party + azpClaim, ok := tokenDetails.Get("azp") + s.Require().True(ok) + s.Require().Equal(exchangeCredentials.ClientID, azpClaim) + + // verify that the exchanged token has a scope that is only allowed for the client that got the original token + scope, ok := tokenDetails.Get("scope") + s.Require().True(ok) + scopeString, ok := scope.(string) + s.Require().True(ok) + s.Require().Contains(scopeString, "testscope") +} + +func (s *OAuthSuite) TestClientSecretNoNonce() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.Equal("/token", r.URL.Path) + s.NoError(r.ParseForm()) + + validateBasicAuth(r, s.T()) + extractDPoPToken(r, s.T()) + + tok, err := jwt.NewBuilder(). + Issuer("example.org/fake"). + IssuedAt(time.Now()). + Build() + s.NoError(err) + + responseBytes, err := json.Marshal(tok) + s.NoError(err, "error writing response") + + w.Header().Add("Content-Type", "application/json") + _, err = w.Write(responseBytes) + s.NoError(err) + })) + defer server.Close() + + clientCredentials := oauth.ClientCredentials{ + ClientID: "theclient", + ClientAuth: "thesecret", + } + _, err := oauth.GetAccessToken(http.DefaultClient, server.URL+"/token", []string{"scope1", "scope2"}, clientCredentials, s.dpopJWK) + s.Require().NoError(err, "didn't get a token back from the IdP") +} + +func (s *OAuthSuite) TestClientSecretWithNonce() { + timesCalled := 0 + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + timesCalled++ + s.Equal("/token", r.URL.Path, "surprise http request to mock oauth service") + err := r.ParseForm() + s.NoError(err, "error parsing oauth request") + + validateBasicAuth(r, s.T()) + + if timesCalled == 1 { + w.Header().Add("DPoP-Nonce", "dfdffdfddf") + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte{}) + s.NoError(err, "error writing response") + return + } else if timesCalled > 2 { + s.T().Logf("made more than two calls to the server: %d", timesCalled) + return + } + + // get the key we used to sign the DPoP token from the header + clientTok := extractDPoPToken(r, s.T()) + + nonce, exists := clientTok.Get("nonce") + if !exists { + s.T().Logf("didn't get nonce assertion") + } + + if nonceStr, ok := nonce.(string); ok { + if nonceStr != "dfdffdfddf" { + s.T().Errorf("Got incorrect nonce: %v", nonce) + } + } else { + s.T().Errorf("Nonce is not a string") + } + + tok, err := jwt.NewBuilder(). + Issuer("example.org/fake"). + IssuedAt(time.Now()). + Build() + s.NoError(err) + + responseBytes, err := json.Marshal(tok) + if err != nil { + s.T().Errorf("error writing response: %v", err) + } + + w.Header().Add("Content-Type", "application/json") + l, err := w.Write(responseBytes) + s.Len(responseBytes, l) + s.NoError(err) + })) + defer server.Close() + + clientCredentials := oauth.ClientCredentials{ + ClientID: "theclient", + ClientAuth: "thesecret", + } + _, err := oauth.GetAccessToken(http.DefaultClient, server.URL+"/token", []string{"scope1", "scope2"}, clientCredentials, s.dpopJWK) + if err != nil { + s.T().Errorf("didn't get a token back from the IdP: %v", err) + } +} + +func (s *OAuthSuite) TestSignedJWTWithNonce() { + // Generate RSA Key to use for DPoP + dpopKey, err := rsa.GenerateKey(rand.Reader, 4096) + s.Require().NoError(err, "error generating dpop key") + dpopJWK, err := jwk.FromRaw(dpopKey) + s.Require().NoError(err) + s.Require().NoError(dpopJWK.Set("use", "sig")) + s.Require().NoError(dpopJWK.Set("alg", jwa.RS256.String())) + + clientAuthKey, err := rsa.GenerateKey(rand.Reader, 4096) + s.Require().NoError(err, "error generating clientAuth key") + clientAuthJWK, err := jwk.FromRaw(clientAuthKey) + s.Require().NoError(err, "error constructing raw JWK") + s.Require().NoError(clientAuthJWK.Set("use", "sig")) + s.Require().NoError(clientAuthJWK.Set("alg", jwa.RS256.String())) + clientPublicKey, err := clientAuthJWK.PublicKey() + s.Require().NoError(err, "error getting public JWK from client auth JWK [%v]", clientAuthJWK) + + timesCalled := 0 + + var url string + getURL := func() string { + return url + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + timesCalled++ + + if r.URL.Path != "/token" { + s.T().Errorf("Expected to request '/token', got: %s", r.URL.Path) + } + s.NoError(r.ParseForm()) + + validateClientAssertionAuth(r, s.T(), getURL, "theclient", clientPublicKey) + + if timesCalled == 1 { + w.Header().Add("DPoP-Nonce", "dfdffdfddf") + w.WriteHeader(http.StatusBadRequest) + if _, err := w.Write([]byte{}); err != nil { + s.T().Errorf("error writing response: %v", err) + } + return + } else if timesCalled > 2 { + s.T().Logf("made more than two calls to the server: %d", timesCalled) + return + } + + // get the key we used to sign the DPoP token from the header + clientTok := extractDPoPToken(r, s.T()) + + nonce, exists := clientTok.Get("nonce") + if exists { + value, ok := nonce.(string) + if !ok { + s.T().Errorf("Nonce is not a string") + } else if value != "dfdffdfddf" { + s.T().Errorf("Got incorrect nonce: %v", value) + } + } else { + s.T().Logf("didn't get nonce assertion") + } + + tok, err := jwt.NewBuilder(). + Issuer("example.org/fake"). + IssuedAt(time.Now()). + Build() + s.NoError(err) + + responseBytes, err := json.Marshal(tok) + if err != nil { + s.T().Errorf("error writing response: %v", err) + } + + w.Header().Add("Content-Type", "application/json") + l, err := w.Write(responseBytes) + s.Len(responseBytes, l) + s.NoError(err) + })) + defer server.Close() + + clientCredentials := oauth.ClientCredentials{ + ClientID: "theclient", + ClientAuth: clientAuthJWK, + } + + url = server.URL + "/token" + + _, err = oauth.GetAccessToken(http.DefaultClient, url, []string{"scope1", "scope2"}, clientCredentials, dpopJWK) + if err != nil { + s.T().Errorf("didn't get a token back from the IdP: %v", err) + } +} + +func validateClientAssertionAuth(r *http.Request, t *testing.T, tokenEndpoint func() string, clientID string, key jwk.Key) { + if grant := r.Form.Get("grant_type"); grant != "client_credentials" { + t.Logf("got the wrong grant type: %s, expected client_credentials", grant) + } + if assertionType := r.Form.Get("client_assertion_type"); assertionType != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" { + t.Errorf("incorrect client assertion type: %s", assertionType) + } + + clientAssertion := r.Form.Get("client_assertion") + if clientAssertion == "" { + t.Errorf("missing client assertion") + } + + alg := key.Algorithm() + if alg == nil { + t.Logf("no key algorithm specified, using RS256 to verify client signature") + alg = jwa.RS256 + } + + tok, err := jwt.ParseString(clientAssertion, jwt.WithVerify(true), jwt.WithKey(alg, key)) + if err != nil { + t.Fatalf("error verifying client signature on token [%s]: %v", clientAssertion, err) + } + + if tok.Subject() != clientID { + t.Fatalf("incorrect subject: %s", tok.Subject()) + } + + if tok.Issuer() != clientID { + t.Fatalf("incorrect issuer: %s", tok.Issuer()) + } + + expectedAudience := tokenEndpoint() + if len(tok.Audience()) != 1 || tok.Audience()[0] != expectedAudience { + t.Fatalf("incorrect audience: %v", tok.Audience()) + } +} + +func validateBasicAuth(r *http.Request, t *testing.T) { + if grant := r.Form.Get("grant_type"); grant != "client_credentials" { + t.Logf("got the wrong grant type: %s, expected client_credentials", grant) + } + + username, password, ok := r.BasicAuth() + if !ok { + t.Errorf("missing basic auth") + } + if username != "theclient" || password != "thesecret" { + t.Errorf("failed to pass correct username and password. got %s:%s", username, password) + } +} + +func extractDPoPToken(r *http.Request, t *testing.T) jwt.Token { + dpop := r.Header.Get("dpop") + jwsMessage, err := jws.ParseString(dpop) + if err != nil { + t.Errorf("error parsing dpop payload as JWS: %v", err) + } + + sig := jwsMessage.Signatures()[0] + signingKey := sig.ProtectedHeaders().JWK() + + clientTok, err := jwt.ParseString(dpop, jwt.WithVerify(true), jwt.WithKey(signingKey.Algorithm(), signingKey)) + if err != nil { + t.Errorf("failed to parse/verify the dpop token: %v", err) + } + + return clientTok +} + +func setupKeycloak(ctx context.Context, t *testing.T) (tc.Container, string, string) { + containerReq := tc.ContainerRequest{ + Image: "ghcr.io/opentdf/keycloak:sha-8a6d35a", + ExposedPorts: []string{"8082/tcp", "8083/tcp"}, + Cmd: []string{ + "start-dev", "--http-port=8082", "--https-port=8083", "--features=preview", "--verbose", + "-Djavax.net.ssl.trustStorePassword=password", "-Djavax.net.ssl.HostnameVerifier=AllowAll", + "-Djavax.net.debug=ssl", + "-Djavax.net.ssl.trustStore=/truststore/truststore.jks", + "--spi-truststore-file-hostname-verification-policy=ANY", + }, + Files: []tc.ContainerFile{ + {HostFilePath: "testdata/new-ca.jks", ContainerFilePath: "/truststore/truststore.jks", FileMode: int64(0o644)}, + {HostFilePath: "testdata/localhost.crt", ContainerFilePath: "/etc/x509/tls/localhost.crt", FileMode: int64(0o644)}, + {HostFilePath: "testdata/localhost.key", ContainerFilePath: "/etc/x509/tls/localhost.key", FileMode: int64(0o600)}, + }, + Env: map[string]string{ + "KEYCLOAK_ADMIN": "admin", + "KEYCLOAK_ADMIN_PASSWORD": "admin", + "KC_HTTPS_KEY_STORE_PASSWORD": "password", + "KC_HTTPS_KEY_STORE_FILE": "/truststore/truststore.jks", + "KC_HTTPS_CERTIFICATE_FILE": "/etc/x509/tls/localhost.crt", + "KC_HTTPS_CERTIFICATE_KEY_FILE": "/etc/x509/tls/localhost.key", + "KC_HTTPS_CLIENT_AUTH": "request", + }, + + WaitingFor: wait.ForLog("Running the server"), + } + + var providerType tc.ProviderType + + if os.Getenv("TESTCONTAINERS_PODMAN") == "true" { + providerType = tc.ProviderPodman + } else { + providerType = tc.ProviderDocker + } + + keycloak, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{ + ProviderType: providerType, + ContainerRequest: containerReq, + Started: true, + }) + if err != nil { + t.Fatalf("error starting keycloak container: %v", err) + } + port, err := keycloak.MappedPort(ctx, "8082") + require.NoError(t, err) + keycloakBase := "http://localhost:" + port.Port() + + httpPort, err := keycloak.MappedPort(ctx, "8083") + require.NoError(t, err) + keycloakHTTPSBase := "https://localhost:" + httpPort.Port() + + realm := "test" + + connectParams := fixtures.KeycloakConnectParams{ + BasePath: keycloakBase, + Username: "admin", + Password: "admin", + Realm: realm, + Audience: "https://test.example.org", + AllowInsecureTLS: true, + } + + err = fixtures.SetupKeycloak(ctx, connectParams) + require.NoError(t, err) + + return keycloak, keycloakBase + "/realms/" + realm + "/protocol/openid-connect/token", keycloakHTTPSBase + "/realms/" + realm + "/protocol/openid-connect/token" +} diff --git a/sdk/auth/oauth/testdata/localhost.crt b/test/integration/oauth/testdata/localhost.crt similarity index 100% rename from sdk/auth/oauth/testdata/localhost.crt rename to test/integration/oauth/testdata/localhost.crt diff --git a/sdk/auth/oauth/testdata/localhost.key b/test/integration/oauth/testdata/localhost.key similarity index 100% rename from sdk/auth/oauth/testdata/localhost.key rename to test/integration/oauth/testdata/localhost.key diff --git a/sdk/auth/oauth/testdata/new-ca.crt b/test/integration/oauth/testdata/new-ca.crt similarity index 100% rename from sdk/auth/oauth/testdata/new-ca.crt rename to test/integration/oauth/testdata/new-ca.crt diff --git a/sdk/auth/oauth/testdata/new-ca.jks b/test/integration/oauth/testdata/new-ca.jks similarity index 100% rename from sdk/auth/oauth/testdata/new-ca.jks rename to test/integration/oauth/testdata/new-ca.jks diff --git a/sdk/auth/oauth/testdata/new-ca.key b/test/integration/oauth/testdata/new-ca.key similarity index 100% rename from sdk/auth/oauth/testdata/new-ca.key rename to test/integration/oauth/testdata/new-ca.key diff --git a/sdk/auth/oauth/testdata/sampleuser.crt b/test/integration/oauth/testdata/sampleuser.crt similarity index 100% rename from sdk/auth/oauth/testdata/sampleuser.crt rename to test/integration/oauth/testdata/sampleuser.crt diff --git a/sdk/auth/oauth/testdata/sampleuser.key b/test/integration/oauth/testdata/sampleuser.key similarity index 100% rename from sdk/auth/oauth/testdata/sampleuser.key rename to test/integration/oauth/testdata/sampleuser.key diff --git a/sdk/auth/oauth/testdata/sanX509.conf b/test/integration/oauth/testdata/sanX509.conf similarity index 100% rename from sdk/auth/oauth/testdata/sanX509.conf rename to test/integration/oauth/testdata/sanX509.conf diff --git a/sdk/auth/oauth/testdata/sanX509su.conf b/test/integration/oauth/testdata/sanX509su.conf similarity index 100% rename from sdk/auth/oauth/testdata/sanX509su.conf rename to test/integration/oauth/testdata/sanX509su.conf diff --git a/test/service-start.bats b/test/service-start.bats index 378328becd..27b22d1f57 100755 --- a/test/service-start.bats +++ b/test/service-start.bats @@ -24,37 +24,6 @@ [ $(jq -r .kid <<<"${output}") = r1 ] } -@test "REST: new public key endpoint (no algorithm)" { - run curl -s --show-error --fail-with-body "https://localhost:8080/kas/v2/kas_public_key" - echo "output=$output" - p=$(jq -r .publicKey <<<"${output}") - - # Is public key - [[ "$p" = "-----BEGIN PUBLIC KEY"-----* ]] - - # Is an RSA key - printf '%s\n' "$p" | openssl asn1parse | grep rsaEncryption - - # Has expected kid - [ $(jq -r .kid <<<"${output}") = r1 ] -} - -@test "REST: new public key endpoint (ec)" { - run curl -s --show-error --fail-with-body "https://localhost:8080/kas/v2/kas_public_key?algorithm=ec:secp256r1" - echo "$output" - - # Is an EC P256r1 curve - echo "$output" | jq -r .publicKey | openssl asn1parse | grep prime256v1 - - # Has expected kid - [ $(jq -r .kid <<<"${output}") = e1 ] -} - -@test "REST: public key endpoint (unknown algorithm)" { - run curl -o /dev/null -s -w "%{http_code}" "https://localhost:8080/kas/v2/kas_public_key?algorithm=invalid" - echo "$output" - [ $output = 404 ] -} @test "gRPC: public key endpoint (unknown algorithm)" { run grpcurl -d '{"algorithm":"invalid"}' "localhost:8080" "kas.AccessService/PublicKey" diff --git a/test/start-additional-kas/action.yaml b/test/start-additional-kas/action.yaml index 1d1b6b45ce..94040f6ca5 100644 --- a/test/start-additional-kas/action.yaml +++ b/test/start-additional-kas/action.yaml @@ -100,19 +100,22 @@ runs: LOG_LEVEL: ${{ inputs.log-level }} LOG_TYPE: ${{ inputs.log-type }} with: - run: > - opentdf-${KAS_NAME}.yaml yq e ' + run: | + yq e ' (.server.port = env(KAS_PORT)) | (.mode = ["kas"]) | (.services.kas.preview.ec_tdf_enabled = env(EC_TDF_ENABLED)) | (.services.kas.preview.key_management = env(KEY_MANAGEMENT)) | (.services.kas.registered_kas_uri = "http://localhost:" + env(KAS_PORT)) - | (.services.kas.root_key = env(ROOT_KEY)) + | del(.services.kas.root_key) | (.logger.level = env(LOG_LEVEL)) | (.logger.type = env(LOG_TYPE)) | (.sdk_config = {"client_id":"opentdf","client_secret":"secret","core":{"endpoint":"http://localhost:8080","plaintext":true}}) - ' - && .github/scripts/watch.sh --tee-err-to logs/kas-${KAS_NAME}.log opentdf-${KAS_NAME}.yaml ./opentdf --config-file ./opentdf-${KAS_NAME}.yaml start + ' opentdf-dev.yaml > opentdf-${KAS_NAME}.yaml + if [ "${KEY_MANAGEMENT}" == "true" ]; then + yq e -i '.services.kas.root_key = env(ROOT_KEY)' opentdf-${KAS_NAME}.yaml + fi + .github/scripts/watch.sh --tee-err-to logs/kas-${KAS_NAME}.log opentdf-${KAS_NAME}.yaml ./opentdf --config-file ./opentdf-${KAS_NAME}.yaml start wait-on: | tcp:localhost:${{ inputs.kas-port }} log-output-if: true diff --git a/test/start-up-with-containers/action.yaml b/test/start-up-with-containers/action.yaml index c0b4e3392c..ad206c9dbc 100644 --- a/test/start-up-with-containers/action.yaml +++ b/test/start-up-with-containers/action.yaml @@ -23,6 +23,10 @@ inputs: default: "text" description: 'Log format type (text, json)' required: false + provision-policy-fixtures: + default: "true" + description: 'Whether to provision fixture policy data after bootstrapping the platform' + required: false outputs: platform-working-dir: @@ -53,7 +57,7 @@ runs: id: setup-go uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: - go-version-file: 'otdf-test-platform/service/go.mod' + go-version-file: 'otdf-test-platform/go.work' check-latest: false cache-dependency-path: | otdf-test-platform/service/go.sum @@ -111,12 +115,12 @@ runs: set -e allowed_algorithms=(ec:secp256r1 rsa:2048) if echo $PLATFORM_VERSION | awk -F. '{ if ($1 > 0 || ($1 == 0 && $2 > 7) || ($1 == 0 && $2 == 7 && $3 >= 1)) exit 0; else exit 1; }'; then - # For versions 0.7.1 and later, we allow rsa:4096 ec:secp384r1 ec:secp521r1 - allowed_algorithms+=(rsa:4096 ec:secp384r1 ec:secp521r1) + # For versions 0.7.1 and later, we allow rsa:4096 ec:secp384r1 ec:secp521r1 + allowed_algorithms+=(rsa:4096 ec:secp384r1 ec:secp521r1 hpqt:xwing hpqt:secp256r1-mlkem768 hpqt:secp384r1-mlkem1024) fi keyring='[{"kid":"ec1","alg":"ec:secp256r1"},{"kid":"r1","alg":"rsa:2048"}]' keys='[{"kid":"e1","alg":"ec:secp256r1","private":"kas-ec-private.pem","cert":"kas-ec-cert.pem"},{"kid":"ec1","alg":"ec:secp256r1","private":"kas-ec-private.pem","cert":"kas-ec-cert.pem"},{"kid":"r1","alg":"rsa:2048","private":"kas-private.pem","cert":"kas-cert.pem"}]' - while IFS= read -r -d $'\0' key_json <&3; do + while IFS= read -r key_json; do printf 'processing %s\n' "${key_json}" alg="$(jq -r '.alg' <<< "${key_json}")" if [[ ! " ${allowed_algorithms[*]} " =~ " ${alg} " ]]; then @@ -146,7 +150,7 @@ runs: keyring_obj="$(jq '{kid, alg}' <<< "${key_json}")" keyring="$(jq '. + [$keyring_obj]' --argjson keyring_obj "${keyring_obj}" <<< "${keyring}")" - done 3< <(jq -c --raw-output0 '.[]' <<< "${EXTRA_KEYS}") + done < <(jq -c '.[]' <<< "${EXTRA_KEYS}") printf 'adding the following keys:\n [%s]\n[%s] \n' "${keys}" "${keyring}" @@ -211,6 +215,7 @@ runs: run: go run ./service provision keycloak working-directory: otdf-test-platform - name: Provision test fixture policy + if: ${{ inputs.provision-policy-fixtures == 'true' }} shell: bash run: go run ./service provision fixtures working-directory: otdf-test-platform diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index 128a582a54..e6f63f4fca 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -1,7 +1,7 @@ #!/usr/bin/env bats # Tests for creating and reading TDF files with various settings -# Notably, tests both 'ztdf' and 'nano' formats. +# Notably, tests both 'ztdf' formats. @test "examples: roundtrip Z-TDF with EC wrapped KAO" { # TODO: add subject mapping here to remove reliance on `provision fixtures` @@ -26,28 +26,67 @@ printf '%s\n' "$output" | grep "Hello EC wrappers!" } -@test "examples: roundtrip nanoTDF (encrypted policy)" { - echo "[INFO] creating nanotdf file" - go run ./examples encrypt -o sensitive.txt.ntdf --nano --no-kid-in-nano "Hello NanoTDF" - go run ./examples encrypt -o sensitive-kid.txt.ntdf --nano "Hello NanoTDF KID" +@test "examples: roundtrip Z-TDF with X-Wing wrapped KAO" { + echo "[INFO] create a tdf3 format file" + run go run ./examples encrypt -o sensitive-with-xwing.txt.tdf --autoconfigure=false -A "hpqt:xwing" "Hello X-Wing wrappers!" + echo "[INFO] echoing output; if successful, this is just the manifest" + echo "$output" + + echo "[INFO] Validate the manifest lists the expected type in its KAO" + kaotype=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"${output}") + echo "kao.type=$kaotype" + [ "$kaotype" = hybrid-wrapped ] - echo "[INFO] decrypting nanotdf..." - go run ./examples decrypt sensitive.txt.ntdf - go run ./examples decrypt sensitive.txt.ntdf | grep "Hello NanoTDF" - go run ./examples decrypt sensitive-kid.txt.ntdf - go run ./examples decrypt sensitive-kid.txt.ntdf | grep "Hello NanoTDF KID" + kid=$(jq -r '.encryptionInformation.keyAccess[0].kid' <<<"${output}") + echo "kao.kid=$kid" + [ "$kid" = x1 ] + + echo "[INFO] decrypting..." + run go run ./examples decrypt sensitive-with-xwing.txt.tdf + echo "$output" + printf '%s\n' "$output" | grep "Hello X-Wing wrappers!" } -@test "examples: roundtrip nanoTDF (plaintext policy)" { - echo "[INFO] creating nanotdf file" - go run ./examples encrypt -o sensitive-plaintext_policy.txt.ntdf --policy-mode plaintext --nano --no-kid-in-nano "Hello NanoTDF" - go run ./examples encrypt -o sensitive-kid-plaintext_policy.txt.ntdf --policy-mode plaintext --nano "Hello NanoTDF KID" +@test "examples: roundtrip Z-TDF with P256+ML-KEM-768 wrapped KAO" { + echo "[INFO] create a tdf3 format file" + run go run ./examples encrypt -o sensitive-with-p256mlkem768.txt.tdf --autoconfigure=false -A "hpqt:secp256r1-mlkem768" "Hello P256+ML-KEM-768 wrappers!" + echo "[INFO] echoing output; if successful, this is just the manifest" + echo "$output" + + echo "[INFO] Validate the manifest lists the expected type in its KAO" + kaotype=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"${output}") + echo "$kaotype" + [ "$kaotype" = hybrid-wrapped ] - echo "[INFO] decrypting nanotdf..." - go run ./examples decrypt sensitive-plaintext_policy.txt.ntdf - go run ./examples decrypt sensitive-plaintext_policy.txt.ntdf | grep "Hello NanoTDF" - go run ./examples decrypt sensitive-kid-plaintext_policy.txt.ntdf - go run ./examples decrypt sensitive-kid-plaintext_policy.txt.ntdf | grep "Hello NanoTDF KID" + kid=$(jq -r '.encryptionInformation.keyAccess[0].kid' <<<"${output}") + echo "kao.kid=$kid" + [ "$kid" = h1 ] + + echo "[INFO] decrypting..." + run go run ./examples decrypt sensitive-with-p256mlkem768.txt.tdf + echo "$output" + printf '%s\n' "$output" | grep "Hello P256+ML-KEM-768 wrappers!" +} + +@test "examples: roundtrip Z-TDF with P384+ML-KEM-1024 wrapped KAO" { + echo "[INFO] create a tdf3 format file" + run go run ./examples encrypt -o sensitive-with-p384mlkem1024.txt.tdf --autoconfigure=false -A "hpqt:secp384r1-mlkem1024" "Hello P384+ML-KEM-1024 wrappers!" + echo "[INFO] echoing output; if successful, this is just the manifest" + echo "$output" + + echo "[INFO] Validate the manifest lists the expected type in its KAO" + kaotype=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"${output}") + echo "$kaotype" + [ "$kaotype" = hybrid-wrapped ] + + kid=$(jq -r '.encryptionInformation.keyAccess[0].kid' <<<"${output}") + echo "kao.kid=$kid" + [ "$kid" = h2 ] + + echo "[INFO] decrypting..." + run go run ./examples decrypt sensitive-with-p384mlkem1024.txt.tdf + echo "$output" + printf '%s\n' "$output" | grep "Hello P384+ML-KEM-1024 wrappers!" } @test "examples: legacy key support Z-TDF" { @@ -101,7 +140,7 @@ echo "[INFO] validating default key is r1" echo "[INFO] default key result: $(grpcurl "localhost:8080" "kas.AccessService/PublicKey")" - [ $(grpcurl "localhost:8080" "kas.AccessService/PublicKey" | jq -e -r .kid) = r1 ] + [ "$(grpcurl "localhost:8080" "kas.AccessService/PublicKey" | jq -e -r .kid)" = r1 ] echo "[INFO] validating keys are correct by alg" [ "$(grpcurl -d '{"algorithm":"ec:secp256r1"}' "localhost:8080" "kas.AccessService/PublicKey" | jq -e -r .kid)" = e1 ] @@ -120,12 +159,25 @@ wait_for_green() { fi sleep 4 done + echo "WARNING: service failed health check after sleep and polling" >&2 + return 1 +} + +write_opentdf_config() { + local tmp + + tmp=$(mktemp "./opentdf.yaml.tmp.XXXXXX") + if ! cat >"$tmp"; then + rm -f "$tmp" + return 1 + fi + mv -f "$tmp" opentdf.yaml } downgrade_config() { ec_current_key=$1 rsa_current_key=$2 - cat >opentdf.yaml <opentdf.yaml <"`: same as default, but loads the fixture YAML at `` (absolute, or relative to the project root) instead of `policy_default.yaml`. Useful for scenarios that need a custom seed policy. +- `Given a local platform with platform template "" and keycloak template ""`: stands up the platform with explicit platform and keycloak templates. See the [default keycloak template](cukes/resources/keycloak_base.template). ## Test Feature Authoring diff --git a/tests-bdd/cukes/glue_platform.go b/tests-bdd/cukes/glue_platform.go index afe54024eb..5c2b3ed09c 100644 --- a/tests-bdd/cukes/glue_platform.go +++ b/tests-bdd/cukes/glue_platform.go @@ -20,7 +20,7 @@ import ( "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" otdf "github.com/opentdf/platform/sdk" - "github.com/opentdf/platform/tests-bdd/cukes/utils" + testhelpers "github.com/opentdf/platform/tests-bdd/cukes/utils" tcb "github.com/testcontainers/testcontainers-go" tc "github.com/testcontainers/testcontainers-go/modules/compose" ) @@ -33,7 +33,13 @@ type PlatformTestSuiteContext struct { FeatureTracker map[string]*PlatformScenarioContext Logger *slog.Logger ComposeLogger *slog.Logger + PlatformLogger *slog.Logger HasFailures bool // Track if any test failures occurred + + // defaultPolicyDBs tracks databases where provisionDefaultPolicy has + // already run, keyed by DatabaseName. Prevents duplicate provisioning + // and lets stateless-reuse scenarios detect missing policy. + defaultPolicyDBs map[string]bool } type DockerComposeLogger struct { Logger *slog.Logger @@ -70,11 +76,13 @@ func (d *DockerComposeLogger) Printf(format string, v ...interface{}) { d.Logger.Info("docker compose log", slog.String("message", fmt.Sprintf(format, v...))) } -func CreatePlatformCukesContext(logger *slog.Logger, composeLogger *slog.Logger) *PlatformTestSuiteContext { +func CreatePlatformCukesContext(logger *slog.Logger, composeLogger *slog.Logger, platformLogger *slog.Logger) *PlatformTestSuiteContext { return &PlatformTestSuiteContext{ - FeatureTracker: make(map[string]*PlatformScenarioContext), - Logger: logger, - ComposeLogger: composeLogger, + FeatureTracker: make(map[string]*PlatformScenarioContext), + Logger: logger, + ComposeLogger: composeLogger, + PlatformLogger: platformLogger, + defaultPolicyDBs: make(map[string]bool), } } @@ -99,7 +107,7 @@ func (c *PlatformTestSuiteContext) InitializeScenario(scenarioContext *godog.Sce } scenarioID := uuid.New().String() if !featureTracked || !statelessFeature { - platformPort := openPort() + platformPort := openPort(ctx) platformDBName := strings.ReplaceAll("opentdf"+scenarioID, "-", "_") platformScenarioContext = &PlatformScenarioContext{ ID: scenarioID, @@ -195,6 +203,19 @@ func (c *PlatformScenarioContext) RegisterShutdownHook(hook func() error) { c.ShutdownHooks = append(c.ShutdownHooks, hook) } +// RegisterPlatformShutdownHook registers a hook that tears down or captures +// output from platform-lifecycle resources (compose stacks, inline servers, +// log capture). In stateless mode the platform is shared across scenarios, so +// these hooks must run once at suite teardown rather than after every +// scenario. Non-stateless features retain per-scenario teardown. +func (c *PlatformScenarioContext) RegisterPlatformShutdownHook(hook func() error) { + if c.Stateless { + c.TestSuiteContext.ShutdownFunctions = append(c.TestSuiteContext.ShutdownFunctions, hook) + return + } + c.ShutdownHooks = append(c.ShutdownHooks, hook) +} + func (c *PlatformScenarioContext) ClearError() { c.err = nil } @@ -350,14 +371,14 @@ func (l *LocalDevPlatformGlue) Setup(platformCukesContext *PlatformTestSuiteCont return err } logger.Info("setup temp keys") - utils.GenerateTempKeys(l.Options.KeysDir) + testhelpers.GenerateTempKeys(ctx, l.Options.KeysDir) err := changePermissions(l.Options.CukesDir, os.FileMode(0o755)) //nolint:mnd // mkdir dir ensure all files are readable by docker if err != nil { return err } // random open ports to expose - l.Options.keycloakPort = openPort() - l.Options.postgresPort = openPort() + l.Options.keycloakPort = openPort(l.Context) + l.Options.postgresPort = openPort(l.Context) logger.Info("starting with ports", slog.Int("keycloak_port", l.Options.keycloakPort), @@ -411,13 +432,13 @@ func (l *LocalDevPlatformGlue) Setup(platformCukesContext *PlatformTestSuiteCont } func (l *LocalDevPlatformGlue) mkCert() error { - cmd := exec.Command("mkcert", "-install") + cmd := exec.CommandContext(l.Context, "mkcert", "-install") cmd.Dir = l.Options.CukesDir if err := cmd.Run(); err != nil { return err } //nolint:gosec // G204 - cmd = exec.Command("mkcert", "-cert-file", + cmd = exec.CommandContext(l.Context, "mkcert", "-cert-file", path.Join(l.Options.KeysDir, l.Options.Hostname+".crt"), "-key-file", path.Join(l.Options.KeysDir, l.Options.Hostname+".key"), l.Options.Hostname, "*."+l.Options.Hostname, @@ -475,15 +496,14 @@ func LogComposeServices(c interface{}, logger *slog.Logger) { } } -func openPort() int { - //nolint:gosec // G102 - listener, err := net.Listen("tcp", ":0") +func openPort(ctx context.Context) int { + lc := &net.ListenConfig{} + listener, err := lc.Listen(ctx, "tcp", ":0") if err != nil { log.Fatal(err) } defer listener.Close() - // Get the port number from the listener address tcpAddr, ok := listener.Addr().(*net.TCPAddr) if !ok { panic(errors.New("address is not a TCP Address")) diff --git a/tests-bdd/cukes/resources/keycloak_base.template b/tests-bdd/cukes/resources/keycloak_base.template index de3f34c11b..73ddccf39d 100644 --- a/tests-bdd/cukes/resources/keycloak_base.template +++ b/tests-bdd/cukes/resources/keycloak_base.template @@ -1,5 +1,11 @@ +# This template tracks service/cmd/keycloak_data.yaml. Only the templated +# values (hostname/kcPort/platformPort/realm) and BDD-specific additions +# marked inline (e.g., the `impersonation` client role on opentdf's +# service account, used for RFC 8693 token-exchange in the encrypt/decrypt +# scenarios) should differ from that file; when keycloak_data.yaml +# changes, update this template to match. baseUrl: &baseUrl http://{{.hostname}}:{{.kcPort}} -serverBaseUrl: &serverBaseUrl https://{{.hostname}}:{{.platformPort}} +serverBaseUrl: &serverBaseUrl http://{{.hostname}}:{{.platformPort}} customAudMapper: &customAudMapper name: audience-mapper protocol: openid-connect @@ -35,6 +41,11 @@ realms: - *customAudMapper sa_realm_roles: - opentdf-admin + # BDD-only: grant impersonation so the admin SDK can exchange its + # token for a user-scoped token in encrypt/decrypt scenarios. + sa_client_roles: + realm-management: + - impersonation copies: 10 - client: clientID: opentdf-sdk diff --git a/tests-bdd/cukes/resources/platform.namespaced_policy.template b/tests-bdd/cukes/resources/platform.namespaced_policy.template new file mode 100644 index 0000000000..40cef30707 --- /dev/null +++ b/tests-bdd/cukes/resources/platform.namespaced_policy.template @@ -0,0 +1,186 @@ +# This template is platform.template plus the single authorization override +# `enforce_namespaced_entitlements: true`. Keep in sync with platform.template; +# the diff between the two files should be only the authorization block below. +logger: + level: debug + type: text + output: stderr +# BDD-specific: scenarios run a full in-process platform and need their own +# schema-isolated database per scenario. +mode: all +db: + host: {{ .pgHost }} + port: {{ .pgPort }} + database: {{ .pgDatabase }} + user: postgres + password: changeme + schema: otdf +services: + authorization: + enforce_namespaced_entitlements: true + kas: + registered_kas_uri: http://{{ .hostname }}:{{ .platformPort }} # Should match what you have registered for *this* KAS in the policy db. + preview: + ec_tdf_enabled: false + key_management: false + root_key: a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1 # For local development testing only + keyring: + - kid: e1 + alg: ec:secp256r1 + - kid: e1 + alg: ec:secp256r1 + legacy: true + - kid: r1 + alg: rsa:2048 + - kid: r1 + alg: rsa:2048 + legacy: true + entityresolution: + log_level: info + url: http://{{ .hostname }}:{{ .kcPort }}/auth + clientid: "tdf-entity-resolution" + clientsecret: "secret" + realm: "{{ .authRealm }}" + legacykeycloak: true + inferid: + from: + email: true + username: true + # cache_expiration: 30s # disabled unless present and > 0 + # policy is enabled by default in mode 'all' + # policy: + # enabled: true + # list_request_limit_default: 1000 + # list_request_limit_max: 2500 + # authorization: + # entitlement_policy_cache: + # enabled: false + # refresh_interval: 30s +server: + public_hostname: {{ .hostname }} + tls: + enabled: false + cert: ./keys/platform.crt + key: ./keys/platform-key.pem + auth: + enabled: true + enforceDPoP: false + audience: "http://{{ .hostname }}:{{ .platformPort }}" + issuer: http://{{ .hostname }}:{{ .kcPort }}/auth/realms/{{ .authRealm }} + policy: + ## Dot notation is used to access nested claims (i.e. realm_access.roles) + # Claim that represents the user (i.e. email) + username_claim: # preferred_username + # That claim to access groups (i.e. realm_access.roles) + groups_claim: # realm_access.roles + # Claim the represents the idP client ID + client_id_claim: # azp + # Optional external role provider (name is resolved via StartOptions) + # roles_provider: + # name: external + # config: {} # provider-specific (any object) + ## Extends the builtin policy + extension: | + g, opentdf-admin, role:admin + g, opentdf-standard, role:standard + ## Custom policy that overrides builtin policy (see examples https://github.com/casbin/casbin/tree/master/examples) + csv: #| + # p, role:admin, *, *, allow + ## Custom model (see https://casbin.org/docs/syntax-for-models/) + model: #| + # [request_definition] + # r = sub, res, act, obj + # + # [policy_definition] + # p = sub, res, act, obj, eft + # + # [role_definition] + # g = _, _ + # + # [policy_effect] + # e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + # + # [matchers] + # m = g(r.sub, p.sub) && globOrRegexMatch(r.res, p.res) && globOrRegexMatch(r.act, p.act) && globOrRegexMatch(r.obj, p.obj) + trace: + enabled: false + provider: + name: file # file | otlp + file: + path: "./traces/traces.log" + prettyPrint: true # Optional, default is compact JSON + maxSize: 50 # Optional, default 20MB + maxBackups: 5 # Optional, default 10 + maxAge: 14 # Optional, default 30 days + compress: true # Optional, default false + # otlp: + # protocol: grpc # Optional, defaults to grpc + # endpoint: "localhost:4317" + # insecure: true # Set to false if Jaeger requires TLS + # headers: {} # Add if authentication is needed + # HTTP + # protocol: "http/protobuf" + # endpoint: "http://localhost:4318" # Default OTLP HTTP port + # insecure: true # If collector is just HTTP, not HTTPS + # headers: {} # Add if authentication is needed + cors: + enabled: true + # "*" to allow any origin or a specific domain like "https://yourdomain.com" + allowedorigins: + - "*" + # List of methods. Examples: "GET,POST,PUT" + allowedmethods: + - GET + - POST + - PATCH + - PUT + - DELETE + - OPTIONS + # List of headers that are allowed in a request + allowedheaders: + - Accept + - Accept-Encoding + - Authorization + - Connect-Protocol-Version + - Content-Length + - Content-Type + - Dpop + - X-CSRF-Token + - X-Requested-With + - X-Rewrap-Additional-Context + # List of response headers that browsers are allowed to access + exposedheaders: + - Link + # Sets whether credentials are included in the CORS request + allowcredentials: true + # Sets the maximum age (in seconds) of a specific CORS preflight request + maxage: 3600 + # Additive fields - append to base lists without replacing defaults + # Use these to add custom values without having to copy all defaults + # additionalmethods: [] + # additionalheaders: + # - X-Custom-Header + # additionalexposedheaders: [] + grpc: + reflectionEnabled: true # Default is false + # http: + # # HTTP server configuration + # # Negative values indicate no timeout, default will be used if the timeout is set to 0 + # readTimeout: 15s + # writeTimeout: 15s + # readHeaderTimeout: 10s + # idleTimeout: 20s + # maxHeaderBytes: 1048576 # 1 MB + cryptoProvider: + type: standard + standard: + keys: + - kid: r1 + alg: rsa:2048 + private: {{ .platformKeysDir }}/kas-private.pem + cert: {{ .platformKeysDir }}/kas-cert.pem + - kid: e1 + alg: ec:secp256r1 + private: {{ .platformKeysDir }}/kas-ec-private.pem + cert: {{ .platformKeysDir }}/kas-ec-cert.pem + port: {{ .platformPort }} diff --git a/tests-bdd/cukes/resources/platform.template b/tests-bdd/cukes/resources/platform.template index 51c29efb89..de46961faf 100644 --- a/tests-bdd/cukes/resources/platform.template +++ b/tests-bdd/cukes/resources/platform.template @@ -1,25 +1,14 @@ -authEndpoint: &authEndpoint http://{{ .hostname }}:{{.kcPort }}/auth -issuerEndpoint: &issuerEndpoint http://{{ .hostname }}:{{.kcPort }}/auth/realms/{{.authRealm}} -tokenEndpoint: &tokenEndpoint http://{{ .hostname }}:{{.kcPort }}/auth/realms/{{.authRealm}}/protocol/openid-connect/token -entityResolutionServiceUrl: &entityResolutionServiceUrl https://{{ .hostname }}:{{.platformPort }}/entityresolution/resolve -platformEndpoint: &platformEndpoint https://{{.hostname }}:{{.platformPort }} -authRealm: &authRealm {{.authRealm}} -mode: all +# This template tracks opentdf-dev.yaml. Only the templated values +# (hostname/ports/realm/db/platformKeysDir) and the BDD-specific additions +# marked below should differ from that file; when opentdf-dev.yaml changes, +# update this template to match. logger: level: debug type: text - output: stdout -server: - port: {{.platformPort}} - auth: - enabled: true - enforceDPoP: false - audience: *platformEndpoint - issuer: *issuerEndpoint - policy: - extension: | - g, opentdf-admin, role:admin - g, opentdf-standard, role:standard + output: stderr +# BDD-specific: scenarios run a full in-process platform and need their own +# schema-isolated database per scenario. +mode: all db: host: {{ .pgHost }} port: {{ .pgPort }} @@ -29,28 +18,168 @@ db: schema: otdf services: kas: + registered_kas_uri: http://{{ .hostname }}:{{ .platformPort }} # Should match what you have registered for *this* KAS in the policy db. + preview: + ec_tdf_enabled: false + key_management: false + root_key: a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1 # For local development testing only keyring: - kid: e1 alg: ec:secp256r1 + - kid: e1 + alg: ec:secp256r1 + legacy: true - kid: r1 alg: rsa:2048 + - kid: r1 + alg: rsa:2048 + legacy: true entityresolution: - url: *authEndpoint - clientid: 'tdf-entity-resolution' - clientsecret: 'secret' - realm: *authRealm + log_level: info + url: http://{{ .hostname }}:{{ .kcPort }}/auth + clientid: "tdf-entity-resolution" + clientsecret: "secret" + realm: "{{ .authRealm }}" legacykeycloak: true inferid: from: email: true username: true - shared: - clientId: otdf-shared - clientSecret: secret - authClientId: otdf-shared-auth - serviceHostName: shared - platformEndpoint: *platformEndpoint - platformAuthEndpoint: *authEndpoint - platformAuthRealm: *authRealm - tokenEndpoint: *tokenEndpoint - # ...other service configs as needed... + # cache_expiration: 30s # disabled unless present and > 0 + # policy is enabled by default in mode 'all' + # policy: + # enabled: true + # list_request_limit_default: 1000 + # list_request_limit_max: 2500 + # authorization: + # entitlement_policy_cache: + # enabled: false + # refresh_interval: 30s +server: + public_hostname: {{ .hostname }} + tls: + enabled: false + cert: ./keys/platform.crt + key: ./keys/platform-key.pem + auth: + enabled: true + enforceDPoP: false + audience: "http://{{ .hostname }}:{{ .platformPort }}" + issuer: http://{{ .hostname }}:{{ .kcPort }}/auth/realms/{{ .authRealm }} + policy: + ## Dot notation is used to access nested claims (i.e. realm_access.roles) + # Claim that represents the user (i.e. email) + username_claim: # preferred_username + # That claim to access groups (i.e. realm_access.roles) + groups_claim: # realm_access.roles + # Claim the represents the idP client ID + client_id_claim: # azp + # Optional external role provider (name is resolved via StartOptions) + # roles_provider: + # name: external + # config: {} # provider-specific (any object) + ## Extends the builtin policy + extension: | + g, opentdf-admin, role:admin + g, opentdf-standard, role:standard + ## Custom policy that overrides builtin policy (see examples https://github.com/casbin/casbin/tree/master/examples) + csv: #| + # p, role:admin, *, *, allow + ## Custom model (see https://casbin.org/docs/syntax-for-models/) + model: #| + # [request_definition] + # r = sub, res, act, obj + # + # [policy_definition] + # p = sub, res, act, obj, eft + # + # [role_definition] + # g = _, _ + # + # [policy_effect] + # e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + # + # [matchers] + # m = g(r.sub, p.sub) && globOrRegexMatch(r.res, p.res) && globOrRegexMatch(r.act, p.act) && globOrRegexMatch(r.obj, p.obj) + trace: + enabled: false + provider: + name: file # file | otlp + file: + path: "./traces/traces.log" + prettyPrint: true # Optional, default is compact JSON + maxSize: 50 # Optional, default 20MB + maxBackups: 5 # Optional, default 10 + maxAge: 14 # Optional, default 30 days + compress: true # Optional, default false + # otlp: + # protocol: grpc # Optional, defaults to grpc + # endpoint: "localhost:4317" + # insecure: true # Set to false if Jaeger requires TLS + # headers: {} # Add if authentication is needed + # HTTP + # protocol: "http/protobuf" + # endpoint: "http://localhost:4318" # Default OTLP HTTP port + # insecure: true # If collector is just HTTP, not HTTPS + # headers: {} # Add if authentication is needed + cors: + enabled: true + # "*" to allow any origin or a specific domain like "https://yourdomain.com" + allowedorigins: + - "*" + # List of methods. Examples: "GET,POST,PUT" + allowedmethods: + - GET + - POST + - PATCH + - PUT + - DELETE + - OPTIONS + # List of headers that are allowed in a request + allowedheaders: + - Accept + - Accept-Encoding + - Authorization + - Connect-Protocol-Version + - Content-Length + - Content-Type + - Dpop + - X-CSRF-Token + - X-Requested-With + - X-Rewrap-Additional-Context + # List of response headers that browsers are allowed to access + exposedheaders: + - Link + # Sets whether credentials are included in the CORS request + allowcredentials: true + # Sets the maximum age (in seconds) of a specific CORS preflight request + maxage: 3600 + # Additive fields - append to base lists without replacing defaults + # Use these to add custom values without having to copy all defaults + # additionalmethods: [] + # additionalheaders: + # - X-Custom-Header + # additionalexposedheaders: [] + grpc: + reflectionEnabled: true # Default is false + # http: + # # HTTP server configuration + # # Negative values indicate no timeout, default will be used if the timeout is set to 0 + # readTimeout: 15s + # writeTimeout: 15s + # readHeaderTimeout: 10s + # idleTimeout: 20s + # maxHeaderBytes: 1048576 # 1 MB + cryptoProvider: + type: standard + standard: + keys: + - kid: r1 + alg: rsa:2048 + private: {{ .platformKeysDir }}/kas-private.pem + cert: {{ .platformKeysDir }}/kas-cert.pem + - kid: e1 + alg: ec:secp256r1 + private: {{ .platformKeysDir }}/kas-ec-private.pem + cert: {{ .platformKeysDir }}/kas-ec-cert.pem + port: {{ .platformPort }} diff --git a/tests-bdd/cukes/steps_attributes.go b/tests-bdd/cukes/steps_attributes.go index 20bd497439..53739a8e7a 100644 --- a/tests-bdd/cukes/steps_attributes.go +++ b/tests-bdd/cukes/steps_attributes.go @@ -56,10 +56,10 @@ func (s *AttributesStepDefinitions) createAttributeRequestFromTable(scenarioCont return nil, errors.New("unable to extract namespace ID") } createAttributeRequest.NamespaceId = id - case "name": - createAttributeRequest.Name = cell.Value + case nameKey: + createAttributeRequest.Name = strings.TrimSpace(cell.Value) case "rule": - switch cell.Value { + switch strings.TrimSpace(cell.Value) { case "anyOf": createAttributeRequest.Rule = policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF case "allOf": diff --git a/tests-bdd/cukes/steps_encryption.go b/tests-bdd/cukes/steps_encryption.go new file mode 100644 index 0000000000..0ef55ccacf --- /dev/null +++ b/tests-bdd/cukes/steps_encryption.go @@ -0,0 +1,278 @@ +package cukes + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/cucumber/godog" + otdf "github.com/opentdf/platform/sdk" + "golang.org/x/oauth2" +) + +const ( + // Admin client used for both the platform SDK and as the exchanger in + // RFC 8693 user-impersonation token exchange. keycloak_base.template + // grants its service account the `impersonation` role on + // realm-management so it may obtain user-scoped tokens. + exchangeClientID = "opentdf" + exchangeClientSecret = "secret" + // Target audience of the exchanged token; matches the token_exchanges + // policy wired in keycloak_base.template (start_client: opentdf, + // target_client: opentdf-sdk). + exchangeTargetClientID = "opentdf-sdk" + // Standard OAuth grant-type and token-type URNs from RFC 8693 — not credentials. + tokenExchangeGrant = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // URN identifier, not a credential + accessTokenType = "urn:ietf:params:oauth:token-type:access_token" //nolint:gosec // URN identifier, not a credential + + tokenEndpointTimeout = 10 * time.Second +) + +// decryptResult captures whether a decrypt attempt succeeded, and if not, +// whether it was denied (rewrap forbidden) or failed for another reason. +type decryptResult struct { + plaintext []byte + err error + denied bool +} + +type EncryptionStepDefinitions struct{} + +// userTokenForStoredAs mints a user-scoped SDK via RFC 8693 token exchange: +// the admin client authenticates with client_credentials, then exchanges +// that token for one representing the target user. Stashes the resulting +// SDK under the given reference key. +func (s *EncryptionStepDefinitions) userTokenForStoredAs(ctx context.Context, username, ref string) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + localPlatformGlue, ok := (*scenarioContext.TestSuiteContext.PlatformGlue).(*LocalDevPlatformGlue) + if !ok { + return ctx, errors.New("failed to load local platform glue") + } + + kcHostPort := net.JoinHostPort(localPlatformGlue.Options.Hostname, strconv.Itoa(localPlatformGlue.Options.keycloakPort)) + tokenURL := fmt.Sprintf("http://%s/auth/realms/%s/protocol/openid-connect/token", + kcHostPort, + scenarioContext.ScenarioOptions.KeycloakRealm, + ) + + adminToken, err := fetchAdminAccessToken(ctx, tokenURL) + if err != nil { + return ctx, fmt.Errorf("fetch admin token for exchange: %w", err) + } + token, err := exchangeForUserToken(ctx, tokenURL, adminToken.AccessToken, username) + if err != nil { + return ctx, fmt.Errorf("exchange admin token for user %q: %w", username, err) + } + + userSDK, err := otdf.New( + scenarioContext.ScenarioOptions.PlatformEndpoint, + otdf.WithInsecureSkipVerifyConn(), + otdf.WithOAuthAccessTokenSource(oauth2.StaticTokenSource(token)), + ) + if err != nil { + return ctx, fmt.Errorf("build user SDK for %q: %w", username, err) + } + scenarioContext.RecordObject(ref, userSDK) + return ctx, nil +} + +// encryptPlaintextStoredAs encrypts the given text using the admin SDK, binds +// it to the comma-separated attribute FQNs, and stashes the resulting TDF bytes +// under the given reference key. The platform's own KAS is used as the default; +// its public key is fetched on demand by the SDK. +func (s *EncryptionStepDefinitions) encryptPlaintextStoredAs(ctx context.Context, plaintext, attributeFQNs, ref string) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + fqns := splitAndTrim(attributeFQNs, ",") + + kasURL := scenarioContext.ScenarioOptions.PlatformEndpoint + var tdfBuf bytes.Buffer + _, err := scenarioContext.SDK.CreateTDFContext( + ctx, + &tdfBuf, + strings.NewReader(plaintext), + otdf.WithKasInformation(otdf.KASInfo{URL: kasURL, Default: true}), + otdf.WithDataAttributes(fqns...), + ) + if err != nil { + return ctx, fmt.Errorf("encrypt: %w", err) + } + scenarioContext.RecordObject(ref, tdfBuf.Bytes()) + return ctx, nil +} + +// userDecryptsStoredAs pulls the user SDK and TDF bytes from scenario state, +// attempts decrypt, and stashes a decryptResult under the plaintext ref. +func (s *EncryptionStepDefinitions) userDecryptsStoredAs(ctx context.Context, tokenRef, tdfRef, plainRef string) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + + userSDKAny := scenarioContext.GetObject(tokenRef) + userSDK, ok := userSDKAny.(*otdf.SDK) + if !ok || userSDK == nil { + return ctx, fmt.Errorf("no user SDK stored under %q; did you run `a user token for ... stored as %q`?", tokenRef, tokenRef) + } + + tdfBytesAny := scenarioContext.GetObject(tdfRef) + tdfBytes, ok := tdfBytesAny.([]byte) + if !ok { + return ctx, fmt.Errorf("no TDF bytes stored under %q", tdfRef) + } + + // LoadTDF only parses the ZIP/manifest — it does not contact KAS, so + // access-denied (rewrap-forbidden) cannot surface here. KAS rewrap + // happens during io.Copy / Reader.Read below. + reader, err := userSDK.LoadTDF(bytes.NewReader(tdfBytes)) + if err != nil { + scenarioContext.RecordObject(plainRef, &decryptResult{err: err}) + return ctx, nil //nolint:nilerr // error is captured on decryptResult for the assertion step + } + + var plainBuf bytes.Buffer + if _, err := io.Copy(&plainBuf, reader); err != nil { + scenarioContext.RecordObject(plainRef, &decryptResult{err: err, denied: errors.Is(err, otdf.ErrRewrapForbidden)}) + return ctx, nil + } + scenarioContext.RecordObject(plainRef, &decryptResult{plaintext: plainBuf.Bytes()}) + return ctx, nil +} + +func (s *EncryptionStepDefinitions) decryptionShouldSucceedWithPlaintext(ctx context.Context, plainRef, expected string) (context.Context, error) { + result, err := getDecryptResult(ctx, plainRef) + if err != nil { + return ctx, err + } + if result.err != nil { + return ctx, fmt.Errorf("decryption %q failed: %w", plainRef, result.err) + } + if got := string(result.plaintext); got != expected { + return ctx, fmt.Errorf("decryption %q plaintext mismatch: got %q, want %q", plainRef, got, expected) + } + return ctx, nil +} + +func (s *EncryptionStepDefinitions) decryptionShouldBeDenied(ctx context.Context, plainRef string) (context.Context, error) { + result, err := getDecryptResult(ctx, plainRef) + if err != nil { + return ctx, err + } + if result.err == nil { + return ctx, fmt.Errorf("decryption %q unexpectedly succeeded (plaintext: %q)", plainRef, string(result.plaintext)) + } + if !result.denied { + return ctx, fmt.Errorf("decryption %q failed but not with access-denied: %w", plainRef, result.err) + } + return ctx, nil +} + +func getDecryptResult(ctx context.Context, ref string) (*decryptResult, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + v := scenarioContext.GetObject(ref) + if v == nil { + return nil, fmt.Errorf("no decryption result stored under %q", ref) + } + result, ok := v.(*decryptResult) + if !ok { + return nil, fmt.Errorf("object stored under %q is not a decryptResult (%T)", ref, v) + } + return result, nil +} + +// fetchAdminAccessToken mints an admin service-account token via the +// client_credentials grant against the exchange client. +func fetchAdminAccessToken(ctx context.Context, tokenURL string) (*oauth2.Token, error) { + form := url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {exchangeClientID}, + "client_secret": {exchangeClientSecret}, + } + return postForTokenEndpoint(ctx, tokenURL, form, "client_credentials") +} + +// exchangeForUserToken takes a valid admin token and asks Keycloak for a +// user-scoped token via RFC 8693 token exchange with `requested_subject`. +// Requires the exchange client to have the realm-management `impersonation` +// role. `audience` routes the exchange through the opentdf->opentdf-sdk +// policy configured in keycloak_base.template so the returned token is +// minted for opentdf-sdk, matching the SDK's own token-exchange flow +// (see sdk/auth/oauth/oauth.go). +func exchangeForUserToken(ctx context.Context, tokenURL, adminAccessToken, username string) (*oauth2.Token, error) { + form := url.Values{ + "grant_type": {tokenExchangeGrant}, + "client_id": {exchangeClientID}, + "client_secret": {exchangeClientSecret}, + "subject_token": {adminAccessToken}, + "subject_token_type": {accessTokenType}, + "requested_token_type": {accessTokenType}, + "audience": {exchangeTargetClientID}, + "requested_subject": {username}, + } + return postForTokenEndpoint(ctx, tokenURL, form, "token-exchange") +} + +// postForTokenEndpoint POSTs a token-endpoint form and decodes the standard +// access_token response. +func postForTokenEndpoint(ctx context.Context, tokenURL string, form url.Values, kind string) (*oauth2.Token, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + httpClient := &http.Client{Timeout: tokenEndpointTimeout} + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: token endpoint returned %d: %s", kind, resp.StatusCode, string(body)) + } + var payload struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + } + if err := json.Unmarshal(body, &payload); err != nil { + return nil, fmt.Errorf("%s: decode token response: %w", kind, err) + } + if payload.AccessToken == "" { + return nil, fmt.Errorf("%s: token endpoint response missing access_token", kind) + } + return &oauth2.Token{ + AccessToken: payload.AccessToken, + TokenType: payload.TokenType, + Expiry: time.Now().Add(time.Duration(payload.ExpiresIn) * time.Second), + }, nil +} + +func splitAndTrim(s, sep string) []string { + parts := strings.Split(s, sep) + out := parts[:0] + for _, p := range parts { + if trimmed := strings.TrimSpace(p); trimmed != "" { + out = append(out, trimmed) + } + } + return out +} + +func RegisterEncryptionStepDefinitions(ctx *godog.ScenarioContext) { + stepDefs := EncryptionStepDefinitions{} + ctx.Step(`^a user token for "([^"]*)" stored as "([^"]*)"$`, stepDefs.userTokenForStoredAs) + ctx.Step(`^I encrypt plaintext "([^"]*)" with attributes "([^"]*)" stored as "([^"]*)"$`, stepDefs.encryptPlaintextStoredAs) + ctx.Step(`^using token "([^"]*)", decrypt "([^"]*)" stored as "([^"]*)"$`, stepDefs.userDecryptsStoredAs) + ctx.Step(`^the decryption stored as "([^"]*)" should succeed with plaintext "([^"]*)"$`, stepDefs.decryptionShouldSucceedWithPlaintext) + ctx.Step(`^the decryption stored as "([^"]*)" should be denied$`, stepDefs.decryptionShouldBeDenied) +} diff --git a/tests-bdd/cukes/steps_localplatform.go b/tests-bdd/cukes/steps_localplatform.go index dc40600c0b..1623f0bae9 100644 --- a/tests-bdd/cukes/steps_localplatform.go +++ b/tests-bdd/cukes/steps_localplatform.go @@ -21,6 +21,10 @@ import ( "github.com/cucumber/godog" "github.com/jackc/pgx/v5/pgxpool" "github.com/opentdf/platform/lib/fixtures" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" otdf "github.com/opentdf/platform/sdk" "github.com/opentdf/platform/service/cmd" "github.com/opentdf/platform/service/pkg/server" @@ -50,8 +54,9 @@ type LocalPlatformStepDefinitions struct { } type platformStartOptions struct { - platformProvisionPath *string - kcProvisionPath *template.Template + platformProvisionPath *string + kcProvisionPath *template.Template + provisionDefaultPolicy bool } func (s *LocalPlatformStepDefinitions) aUser(ctx context.Context, username string, email string, attributes *godog.Table) (context.Context, error) { @@ -110,6 +115,28 @@ func (s *LocalPlatformStepDefinitions) commonLocalPlatform(ctx context.Context, scenarioContext := GetPlatformScenarioContext(ctx) logger := scenarioContext.TestSuiteContext.Logger if !scenarioContext.FirstScenario && scenarioContext.Stateless { + // Platform is already up (shared across @stateless scenarios), but + // each godog scenario gets its own PlatformScenarioContext with a + // nil SDK. Re-attach the SDK to the shared endpoint so step + // definitions (and the Background) keep working. + if scenarioContext.SDK == nil { + platformSDK, err := otdf.New( + scenarioContext.ScenarioOptions.PlatformEndpoint, + otdf.WithInsecureSkipVerifyConn(), + otdf.WithClientCredentials("opentdf", "secret", nil), + ) + if err != nil { + return ctx, err + } + scenarioContext.SDK = platformSDK + } + dbName := scenarioContext.ScenarioOptions.DatabaseName + if options.provisionDefaultPolicy && !scenarioContext.TestSuiteContext.defaultPolicyDBs[dbName] { + if err := provisionDefaultPolicy(ctx, scenarioContext.SDK); err != nil { + return ctx, fmt.Errorf("provision default policy: %w", err) + } + scenarioContext.TestSuiteContext.defaultPolicyDBs[dbName] = true + } return ctx, nil } localPlatformGlue, ok := (*scenarioContext.TestSuiteContext.PlatformGlue).(*LocalDevPlatformGlue) @@ -136,11 +163,12 @@ func (s *LocalPlatformStepDefinitions) commonLocalPlatform(ctx context.Context, if err := provisionKeycloak(ctx, localPlatformOptions, scenarioContext, options); err != nil { return ctx, err } + version, exists := os.LookupEnv(platformImageEnvironment) if !exists { version = platformImageEnvironmentLocalImage } - platformConfigPath, err := createPlatformConfiguration(localPlatformOptions, scenarioContext.ScenarioOptions, version == debugVersion) + platformConfigPath, err := createPlatformConfiguration(localPlatformOptions, scenarioContext.ScenarioOptions, version == debugVersion, options.platformProvisionPath) if err != nil { return ctx, err } @@ -162,7 +190,7 @@ func (s *LocalPlatformStepDefinitions) commonLocalPlatform(ctx context.Context, }() // Register shutdown hook to stop the platform - scenarioContext.RegisterShutdownHook(func() error { + scenarioContext.RegisterPlatformShutdownHook(func() error { logger.Debug("shutting down inline platform") platformCancel() return nil @@ -196,14 +224,16 @@ func (s *LocalPlatformStepDefinitions) commonLocalPlatform(ctx context.Context, return ctx, err } + attachPlatformServiceLogs(ctx, scenarioContext, platformDockerCompose) + // Wait for platform to be ready logger.Debug("waiting for platform to start") if err := waitForPlatform(platformEndpoint); err != nil { return ctx, err } - scenarioContext.RegisterShutdownHook(func() error { - return platformDockerCompose.Down(ctx, tc.RemoveOrphans(true)) + scenarioContext.RegisterPlatformShutdownHook(func() error { + return platformDockerCompose.Down(context.WithoutCancel(ctx), tc.RemoveOrphans(true)) }) } @@ -224,24 +254,47 @@ func (s *LocalPlatformStepDefinitions) commonLocalPlatform(ctx context.Context, slog.String("realm", scenarioContext.ScenarioOptions.KeycloakRealm), slog.String("endpoint", te)) + if options.provisionDefaultPolicy { + if err := provisionDefaultPolicy(ctx, platformSDK); err != nil { + return ctx, fmt.Errorf("provision default policy: %w", err) + } + scenarioContext.TestSuiteContext.defaultPolicyDBs[scenarioContext.ScenarioOptions.DatabaseName] = true + } + return ctx, nil } +func attachPlatformServiceLogs(ctx context.Context, scenarioContext *PlatformScenarioContext, platformDockerCompose tc.ComposeStack) { + platformLogger := scenarioContext.TestSuiteContext.PlatformLogger + if platformLogger == nil { + return + } + + scenarioContext.RegisterPlatformShutdownHook(func() error { + logCtx := context.WithoutCancel(ctx) + platformLogger.Info("capturing platform service logs", slog.String("service", "otdf")) + container, err := platformDockerCompose.ServiceContainer(logCtx, "otdf") + if err != nil { + platformLogger.Warn("failed to get platform service container for log capture", slog.String("error", err.Error())) + return nil + } + LogComposeService(logCtx, container, platformLogger, "otdf") + return nil + }) +} + func (s *LocalPlatformStepDefinitions) aEmptyLocalPlatform(ctx context.Context) (context.Context, error) { kt := template.Must(template.New("kc").Parse(keycloakBaseTemplate)) return s.commonLocalPlatform(ctx, &platformStartOptions{kcProvisionPath: kt}) } -// func (s *LocalPlatformStepDefinitions) aDefaultLocalPlatform(ctx context.Context) (context.Context, error) { -// kt := template.Must(template.New("kc").Parse(keycloakFederalTemplate)) -// scenarioContext := GetPlatformScenarioContext(ctx) -// localPlatformGlue, ok := (*scenarioContext.TestSuiteContext.PlatformGlue).(*LocalDevPlatformGlue) -// if !ok { -// return ctx, errors.New("no local platform glue found") -// } -// platformProvisionPath := path.Join(localPlatformGlue.Options.ProjectDir, "samples", "defaults", "federal.yaml") -// return s.commonLocalPlatform(ctx, &platformStartOptions{platformProvisionPath: &platformProvisionPath, kcProvisionPath: kt}) -// } +func (s *LocalPlatformStepDefinitions) aDefaultLocalPlatform(ctx context.Context) (context.Context, error) { + kt := template.Must(template.New("kc").Parse(keycloakBaseTemplate)) + return s.commonLocalPlatform(ctx, &platformStartOptions{ + kcProvisionPath: kt, + provisionDefaultPolicy: true, + }) +} func (s *LocalPlatformStepDefinitions) aLocalPlatformWithTemplates(ctx context.Context, platformTemplate string, kcTemplate string) (context.Context, error) { kcTemplateBytes, err := os.ReadFile(kcTemplate) @@ -362,6 +415,81 @@ func provisionKeycloak(ctx context.Context, suiteOptions *LocalDevOptions, scena }, kcData) } +func provisionDefaultPolicy(ctx context.Context, sdk *otdf.SDK) error { + nsResp, err := sdk.Namespaces.CreateNamespace(ctx, &namespaces.CreateNamespaceRequest{ + Name: "demo.com", + }) + if err != nil { + return fmt.Errorf("create namespace: %w", err) + } + nsID := nsResp.GetNamespace().GetId() + + deptResp, err := sdk.Attributes.CreateAttribute(ctx, &attributes.CreateAttributeRequest{ + NamespaceId: nsID, + Name: "department", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + Values: []string{"engineering", "finance", "hr"}, + }) + if err != nil { + return fmt.Errorf("create department attribute: %w", err) + } + + classResp, err := sdk.Attributes.CreateAttribute(ctx, &attributes.CreateAttributeRequest{ + NamespaceId: nsID, + Name: "classification", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + Values: []string{"secret", "confidential", "internal", "public"}, + }) + if err != nil { + return fmt.Errorf("create classification attribute: %w", err) + } + + type valueMapping struct { + selector string + match string + valueID string + } + var mappings []valueMapping + for _, v := range deptResp.GetAttribute().GetValues() { + mappings = append(mappings, valueMapping{ + selector: ".attributes.department[]", + match: v.GetValue(), + valueID: v.GetId(), + }) + } + for _, v := range classResp.GetAttribute().GetValues() { + mappings = append(mappings, valueMapping{ + selector: ".attributes.classification[]", + match: v.GetValue(), + valueID: v.GetId(), + }) + } + + for _, m := range mappings { + _, err := sdk.SubjectMapping.CreateSubjectMapping(ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: m.valueID, + Actions: []*policy.Action{{Name: "read"}}, + NewSubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{{ + ConditionGroups: []*policy.ConditionGroup{{ + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{{ + SubjectExternalSelectorValue: m.selector, + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{m.match}, + }}, + }}, + }}, + }, + }) + if err != nil { + return fmt.Errorf("create subject mapping for %s: %w", m.match, err) + } + } + + return nil +} + func createPlatformComposeConfiguration(options *LocalDevOptions) (string, error) { tmpFile, err := os.CreateTemp(options.CukesDir, "docker_compose.yaml") if err != nil { @@ -376,7 +504,7 @@ func createPlatformComposeConfiguration(options *LocalDevOptions) (string, error } // createPlatformConfiguration generates a platform configuration from a go text template for platform option settings -func createPlatformConfiguration(options *LocalDevOptions, scenarioOptions *LocalDevScenarioOptions, devMode bool) (string, error) { +func createPlatformConfiguration(options *LocalDevOptions, scenarioOptions *LocalDevScenarioOptions, devMode bool, platformTemplatePath *string) (string, error) { tempFileName := path.Join(options.CukesDir, "opentdf.yaml") platformKeysDir := options.KeysDir pgHost := "localhost" @@ -384,7 +512,15 @@ func createPlatformConfiguration(options *LocalDevOptions, scenarioOptions *Loca platformKeysDir = containerKeyPath pgHost = options.Hostname } - t := template.Must(template.New("platform").Parse(platformTemplate)) + templateSource := platformTemplate + if platformTemplatePath != nil && *platformTemplatePath != "" { + templateBytes, err := os.ReadFile(*platformTemplatePath) + if err != nil { + return tempFileName, err + } + templateSource = string(templateBytes) + } + t := template.Must(template.New("platform").Parse(templateSource)) var strBuffer bytes.Buffer if err := t.Execute(&strBuffer, map[string]any{ "hostname": options.Hostname, @@ -411,6 +547,7 @@ func RegisterLocalPlatformStepDefinitions(ctx *godog.ScenarioContext, x *Platfor PlatformCukesContext: x, } ctx.Step(`^an empty local platform$`, platformStepDefinitions.aEmptyLocalPlatform) + ctx.Step(`^a default local platform$`, platformStepDefinitions.aDefaultLocalPlatform) ctx.Step(`^a user exists with username "([^"]*)" and email "([^"]*)" and the following attributes:$`, platformStepDefinitions.aUser) ctx.Step(`^a local platform with platform template "([^"]*)" and keycloak template "([^"]*)"$`, platformStepDefinitions.aLocalPlatformWithTemplates) } diff --git a/tests-bdd/cukes/steps_namespaces.go b/tests-bdd/cukes/steps_namespaces.go index 92b7586047..542a4ee61b 100644 --- a/tests-bdd/cukes/steps_namespaces.go +++ b/tests-bdd/cukes/steps_namespaces.go @@ -10,6 +10,8 @@ import ( const ( createNamespaceResponseKey = "createNamespaceResponse" + namespaceIDKey = "namespace_id" + namespaceFQNKey = "namespace_fqn" ) type NamespacesStepDefinitions struct{} diff --git a/tests-bdd/cukes/steps_obligations.go b/tests-bdd/cukes/steps_obligations.go index 710d96e226..05451f2c7a 100644 --- a/tests-bdd/cukes/steps_obligations.go +++ b/tests-bdd/cukes/steps_obligations.go @@ -21,6 +21,7 @@ const ( obligationTriggerResponseKey = "obligationTriggerResponse" multiDecisionResponseKey = "multiDecisionResponse" valuesKey = "values" + nameKey = "name" ) // Step: I send a request to create an obligation with table @@ -43,13 +44,13 @@ func (s *ObligationsStepDefinitions) iSendARequestToCreateAnObligationWith(ctx c for ci, c := range r.Cells { switch cellIndexMap[ci] { - case "namespace_id": + case namespaceIDKey: nsID, ok := scenarioContext.GetObject(strings.TrimSpace(c.Value)).(string) if !ok { - return ctx, fmt.Errorf("namespace_id %s not found", c.Value) + return ctx, fmt.Errorf("%s %s not found", namespaceIDKey, c.Value) } req.NamespaceId = nsID - case "name": + case nameKey: obligationName = strings.TrimSpace(c.Value) req.Name = obligationName case valuesKey: diff --git a/tests-bdd/cukes/steps_registeredresources.go b/tests-bdd/cukes/steps_registeredresources.go new file mode 100644 index 0000000000..712e4718d6 --- /dev/null +++ b/tests-bdd/cukes/steps_registeredresources.go @@ -0,0 +1,305 @@ +package cukes + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/cucumber/godog" + "github.com/opentdf/platform/lib/identifier" + authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" +) + +type RegisteredResourcesStepDefinitions struct{} + +const ( + referenceIDColumn = "reference_id" + aavPairParts = 2 +) + +func (s *RegisteredResourcesStepDefinitions) iSendARequestToCreateARegisteredResourceWith(ctx context.Context, tbl *godog.Table) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + scenarioContext.ClearError() + + cellIndexMap := make(map[int]string) + for ri, r := range tbl.Rows { + if ri == 0 { + for ci, c := range r.Cells { + cellIndexMap[ci] = c.Value + } + continue + } + + req := ®isteredresources.CreateRegisteredResourceRequest{} + referenceID := "" + + for ci, c := range r.Cells { + v := strings.TrimSpace(c.Value) + switch cellIndexMap[ci] { + case referenceIDColumn: + referenceID = v + case nameKey: + req.Name = v + case namespaceIDKey: + nsID, ok := scenarioContext.GetObject(v).(string) + if !ok { + return ctx, fmt.Errorf("%s %s not found", namespaceIDKey, v) + } + req.NamespaceId = nsID + case namespaceFQNKey: + req.NamespaceFqn = v + } + } + + resp, err := scenarioContext.SDK.RegisteredResources.CreateRegisteredResource(ctx, req) + scenarioContext.SetError(err) + if err == nil && resp != nil { + if referenceID != "" { + scenarioContext.RecordObject(referenceID, resp.GetResource()) + } + scenarioContext.RecordObject(req.GetName(), resp.GetResource()) + } + } + + return ctx, nil +} + +func (s *RegisteredResourcesStepDefinitions) iSendARequestToCreateARegisteredResourceValueWith(ctx context.Context, tbl *godog.Table) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + scenarioContext.ClearError() + + cellIndexMap := make(map[int]string) + for ri, r := range tbl.Rows { + if ri == 0 { + for ci, c := range r.Cells { + cellIndexMap[ci] = c.Value + } + continue + } + + req := ®isteredresources.CreateRegisteredResourceValueRequest{} + referenceID := "" + + for ci, c := range r.Cells { + v := strings.TrimSpace(c.Value) + switch cellIndexMap[ci] { + case referenceIDColumn: + referenceID = v + case "resource_ref": + resource, ok := scenarioContext.GetObject(v).(*policy.RegisteredResource) + if !ok || resource == nil { + return ctx, fmt.Errorf("resource_ref %s not found", v) + } + req.ResourceId = resource.GetId() + case "value": + req.Value = v + case "action_attribute_values": + if v == "" { + continue + } + aavs, err := parseAAVs(v) + if err != nil { + return ctx, err + } + req.ActionAttributeValues = aavs + } + } + + resp, err := scenarioContext.SDK.RegisteredResources.CreateRegisteredResourceValue(ctx, req) + scenarioContext.SetError(err) + if err == nil && resp != nil { + if referenceID != "" { + scenarioContext.RecordObject(referenceID, resp.GetValue()) + } + scenarioContext.RecordObject(req.GetValue(), resp.GetValue()) + } + } + + return ctx, nil +} + +func parseAAVs(raw string) ([]*registeredresources.ActionAttributeValue, error) { + parts := strings.Split(raw, ",") + out := make([]*registeredresources.ActionAttributeValue, 0, len(parts)) + + for _, part := range parts { + entry := strings.TrimSpace(part) + pair := strings.SplitN(entry, "=>", aavPairParts) + if len(pair) != aavPairParts { + pair = strings.SplitN(entry, "|", aavPairParts) + } + if len(pair) != aavPairParts { + return nil, fmt.Errorf("invalid action_attribute_values entry %q, expected action=>attribute_value_fqn", part) + } + + actionName := strings.TrimSpace(pair[0]) + attributeValueFQN := strings.TrimSpace(pair[1]) + if actionName == "" || attributeValueFQN == "" { + return nil, fmt.Errorf("invalid action_attribute_values entry %q, action and attribute value fqn are required", part) + } + + out = append(out, ®isteredresources.ActionAttributeValue{ + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: strings.ToLower(actionName), + }, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: attributeValueFQN, + }, + }) + } + + return out, nil +} + +func (s *RegisteredResourcesStepDefinitions) iSendADecisionRequestForEntityChainForActionOnRegisteredResourceValue(ctx context.Context, entityChainID string, action string, resourceValueRef string) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + + var entities []*entity.Entity + for _, entityID := range strings.Split(entityChainID, ",") { + ent, ok := scenarioContext.GetObject(strings.TrimSpace(entityID)).(*entity.Entity) + if !ok { + return ctx, fmt.Errorf("entity %s not found or invalid type", entityID) + } + entities = append(entities, ent) + } + + entityChain := &entity.EntityChain{Entities: entities} + + resourceValueFQN := strings.TrimSpace(resourceValueRef) + if rrValue, ok := scenarioContext.GetObject(resourceValueFQN).(*policy.RegisteredResourceValue); ok && rrValue != nil { + if rrValue.GetResource() == nil { + return ctx, fmt.Errorf("registered resource value %s missing resource", resourceValueRef) + } + namespaceName := "" + if rrValue.GetResource() != nil && rrValue.GetResource().GetNamespace() != nil { + namespaceName = rrValue.GetResource().GetNamespace().GetName() + } + resourceValueFQN = (&identifier.FullyQualifiedRegisteredResourceValue{ + Namespace: namespaceName, + Name: rrValue.GetResource().GetName(), + Value: rrValue.GetValue(), + }).FQN() + } + + req := &authzV2.GetDecisionRequest{ + EntityIdentifier: &authzV2.EntityIdentifier{ + Identifier: &authzV2.EntityIdentifier_EntityChain{EntityChain: entityChain}, + }, + Action: &policy.Action{Name: strings.ToLower(action)}, + Resource: &authzV2.Resource{ + EphemeralId: "resource1", + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: resourceValueFQN, + }, + }, + FulfillableObligationFqns: getAllObligationsFromScenario(scenarioContext), + } + + resp, err := scenarioContext.SDK.AuthorizationV2.GetDecision(ctx, req) + if err != nil { + scenarioContext.SetError(err) + return ctx, err + } + + scenarioContext.RecordObject(decisionResponse, resp) + return ctx, nil +} + +func (s *RegisteredResourcesStepDefinitions) iSendAMultiResourceDecisionRequestForEntityChainForActionOnRegisteredResourceValues(ctx context.Context, entityChainID string, action string, resourceValueRefs string) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + scenarioContext.ClearError() + + var entities []*entity.Entity + for _, entityID := range strings.Split(entityChainID, ",") { + ent, ok := scenarioContext.GetObject(strings.TrimSpace(entityID)).(*entity.Entity) + if !ok { + return ctx, fmt.Errorf("entity %s not found or invalid type", entityID) + } + entities = append(entities, ent) + } + + entityChain := &entity.EntityChain{Entities: entities} + + resources := make([]*authzV2.Resource, 0) + resourceFQNMap := make(map[string]string) + for idx, resourceValueRef := range strings.Split(resourceValueRefs, ",") { + resourceValueRef = strings.TrimSpace(resourceValueRef) + resourceValueFQN := resourceValueRef + if rrValue, ok := scenarioContext.GetObject(resourceValueRef).(*policy.RegisteredResourceValue); ok && rrValue != nil { + if rrValue.GetResource() == nil { + return ctx, fmt.Errorf("registered resource value %s missing resource", resourceValueRef) + } + namespaceName := "" + if rrValue.GetResource().GetNamespace() != nil { + namespaceName = rrValue.GetResource().GetNamespace().GetName() + } + resourceValueFQN = (&identifier.FullyQualifiedRegisteredResourceValue{ + Namespace: namespaceName, + Name: rrValue.GetResource().GetName(), + Value: rrValue.GetValue(), + }).FQN() + } + + ephemeralID := fmt.Sprintf("rrv-%d", idx) + resourceFQNMap[ephemeralID] = resourceValueFQN + resources = append(resources, &authzV2.Resource{ + EphemeralId: ephemeralID, + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: resourceValueFQN, + }, + }) + } + + req := &authzV2.GetDecisionMultiResourceRequest{ + EntityIdentifier: &authzV2.EntityIdentifier{ + Identifier: &authzV2.EntityIdentifier_EntityChain{EntityChain: entityChain}, + }, + Action: &policy.Action{Name: strings.ToLower(action)}, + Resources: resources, + FulfillableObligationFqns: getAllObligationsFromScenario(scenarioContext), + } + + resp, err := scenarioContext.SDK.AuthorizationV2.GetDecisionMultiResource(ctx, req) + scenarioContext.SetError(err) + if err != nil { + return ctx, err + } + + scenarioContext.RecordObject(multiDecisionResponseKey, resp) + scenarioContext.RecordObject(decisionResponse, resp) + scenarioContext.RecordObject("resourceFQNMap", resourceFQNMap) + return ctx, nil +} + +func (s *RegisteredResourcesStepDefinitions) theMultiResourceDecisionShouldBe(ctx context.Context, expectedDecision string) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + resp, ok := scenarioContext.GetObject(multiDecisionResponseKey).(*authzV2.GetDecisionMultiResourceResponse) + if !ok || resp == nil { + return ctx, errors.New("multi-decision response not found or invalid") + } + + allPermitted := resp.GetAllPermitted() + if allPermitted == nil { + return ctx, errors.New("multi-decision missing all_permitted flag") + } + + expected := strings.EqualFold(expectedDecision, "PERMIT") + if allPermitted.GetValue() != expected { + return ctx, fmt.Errorf("unexpected multi-decision result: got %v expected %v", allPermitted.GetValue(), expected) + } + + return ctx, nil +} + +func RegisterRegisteredResourcesStepDefinitions(ctx *godog.ScenarioContext) { + stepDefinitions := &RegisteredResourcesStepDefinitions{} + ctx.Step(`^I send a request to create a registered resource with:$`, stepDefinitions.iSendARequestToCreateARegisteredResourceWith) + ctx.Step(`^I send a request to create a registered resource value with:$`, stepDefinitions.iSendARequestToCreateARegisteredResourceValueWith) + ctx.Step(`^I send a decision request for entity chain "([^"]*)" for "([^"]*)" action on registered resource value "([^"]*)"$`, stepDefinitions.iSendADecisionRequestForEntityChainForActionOnRegisteredResourceValue) + ctx.Step(`^I send a multi-resource decision request for entity chain "([^"]*)" for "([^"]*)" action on registered resource values "([^"]*)"$`, stepDefinitions.iSendAMultiResourceDecisionRequestForEntityChainForActionOnRegisteredResourceValues) + ctx.Step(`^the multi-resource decision should be "([^"]*)"$`, stepDefinitions.theMultiResourceDecisionShouldBe) +} diff --git a/tests-bdd/cukes/steps_subjectmappings.go b/tests-bdd/cukes/steps_subjectmappings.go index 7f2c24fe6a..f79eefc650 100644 --- a/tests-bdd/cukes/steps_subjectmappings.go +++ b/tests-bdd/cukes/steps_subjectmappings.go @@ -29,6 +29,14 @@ func (s *SubjectMappingsStepDefinitions) iSendARequestToCreateSubjectMapping(ctx cellIndexMap[ci] = c.Value } else { switch cellIndexMap[ci] { + case "namespace_id": + nsID, ok := scenarioContext.GetObject(strings.TrimSpace(c.Value)).(string) + if !ok { + return ctx, fmt.Errorf("unable to get namespace id for %s", c.Value) + } + subjectMappingRequest.NamespaceId = nsID + case "namespace_fqn": + subjectMappingRequest.NamespaceFqn = strings.TrimSpace(c.Value) case "attribute_value": av, err := scenarioContext.GetAttributeValue(ctx, strings.TrimSpace(c.Value)) if err != nil { @@ -72,6 +80,14 @@ func (s *SubjectMappingsStepDefinitions) iSendARequestToCreateSubjectMapping(ctx } func (s *SubjectMappingsStepDefinitions) iSendARequestToCreateSubjectConditionSet(ctx context.Context, referenceID string, subjectSetIDs string) (context.Context, error) { + return s.createSubjectConditionSet(ctx, referenceID, subjectSetIDs, "") +} + +func (s *SubjectMappingsStepDefinitions) iSendARequestToCreateSubjectConditionSetInNamespace(ctx context.Context, referenceID string, namespaceRef string, subjectSetIDs string) (context.Context, error) { + return s.createSubjectConditionSet(ctx, referenceID, subjectSetIDs, namespaceRef) +} + +func (s *SubjectMappingsStepDefinitions) createSubjectConditionSet(ctx context.Context, referenceID string, subjectSetIDs string, namespaceRef string) (context.Context, error) { scenarioContext := GetPlatformScenarioContext(ctx) scenarioContext.ClearError() subjectSets := []*policy.SubjectSet{} @@ -82,11 +98,21 @@ func (s *SubjectMappingsStepDefinitions) iSendARequestToCreateSubjectConditionSe } subjectSets = append(subjectSets, ss) } - resp, respErr := scenarioContext.SDK.SubjectMapping.CreateSubjectConditionSet(ctx, &subjectmapping.CreateSubjectConditionSetRequest{ + req := &subjectmapping.CreateSubjectConditionSetRequest{ SubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ SubjectSets: subjectSets, }, - }) + } + + if namespaceRef != "" { + nsID, ok := scenarioContext.GetObject(strings.TrimSpace(namespaceRef)).(string) + if !ok { + return ctx, fmt.Errorf("unable to get namespace id for %s", namespaceRef) + } + req.NamespaceId = nsID + } + + resp, respErr := scenarioContext.SDK.SubjectMapping.CreateSubjectConditionSet(ctx, req) if resp != nil { scenarioContext.RecordObject(referenceID, resp.GetSubjectConditionSet()) } @@ -163,5 +189,6 @@ func RegisterSubjectMappingsStepsDefinitions(ctx *godog.ScenarioContext) { ctx.Step(`a condition group referenced as "([^"]*)" with an "([^"]*)" operator with conditions:$`, subjectMappingStepDefinitions.aConditionGroup) ctx.Step(`^a subject set referenced as "([^"]*)" containing the condition groups "([^"]*)"$`, subjectMappingStepDefinitions.aSubjectSet) ctx.Step(`^I send a request to create a subject condition set referenced as "([^"]*)" containing subject sets "([^"]*)"$`, subjectMappingStepDefinitions.iSendARequestToCreateSubjectConditionSet) + ctx.Step(`^I send a request to create a subject condition set referenced as "([^"]*)" in namespace "([^"]*)" containing subject sets "([^"]*)"$`, subjectMappingStepDefinitions.iSendARequestToCreateSubjectConditionSetInNamespace) ctx.Step(`^I send a request to create a subject mapping with:$`, subjectMappingStepDefinitions.iSendARequestToCreateSubjectMapping) } diff --git a/tests-bdd/cukes/utils/utils_genKeys.go b/tests-bdd/cukes/utils/utils_genKeys.go index 345092d8bf..3ad0b57599 100644 --- a/tests-bdd/cukes/utils/utils_genKeys.go +++ b/tests-bdd/cukes/utils/utils_genKeys.go @@ -1,6 +1,7 @@ -package utils +package testhelpers import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -14,6 +15,8 @@ import ( "os/exec" "path" "time" + + "github.com/opentdf/platform/lib/ocrypto" ) const ( @@ -22,7 +25,7 @@ const ( ) // GenerateTempKeys creates a set of RSA and EC certificates and cosign keys in the specified outputPath. -func GenerateTempKeys(outputPath string) { +func GenerateTempKeys(ctx context.Context, outputPath string) { // Create directory if it doesn't exist err := os.MkdirAll(outputPath, 0o755) if err != nil { @@ -32,7 +35,8 @@ func GenerateTempKeys(outputPath string) { generateRSACertificate(outputPath) generateECParameters(outputPath) generateECCertificate(outputPath) - generateJavaKeystore(outputPath) + generateHybridKeys(outputPath) + generateJavaKeystore(ctx, outputPath) } // generateRSACertificate creates a self-signed RSA certificate and private key. @@ -180,17 +184,17 @@ func generateECCertificate(outputPath string) { } // generateJavaKeystore creates a Java keystore (ca.jks) from the RSA certificate. -func generateJavaKeystore(outputPath string) { +func generateJavaKeystore(ctx context.Context, outputPath string) { // Use keytool to create a Java keystore from the RSA certificate certPath := path.Join(outputPath, "kas-cert.pem") keystorePath := path.Join(outputPath, "ca.jks") // Create keystore using keytool command - createJavaKeystore(certPath, keystorePath) + createJavaKeystore(ctx, certPath, keystorePath) } -func createJavaKeystore(certPath, keystorePath string) { - cmd := exec.Command("keytool", "-import", "-trustcacerts", "-noprompt", +func createJavaKeystore(ctx context.Context, certPath, keystorePath string) { + cmd := exec.CommandContext(ctx, "keytool", "-import", "-trustcacerts", "-noprompt", "-alias", "ca", "-file", certPath, "-keystore", keystorePath, @@ -202,3 +206,86 @@ func createJavaKeystore(certPath, keystorePath string) { log.Printf("Java keystore generated successfully: %s", keystorePath) } + +// generateHybridKeys creates X-Wing, P256+ML-KEM-768, and P384+ML-KEM-1024 key pairs. +func generateHybridKeys(outputPath string) { + specs := []struct { + name string + newKeyPair func() (priv, pub string, err error) + privateOut string + publicOut string + }{ + {"X-Wing", generateXWingKeyPair, "kas-xwing-private.pem", "kas-xwing-public.pem"}, + {"P256+ML-KEM-768", generateP256MLKEM768KeyPair, "kas-p256mlkem768-private.pem", "kas-p256mlkem768-public.pem"}, + {"P384+ML-KEM-1024", generateP384MLKEM1024KeyPair, "kas-p384mlkem1024-private.pem", "kas-p384mlkem1024-public.pem"}, + } + + for _, s := range specs { + priv, pub, err := s.newKeyPair() + if err != nil { + log.Fatalf("Failed to generate %s key pair: %v", s.name, err) + } + + privPath := path.Join(outputPath, s.privateOut) + pubPath := path.Join(outputPath, s.publicOut) + + if err := os.WriteFile(privPath, []byte(priv), 0o600); err != nil { + log.Fatalf("Failed to write %s: %v", privPath, err) + } + if err := os.WriteFile(pubPath, []byte(pub), 0o600); err != nil { + log.Fatalf("Failed to write %s: %v", pubPath, err) + } + + log.Printf("%s key pair generated successfully:", s.name) + log.Printf(" - Private: %s", privPath) + log.Printf(" - Public: %s", pubPath) + } +} + +func generateXWingKeyPair() (string, string, error) { + kp, err := ocrypto.NewXWingKeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} + +func generateP256MLKEM768KeyPair() (string, string, error) { + kp, err := ocrypto.NewP256MLKEM768KeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} + +func generateP384MLKEM1024KeyPair() (string, string, error) { + kp, err := ocrypto.NewP384MLKEM1024KeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} diff --git a/tests-bdd/features/encrypt-decrypt.feature b/tests-bdd/features/encrypt-decrypt.feature new file mode 100644 index 0000000000..12d73cbb22 --- /dev/null +++ b/tests-bdd/features/encrypt-decrypt.feature @@ -0,0 +1,52 @@ +@encrypt-decrypt @stateless +Feature: Encrypt and decrypt with ABAC + Demonstrates OpenTDF's attribute-based access control (ABAC) using the + default demo policy loaded by `a default local platform`: + + demo.com/attr/department rule: ANY_OF + engineering, finance, hr + + demo.com/attr/classification rule: HIERARCHY (public < internal < confidential < secret) + + Each scenario encrypts a TDF bound to one or more attribute values, + then has a user attempt to decrypt it. Decryption succeeds when the + user's Keycloak claims satisfy every attribute on the TDF per its rule; + otherwise the platform returns access-denied. + + Background: + Given a user exists with username "alice" and email "alice@demo.com" and the following attributes: + | name | value | + | department | ["engineering"] | + | classification | ["confidential"] | + And a user exists with username "bob" and email "bob@demo.com" and the following attributes: + | name | value | + | department | ["finance"] | + | classification | ["public"] | + And a default local platform + And a user token for "alice" stored as "alice_tok" + And a user token for "bob" stored as "bob_tok" + + Scenario: ANY_OF allow — engineering user decrypts engineering-bound TDF + When I encrypt plaintext "hello engineering" with attributes "https://demo.com/attr/department/value/engineering" stored as "tdf1" + And using token "alice_tok", decrypt "tdf1" stored as "plain1" + Then the decryption stored as "plain1" should succeed with plaintext "hello engineering" + + Scenario: ANY_OF deny — finance user cannot decrypt engineering-bound TDF + When I encrypt plaintext "hello engineering" with attributes "https://demo.com/attr/department/value/engineering" stored as "tdf2" + And using token "bob_tok", decrypt "tdf2" stored as "plain2" + Then the decryption stored as "plain2" should be denied + + Scenario: HIERARCHY allow — confidential clearance decrypts internal TDF + When I encrypt plaintext "internal memo" with attributes "https://demo.com/attr/classification/value/internal" stored as "tdf3" + And using token "alice_tok", decrypt "tdf3" stored as "plain3" + Then the decryption stored as "plain3" should succeed with plaintext "internal memo" + + Scenario: HIERARCHY deny — public clearance cannot decrypt confidential TDF + When I encrypt plaintext "confidential memo" with attributes "https://demo.com/attr/classification/value/confidential" stored as "tdf4" + And using token "bob_tok", decrypt "tdf4" stored as "plain4" + Then the decryption stored as "plain4" should be denied + + Scenario: Combined attributes — user qualifies for both department and classification + When I encrypt plaintext "eng+internal" with attributes "https://demo.com/attr/department/value/engineering,https://demo.com/attr/classification/value/internal" stored as "tdf5" + And using token "alice_tok", decrypt "tdf5" stored as "plain5" + Then the decryption stored as "plain5" should succeed with plaintext "eng+internal" diff --git a/tests-bdd/features/namespaced-decisioning.feature b/tests-bdd/features/namespaced-decisioning.feature new file mode 100644 index 0000000000..7f3b0fd414 --- /dev/null +++ b/tests-bdd/features/namespaced-decisioning.feature @@ -0,0 +1,206 @@ +@authorization @namespaced-decisioning +Feature: Namespaced Policy Decisioning (name-only action requests) + Validate strict namespaced decisioning behavior for action-name requests + using both standard and custom actions. + + Background: + Given a user exists with username "alice" and email "alice@example.com" and the following attributes: + | name | value | + | department | ["eng"] | + And a local platform with platform template "cukes/resources/platform.namespaced_policy.template" and keycloak template "cukes/resources/keycloak_base.template" + And I submit a request to create a namespace with name "ns-one.example.com" and reference id "ns1" + And I submit a request to create a namespace with name "ns-two.example.com" and reference id "ns2" + And I send a request to create an attribute with: + | namespace_id | name | rule | values | + | ns1 | department | anyOf | eng | + | ns2 | department | anyOf | eng | + Then the response should be successful + And a condition group referenced as "cg_eng_department" with an "or" operator with conditions: + | selector_value | operator | values | + | .attributes.department[] | in | eng | + And a subject set referenced as "ss_eng_department" containing the condition groups "cg_eng_department" + And I send a request to create a subject condition set referenced as "scs_department_eng_ns1" in namespace "ns1" containing subject sets "ss_eng_department" + And I send a request to create a subject condition set referenced as "scs_department_eng_ns2" in namespace "ns2" containing subject sets "ss_eng_department" + And there is a "user_name" subject entity with value "alice" and referenced as "alice" + + Scenario: Standard action name permits when entitled in resource namespace + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_ns1_read_eng | ns1 | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_ns1 | read | | + Then the response should be successful + When I send a decision request for entity chain "alice" for "read" action on resource "https://ns-one.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "PERMIT" decision response + + Scenario: Standard action name denies when subject mapping is unnamespaced + And I send a request to create a subject condition set referenced as "scs_department_eng_unns" containing subject sets "ss_eng_department" + And I send a request to create a subject mapping with: + | reference_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_unns_read_eng | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_unns | read | | + Then the response should be successful + When I send a decision request for entity chain "alice" for "read" action on resource "https://ns-one.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "DENY" decision response + + Scenario: Standard action name denies when entitled only in different namespace + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_ns2_read_eng | ns2 | https://ns-two.example.com/attr/department/value/eng | scs_department_eng_ns2 | read | | + Then the response should be successful + When I send a decision request for entity chain "alice" for "read" action on resource "https://ns-one.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "DENY" decision response + + Scenario: Custom action name permits when entitled in resource namespace + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_ns1_export_eng | ns1 | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_ns1 | | export | + Then the response should be successful + When I send a decision request for entity chain "alice" for "custom_action_export" action on resource "https://ns-one.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "PERMIT" decision response + + Scenario: Custom action name denies when subject mapping is unnamespaced + And I send a request to create a subject condition set referenced as "scs_department_eng_unns" containing subject sets "ss_eng_department" + And I send a request to create a subject mapping with: + | reference_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_unns_export_eng | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_unns | | export | + Then the response should be successful + When I send a decision request for entity chain "alice" for "custom_action_export" action on resource "https://ns-one.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "DENY" decision response + + Scenario: Custom action name denies when entitled only in different namespace + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_ns2_export_eng | ns2 | https://ns-two.example.com/attr/department/value/eng | scs_department_eng_ns2 | | export | + Then the response should be successful + When I send a decision request for entity chain "alice" for "custom_action_export" action on resource "https://ns-one.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "DENY" decision response + + Scenario: Standard action AND behavior across mixed namespaces + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_ns1_read_eng | ns1 | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_ns1 | read | | + Then the response should be successful + When I send a decision request for entity chain "alice" for "read" action on resource "https://ns-one.example.com/attr/department/value/eng,https://ns-two.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "DENY" decision response + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_ns2_read_eng_2 | ns2 | https://ns-two.example.com/attr/department/value/eng | scs_department_eng_ns2 | read | | + Then the response should be successful + When I send a decision request for entity chain "alice" for "read" action on resource "https://ns-one.example.com/attr/department/value/eng,https://ns-two.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "PERMIT" decision response + + Scenario: Custom action AND behavior across mixed namespaces + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_ns1_export_eng | ns1 | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_ns1 | | export | + Then the response should be successful + When I send a decision request for entity chain "alice" for "custom_action_export" action on resource "https://ns-one.example.com/attr/department/value/eng,https://ns-two.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "DENY" decision response + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_ns2_export_eng_2 | ns2 | https://ns-two.example.com/attr/department/value/eng | scs_department_eng_ns2 | | export | + Then the response should be successful + When I send a decision request for entity chain "alice" for "custom_action_export" action on resource "https://ns-one.example.com/attr/department/value/eng,https://ns-two.example.com/attr/department/value/eng" + Then the response should be successful + And I should get a "PERMIT" decision response + + Scenario: Registered resource value permits when action is entitled in same namespace + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_rr_ns1_read | ns1 | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_ns1 | read | | + Then the response should be successful + And I send a request to create a registered resource with: + | reference_id | namespace_id | name | + | rr_ns1 | ns1 | app-config | + Then the response should be successful + And I send a request to create a registered resource value with: + | reference_id | resource_ref | value | action_attribute_values | + | rrv_ns1 | rr_ns1 | prod-config | read=>https://ns-one.example.com/attr/department/value/eng | + Then the response should be successful + When I send a decision request for entity chain "alice" for "read" action on registered resource value "rrv_ns1" + Then the response should be successful + And I should get a "PERMIT" decision response + + Scenario: Registered resource value denies when action is only entitled in different namespace + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_rr_ns2_read | ns2 | https://ns-two.example.com/attr/department/value/eng | scs_department_eng_ns2 | read | | + Then the response should be successful + And I send a request to create a registered resource with: + | reference_id | namespace_id | name | + | rr_ns1 | ns1 | app-config | + Then the response should be successful + And I send a request to create a registered resource value with: + | reference_id | resource_ref | value | action_attribute_values | + | rrv_ns1 | rr_ns1 | prod-config | read=>https://ns-one.example.com/attr/department/value/eng | + Then the response should be successful + When I send a decision request for entity chain "alice" for "read" action on registered resource value "rrv_ns1" + Then the response should be successful + And I should get a "DENY" decision response + + Scenario: Registered resource value permits for custom action in same namespace + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_rr_ns1_export | ns1 | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_ns1 | | export | + Then the response should be successful + And I send a request to create a registered resource with: + | reference_id | namespace_id | name | + | rr_ns1 | ns1 | app-config | + Then the response should be successful + And I send a request to create a registered resource value with: + | reference_id | resource_ref | value | action_attribute_values | + | rrv_ns1 | rr_ns1 | prod-config | custom_action_export=>https://ns-one.example.com/attr/department/value/eng | + Then the response should be successful + When I send a decision request for entity chain "alice" for "custom_action_export" action on registered resource value "rrv_ns1" + Then the response should be successful + And I should get a "PERMIT" decision response + + Scenario: Registered resource value denies for custom action entitled only in different namespace + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_rr_ns2_export | ns2 | https://ns-two.example.com/attr/department/value/eng | scs_department_eng_ns2 | | export | + Then the response should be successful + And I send a request to create a registered resource with: + | reference_id | namespace_id | name | + | rr_ns1 | ns1 | app-config | + Then the response should be successful + And I send a request to create a registered resource value with: + | reference_id | resource_ref | value | action_attribute_values | + | rrv_ns1 | rr_ns1 | prod-config | custom_action_export=>https://ns-one.example.com/attr/department/value/eng | + Then the response should be successful + When I send a decision request for entity chain "alice" for "custom_action_export" action on registered resource value "rrv_ns1" + Then the response should be successful + And I should get a "DENY" decision response + + Scenario: Registered resources mixed-namespace decision is fail-closed (AND) + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_rr_mix_ns1_read | ns1 | https://ns-one.example.com/attr/department/value/eng | scs_department_eng_ns1 | read | | + Then the response should be successful + And I send a request to create a registered resource with: + | reference_id | namespace_id | name | + | rr_mix_ns1 | ns1 | app-config-a | + | rr_mix_ns2 | ns2 | app-config-b | + Then the response should be successful + And I send a request to create a registered resource value with: + | reference_id | resource_ref | value | action_attribute_values | + | rrv_mix_ns1 | rr_mix_ns1 | prod-config-a | read=>https://ns-one.example.com/attr/department/value/eng | + | rrv_mix_ns2 | rr_mix_ns2 | prod-config-b | read=>https://ns-two.example.com/attr/department/value/eng | + Then the response should be successful + When I send a multi-resource decision request for entity chain "alice" for "read" action on registered resource values "rrv_mix_ns1,rrv_mix_ns2" + Then the response should be successful + And the multi-resource decision should be "DENY" + And I send a request to create a subject mapping with: + | reference_id | namespace_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_rr_mix_ns2_read | ns2 | https://ns-two.example.com/attr/department/value/eng | scs_department_eng_ns2 | read | | + Then the response should be successful + When I send a multi-resource decision request for entity chain "alice" for "read" action on registered resource values "rrv_mix_ns1,rrv_mix_ns2" + Then the response should be successful + And the multi-resource decision should be "PERMIT" diff --git a/tests-bdd/go.mod b/tests-bdd/go.mod index 96a9f30aa7..620b9828c2 100644 --- a/tests-bdd/go.mod +++ b/tests-bdd/go.mod @@ -1,27 +1,30 @@ module github.com/opentdf/platform/tests-bdd -go 1.24.11 +go 1.25.5 require ( - github.com/cucumber/godog v0.15.0 + github.com/cucumber/godog v0.15.1 github.com/google/uuid v1.6.0 - github.com/jackc/pgx/v5 v5.7.5 + github.com/jackc/pgx/v5 v5.9.2 github.com/opentdf/platform/lib/fixtures v0.3.0 - github.com/opentdf/platform/protocol/go v0.13.0 - github.com/opentdf/platform/sdk v0.5.0 + github.com/opentdf/platform/lib/identifier v0.0.2 + github.com/opentdf/platform/lib/ocrypto v0.10.0 + github.com/opentdf/platform/protocol/go v0.30.0 + github.com/opentdf/platform/sdk v0.17.0 github.com/opentdf/platform/service v0.7.2 github.com/spf13/pflag v1.0.10 - github.com/testcontainers/testcontainers-go v0.39.0 - github.com/testcontainers/testcontainers-go/modules/compose v0.39.1 - google.golang.org/protobuf v1.36.9 + github.com/testcontainers/testcontainers-go v0.42.0 + github.com/testcontainers/testcontainers-go/modules/compose v0.42.0 + golang.org/x/oauth2 v0.36.0 + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 ) require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1 // indirect buf.build/go/protovalidate v0.13.1 // indirect - cel.dev/expr v0.24.0 // indirect - connectrpc.com/connect v1.18.1 // indirect + cel.dev/expr v0.25.1 // indirect + connectrpc.com/connect v1.20.0 // indirect connectrpc.com/grpchealth v1.4.0 // indirect connectrpc.com/grpcreflect v1.3.0 // indirect connectrpc.com/validate v0.3.0 // indirect @@ -35,20 +38,6 @@ require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect - github.com/aws/aws-sdk-go-v2/config v1.29.16 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.69 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 // indirect - github.com/aws/smithy-go v1.22.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect @@ -56,18 +45,18 @@ require ( github.com/casbin/casbin/v2 v2.108.0 // indirect github.com/casbin/govaluate v1.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/compose-spec/compose-go/v2 v2.9.1 // indirect + github.com/compose-spec/compose-go/v2 v2.10.2 // indirect github.com/containerd/console v1.0.5 // indirect - github.com/containerd/containerd/api v1.9.0 // indirect - github.com/containerd/containerd/v2 v2.1.5 // indirect + github.com/containerd/containerd/api v1.10.0 // indirect + github.com/containerd/containerd/v2 v2.2.4 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v1.0.0-rc.1 // indirect - github.com/containerd/ttrpc v1.2.7 // indirect + github.com/containerd/platforms v1.0.0-rc.4 // indirect + github.com/containerd/ttrpc v1.2.8 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/creasty/defaults v1.8.0 // indirect @@ -77,68 +66,56 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/buildx v0.29.1 // indirect - github.com/docker/cli v28.5.1+incompatible // indirect - github.com/docker/cli-docs-tool v0.10.0 // indirect - github.com/docker/compose/v2 v2.40.2 // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v28.5.1+incompatible // indirect - github.com/docker/docker-credential-helpers v0.9.3 // indirect - github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect + github.com/docker/buildx v0.33.0 // indirect + github.com/docker/cli v29.4.0+incompatible // indirect + github.com/docker/compose/v5 v5.1.2 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.6.0 // indirect - github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ebitengine/purego v0.8.4 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect github.com/eko/gocache/lib/v4 v4.2.0 // indirect github.com/eko/gocache/store/ristretto/v4 v4.2.2 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsevents v0.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/go-chi/cors v1.2.1 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-resty/resty/v2 v2.16.5 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/gofrs/flock v0.12.1 // indirect + github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/uuid v4.3.1+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/mock v1.7.0-rc.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/cel-go v0.25.0 // indirect - github.com/google/certificate-transparency-go v1.3.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gowebpki/jcs v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-memdb v1.3.4 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/go-version v1.9.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/in-toto/in-toto-golang v0.9.0 // indirect + github.com/in-toto/attestation v1.1.2 // indirect + github.com/in-toto/in-toto-golang v0.11.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect @@ -146,150 +123,135 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.0.0 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect + github.com/lestrrat-go/jwx/v3 v3.0.11 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect github.com/magiconair/properties v1.8.10 // indirect - github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/miekg/dns v1.1.58 // indirect - github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/moby/buildkit v0.25.1 // indirect + github.com/moby/buildkit v0.29.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/go-archive v0.2.0 // indirect github.com/moby/locker v1.0.1 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect - github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect - github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/symlink v0.3.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect + github.com/morikuni/aec v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/open-policy-agent/opa v1.5.1 // indirect + github.com/open-policy-agent/opa v1.10.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opentdf/platform/lib/flattening v0.1.3 // indirect - github.com/opentdf/platform/lib/identifier v0.0.2 // indirect - github.com/opentdf/platform/lib/ocrypto v0.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/pressly/goose/v3 v3.24.3 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.63.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect - github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/ksuid v1.0.4 // indirect - github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect - github.com/shirou/gopsutil/v4 v4.25.6 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/sigstore/sigstore v1.10.4 // indirect + github.com/sigstore/sigstore-go v1.1.4 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/cobra v1.10.1 // indirect - github.com/spf13/viper v1.20.1 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/viper v1.21.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tchap/go-patricia/v2 v2.3.2 // indirect - github.com/theupdateframework/notary v0.7.0 // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect - github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f // indirect + github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect - github.com/vektah/gqlparser/v2 v2.5.26 // indirect - github.com/x448/float16 v0.8.4 // indirect + github.com/valyala/fastjson v1.6.4 // indirect + github.com/vektah/gqlparser/v2 v2.5.30 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zclconf/go-cty v1.17.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - go.opentelemetry.io/proto/otlp v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.12.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.2 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.81.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 // indirect - k8s.io/apimachinery v0.33.1 // indirect - k8s.io/client-go v0.33.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect - tags.cncf.io/container-device-interface v1.0.1 // indirect + gotest.tools/v3 v3.5.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect + tags.cncf.io/container-device-interface v1.1.0 // indirect ) diff --git a/tests-bdd/go.sum b/tests-bdd/go.sum index 44929f37fc..151372894b 100644 --- a/tests-bdd/go.sum +++ b/tests-bdd/go.sum @@ -2,25 +2,24 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-2025061310500 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.13.1 h1:6loHDTWdY/1qmqmt1MijBIKeN4T9Eajrqb9isT1W1s8= buf.build/go/protovalidate v0.13.1/go.mod h1:C/QcOn/CjXRn5udUwYBiLs8y1TGy7RS+GOSKqjS77aU= -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= -connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= connectrpc.com/grpchealth v1.4.0 h1:MJC96JLelARPgZTiRF9KRfY/2N9OcoQvF2EWX07v2IE= connectrpc.com/grpchealth v1.4.0/go.mod h1:WhW6m1EzTmq3Ky1FE8EfkIpSDc6TfUx2M2KqZO3ts/Q= connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= connectrpc.com/validate v0.3.0 h1:eMPASBQM+ztVzuLSXddB61zwJKzvWWZ6RLdIwTgh9Wo= connectrpc.com/validate v0.3.0/go.mod h1:QLGN/m+oDeI4zaDAANK1L1G5K4i8gg6CUUwyl3HAG4A= +cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= +cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= @@ -29,103 +28,63 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= -github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= +github.com/Microsoft/hcsshim v0.14.1 h1:CMuB3fqQVfPdhyXhUqYdUmPUIOhJkmghCx3dJet8Cqs= +github.com/Microsoft/hcsshim v0.14.1/go.mod h1:VnzvPLyWUhxiPVsJ31P6XadxCcTogTguBFDy/1GR/OM= github.com/Nerzal/gocloak/v13 v13.9.0 h1:YWsJsdM5b0yhM2Ba3MLydiOlujkBry4TtdzfIzSVZhw= github.com/Nerzal/gocloak/v13 v13.9.0/go.mod h1:YYuDcXZ7K2zKECyVP7pPqjKxx2AzYSpKDj8d6GuyM10= -github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ= -github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30= +github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= -github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E= -github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A= -github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178= -github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= -github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= -github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= -github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= -github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc= -github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= -github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/bytecodealliance/wasmtime-go/v37 v37.0.0 h1:DPjdn2V3JhXHMoZ2ymRqGK+y1bDyr9wgpyYCvhjMky8= +github.com/bytecodealliance/wasmtime-go/v37 v37.0.0/go.mod h1:Pf1l2JCTUFMnOqDIwkjzx1qfVJ09xbaXETKgRVE4jZ0= github.com/casbin/casbin/v2 v2.108.0 h1:aMc3I81wfLpQe/uzMdElB1OBhEmPZoWMPb2nfEaKygY= github.com/casbin/casbin/v2 v2.108.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= -github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs= -github.com/compose-spec/compose-go/v2 v2.9.1/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= -github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= -github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/compose-spec/compose-go/v2 v2.10.2 h1:USa1NUbDcl/cjb8T9iwnuFsnO79H+2ho2L5SjFKz3uI= +github.com/compose-spec/compose-go/v2 v2.10.2/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= +github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= +github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= -github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.5 h1:pWSmPxUszaLZKQPvOx27iD4iH+aM6o0BoN9+hg77cro= -github.com/containerd/containerd/v2 v2.1.5/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= +github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= +github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= +github.com/containerd/containerd/v2 v2.2.4 h1:8x2UdXqww7NYqGNabQ7i1nAgB5LegzjC9KQzO/900iA= +github.com/containerd/containerd/v2 v2.2.4/go.mod h1:YBcTO8D9149QY9zNmUjy04Mhuc4DlrZQ8FIOwKZEM7o= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -136,44 +95,46 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg= -github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= -github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= -github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/nydus-snapshotter v0.15.13 h1:z9yCiTPMxVBIZlHxOPinZXhly2MdcIqxk9VXPlHIOJY= +github.com/containerd/nydus-snapshotter v0.15.13/go.mod h1:t95dwCb4I0RE4n1iOk0sJCWosNoACA8daOXmU5A2VHI= +github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= +github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= -github.com/containerd/stargz-snapshotter v0.16.3 h1:zbQMm8dRuPHEOD4OqAYGajJJUwCeUzt4j7w9Iaw58u4= -github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= -github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= -github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= -github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= +github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y= +github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= -github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo= -github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= +github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI= +github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= +github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= -github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= +github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= +github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= @@ -182,51 +143,42 @@ github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa5 github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/buildx v0.29.1 h1:58hxM5Z4mnNje3G5NKfULT9xCr8ooM8XFtlfUK9bKaA= -github.com/docker/buildx v0.29.1/go.mod h1:J4EFv6oxlPiV1MjO0VyJx2u5tLM7ImDEl9zyB8d4wPI= -github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= -github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU= -github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U= -github.com/docker/compose/v2 v2.40.2 h1:h2bDBJkOuqmj93XvT2oI0ArPQonE0lGtWiILXdiXvbA= -github.com/docker/compose/v2 v2.40.2/go.mod h1:CbSJpKGw20LInVsPjglZ8z7Squ3OBQOD7Ux5nkjGfIU= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/buildx v0.33.0 h1:xuZeuQe/C/2tvLDgiIA6+Ynq3FFWSfsGNWIHM3q1hD8= +github.com/docker/buildx v0.33.0/go.mod h1:7JVma62htERKE5iy5YD1q64PKiAHUzXuhSBd4oq3I74= +github.com/docker/cli v29.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM= +github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/compose/v5 v5.1.2 h1:HxtfGZA0DESw/+hvrNJDjM8VKmCond7OkmgVJCHku48= +github.com/docker/compose/v5 v5.1.2/go.mod h1:JJ2H+lRSugOH50/zxCbkBwN8jU91qDtRCp7gEHWZdgo= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= -github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= -github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= -github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= +github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw= github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M= github.com/eko/gocache/store/ristretto/v4 v4.2.2 h1:lXFzoZ5ck6Gy6ON7f5DHSkNt122qN7KoroCVgVwF7oo= github.com/eko/gocache/store/ristretto/v4 v4.2.2/go.mod h1:uIvBVJzqRepr5L0RsbkfQ2iYfbyos2fuji/s4yM+aUM= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -237,22 +189,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -261,12 +207,48 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= +github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= +github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -277,74 +259,50 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= -github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= -github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= -github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= +github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 h1:+epNPbD5EqgpEMm5wrl4Hqts3jZt8+kYaqUisuuIGTk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -360,15 +318,15 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= -github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= +github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= +github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= +github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= @@ -379,33 +337,16 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= -github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= -github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= -github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= -github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -423,54 +364,58 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= +github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI= +github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= +github.com/lestrrat-go/jwx/v3 v3.0.11 h1:yEeUGNUuNjcez/Voxvr7XPTYNraSQTENJgtVTfwvG/w= +github.com/lestrrat-go/jwx/v3 v3.0.11/go.mod h1:XSOAh2SiXm0QgRe3DulLZLyt+wUuEdFo81zuKTLcvgQ= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= -github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= -github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/moby/buildkit v0.25.1 h1:j7IlVkeNbEo+ZLoxdudYCHpmTsbwKvhgc/6UJ/mY/o8= -github.com/moby/buildkit v0.25.1/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc= +github.com/moby/buildkit v0.29.0 h1:wxLEFbCOJntEDjSNNN2YWd8zxltZxT5muDQ0LzpbtpU= +github.com/moby/buildkit v0.29.0/go.mod h1:Dmv2FeDe34t75QuzeU87rBoZpAAkcpT5zeu4hXzmASc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/policy-helpers v0.0.0-20260324161837-b7c0b994300b h1:lvBBM2ACrsG5/O1G1caEwlh0XeqA89IQK3xq0Sh/5NI= +github.com/moby/policy-helpers v0.0.0-20260324161837-b7c0b994300b/go.mod h1:Cbc1brDwYl1K294MmZB+6WhQR9Tr24hfhgSGND4UlL0= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= @@ -489,67 +434,45 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= -github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/open-policy-agent/opa v1.5.1 h1:LTxxBJusMVjfs67W4FoRcnMfXADIGFMzpqnfk6D08Cg= -github.com/open-policy-agent/opa v1.5.1/go.mod h1:bYbS7u+uhTI+cxHQIpzvr5hxX0hV7urWtY+38ZtjMgk= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/open-policy-agent/opa v1.10.1 h1:haIvxZSPky8HLjRrvQwWAjCPLg8JDFSZMbbG4yyUHgY= +github.com/open-policy-agent/opa v1.10.1/go.mod h1:7uPI3iRpOalJ0BhK6s1JALWPU9HvaV1XeBSSMZnr/PM= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= -github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= -github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= +github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= github.com/opentdf/platform/lib/fixtures v0.3.0 h1:pgEm9ynMDIFH7Wd/lre2tfvtura8LfaV3iA1w1m7D3Q= github.com/opentdf/platform/lib/fixtures v0.3.0/go.mod h1:K/r0REv5MYClnkuiCxCOT1LTXbuIDP0kqixlGmPQzXc= github.com/opentdf/platform/lib/flattening v0.1.3 h1:IuOm/wJVXNrzOV676Ticgr0wyBkL+lVjsoSfh+WSkNo= github.com/opentdf/platform/lib/flattening v0.1.3/go.mod h1:Gs/T+6FGZKk9OAdz2Jf1R8CTGeNRYrq1lZGDeYT3hrY= github.com/opentdf/platform/lib/identifier v0.0.2 h1:h3zR+IZ/4/X5UOYUp/aTA0OcX1QqsmgSiqjaM0uttFE= github.com/opentdf/platform/lib/identifier v0.0.2/go.mod h1:/tHnLlSVOq3qmbIYSvKrtuZchQfagenv4wG5twl4oRs= -github.com/opentdf/platform/lib/ocrypto v0.2.0 h1:hs7aQ9AfA6HPWkPJURkH/WdFkNFliq542axtMCy+ZX8= -github.com/opentdf/platform/lib/ocrypto v0.2.0/go.mod h1:8gv+GV3OqrMBhkmc+Iw34Q3JnYRFNt0V0oudFlAkRcQ= -github.com/opentdf/platform/protocol/go v0.13.0 h1:vrOOHyhYDPzJgNenz/1g0M5nWtkOYKkPggMNHKzeMcs= -github.com/opentdf/platform/protocol/go v0.13.0/go.mod h1:GRycoDGDxaz91sOvGZFWVEKJLluZFg2wM3NJmhucDHo= -github.com/opentdf/platform/sdk v0.5.0 h1:K4a8kWUtt5EJktFC55egicsp9537SI1+bmJPMIsYrKg= -github.com/opentdf/platform/sdk v0.5.0/go.mod h1:Qxwq9zjUVkBZs3xZUOPls5gdX4d0y2nsoZvJ93hyWAU= +github.com/opentdf/platform/lib/ocrypto v0.10.0 h1:7dn/z/1qH3p+gWCrfOoU7hj9XF/p5N+b2JBJuWF9aK0= +github.com/opentdf/platform/lib/ocrypto v0.10.0/go.mod h1:WASkoHreqgTFImB/gJW42VTdpi9AkgkmaW19y/fU+Ew= +github.com/opentdf/platform/protocol/go v0.30.0 h1:mdYTPpbnSdTPYO7XKixK7a4mDf6RpRaF7J4dZCw0Qq8= +github.com/opentdf/platform/protocol/go v0.30.0/go.mod h1:aiEmtYytvQtbiexZGAKYMe2mYTBPZqbNSh+OXmtrtkA= +github.com/opentdf/platform/sdk v0.17.0 h1:ApVa0/yB5M2wbqu3s0FxlXo17C1ojuul3tsQW1BGpyk= +github.com/opentdf/platform/sdk v0.17.0/go.mod h1:U4Ndr/Pr0c3rYik/JGfymT86I2InPIANX7WiFcvr2Pw= github.com/opentdf/platform/service v0.7.2 h1:GICI2R8yFxiQu10vsS/1c69vN3CLJpkchk9pCLv9vs4= github.com/opentdf/platform/service v0.7.2/go.mod h1:Fs2s0SqIWzlcL6fXxYkD9yLf7G/ChjSUi0g/OKtLQ+A= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU= +github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -561,30 +484,16 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM= github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E= -github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -593,68 +502,67 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= -github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= +github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14= +github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= -github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= +github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/rekor v1.5.0 h1:rL7SghHd5HLCtsCrxw0yQg+NczGvM75EjSPPWuGjaiQ= +github.com/sigstore/rekor v1.5.0/go.mod h1:D7JoVCUkxwQOpPDNYeu+CE8zeBC18Y5uDo6tF8s2rcQ= +github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= +github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= +github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= +github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= +github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= +github.com/sigstore/timestamp-authority/v2 v2.0.3 h1:sRyYNtdED/ttLCMdaYnwpf0zre1A9chvjTnCmWWxN8Y= +github.com/sigstore/timestamp-authority/v2 v2.0.3/go.mod h1:mDaHxkt3HmZYoIlwYj4QWo0RUr7VjYU52aVO5f5Qb3I= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= -github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= +github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -665,36 +573,41 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= -github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= -github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= -github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= -github.com/testcontainers/testcontainers-go/modules/compose v0.39.1 h1:/3kEZY3xH/ibgnUGAACmEcqVIqfXzqD1LdXnJqfsBrM= -github.com/testcontainers/testcontainers-go/modules/compose v0.39.1/go.mod h1:qxH8QmljpneFWkGJ7RzjPOnoyRN760OZdq3bE0aG/bg= -github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= -github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/compose v0.42.0 h1:+t1ZN31TD36cwxmeLqGwe7wIdvblBm0Z+vlj4SX8Mv0= +github.com/testcontainers/testcontainers-go/modules/compose v0.42.0/go.mod h1:CfMpouDHqNTCHC8CijEURU2ZotTV3QhH6pXd48s6ofk= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf/v2 v2.4.1 h1:K6ewW064rKZCPkRo1W/CTbTtm/+IB4+coG1iNURAGCw= +github.com/theupdateframework/go-tuf/v2 v2.4.1/go.mod h1:Nex2enPVYDFCklrnbTzl3OVwD7fgIAj0J5++z/rvCj8= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4= github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY= -github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI= -github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98= +github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f h1:Z4NEQ86qFl1mHuCu9gwcE+EYCwDKfXAYXZbdIXyxmEA= +github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98= github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE= github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= -github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= -github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= -github.com/vektah/gqlparser/v2 v2.5.26 h1:REqqFkO8+SOEgZHR/eHScjjVjGS8Nk3RMO/juiTobN4= -github.com/vektah/gqlparser/v2 v2.5.26/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= +github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= +github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -711,97 +624,83 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= -github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= +golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -810,68 +709,49 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= -gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= -gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -879,18 +759,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= -k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= @@ -899,14 +767,9 @@ modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= -tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY= +tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q= diff --git a/tests-bdd/platform_test.go b/tests-bdd/platform_test.go index 67c0dd51f8..7daf69313a 100644 --- a/tests-bdd/platform_test.go +++ b/tests-bdd/platform_test.go @@ -76,10 +76,27 @@ func runTests() int { Level: slog.LevelDebug, }, )) - slog.SetDefault(logger) + + platformLogOption, ok := os.LookupEnv("PLATFORM_LOG_HANDLER") + var platformIoWriter io.Writer + if ok && strings.ToLower(platformLogOption) == "console" { + platformIoWriter = os.Stdout + } else { + platformLogFile, err := os.Create(path.Join(projectDir, "cukes_platform_service.log")) + if err != nil { + logger.Error("failed to create platform service log file", slog.String("error", err.Error())) + return 1 + } + platformIoWriter = platformLogFile + } + platformLogger := slog.New(slog.NewTextHandler(platformIoWriter, + &slog.HandlerOptions{ + Level: slog.LevelDebug, + }, + )) opts.Paths = pflag.Args() - platformCukesContext := cukes.CreatePlatformCukesContext(logger, composeLogger) + platformCukesContext := cukes.CreatePlatformCukesContext(logger, composeLogger, platformLogger) opts.DefaultContext = cukes.NewPlatformCukesContext(*platformCukesContext) status := godog.TestSuite{ Name: "platform", @@ -91,7 +108,9 @@ func runTests() int { cukes.RegisterSmokeStepDefinitions(ctx, platformCukesContext) cukes.RegisterAuthorizationStepDefinitions(ctx) cukes.RegisterSubjectMappingsStepsDefinitions(ctx) + cukes.RegisterRegisteredResourcesStepDefinitions(ctx) cukes.RegisterObligationsStepDefinitions(ctx, platformCukesContext) + cukes.RegisterEncryptionStepDefinitions(ctx) platformCukesContext.InitializeScenario(ctx) }, Options: &opts,