diff --git a/api/types/events/oneof.go b/api/types/events/oneof.go
index 18c15a094d94e..d1b3ce471d837 100644
--- a/api/types/events/oneof.go
+++ b/api/types/events/oneof.go
@@ -798,6 +798,7 @@ func ToOneOf(in AuditEvent) (*OneOf, error) {
out.Event = &OneOf_ContactDelete{
ContactDelete: e,
}
+
case *WorkloadIdentityCreate:
out.Event = &OneOf_WorkloadIdentityCreate{
WorkloadIdentityCreate: e,
diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go
index 07ba460d29e91..e46e06a157133 100644
--- a/lib/auth/grpcserver.go
+++ b/lib/auth/grpcserver.go
@@ -74,6 +74,7 @@ import (
usersv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
usertaskv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
vnetv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
+ workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
userpreferencesv1pb "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/metadata"
@@ -95,6 +96,7 @@ import (
"github.com/gravitational/teleport/lib/auth/kubewaitingcontainer/kubewaitingcontainerv1"
"github.com/gravitational/teleport/lib/auth/loginrule/loginrulev1"
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
+ "github.com/gravitational/teleport/lib/auth/machineid/workloadidentityv1"
"github.com/gravitational/teleport/lib/auth/notifications/notificationsv1"
"github.com/gravitational/teleport/lib/auth/presence/presencev1"
"github.com/gravitational/teleport/lib/auth/trust/trustv1"
@@ -5095,6 +5097,18 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {
}
machineidv1pb.RegisterSPIFFEFederationServiceServer(server, spiffeFederationService)
+ workloadIdentityResourceService, err := workloadidentityv1.NewResourceService(&workloadidentityv1.ResourceServiceConfig{
+ Authorizer: cfg.Authorizer,
+ Backend: cfg.AuthServer.Services.WorkloadIdentities,
+ Cache: cfg.AuthServer.Cache,
+ Emitter: cfg.Emitter,
+ Clock: cfg.AuthServer.GetClock(),
+ })
+ if err != nil {
+ return nil, trace.Wrap(err, "creating workload identity resource service")
+ }
+ workloadidentityv1pb.RegisterWorkloadIdentityResourceServiceServer(server, workloadIdentityResourceService)
+
dbObjectImportRuleService, err := dbobjectimportrulev1.NewDatabaseObjectImportRuleService(dbobjectimportrulev1.DatabaseObjectImportRuleServiceConfig{
Authorizer: cfg.Authorizer,
Backend: cfg.AuthServer.Services,
diff --git a/lib/auth/machineid/workloadidentityv1/resource_service.go b/lib/auth/machineid/workloadidentityv1/resource_service.go
new file mode 100644
index 0000000000000..6ca893e1b1fa9
--- /dev/null
+++ b/lib/auth/machineid/workloadidentityv1/resource_service.go
@@ -0,0 +1,362 @@
+// 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 .
+
+package workloadidentityv1
+
+import (
+ "context"
+ "log/slog"
+
+ "github.com/gravitational/trace"
+ "github.com/jonboulle/clockwork"
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/types/known/emptypb"
+
+ "github.com/gravitational/teleport"
+ workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
+ "github.com/gravitational/teleport/api/types"
+ apievents "github.com/gravitational/teleport/api/types/events"
+ "github.com/gravitational/teleport/lib/authz"
+ "github.com/gravitational/teleport/lib/events"
+)
+
+type workloadIdentityReader interface {
+ GetWorkloadIdentity(ctx context.Context, name string) (*workloadidentityv1pb.WorkloadIdentity, error)
+ ListWorkloadIdentities(ctx context.Context, pageSize int, token string) ([]*workloadidentityv1pb.WorkloadIdentity, string, error)
+}
+
+type workloadIdentityReadWriter interface {
+ workloadIdentityReader
+
+ CreateWorkloadIdentity(ctx context.Context, identity *workloadidentityv1pb.WorkloadIdentity) (*workloadidentityv1pb.WorkloadIdentity, error)
+ UpdateWorkloadIdentity(ctx context.Context, identity *workloadidentityv1pb.WorkloadIdentity) (*workloadidentityv1pb.WorkloadIdentity, error)
+ DeleteWorkloadIdentity(ctx context.Context, name string) error
+ UpsertWorkloadIdentity(ctx context.Context, identity *workloadidentityv1pb.WorkloadIdentity) (*workloadidentityv1pb.WorkloadIdentity, error)
+}
+
+// ResourceServiceConfig holds configuration options for the ResourceService.
+type ResourceServiceConfig struct {
+ Authorizer authz.Authorizer
+ Backend workloadIdentityReadWriter
+ Cache workloadIdentityReader
+ Clock clockwork.Clock
+ Emitter apievents.Emitter
+ Logger *slog.Logger
+}
+
+// ResourceService is the gRPC service for managing workload identity resources.
+// It implements the workloadidentityv1pb.WorkloadIdentityResourceServiceServer
+type ResourceService struct {
+ workloadidentityv1pb.UnimplementedWorkloadIdentityResourceServiceServer
+
+ authorizer authz.Authorizer
+ backend workloadIdentityReadWriter
+ cache workloadIdentityReader
+ clock clockwork.Clock
+ emitter apievents.Emitter
+ logger *slog.Logger
+}
+
+// NewResourceService returns a new instance of the ResourceService.
+func NewResourceService(cfg *ResourceServiceConfig) (*ResourceService, error) {
+ switch {
+ case cfg.Backend == nil:
+ return nil, trace.BadParameter("backend service is required")
+ case cfg.Cache == nil:
+ return nil, trace.BadParameter("cache service is required")
+ case cfg.Authorizer == nil:
+ return nil, trace.BadParameter("authorizer is required")
+ case cfg.Emitter == nil:
+ return nil, trace.BadParameter("emitter is required")
+ }
+
+ if cfg.Logger == nil {
+ cfg.Logger = slog.With(teleport.ComponentKey, "workload_identity_resource.service")
+ }
+ if cfg.Clock == nil {
+ cfg.Clock = clockwork.NewRealClock()
+ }
+ return &ResourceService{
+ authorizer: cfg.Authorizer,
+ backend: cfg.Backend,
+ cache: cfg.Cache,
+ clock: cfg.Clock,
+ emitter: cfg.Emitter,
+ logger: cfg.Logger,
+ }, nil
+}
+
+// GetWorkloadIdentity returns a WorkloadIdentity by name.
+// Implements teleport.workloadidentity.v1.ResourceService/GetWorkloadIdentity
+func (s *ResourceService) GetWorkloadIdentity(
+ ctx context.Context, req *workloadidentityv1pb.GetWorkloadIdentityRequest,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentity, types.VerbRead); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ if req.Name == "" {
+ return nil, trace.BadParameter("name: must be non-empty")
+ }
+
+ resource, err := s.cache.GetWorkloadIdentity(ctx, req.Name)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ return resource, nil
+}
+
+// ListWorkloadIdentities returns a list of WorkloadIdentity resources. It
+// follows the Google API design guidelines for list pagination.
+// Implements teleport.workloadidentity.v1.ResourceService/ListWorkloadIdentities
+func (s *ResourceService) ListWorkloadIdentities(
+ ctx context.Context, req *workloadidentityv1pb.ListWorkloadIdentitiesRequest,
+) (*workloadidentityv1pb.ListWorkloadIdentitiesResponse, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentity, types.VerbRead, types.VerbList); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ resources, nextToken, err := s.cache.ListWorkloadIdentities(
+ ctx,
+ int(req.PageSize),
+ req.PageToken,
+ )
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ return &workloadidentityv1pb.ListWorkloadIdentitiesResponse{
+ WorkloadIdentities: resources,
+ NextPageToken: nextToken,
+ }, nil
+}
+
+// DeleteWorkloadIdentity deletes a WorkloadIdentity by name.
+// Implements teleport.workloadidentity.v1.ResourceService/DeleteWorkloadIdentity
+func (s *ResourceService) DeleteWorkloadIdentity(
+ ctx context.Context, req *workloadidentityv1pb.DeleteWorkloadIdentityRequest,
+) (*emptypb.Empty, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentity, types.VerbDelete); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ if req.Name == "" {
+ return nil, trace.BadParameter("name: must be non-empty")
+ }
+
+ if err := s.backend.DeleteWorkloadIdentity(ctx, req.Name); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ if err := s.emitter.EmitAuditEvent(ctx, &apievents.WorkloadIdentityDelete{
+ Metadata: apievents.Metadata{
+ Code: events.WorkloadIdentityDeleteCode,
+ Type: events.WorkloadIdentityDeleteEvent,
+ },
+ UserMetadata: authz.ClientUserMetadata(ctx),
+ ConnectionMetadata: authz.ConnectionMetadata(ctx),
+ ResourceMetadata: apievents.ResourceMetadata{
+ Name: req.Name,
+ },
+ }); err != nil {
+ s.logger.ErrorContext(
+ ctx, "Failed to emit audit event for deletion of WorkloadIdentity",
+ "error", err,
+ )
+ }
+
+ return &emptypb.Empty{}, nil
+}
+
+// CreateWorkloadIdentity creates a new WorkloadIdentity.
+// Implements teleport.workloadidentity.v1.ResourceService/CreateWorkloadIdentity
+func (s *ResourceService) CreateWorkloadIdentity(
+ ctx context.Context, req *workloadidentityv1pb.CreateWorkloadIdentityRequest,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentity, types.VerbCreate); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ created, err := s.backend.CreateWorkloadIdentity(ctx, req.WorkloadIdentity)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ evt := &apievents.WorkloadIdentityCreate{
+ Metadata: apievents.Metadata{
+ Code: events.WorkloadIdentityCreateCode,
+ Type: events.WorkloadIdentityCreateEvent,
+ },
+ UserMetadata: authz.ClientUserMetadata(ctx),
+ ConnectionMetadata: authz.ConnectionMetadata(ctx),
+ ResourceMetadata: apievents.ResourceMetadata{
+ Name: req.WorkloadIdentity.Metadata.Name,
+ },
+ }
+ evt.WorkloadIdentityData, err = resourceToStruct(created)
+ if err != nil {
+ s.logger.ErrorContext(
+ ctx,
+ "Failed to convert WorkloadIdentity to struct for audit log",
+ "error", err,
+ )
+ }
+ if err := s.emitter.EmitAuditEvent(ctx, evt); err != nil {
+ s.logger.ErrorContext(
+ ctx, "Failed to emit audit event for creation of WorkloadIdentity",
+ "error", err,
+ )
+ }
+
+ return created, nil
+}
+
+// UpdateWorkloadIdentity updates an existing WorkloadIdentity.
+// Implements teleport.workloadidentity.v1.ResourceService/UpdateWorkloadIdentity
+func (s *ResourceService) UpdateWorkloadIdentity(
+ ctx context.Context, req *workloadidentityv1pb.UpdateWorkloadIdentityRequest,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentity, types.VerbUpdate); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ created, err := s.backend.UpdateWorkloadIdentity(ctx, req.WorkloadIdentity)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ evt := &apievents.WorkloadIdentityUpdate{
+ Metadata: apievents.Metadata{
+ Code: events.WorkloadIdentityUpdateCode,
+ Type: events.WorkloadIdentityUpdateEvent,
+ },
+ UserMetadata: authz.ClientUserMetadata(ctx),
+ ConnectionMetadata: authz.ConnectionMetadata(ctx),
+ ResourceMetadata: apievents.ResourceMetadata{
+ Name: req.WorkloadIdentity.Metadata.Name,
+ },
+ }
+ evt.WorkloadIdentityData, err = resourceToStruct(created)
+ if err != nil {
+ s.logger.ErrorContext(
+ ctx,
+ "Failed to convert WorkloadIdentity to struct for audit log",
+ "error", err,
+ )
+ }
+ if err := s.emitter.EmitAuditEvent(ctx, evt); err != nil {
+ s.logger.ErrorContext(
+ ctx, "Failed to emit audit event for updating of WorkloadIdentity",
+ "error", err,
+ )
+ }
+
+ return created, nil
+}
+
+// UpsertWorkloadIdentity updates or creates an existing WorkloadIdentity.
+// Implements teleport.workloadidentity.v1.ResourceService/UpsertWorkloadIdentity
+func (s *ResourceService) UpsertWorkloadIdentity(
+ ctx context.Context, req *workloadidentityv1pb.UpsertWorkloadIdentityRequest,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(
+ types.KindWorkloadIdentity, types.VerbCreate, types.VerbUpdate,
+ ); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ created, err := s.backend.UpsertWorkloadIdentity(ctx, req.WorkloadIdentity)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ evt := &apievents.WorkloadIdentityCreate{
+ Metadata: apievents.Metadata{
+ Code: events.WorkloadIdentityCreateCode,
+ Type: events.WorkloadIdentityCreateEvent,
+ },
+ UserMetadata: authz.ClientUserMetadata(ctx),
+ ConnectionMetadata: authz.ConnectionMetadata(ctx),
+ ResourceMetadata: apievents.ResourceMetadata{
+ Name: req.WorkloadIdentity.Metadata.Name,
+ },
+ }
+ evt.WorkloadIdentityData, err = resourceToStruct(created)
+ if err != nil {
+ s.logger.ErrorContext(
+ ctx,
+ "Failed to convert WorkloadIdentity to struct for audit log",
+ "error", err,
+ )
+ }
+ if err := s.emitter.EmitAuditEvent(ctx, evt); err != nil {
+ s.logger.ErrorContext(
+ ctx, "Failed to emit audit event for upsertion of WorkloadIdentity",
+ "error", err,
+ )
+ }
+
+ return created, nil
+}
+
+func resourceToStruct(in *workloadidentityv1pb.WorkloadIdentity) (*apievents.Struct, error) {
+ data, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(in)
+ if err != nil {
+ return nil, trace.Wrap(err, "marshaling resource for audit log")
+ }
+ out := &apievents.Struct{}
+ if err := out.UnmarshalJSON(data); err != nil {
+ return nil, trace.Wrap(err, "unmarshaling resource for audit log")
+ }
+ return out, nil
+}
diff --git a/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go
new file mode 100644
index 0000000000000..1c0601a34dd54
--- /dev/null
+++ b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go
@@ -0,0 +1,984 @@
+// 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 .
+
+package workloadidentityv1_test
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "os"
+ "slices"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/gravitational/trace"
+ "github.com/jonboulle/clockwork"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/testing/protocmp"
+
+ 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"
+ "github.com/gravitational/teleport/api/types/events"
+ "github.com/gravitational/teleport/lib/auth"
+ "github.com/gravitational/teleport/lib/auth/authclient"
+ libevents "github.com/gravitational/teleport/lib/events"
+ "github.com/gravitational/teleport/lib/events/eventstest"
+ "github.com/gravitational/teleport/lib/modules"
+)
+
+func TestMain(m *testing.M) {
+ modules.SetInsecureTestMode(true)
+ os.Exit(m.Run())
+}
+
+func newTestTLSServer(t testing.TB) (*auth.TestTLSServer, *eventstest.MockRecorderEmitter) {
+ as, err := auth.NewTestAuthServer(auth.TestAuthServerConfig{
+ Dir: t.TempDir(),
+ Clock: clockwork.NewFakeClockAt(time.Now().Round(time.Second).UTC()),
+ })
+ require.NoError(t, err)
+
+ emitter := &eventstest.MockRecorderEmitter{}
+ srv, err := as.NewTestTLSServer(func(config *auth.TestTLSServerConfig) {
+ config.APIConfig.Emitter = emitter
+ })
+ require.NoError(t, err)
+
+ t.Cleanup(func() {
+ err := srv.Close()
+ if errors.Is(err, net.ErrClosed) {
+ return
+ }
+ require.NoError(t, err)
+ })
+
+ return srv, emitter
+}
+
+func TestResourceService_CreateWorkloadIdentity(t *testing.T) {
+ t.Parallel()
+ srv, eventRecorder := newTestTLSServer(t)
+ ctx := context.Background()
+
+ authorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "authorized",
+ []string{},
+ []types.Rule{
+ {
+ Resources: []string{types.KindWorkloadIdentity},
+ Verbs: []string{types.VerbCreate},
+ },
+ })
+ require.NoError(t, err)
+ authorizedClient, err := srv.NewClient(auth.TestUser(authorizedUser.GetName()))
+ require.NoError(t, err)
+ unauthorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "unauthorized",
+ []string{},
+ []types.Rule{},
+ )
+ require.NoError(t, err)
+ unauthorizedClient, err := srv.NewClient(auth.TestUser(unauthorizedUser.GetName()))
+ require.NoError(t, err)
+
+ // Create a pre-existing workload identity
+ preExisting, err := srv.Auth().CreateWorkloadIdentity(
+ ctx,
+ &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "preexisting",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ client *authclient.Client
+ req *workloadidentityv1pb.CreateWorkloadIdentityRequest
+ requireError require.ErrorAssertionFunc
+ checkResultReturned bool
+ requireEvent *events.WorkloadIdentityCreate
+ }{
+ {
+ name: "success",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.CreateWorkloadIdentityRequest{
+ WorkloadIdentity: &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "new",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ },
+ },
+ requireError: require.NoError,
+ checkResultReturned: true,
+ requireEvent: &events.WorkloadIdentityCreate{
+ Metadata: events.Metadata{
+ Code: libevents.WorkloadIdentityCreateCode,
+ Type: libevents.WorkloadIdentityCreateEvent,
+ },
+ ResourceMetadata: events.ResourceMetadata{
+ Name: "new",
+ },
+ UserMetadata: events.UserMetadata{
+ User: authorizedUser.GetName(),
+ UserKind: events.UserKind_USER_KIND_HUMAN,
+ },
+ },
+ },
+ {
+ name: "pre-existing",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.CreateWorkloadIdentityRequest{
+ WorkloadIdentity: &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: preExisting.GetMetadata().GetName(),
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ },
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsAlreadyExists(err))
+ },
+ },
+ {
+ name: "validation fail",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.CreateWorkloadIdentityRequest{
+ WorkloadIdentity: &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "new",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "",
+ },
+ },
+ },
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsBadParameter(err))
+ require.ErrorContains(t, err, "spec.spiffe.id: is required")
+ },
+ },
+ {
+ name: "unauthorized",
+ client: unauthorizedClient,
+ req: &workloadidentityv1pb.CreateWorkloadIdentityRequest{
+ WorkloadIdentity: &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "unauthorized",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ },
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsAccessDenied(err))
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ eventRecorder.Reset()
+ client := workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(
+ tt.client.GetConnection(),
+ )
+ res, err := client.CreateWorkloadIdentity(ctx, tt.req)
+ tt.requireError(t, err)
+
+ if tt.checkResultReturned {
+ require.NotEmpty(t, res.Metadata.Revision)
+ // Expect returned result to match request, but also have a
+ // revision
+ require.Empty(
+ t,
+ cmp.Diff(
+ res,
+ tt.req.WorkloadIdentity,
+ protocmp.Transform(),
+ protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
+ ),
+ )
+ // Expect the value fetched from the store to match returned
+ // item.
+ fetched, err := srv.Auth().GetWorkloadIdentity(ctx, res.Metadata.Name)
+ require.NoError(t, err)
+ require.Empty(
+ t,
+ cmp.Diff(
+ res,
+ fetched,
+ protocmp.Transform(),
+ ),
+ )
+ }
+ if tt.requireEvent != nil {
+ evt, ok := eventRecorder.LastEvent().(*events.WorkloadIdentityCreate)
+ require.True(t, ok)
+ require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr)
+ require.Empty(t, cmp.Diff(
+ evt,
+ tt.requireEvent,
+ cmpopts.IgnoreFields(events.WorkloadIdentityCreate{}, "ConnectionMetadata", "WorkloadIdentityData"),
+ ))
+ }
+ })
+ }
+}
+
+func TestResourceService_DeleteWorkloadIdentity(t *testing.T) {
+ t.Parallel()
+ srv, eventRecorder := newTestTLSServer(t)
+ ctx := context.Background()
+
+ authorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "authorized",
+ []string{},
+ []types.Rule{
+ {
+ Resources: []string{types.KindWorkloadIdentity},
+ Verbs: []string{types.VerbDelete},
+ },
+ })
+ require.NoError(t, err)
+ authorizedClient, err := srv.NewClient(auth.TestUser(authorizedUser.GetName()))
+ require.NoError(t, err)
+ unauthorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "unauthorized",
+ []string{},
+ []types.Rule{},
+ )
+ require.NoError(t, err)
+ unauthorizedClient, err := srv.NewClient(auth.TestUser(unauthorizedUser.GetName()))
+ require.NoError(t, err)
+
+ // Create a pre-existing workload identity
+ preExisting, err := srv.Auth().CreateWorkloadIdentity(
+ ctx,
+ &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "preexisting",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ client *authclient.Client
+ req *workloadidentityv1pb.DeleteWorkloadIdentityRequest
+ requireError require.ErrorAssertionFunc
+ checkNonExisting bool
+ requireEvent *events.WorkloadIdentityDelete
+ }{
+ {
+ name: "success",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.DeleteWorkloadIdentityRequest{
+ Name: preExisting.GetMetadata().GetName(),
+ },
+ requireError: require.NoError,
+ checkNonExisting: true,
+ requireEvent: &events.WorkloadIdentityDelete{
+ Metadata: events.Metadata{
+ Code: libevents.WorkloadIdentityDeleteCode,
+ Type: libevents.WorkloadIdentityDeleteEvent,
+ },
+ ResourceMetadata: events.ResourceMetadata{
+ Name: preExisting.GetMetadata().GetName(),
+ },
+ UserMetadata: events.UserMetadata{
+ User: authorizedUser.GetName(),
+ UserKind: events.UserKind_USER_KIND_HUMAN,
+ },
+ },
+ },
+ {
+ name: "non-existing",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.DeleteWorkloadIdentityRequest{
+ Name: "i-do-not-exist",
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsNotFound(err))
+ },
+ },
+ {
+ name: "validation fail",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.DeleteWorkloadIdentityRequest{
+ Name: "",
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsBadParameter(err))
+ require.ErrorContains(t, err, "name: must be non-empty")
+ },
+ },
+ {
+ name: "unauthorized",
+ client: unauthorizedClient,
+ req: &workloadidentityv1pb.DeleteWorkloadIdentityRequest{
+ Name: "unauthorized",
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsAccessDenied(err))
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ eventRecorder.Reset()
+ client := workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(
+ tt.client.GetConnection(),
+ )
+ _, err := client.DeleteWorkloadIdentity(ctx, tt.req)
+ tt.requireError(t, err)
+
+ if tt.checkNonExisting {
+ _, err := srv.Auth().GetWorkloadIdentity(ctx, tt.req.Name)
+ require.True(t, trace.IsNotFound(err))
+ }
+ if tt.requireEvent != nil {
+ evt, ok := eventRecorder.LastEvent().(*events.WorkloadIdentityDelete)
+ require.True(t, ok)
+ require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr)
+ require.Empty(t, cmp.Diff(
+ tt.requireEvent,
+ evt,
+ cmpopts.IgnoreFields(events.WorkloadIdentityDelete{}, "ConnectionMetadata"),
+ ))
+ }
+ })
+ }
+}
+
+func TestResourceService_GetWorkloadIdentity(t *testing.T) {
+ t.Parallel()
+ srv, _ := newTestTLSServer(t)
+ ctx := context.Background()
+
+ authorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "authorized",
+ []string{},
+ []types.Rule{
+ {
+ Resources: []string{types.KindWorkloadIdentity},
+ Verbs: []string{types.VerbRead},
+ },
+ })
+ require.NoError(t, err)
+ authorizedClient, err := srv.NewClient(auth.TestUser(authorizedUser.GetName()))
+ require.NoError(t, err)
+ unauthorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "unauthorized",
+ []string{},
+ []types.Rule{},
+ )
+ require.NoError(t, err)
+ unauthorizedClient, err := srv.NewClient(auth.TestUser(unauthorizedUser.GetName()))
+ require.NoError(t, err)
+
+ // Create a pre-existing workload identity
+ preExisting, err := srv.Auth().CreateWorkloadIdentity(
+ ctx,
+ &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "preexisting",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ client *authclient.Client
+ req *workloadidentityv1pb.GetWorkloadIdentityRequest
+ wantRes *workloadidentityv1pb.WorkloadIdentity
+ requireError require.ErrorAssertionFunc
+ }{
+ {
+ name: "success",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.GetWorkloadIdentityRequest{
+ Name: preExisting.GetMetadata().GetName(),
+ },
+ wantRes: preExisting,
+ requireError: require.NoError,
+ },
+ {
+ name: "non-existing",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.GetWorkloadIdentityRequest{
+ Name: "i-do-not-exist",
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsNotFound(err))
+ },
+ },
+ {
+ name: "validation fail",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.GetWorkloadIdentityRequest{
+ Name: "",
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsBadParameter(err))
+ require.ErrorContains(t, err, "name: must be non-empty")
+ },
+ },
+ {
+ name: "unauthorized",
+ client: unauthorizedClient,
+ req: &workloadidentityv1pb.GetWorkloadIdentityRequest{
+ Name: "unauthorized",
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsAccessDenied(err))
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ client := workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(
+ tt.client.GetConnection(),
+ )
+ got, err := client.GetWorkloadIdentity(ctx, tt.req)
+ tt.requireError(t, err)
+
+ if tt.wantRes != nil {
+ require.Empty(
+ t,
+ cmp.Diff(
+ tt.wantRes,
+ got,
+ protocmp.Transform(),
+ ),
+ )
+ }
+ })
+ }
+}
+
+func TestResourceService_ListWorkloadIdentities(t *testing.T) {
+ t.Parallel()
+ srv, _ := newTestTLSServer(t)
+ ctx := context.Background()
+
+ authorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "authorized",
+ []string{},
+ []types.Rule{
+ {
+ Resources: []string{types.KindWorkloadIdentity},
+ Verbs: []string{types.VerbRead, types.VerbList},
+ },
+ })
+ require.NoError(t, err)
+ authorizedClient, err := srv.NewClient(auth.TestUser(authorizedUser.GetName()))
+ require.NoError(t, err)
+ unauthorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "unauthorized",
+ []string{},
+ []types.Rule{},
+ )
+ require.NoError(t, err)
+ unauthorizedClient, err := srv.NewClient(auth.TestUser(unauthorizedUser.GetName()))
+ require.NoError(t, err)
+
+ // Create a pre-existing workload identities
+ // Two complete pages of ten, plus one incomplete page of nine
+ created := []*workloadidentityv1pb.WorkloadIdentity{}
+ for i := 0; i < 29; i++ {
+ r, err := srv.Auth().CreateWorkloadIdentity(
+ ctx,
+ &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: fmt.Sprintf("preexisting-%d", i),
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ })
+ require.NoError(t, err)
+ created = append(created, r)
+ }
+
+ t.Run("unauthorized", func(t *testing.T) {
+ client := workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(
+ unauthorizedClient.GetConnection(),
+ )
+
+ _, err := client.ListWorkloadIdentities(ctx, &workloadidentityv1pb.ListWorkloadIdentitiesRequest{})
+ require.True(t, trace.IsAccessDenied(err))
+ })
+
+ t.Run("success - default page", func(t *testing.T) {
+ client := workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(
+ authorizedClient.GetConnection(),
+ )
+
+ // For the default page size, we expect to get all results in one page
+ res, err := client.ListWorkloadIdentities(ctx, &workloadidentityv1pb.ListWorkloadIdentitiesRequest{})
+ require.NoError(t, err)
+ require.Len(t, res.WorkloadIdentities, 29)
+ require.Empty(t, res.NextPageToken)
+ for _, created := range created {
+ slices.ContainsFunc(res.WorkloadIdentities, func(resource *workloadidentityv1pb.WorkloadIdentity) bool {
+ return proto.Equal(created, resource)
+ })
+ }
+ })
+
+ t.Run("success - page size 10", func(t *testing.T) {
+ client := workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(
+ authorizedClient.GetConnection(),
+ )
+
+ fetched := []*workloadidentityv1pb.WorkloadIdentity{}
+ token := ""
+ iterations := 0
+ for {
+ iterations++
+ res, err := client.ListWorkloadIdentities(ctx, &workloadidentityv1pb.ListWorkloadIdentitiesRequest{
+ PageSize: 10,
+ PageToken: token,
+ })
+ require.NoError(t, err)
+ fetched = append(fetched, res.WorkloadIdentities...)
+ if res.NextPageToken == "" {
+ break
+ }
+ token = res.NextPageToken
+ }
+
+ require.Len(t, fetched, 29)
+ require.Equal(t, 3, iterations)
+ for _, created := range created {
+ slices.ContainsFunc(fetched, func(resource *workloadidentityv1pb.WorkloadIdentity) bool {
+ return proto.Equal(created, resource)
+ })
+ }
+ })
+}
+
+func TestResourceService_UpdateWorkloadIdentity(t *testing.T) {
+ t.Parallel()
+ srv, eventRecorder := newTestTLSServer(t)
+ ctx := context.Background()
+
+ authorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "authorized",
+ []string{},
+ []types.Rule{
+ {
+ Resources: []string{types.KindWorkloadIdentity},
+ Verbs: []string{types.VerbUpdate},
+ },
+ })
+ require.NoError(t, err)
+ authorizedClient, err := srv.NewClient(auth.TestUser(authorizedUser.GetName()))
+ require.NoError(t, err)
+ unauthorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "unauthorized",
+ []string{},
+ []types.Rule{},
+ )
+ require.NoError(t, err)
+ unauthorizedClient, err := srv.NewClient(auth.TestUser(unauthorizedUser.GetName()))
+ require.NoError(t, err)
+
+ // Create a pre-existing workload identity
+ preExisting, err := srv.Auth().CreateWorkloadIdentity(
+ ctx,
+ &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "preexisting",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ })
+ require.NoError(t, err)
+ preExisting2, err := srv.Auth().CreateWorkloadIdentity(
+ ctx,
+ &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "preexisting-2",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ client *authclient.Client
+ req *workloadidentityv1pb.UpdateWorkloadIdentityRequest
+ requireError require.ErrorAssertionFunc
+ checkResultReturned bool
+ requireEvent *events.WorkloadIdentityUpdate
+ }{
+ {
+ name: "success",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.UpdateWorkloadIdentityRequest{
+ WorkloadIdentity: preExisting,
+ },
+ requireError: require.NoError,
+ checkResultReturned: true,
+ requireEvent: &events.WorkloadIdentityUpdate{
+ Metadata: events.Metadata{
+ Code: libevents.WorkloadIdentityUpdateCode,
+ Type: libevents.WorkloadIdentityUpdateEvent,
+ },
+ ResourceMetadata: events.ResourceMetadata{
+ Name: preExisting.GetMetadata().GetName(),
+ },
+ UserMetadata: events.UserMetadata{
+ User: authorizedUser.GetName(),
+ UserKind: events.UserKind_USER_KIND_HUMAN,
+ },
+ },
+ },
+ {
+ name: "incorrect revision",
+ client: authorizedClient,
+ req: (func() *workloadidentityv1pb.UpdateWorkloadIdentityRequest {
+ preExisting2.Metadata.Revision = "incorrect"
+ return &workloadidentityv1pb.UpdateWorkloadIdentityRequest{
+ WorkloadIdentity: preExisting2,
+ }
+ })(),
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsCompareFailed(err))
+ },
+ },
+ {
+ name: "not existing",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.UpdateWorkloadIdentityRequest{
+ WorkloadIdentity: &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "new",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/test",
+ },
+ },
+ },
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.Error(t, err)
+ },
+ },
+ {
+ name: "unauthorized",
+ client: unauthorizedClient,
+ req: &workloadidentityv1pb.UpdateWorkloadIdentityRequest{
+ WorkloadIdentity: preExisting,
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsAccessDenied(err))
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ eventRecorder.Reset()
+ client := workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(
+ tt.client.GetConnection(),
+ )
+ res, err := client.UpdateWorkloadIdentity(ctx, tt.req)
+ tt.requireError(t, err)
+
+ if tt.checkResultReturned {
+ require.NotEmpty(t, res.Metadata.Revision)
+ require.NotEqual(t, tt.req.WorkloadIdentity.GetMetadata().GetRevision(), res.Metadata.Revision)
+ // Expect returned result to match request, but also have a
+ // revision
+ require.Empty(
+ t,
+ cmp.Diff(
+ res,
+ tt.req.WorkloadIdentity,
+ protocmp.Transform(),
+ protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
+ ),
+ )
+ // Expect the value fetched from the store to match returned
+ // item.
+ fetched, err := srv.Auth().GetWorkloadIdentity(ctx, res.Metadata.Name)
+ require.NoError(t, err)
+ require.Empty(
+ t,
+ cmp.Diff(
+ res,
+ fetched,
+ protocmp.Transform(),
+ ),
+ )
+ }
+ if tt.requireEvent != nil {
+ evt, ok := eventRecorder.LastEvent().(*events.WorkloadIdentityUpdate)
+ require.True(t, ok)
+ require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr)
+ require.Empty(t, cmp.Diff(
+ evt,
+ tt.requireEvent,
+ cmpopts.IgnoreFields(events.WorkloadIdentityUpdate{}, "ConnectionMetadata", "WorkloadIdentityData"),
+ ))
+ }
+ })
+ }
+}
+
+func TestResourceService_UpsertWorkloadIdentity(t *testing.T) {
+ t.Parallel()
+ srv, eventRecorder := newTestTLSServer(t)
+ ctx := context.Background()
+
+ authorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "authorized",
+ []string{},
+ []types.Rule{
+ {
+ Resources: []string{types.KindWorkloadIdentity},
+ Verbs: []string{types.VerbCreate, types.VerbUpdate},
+ },
+ })
+ require.NoError(t, err)
+ authorizedClient, err := srv.NewClient(auth.TestUser(authorizedUser.GetName()))
+ require.NoError(t, err)
+ unauthorizedUser, _, err := auth.CreateUserAndRole(
+ srv.Auth(),
+ "unauthorized",
+ []string{},
+ []types.Rule{},
+ )
+ require.NoError(t, err)
+ unauthorizedClient, err := srv.NewClient(auth.TestUser(unauthorizedUser.GetName()))
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ client *authclient.Client
+ req *workloadidentityv1pb.UpsertWorkloadIdentityRequest
+ requireError require.ErrorAssertionFunc
+ checkResultReturned bool
+ requireEvent *events.WorkloadIdentityCreate
+ }{
+ {
+ name: "success",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.UpsertWorkloadIdentityRequest{
+ WorkloadIdentity: &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "new",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ },
+ },
+ requireError: require.NoError,
+ checkResultReturned: true,
+ requireEvent: &events.WorkloadIdentityCreate{
+ Metadata: events.Metadata{
+ Code: libevents.WorkloadIdentityCreateCode,
+ Type: libevents.WorkloadIdentityCreateEvent,
+ },
+ ResourceMetadata: events.ResourceMetadata{
+ Name: "new",
+ },
+ UserMetadata: events.UserMetadata{
+ User: authorizedUser.GetName(),
+ UserKind: events.UserKind_USER_KIND_HUMAN,
+ },
+ },
+ },
+ {
+ name: "validation fail",
+ client: authorizedClient,
+ req: &workloadidentityv1pb.UpsertWorkloadIdentityRequest{
+ WorkloadIdentity: &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "new",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "",
+ },
+ },
+ },
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsBadParameter(err))
+ require.ErrorContains(t, err, "spec.spiffe.id: is required")
+ },
+ },
+ {
+ name: "unauthorized",
+ client: unauthorizedClient,
+ req: &workloadidentityv1pb.UpsertWorkloadIdentityRequest{
+ WorkloadIdentity: &workloadidentityv1pb.WorkloadIdentity{
+ Kind: types.KindWorkloadIdentity,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "unauthorized",
+ },
+ Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
+ Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
+ Id: "/example",
+ },
+ },
+ },
+ },
+ requireError: func(t require.TestingT, err error, i ...interface{}) {
+ require.True(t, trace.IsAccessDenied(err))
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ eventRecorder.Reset()
+ client := workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(
+ tt.client.GetConnection(),
+ )
+ res, err := client.UpsertWorkloadIdentity(ctx, tt.req)
+ tt.requireError(t, err)
+
+ if tt.checkResultReturned {
+ require.NotEmpty(t, res.Metadata.Revision)
+ // Expect returned result to match request, but also have a
+ // revision
+ require.Empty(
+ t,
+ cmp.Diff(
+ res,
+ tt.req.WorkloadIdentity,
+ protocmp.Transform(),
+ protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
+ ),
+ )
+ // Expect the value fetched from the store to match returned
+ // item.
+ fetched, err := srv.Auth().GetWorkloadIdentity(ctx, res.Metadata.Name)
+ require.NoError(t, err)
+ require.Empty(
+ t,
+ cmp.Diff(
+ res,
+ fetched,
+ protocmp.Transform(),
+ ),
+ )
+ }
+ if tt.requireEvent != nil {
+ evt, ok := eventRecorder.LastEvent().(*events.WorkloadIdentityCreate)
+ require.True(t, ok)
+ require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr)
+ require.Empty(t, cmp.Diff(
+ evt,
+ tt.requireEvent,
+ cmpopts.IgnoreFields(events.WorkloadIdentityCreate{}, "ConnectionMetadata", "WorkloadIdentityData"),
+ ))
+ }
+ })
+ }
+}
diff --git a/lib/services/local/spiffe_federations_test.go b/lib/services/local/spiffe_federations_test.go
index edc9a1ac3c1f9..7ce3ffaa25529 100644
--- a/lib/services/local/spiffe_federations_test.go
+++ b/lib/services/local/spiffe_federations_test.go
@@ -246,7 +246,7 @@ func TestSPIFFEFederationService_DeleteSPIFFEFederation(t *testing.T) {
require.True(t, trace.IsNotFound(err))
})
t.Run("not found", func(t *testing.T) {
- _, err := service.GetSPIFFEFederation(ctx, "foo.example.com")
+ err := service.DeleteSPIFFEFederation(ctx, "foo.example.com")
require.Error(t, err)
require.True(t, trace.IsNotFound(err))
})
diff --git a/lib/services/presets.go b/lib/services/presets.go
index 6af90e02110c4..d82ba05a4f4b2 100644
--- a/lib/services/presets.go
+++ b/lib/services/presets.go
@@ -184,6 +184,7 @@ func NewPresetEditorRole() types.Role {
types.NewRule(types.KindUserTask, RW()),
types.NewRule(types.KindIdentityCenter, RW()),
types.NewRule(types.KindContact, RW()),
+ types.NewRule(types.KindWorkloadIdentity, RW()),
},
},
},
@@ -614,6 +615,7 @@ func NewPresetTerraformProviderRole() types.Role {
types.KindInstaller,
types.KindAccessMonitoringRule,
types.KindStaticHostUser,
+ types.KindWorkloadIdentity,
},
Verbs: RW(),
},