mirror of
https://github.com/gravitational/teleport.git
synced 2026-08-30 17:45:43 +08:00
0cbc5fc40a
* RFD 229C: Add scope field to WorkloadIdentity resource and validation rules (2/?) * simplify/clean up validation
576 lines
18 KiB
Go
576 lines
18 KiB
Go
// Teleport
|
|
// Copyright (C) 2024 Gravitational, Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
|
|
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
|
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
)
|
|
|
|
func TestWorkloadIdentityMarshaling(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in *workloadidentityv1pb.WorkloadIdentity
|
|
}{
|
|
{
|
|
name: "normal",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gotBytes, err := MarshalWorkloadIdentity(tc.in)
|
|
require.NoError(t, err)
|
|
// Test that unmarshaling gives us the same object
|
|
got, err := UnmarshalWorkloadIdentity(gotBytes)
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(tc.in, got, protocmp.Transform()))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateWorkloadIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var errContains = func(contains string) require.ErrorAssertionFunc {
|
|
return func(t require.TestingT, err error, msgAndArgs ...any) {
|
|
require.ErrorContains(t, err, contains, msgAndArgs...)
|
|
}
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in *workloadidentityv1pb.WorkloadIdentity
|
|
requireErr require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "success - full",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Rules: workloadidentityv1pb.WorkloadIdentityRules_builder{
|
|
Allow: []*workloadidentityv1pb.WorkloadIdentityRule{
|
|
workloadidentityv1pb.WorkloadIdentityRule_builder{
|
|
Conditions: []*workloadidentityv1pb.WorkloadIdentityCondition{
|
|
workloadidentityv1pb.WorkloadIdentityCondition_builder{
|
|
Attribute: "example",
|
|
Eq: workloadidentityv1pb.WorkloadIdentityConditionEq_builder{
|
|
Value: "foo",
|
|
}.Build(),
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
X509: workloadidentityv1pb.WorkloadIdentitySPIFFEX509_builder{
|
|
MaximumTtl: durationpb.New(time.Hour * 24 * 14),
|
|
}.Build(),
|
|
Jwt: workloadidentityv1pb.WorkloadIdentitySPIFFEJWT_builder{
|
|
MaximumTtl: durationpb.New(time.Hour * 24),
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: require.NoError,
|
|
},
|
|
{
|
|
name: "success - minimal",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: require.NoError,
|
|
},
|
|
{
|
|
name: "missing name",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: &headerv1.Metadata{},
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("metadata.name: is required"),
|
|
},
|
|
{
|
|
name: "missing spiffe id",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{},
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("spec.spiffe.id: is required"),
|
|
},
|
|
{
|
|
name: "spiffe id must have leading /",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("spec.spiffe.id: must start with a /"),
|
|
},
|
|
{
|
|
name: "missing attribute",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Rules: workloadidentityv1pb.WorkloadIdentityRules_builder{
|
|
Allow: []*workloadidentityv1pb.WorkloadIdentityRule{
|
|
workloadidentityv1pb.WorkloadIdentityRule_builder{
|
|
Conditions: []*workloadidentityv1pb.WorkloadIdentityCondition{
|
|
workloadidentityv1pb.WorkloadIdentityCondition_builder{
|
|
Attribute: "",
|
|
Eq: workloadidentityv1pb.WorkloadIdentityConditionEq_builder{
|
|
Value: "foo",
|
|
}.Build(),
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("spec.rules.allow[0].conditions[0].attribute: must be non-empty"),
|
|
},
|
|
{
|
|
name: "missing operator",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Rules: workloadidentityv1pb.WorkloadIdentityRules_builder{
|
|
Allow: []*workloadidentityv1pb.WorkloadIdentityRule{
|
|
workloadidentityv1pb.WorkloadIdentityRule_builder{
|
|
Conditions: []*workloadidentityv1pb.WorkloadIdentityCondition{
|
|
workloadidentityv1pb.WorkloadIdentityCondition_builder{
|
|
Attribute: "example",
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("spec.rules.allow[0].conditions[0]: operator must be specified"),
|
|
},
|
|
{
|
|
name: "expression and conditions",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Rules: workloadidentityv1pb.WorkloadIdentityRules_builder{
|
|
Allow: []*workloadidentityv1pb.WorkloadIdentityRule{
|
|
workloadidentityv1pb.WorkloadIdentityRule_builder{
|
|
Expression: `user.name == "Alan Partridge"`,
|
|
Conditions: []*workloadidentityv1pb.WorkloadIdentityCondition{
|
|
workloadidentityv1pb.WorkloadIdentityCondition_builder{
|
|
Attribute: "example",
|
|
Eq: workloadidentityv1pb.WorkloadIdentityConditionEq_builder{
|
|
Value: "foo",
|
|
}.Build(),
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("spec.rules.allow[0].conditions: is mutually exclusive with expression"),
|
|
},
|
|
{
|
|
name: "neither expression or conditions",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Rules: workloadidentityv1pb.WorkloadIdentityRules_builder{
|
|
Allow: []*workloadidentityv1pb.WorkloadIdentityRule{
|
|
{}, // Empty rule.
|
|
},
|
|
}.Build(),
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("spec.rules.allow[0].conditions: must be non-empty"),
|
|
},
|
|
{
|
|
name: "invalid expression",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Rules: workloadidentityv1pb.WorkloadIdentityRules_builder{
|
|
Allow: []*workloadidentityv1pb.WorkloadIdentityRule{
|
|
workloadidentityv1pb.WorkloadIdentityRule_builder{
|
|
Expression: `does_not_exist`,
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains(`unknown identifier: "does_not_exist"`),
|
|
},
|
|
{
|
|
name: "maximum x509 ttl too large",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
X509: workloadidentityv1pb.WorkloadIdentitySPIFFEX509_builder{
|
|
MaximumTtl: durationpb.New(time.Hour * 24 * 365),
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains(`spec.spiffe.x509.maximum_ttl: must be less than 336h0m0s`),
|
|
},
|
|
{
|
|
name: "maximum jwt ttl too large",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/example",
|
|
Jwt: workloadidentityv1pb.WorkloadIdentitySPIFFEJWT_builder{
|
|
MaximumTtl: durationpb.New(time.Hour * 24 * 365),
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains(`spec.spiffe.jwt.maximum_ttl: must be less than 24h0m0s`),
|
|
},
|
|
{
|
|
name: "scoped success",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Scope: "/security/eu",
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/security/eu/_/k8s/cluster-a",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: require.NoError,
|
|
},
|
|
{
|
|
name: "scoped success with templated spiffe id",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Scope: "/security/eu",
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/security/eu/_/k8s/{{ workload.kubernetes.namespace }}/{{ workload.kubernetes.pod_name }}",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: require.NoError,
|
|
},
|
|
{
|
|
name: "scoped templated spiffe id cannot escape scope prefix",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Scope: "/security",
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/{{ user.name }}/_/svc",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("must be prefixed with the scope"),
|
|
},
|
|
{
|
|
name: "scoped spiffe id not prefixed with scope",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Scope: "/security",
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/other/_/svc",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("must be prefixed with the scope"),
|
|
},
|
|
{
|
|
name: "scoped root scope rejected",
|
|
in: workloadidentityv1pb.WorkloadIdentity_builder{
|
|
Kind: types.KindWorkloadIdentity,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "example",
|
|
}.Build(),
|
|
Scope: "/",
|
|
Spec: workloadidentityv1pb.WorkloadIdentitySpec_builder{
|
|
Spiffe: workloadidentityv1pb.WorkloadIdentitySPIFFE_builder{
|
|
Id: "/_/svc",
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build(),
|
|
requireErr: errContains("must not be the root scope"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := ValidateWorkloadIdentity(tc.in)
|
|
tc.requireErr(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateScopedSPIFFEID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var errContains = func(contains string) require.ErrorAssertionFunc {
|
|
return func(t require.TestingT, err error, msgAndArgs ...any) {
|
|
require.ErrorContains(t, err, contains, msgAndArgs...)
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
scope string
|
|
id string
|
|
assertErr require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "valid single segment scope",
|
|
scope: "/security",
|
|
id: "/security/_/foo-svc",
|
|
assertErr: require.NoError,
|
|
},
|
|
{
|
|
name: "valid multi segment scope",
|
|
scope: "/security/eu",
|
|
id: "/security/eu/_/k8s/cluster-a/default/default",
|
|
assertErr: require.NoError,
|
|
},
|
|
{
|
|
name: "valid multi segment admin section",
|
|
scope: "/security",
|
|
id: "/security/_/k8s/cluster-a",
|
|
assertErr: require.NoError,
|
|
},
|
|
{
|
|
name: "missing leading slash",
|
|
scope: "/security",
|
|
id: "security/_/foo",
|
|
assertErr: errContains("must begin with a forward slash"),
|
|
},
|
|
{
|
|
name: "id not prefixed with scope",
|
|
scope: "/security",
|
|
id: "/other/_/foo",
|
|
assertErr: errContains("must be prefixed with the scope"),
|
|
},
|
|
{
|
|
name: "segment-aware prefix - not a string prefix match",
|
|
scope: "/foo",
|
|
id: "/foo-buzz/_/svc",
|
|
assertErr: errContains("must be prefixed with the scope"),
|
|
},
|
|
{
|
|
name: "scope section is ancestor of scope",
|
|
scope: "/foo/bar",
|
|
id: "/foo/_/svc",
|
|
assertErr: errContains("must be prefixed with the scope"),
|
|
},
|
|
{
|
|
name: "scope section is descendant of scope",
|
|
scope: "/foo/bar",
|
|
id: "/foo/bar/baz/_/svc",
|
|
assertErr: errContains("must be prefixed with the scope"),
|
|
},
|
|
{
|
|
name: "missing separator - too short",
|
|
scope: "/security",
|
|
id: "/security/foo",
|
|
assertErr: errContains("is missing the"),
|
|
},
|
|
{
|
|
name: "missing separator - long enough",
|
|
scope: "/security",
|
|
id: "/security/foo/bar",
|
|
assertErr: errContains("is missing the"),
|
|
},
|
|
{
|
|
name: "separator is last segment - no admin section",
|
|
scope: "/security/eu",
|
|
id: "/security/eu/_",
|
|
assertErr: errContains("at least one segment after"),
|
|
},
|
|
{
|
|
name: "separator in admin section",
|
|
scope: "/security/eu",
|
|
id: "/security/eu/_/k8s/_/probes/liveness",
|
|
assertErr: errContains("administratively-defined section"),
|
|
},
|
|
{
|
|
name: "trailing slash",
|
|
scope: "/security",
|
|
id: "/security/_/foo/",
|
|
assertErr: errContains("empty path segments"),
|
|
},
|
|
{
|
|
name: "empty segment in admin section",
|
|
scope: "/security",
|
|
id: "/security/_/foo//bar",
|
|
assertErr: errContains("empty path segments"),
|
|
},
|
|
// Templated SPIFFE IDs are validated in their unrendered form at
|
|
// create/update time; the rendered form is re-validated at issuance.
|
|
// Templates are only permitted in the administratively-defined section.
|
|
{
|
|
name: "template in admin section",
|
|
scope: "/security/eu",
|
|
id: "/security/eu/_/k8s/{{ workload.kubernetes.namespace }}/{{ workload.kubernetes.pod_name }}",
|
|
assertErr: require.NoError,
|
|
},
|
|
{
|
|
name: "template as sole admin segment",
|
|
scope: "/security",
|
|
id: "/security/_/{{ workload.kubernetes.pod_name }}",
|
|
assertErr: require.NoError,
|
|
},
|
|
{
|
|
name: "template cannot substitute for a scope segment",
|
|
scope: "/security",
|
|
id: "/{{ user.name }}/_/svc",
|
|
assertErr: errContains("must be prefixed with the scope"),
|
|
},
|
|
{
|
|
name: "template cannot substitute for the separator",
|
|
scope: "/security",
|
|
id: "/security/{{ user.name }}/svc",
|
|
assertErr: errContains("is missing the"),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.assertErr(t, validateScopedSPIFFEID(tt.scope, tt.id))
|
|
})
|
|
}
|
|
}
|