From 5a29ab9f00dedb09d22ee25ae232194236543fcb Mon Sep 17 00:00:00 2001 From: Thobias Karlsson Date: Tue, 17 Mar 2026 18:58:27 +0100 Subject: [PATCH 1/3] feat: remove unused `status.signingKey` from Account CRD The status signing key struct has not been populated since an unknown version of nAuth. Since we are planning on supporting multiple signing keys, including optional scope, this single signing key struct is no longer needed. Removing it. Fixes: #163 Signed-off-by: Thobias Karlsson --- api/v1alpha1/account_types.go | 8 -------- api/v1alpha1/zz_generated.deepcopy.go | 18 ------------------ charts/nauth-crds/crds/nauth.io_accounts.yaml | 11 ----------- .../resources/crds/nauth.io_accounts.yaml | 11 ----------- 4 files changed, 48 deletions(-) diff --git a/api/v1alpha1/account_types.go b/api/v1alpha1/account_types.go index 7714eac..96b0488 100644 --- a/api/v1alpha1/account_types.go +++ b/api/v1alpha1/account_types.go @@ -82,17 +82,9 @@ type AccountStatus struct { // +optional ReconcileTimestamp metav1.Time `json:"reconcileTimestamp,omitempty"` // +optional - SigningKey KeyInfo `json:"signingKey"` - // +optional OperatorVersion string `json:"operatorVersion,omitempty"` } -type KeyInfo struct { - Name string `json:"name,omitempty"` - CreationDate metav1.Time `json:"creationDate,omitempty"` - ExpirationDate metav1.Time `json:"expirationDate,omitempty"` -} - // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b0a56e7..2f99d55 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -260,7 +260,6 @@ func (in *AccountStatus) DeepCopyInto(out *AccountStatus) { } } in.ReconcileTimestamp.DeepCopyInto(&out.ReconcileTimestamp) - in.SigningKey.DeepCopyInto(&out.SigningKey) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccountStatus. @@ -450,23 +449,6 @@ func (in *JetStreamLimits) DeepCopy() *JetStreamLimits { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *KeyInfo) DeepCopyInto(out *KeyInfo) { - *out = *in - in.CreationDate.DeepCopyInto(&out.CreationDate) - in.ExpirationDate.DeepCopyInto(&out.ExpirationDate) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyInfo. -func (in *KeyInfo) DeepCopy() *KeyInfo { - if in == nil { - return nil - } - out := new(KeyInfo) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NatsCluster) DeepCopyInto(out *NatsCluster) { *out = *in diff --git a/charts/nauth-crds/crds/nauth.io_accounts.yaml b/charts/nauth-crds/crds/nauth.io_accounts.yaml index 4b99f59..44586bb 100644 --- a/charts/nauth-crds/crds/nauth.io_accounts.yaml +++ b/charts/nauth-crds/crds/nauth.io_accounts.yaml @@ -491,17 +491,6 @@ spec: reconcileTimestamp: format: date-time type: string - signingKey: - properties: - creationDate: - format: date-time - type: string - expirationDate: - format: date-time - type: string - name: - type: string - type: object type: object type: object served: true diff --git a/charts/nauth/resources/crds/nauth.io_accounts.yaml b/charts/nauth/resources/crds/nauth.io_accounts.yaml index 4b99f59..44586bb 100644 --- a/charts/nauth/resources/crds/nauth.io_accounts.yaml +++ b/charts/nauth/resources/crds/nauth.io_accounts.yaml @@ -491,17 +491,6 @@ spec: reconcileTimestamp: format: date-time type: string - signingKey: - properties: - creationDate: - format: date-time - type: string - expirationDate: - format: date-time - type: string - name: - type: string - type: object type: object type: object served: true From 809fb1a6046f842f0008546533c3826f903deccf Mon Sep 17 00:00:00 2001 From: Thobias Karlsson Date: Tue, 17 Mar 2026 22:16:24 +0100 Subject: [PATCH 2/3] test: fix e2e kuttl test assertions The currently used kuttl version 0.15.0 does not support `resourceRefs` nor `assertAll` in `TestAssert` CRD, hence bumping to (fixed) v0.24.0. kuttl only supports _one_ `TestAssert` resource per assertion file, hence merging duplicates to ensure all assertions are being validated. Using mise to install kuttl in github action e2e test workflow to ensure same version is being used in workflow as when running `mise nauth:e2e-test` locally. Signed-off-by: Thobias Karlsson --- .github/workflows/test-e2e.yml | 16 ++++++++-------- mise.toml | 4 ++-- test/e2e/basic-test/01-assert-account.yaml | 6 ------ test/e2e/basic-test/02-assert-user.yaml | 6 ------ test/e2e/cluster-ref-test/02-assert.yaml | 4 ---- 5 files changed, 10 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index ccd3926..93dde12 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -38,16 +38,16 @@ jobs: with: go-version-file: go.mod - - name: Install KUTTL - run: | - curl -L https://github.com/kudobuilder/kuttl/releases/download/v0.15.0/kubectl-kuttl_0.15.0_linux_x86_64 -o /usr/local/bin/kubectl-kuttl - chmod +x /usr/local/bin/kubectl-kuttl + - name: Setup mise tools + uses: jdx/mise-action@v3 + with: + install: true + install_args: "kubectl-kuttl kind jq" + experimental: true - - name: Install the latest version of KIND + - name: Verify test tools run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 - chmod +x ./kind - sudo mv ./kind /usr/local/bin/kind + kubectl kuttl version kind version - name: Running E2E Tests diff --git a/mise.toml b/mise.toml index 0864ba4..81d8ac7 100644 --- a/mise.toml +++ b/mise.toml @@ -16,7 +16,7 @@ helm-docs = "latest" golangci-lint = "latest" "ubi:elastic/crd-ref-docs" = "latest" "go:github.com/nats-io/natscli/nats" = "latest" -kubectl-kuttl = "latest" +kubectl-kuttl = "0.24.0" jq = "latest" [settings] @@ -37,4 +37,4 @@ run = "kubectl kuttl test" silent = true alias = "kk9s" description = "Run K9s in the test environment (kind)" -run = "KUBECONFIG=kubeconfig k9s" \ No newline at end of file +run = "KUBECONFIG=kubeconfig k9s" diff --git a/test/e2e/basic-test/01-assert-account.yaml b/test/e2e/basic-test/01-assert-account.yaml index de73610..ea7e7f2 100644 --- a/test/e2e/basic-test/01-assert-account.yaml +++ b/test/e2e/basic-test/01-assert-account.yaml @@ -47,12 +47,6 @@ resourceRefs: assertAll: - celExpr: matches(example_account.metadata.labels["account.nauth.io/id"], "^A.{55}$") - celExpr: example_account.metadata.labels["account.nauth.io/signed-by"] == "OCRTCOTZAWYINN4U4XVNEM6TDJNOBLZQMGDIZ765WFA5ZLYMRB32HCG4" - - celExpr: matches(example_account.status.signingKey.name, "^.{56}$") - ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -timeout: 20 commands: - script: | set -eu diff --git a/test/e2e/basic-test/02-assert-user.yaml b/test/e2e/basic-test/02-assert-user.yaml index 729ff82..89c00b9 100644 --- a/test/e2e/basic-test/02-assert-user.yaml +++ b/test/e2e/basic-test/02-assert-user.yaml @@ -47,12 +47,6 @@ resourceRefs: assertAll: - celExpr: matches(example_user.metadata.labels["user.nauth.io/id"], "^U.{55}$") - celExpr: example_user.metadata.labels["user.nauth.io/account-id"] == example_account.metadata.labels["account.nauth.io/id"] - - celExpr: example_user.metadata.labels["user.nauth.io/signed-by"] == example_account.status.signingKey.name - ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -timeout: 20 commands: - script: | set -eu diff --git a/test/e2e/cluster-ref-test/02-assert.yaml b/test/e2e/cluster-ref-test/02-assert.yaml index 187f75e..432a377 100644 --- a/test/e2e/cluster-ref-test/02-assert.yaml +++ b/test/e2e/cluster-ref-test/02-assert.yaml @@ -39,10 +39,6 @@ resourceRefs: assertAll: # Verify account ID label is set (starts with A) - celExpr: matches(account.metadata.labels["account.nauth.io/id"], "^A.{55}$") ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -timeout: 30 commands: - script: | set -eu From 4fb27af2c08f99bd18ebf269641e2f3f3b67b38a Mon Sep 17 00:00:00 2001 From: Thobias Karlsson Date: Tue, 17 Mar 2026 22:27:19 +0100 Subject: [PATCH 3/3] feat: account signing keys in account status claims To ensure full transparency between Account JWT stored in NATS cluster and the Account CR, we should include the claimed signing keys in the `Account.status.claims` struct. The optional (user) scope of signing keys are actively ignored until fully supported (via e.g. #140). TODOs added for this in the right places. Closes: #162 Signed-off-by: Thobias Karlsson --- api/v1alpha1/account_types.go | 9 ++++ api/v1alpha1/zz_generated.deepcopy.go | 51 +++++++++++++++++++ charts/nauth-crds/crds/nauth.io_accounts.yaml | 7 +++ .../resources/crds/nauth.io_accounts.yaml | 7 +++ ....account-limits.output.nauth.approved.yaml | 3 ++ ....default-limits.output.nauth.approved.yaml | 3 ++ ...tClaims.exports.output.nauth.approved.yaml | 3 ++ ...tClaims.imports.output.nauth.approved.yaml | 3 ++ ...etstream-limits.output.nauth.approved.yaml | 3 ++ ...ims.nats-limits.output.nauth.approved.yaml | 3 ++ internal/account/claims.go | 18 +++++++ internal/account/claims_test.go | 37 ++++++++++++++ test/e2e/basic-test/01-assert-account.yaml | 5 +- test/e2e/basic-test/02-assert-user.yaml | 2 + 14 files changed, 152 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/account_types.go b/api/v1alpha1/account_types.go index 96b0488..d7c0a74 100644 --- a/api/v1alpha1/account_types.go +++ b/api/v1alpha1/account_types.go @@ -58,6 +58,8 @@ type AccountClaims struct { // +optional DisplayName string `json:"displayName,omitempty"` // +optional + SigningKeys SigningKeys `json:"signingKeys,omitempty"` + // +optional Exports Exports `json:"exports,omitempty"` // +optional Imports Imports `json:"imports,omitempty"` @@ -163,6 +165,13 @@ func init() { SchemeBuilder.Register(&Account{}, &AccountList{}) } +type SigningKeys []*SigningKey + +type SigningKey struct { + Key string `json:"key,omitempty"` + // TODO: [https://github.com/WirelessCar/nauth/issues/140] Support optional *UserScope +} + type Exports []*Export type Export struct { Name string `json:"name,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2f99d55..67fcdfc 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -60,6 +60,17 @@ func (in *AccountClaims) DeepCopyInto(out *AccountClaims) { *out = new(AccountLimits) (*in).DeepCopyInto(*out) } + if in.SigningKeys != nil { + in, out := &in.SigningKeys, &out.SigningKeys + *out = make(SigningKeys, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(SigningKey) + **out = **in + } + } + } if in.Exports != nil { in, out := &in.Exports, &out.Exports *out = make(Exports, len(*in)) @@ -687,6 +698,46 @@ func (in *ServiceLatency) DeepCopy() *ServiceLatency { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SigningKey) DeepCopyInto(out *SigningKey) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SigningKey. +func (in *SigningKey) DeepCopy() *SigningKey { + if in == nil { + return nil + } + out := new(SigningKey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in SigningKeys) DeepCopyInto(out *SigningKeys) { + { + in := &in + *out = make(SigningKeys, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(SigningKey) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SigningKeys. +func (in SigningKeys) DeepCopy() SigningKeys { + if in == nil { + return nil + } + out := new(SigningKeys) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in StringList) DeepCopyInto(out *StringList) { { diff --git a/charts/nauth-crds/crds/nauth.io_accounts.yaml b/charts/nauth-crds/crds/nauth.io_accounts.yaml index 44586bb..05e472f 100644 --- a/charts/nauth-crds/crds/nauth.io_accounts.yaml +++ b/charts/nauth-crds/crds/nauth.io_accounts.yaml @@ -423,6 +423,13 @@ spec: format: int64 type: integer type: object + signingKeys: + items: + properties: + key: + type: string + type: object + type: array type: object conditions: items: diff --git a/charts/nauth/resources/crds/nauth.io_accounts.yaml b/charts/nauth/resources/crds/nauth.io_accounts.yaml index 44586bb..05e472f 100644 --- a/charts/nauth/resources/crds/nauth.io_accounts.yaml +++ b/charts/nauth/resources/crds/nauth.io_accounts.yaml @@ -423,6 +423,13 @@ spec: format: int64 type: integer type: object + signingKeys: + items: + properties: + key: + type: string + type: object + type: array type: object conditions: items: diff --git a/internal/account/approvals/claims_test.TestClaims.account-limits.output.nauth.approved.yaml b/internal/account/approvals/claims_test.TestClaims.account-limits.output.nauth.approved.yaml index ef12553..d904a0d 100644 --- a/internal/account/approvals/claims_test.TestClaims.account-limits.output.nauth.approved.yaml +++ b/internal/account/approvals/claims_test.TestClaims.account-limits.output.nauth.approved.yaml @@ -17,3 +17,6 @@ natsLimits: data: -1 payload: -1 subs: -1 +signingKeys: +- key: ACI73NE4LXWVHSYSFXY73WTZVKIKE54PQUMRDYA4EUFYFGEGHKTPCOI4 +- key: ADCECGT44IBBMSNGOEZTVK2QUQSVTJW6FABW7JBFFTITDBHMP6TXM4XG diff --git a/internal/account/approvals/claims_test.TestClaims.default-limits.output.nauth.approved.yaml b/internal/account/approvals/claims_test.TestClaims.default-limits.output.nauth.approved.yaml index 849f066..29dc6bd 100644 --- a/internal/account/approvals/claims_test.TestClaims.default-limits.output.nauth.approved.yaml +++ b/internal/account/approvals/claims_test.TestClaims.default-limits.output.nauth.approved.yaml @@ -17,3 +17,6 @@ natsLimits: data: -1 payload: -1 subs: -1 +signingKeys: +- key: ACI73NE4LXWVHSYSFXY73WTZVKIKE54PQUMRDYA4EUFYFGEGHKTPCOI4 +- key: ADCECGT44IBBMSNGOEZTVK2QUQSVTJW6FABW7JBFFTITDBHMP6TXM4XG diff --git a/internal/account/approvals/claims_test.TestClaims.exports.output.nauth.approved.yaml b/internal/account/approvals/claims_test.TestClaims.exports.output.nauth.approved.yaml index 124f680..615dc80 100644 --- a/internal/account/approvals/claims_test.TestClaims.exports.output.nauth.approved.yaml +++ b/internal/account/approvals/claims_test.TestClaims.exports.output.nauth.approved.yaml @@ -48,3 +48,6 @@ natsLimits: data: -1 payload: -1 subs: -1 +signingKeys: +- key: ACI73NE4LXWVHSYSFXY73WTZVKIKE54PQUMRDYA4EUFYFGEGHKTPCOI4 +- key: ADCECGT44IBBMSNGOEZTVK2QUQSVTJW6FABW7JBFFTITDBHMP6TXM4XG diff --git a/internal/account/approvals/claims_test.TestClaims.imports.output.nauth.approved.yaml b/internal/account/approvals/claims_test.TestClaims.imports.output.nauth.approved.yaml index b00fc82..5438124 100644 --- a/internal/account/approvals/claims_test.TestClaims.imports.output.nauth.approved.yaml +++ b/internal/account/approvals/claims_test.TestClaims.imports.output.nauth.approved.yaml @@ -42,3 +42,6 @@ natsLimits: data: -1 payload: -1 subs: -1 +signingKeys: +- key: ACI73NE4LXWVHSYSFXY73WTZVKIKE54PQUMRDYA4EUFYFGEGHKTPCOI4 +- key: ADCECGT44IBBMSNGOEZTVK2QUQSVTJW6FABW7JBFFTITDBHMP6TXM4XG diff --git a/internal/account/approvals/claims_test.TestClaims.jetstream-limits.output.nauth.approved.yaml b/internal/account/approvals/claims_test.TestClaims.jetstream-limits.output.nauth.approved.yaml index 219b122..8d4e6b6 100644 --- a/internal/account/approvals/claims_test.TestClaims.jetstream-limits.output.nauth.approved.yaml +++ b/internal/account/approvals/claims_test.TestClaims.jetstream-limits.output.nauth.approved.yaml @@ -18,3 +18,6 @@ natsLimits: data: -1 payload: -1 subs: -1 +signingKeys: +- key: ACI73NE4LXWVHSYSFXY73WTZVKIKE54PQUMRDYA4EUFYFGEGHKTPCOI4 +- key: ADCECGT44IBBMSNGOEZTVK2QUQSVTJW6FABW7JBFFTITDBHMP6TXM4XG diff --git a/internal/account/approvals/claims_test.TestClaims.nats-limits.output.nauth.approved.yaml b/internal/account/approvals/claims_test.TestClaims.nats-limits.output.nauth.approved.yaml index 37c6cb1..8deb2a8 100644 --- a/internal/account/approvals/claims_test.TestClaims.nats-limits.output.nauth.approved.yaml +++ b/internal/account/approvals/claims_test.TestClaims.nats-limits.output.nauth.approved.yaml @@ -17,3 +17,6 @@ natsLimits: data: 1048576 payload: 1024 subs: 1000 +signingKeys: +- key: ACI73NE4LXWVHSYSFXY73WTZVKIKE54PQUMRDYA4EUFYFGEGHKTPCOI4 +- key: ADCECGT44IBBMSNGOEZTVK2QUQSVTJW6FABW7JBFFTITDBHMP6TXM4XG diff --git a/internal/account/claims.go b/internal/account/claims.go index 27b37be..0fd0097 100644 --- a/internal/account/claims.go +++ b/internal/account/claims.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "github.com/WirelessCar/nauth/api/v1alpha1" "github.com/WirelessCar/nauth/internal/domain" @@ -287,6 +288,23 @@ func convertNatsAccountClaims(claims *jwt.AccountClaims) v1alpha1.AccountClaims } } + // Signing Keys + if len(claims.SigningKeys) > 0 { + signingKeys := make(v1alpha1.SigningKeys, 0, len(claims.SigningKeys)) + for key := range claims.SigningKeys { + signingKey := v1alpha1.SigningKey{ + Key: key, + } + signingKeys = append(signingKeys, &signingKey) + // TODO: [https://github.com/WirelessCar/nauth/issues/140] Populate optional *UserScope + } + // Sort by key to ensure predictable, and human searchable, order. + sort.Slice(signingKeys, func(i, j int) bool { + return signingKeys[i].Key < signingKeys[j].Key + }) + out.SigningKeys = signingKeys + } + // Exports if len(claims.Exports) > 0 { exports := make(v1alpha1.Exports, 0, len(claims.Exports)) diff --git a/internal/account/claims_test.go b/internal/account/claims_test.go index 02800a8..f344d95 100644 --- a/internal/account/claims_test.go +++ b/internal/account/claims_test.go @@ -124,6 +124,43 @@ func TestClaims(t *testing.T) { } } +func TestClaims_convertNatsAccountClaims_ShouldSucceed_WhenMinimal(t *testing.T) { + // Given + claims := jwt.NewAccountClaims(testClaimsFakeAccountID) + + // When + result := convertNatsAccountClaims(claims) + + // Then + var ptrNoLimit int64 = -1 + var ptrDisabled int64 = 0 + var ptrTrue = true + require.Equal(t, v1alpha1.AccountClaims{ + AccountLimits: &v1alpha1.AccountLimits{ + Imports: &ptrNoLimit, + Exports: &ptrNoLimit, + WildcardExports: &ptrTrue, + Conn: &ptrNoLimit, + LeafNodeConn: &ptrNoLimit, + }, + JetStreamLimits: &v1alpha1.JetStreamLimits{ + MemoryStorage: &ptrDisabled, + DiskStorage: &ptrDisabled, + Streams: &ptrDisabled, + Consumer: &ptrDisabled, + MaxAckPending: &ptrDisabled, + MemoryMaxStreamBytes: &ptrDisabled, + DiskMaxStreamBytes: &ptrDisabled, + MaxBytesRequired: false, + }, + NatsLimits: &v1alpha1.NatsLimits{ + Subs: &ptrNoLimit, + Data: &ptrNoLimit, + Payload: &ptrNoLimit, + }, + }, result) +} + type TestCaseInputFile struct { TestName string InputFile string diff --git a/test/e2e/basic-test/01-assert-account.yaml b/test/e2e/basic-test/01-assert-account.yaml index ea7e7f2..a4ad3cd 100644 --- a/test/e2e/basic-test/01-assert-account.yaml +++ b/test/e2e/basic-test/01-assert-account.yaml @@ -47,14 +47,15 @@ resourceRefs: assertAll: - celExpr: matches(example_account.metadata.labels["account.nauth.io/id"], "^A.{55}$") - celExpr: example_account.metadata.labels["account.nauth.io/signed-by"] == "OCRTCOTZAWYINN4U4XVNEM6TDJNOBLZQMGDIZ765WFA5ZLYMRB32HCG4" + - celExpr: matches(example_account.status.claims.signingKeys[0].key, "^A.{55}$") commands: - script: | set -eu - + # read account id from account aid="$(kubectl get accounts.nauth.io example-account -n "$NAMESPACE" -o jsonpath='{.metadata.labels.account\.nauth\.io/id}')" test -n "$aid" - + # verify account root secret exists sec="$(kubectl get secret -n "$NAMESPACE" -l account.nauth.io/id="$aid",nauth.io/secret-type=account-root -o jsonpath='{.items[0].data.default}')" test -n "$sec" diff --git a/test/e2e/basic-test/02-assert-user.yaml b/test/e2e/basic-test/02-assert-user.yaml index 89c00b9..113b5b8 100644 --- a/test/e2e/basic-test/02-assert-user.yaml +++ b/test/e2e/basic-test/02-assert-user.yaml @@ -45,8 +45,10 @@ resourceRefs: name: example-user ref: example_user assertAll: + - celExpr: matches(example_account.status.claims.signingKeys[0].key, "^A.{55}$") - celExpr: matches(example_user.metadata.labels["user.nauth.io/id"], "^U.{55}$") - celExpr: example_user.metadata.labels["user.nauth.io/account-id"] == example_account.metadata.labels["account.nauth.io/id"] + - celExpr: example_user.metadata.labels["user.nauth.io/signed-by"] == example_account.status.claims.signingKeys[0].key commands: - script: | set -eu