diff --git a/api/types/constants.go b/api/types/constants.go
index 9f11b337a33a6..67582ecbabe30 100644
--- a/api/types/constants.go
+++ b/api/types/constants.go
@@ -71,6 +71,8 @@ const (
KindBotInstance = "bot_instance"
// KindSPIFFEFederation is a SPIFFE federation resource
KindSPIFFEFederation = "spiffe_federation"
+ // KindWorkloadIdentity is a workload identity resource
+ KindWorkloadIdentity = "workload_identity"
// KindHostCert is a host certificate
KindHostCert = "host_cert"
diff --git a/api/types/events/oneof.go b/api/types/events/oneof.go
index 8f3c73e7ceb24..d71721fba910f 100644
--- a/api/types/events/oneof.go
+++ b/api/types/events/oneof.go
@@ -790,6 +790,7 @@ func ToOneOf(in AuditEvent) (*OneOf, error) {
out.Event = &OneOf_AutoUpdateVersionDelete{
AutoUpdateVersionDelete: e,
}
+
case *WorkloadIdentityCreate:
out.Event = &OneOf_WorkloadIdentityCreate{
WorkloadIdentityCreate: e,
diff --git a/lib/auth/machineid/workloadidentityv1/resource_service.go b/lib/auth/machineid/workloadidentityv1/resource_service.go
new file mode 100644
index 0000000000000..46ade70df0645
--- /dev/null
+++ b/lib/auth/machineid/workloadidentityv1/resource_service.go
@@ -0,0 +1,278 @@
+// 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/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
+}
+
+// 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.SPIFFEFederationDeleteCode,
+ Type: events.SPIFFEFederationDeleteEvent,
+ },
+ 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)
+ }
+
+ if err := s.emitter.EmitAuditEvent(ctx, &apievents.WorkloadIdentityCreate{
+ Metadata: apievents.Metadata{
+ Code: events.SPIFFEFederationCreateCode,
+ Type: events.SPIFFEFederationCreateEvent,
+ },
+ UserMetadata: authz.ClientUserMetadata(ctx),
+ ConnectionMetadata: authz.ConnectionMetadata(ctx),
+ ResourceMetadata: apievents.ResourceMetadata{
+ Name: req.WorkloadIdentity.Metadata.Name,
+ },
+ }); 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.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.VerbUpdate); 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)
+ }
+
+ if err := s.emitter.EmitAuditEvent(ctx, &apievents.WorkloadIdentityUpdate{
+ Metadata: apievents.Metadata{
+ Code: events.SPIFFEFederationCreateCode,
+ Type: events.SPIFFEFederationCreateEvent,
+ },
+ UserMetadata: authz.ClientUserMetadata(ctx),
+ ConnectionMetadata: authz.ConnectionMetadata(ctx),
+ ResourceMetadata: apievents.ResourceMetadata{
+ Name: req.WorkloadIdentity.Metadata.Name,
+ },
+ }); err != nil {
+ s.logger.ErrorContext(
+ ctx, "Failed to emit audit event for creation of WorkloadIdentity",
+ "error", err,
+ )
+ }
+
+ return created, nil
+}
diff --git a/lib/events/api.go b/lib/events/api.go
index 92eafcff2b8d9..25175d5472f56 100644
--- a/lib/events/api.go
+++ b/lib/events/api.go
@@ -766,6 +766,13 @@ const (
// SPIFFEFederationDeleteEvent is emitted when a SPIFFE federation is deleted.
SPIFFEFederationDeleteEvent = "spiffe.federation.delete"
+ // WorkloadIdentityCreateEvent is emitted when a WorkloadIdentity is created.
+ WorkloadIdentityCreateEvent = "workload_identity.create"
+ // WorkloadIdentityUpdateEvent is emitted when a WorkloadIdentity is updated.
+ WorkloadIdentityUpdateEvent = "workload_identity.update"
+ // WorkloadIdentityDeleteEvent is emitted when a WorkloadIdentity is deleted.
+ WorkloadIdentityDeleteEvent = "workload_identity.delete"
+
// AuthPreferenceUpdateEvent is emitted when a user updates the cluster authentication preferences.
AuthPreferenceUpdateEvent = "auth_preference.update"
// ClusterNetworkingConfigUpdateEvent is emitted when a user updates the cluster networking configuration.
diff --git a/lib/events/codes.go b/lib/events/codes.go
index bb916ea2df543..d1c9837b485ab 100644
--- a/lib/events/codes.go
+++ b/lib/events/codes.go
@@ -427,6 +427,13 @@ const (
// BotDeleteCode is the `bot.delete` event code.
BotDeleteCode = "TB003I"
+ // WorkloadIdentityCreateCode is the `workload_identity.create` event code.
+ WorkloadIdentityCreateCode = "TWID001I"
+ // WorkloadIdentityDeleteCode is the `workload_identity.delete` event code.
+ WorkloadIdentityDeleteCode = "TWID002I"
+ // WorkloadIdentityUpdateCode is the `workload_identity.update` event code.
+ WorkloadIdentityUpdateCode = "TWID003I"
+
// LockCreatedCode is the lock created event code.
LockCreatedCode = "TLK00I"
// LockDeletedCode is the lock deleted event code.
diff --git a/lib/services/local/workload_identity.go b/lib/services/local/workload_identity.go
new file mode 100644
index 0000000000000..49b6da073a928
--- /dev/null
+++ b/lib/services/local/workload_identity.go
@@ -0,0 +1,115 @@
+// 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 local
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+
+ workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/backend"
+ "github.com/gravitational/teleport/lib/services"
+ "github.com/gravitational/teleport/lib/services/local/generic"
+)
+
+const (
+ workloadIdentityPrefix = "workload_identity"
+)
+
+// WorkloadIdentityService exposes backend functionality for storing
+// WorkloadIdentity resxources
+type WorkloadIdentityService struct {
+ service *generic.ServiceWrapper[*workloadidentityv1pb.WorkloadIdentity]
+}
+
+// NewWorkloadIdentityService creates a new WorkloadIdentityService
+func NewWorkloadIdentityService(b backend.Backend) (*WorkloadIdentityService, error) {
+ service, err := generic.NewServiceWrapper(
+ generic.ServiceWrapperConfig[*workloadidentityv1pb.WorkloadIdentity]{
+ Backend: b,
+ ResourceKind: types.KindWorkloadIdentity,
+ BackendPrefix: backend.NewKey(workloadIdentityPrefix),
+ MarshalFunc: services.MarshalWorkloadIdentity,
+ UnmarshalFunc: services.UnmarshalWorkloadIdentity,
+ ValidateFunc: services.ValidateWorkloadIdentity,
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &WorkloadIdentityService{
+ service: service,
+ }, nil
+}
+
+// CreateWorkloadIdentity inserts a new WorkloadIdentity into the backend.
+func (b *WorkloadIdentityService) CreateWorkloadIdentity(
+ ctx context.Context, resource *workloadidentityv1pb.WorkloadIdentity,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ created, err := b.service.CreateResource(ctx, resource)
+ return created, trace.Wrap(err)
+}
+
+// GetWorkloadIdentity retrieves a specific WorkloadIdentity given a name
+func (b *WorkloadIdentityService) GetWorkloadIdentity(
+ ctx context.Context, name string,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ resource, err := b.service.GetResource(ctx, name)
+ return resource, trace.Wrap(err)
+}
+
+// ListWorkloadIdentities lists all WorkloadIdentitys using a given page size
+// and last key.
+func (b *WorkloadIdentityService) ListWorkloadIdentities(
+ ctx context.Context, pageSize int, currentToken string,
+) ([]*workloadidentityv1pb.WorkloadIdentity, string, error) {
+ r, nextToken, err := b.service.ListResources(ctx, pageSize, currentToken)
+ return r, nextToken, trace.Wrap(err)
+}
+
+// DeleteWorkloadIdentity deletes a specific WorkloadIdentitys.
+func (b *WorkloadIdentityService) DeleteWorkloadIdentity(
+ ctx context.Context, name string,
+) error {
+ return trace.Wrap(b.service.DeleteResource(ctx, name))
+}
+
+// DeleteAllWorkloadIdentitys deletes all SPIFFE resources, this is typically
+// only meant to be used by the cache.
+func (b *WorkloadIdentityService) DeleteAllWorkloadIdentitys(
+ ctx context.Context,
+) error {
+ return trace.Wrap(b.service.DeleteAllResources(ctx))
+}
+
+// UpsertWorkloadIdentity upserts a WorkloadIdentitys. Prefer using
+// CreateWorkloadIdentity. This is only designed for usage by the cache.
+func (b *WorkloadIdentityService) UpsertWorkloadIdentity(
+ ctx context.Context, resource *workloadidentityv1pb.WorkloadIdentity,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ upserted, err := b.service.UpsertResource(ctx, resource)
+ return upserted, trace.Wrap(err)
+}
+
+// UpdateWorkloadIdentity updates a specific WorkloadIdentity.
+func (b *WorkloadIdentityService) UpdateWorkloadIdentity(
+ ctx context.Context, resource *workloadidentityv1pb.WorkloadIdentity,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ updated, err := b.service.ConditionalUpdateResource(ctx, resource)
+ return updated, trace.Wrap(err)
+}
diff --git a/lib/services/local/workload_identity_test.go b/lib/services/local/workload_identity_test.go
new file mode 100644
index 0000000000000..1aa4a046eb79f
--- /dev/null
+++ b/lib/services/local/workload_identity_test.go
@@ -0,0 +1,17 @@
+// 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 local
diff --git a/lib/services/workload_identity.go b/lib/services/workload_identity.go
new file mode 100644
index 0000000000000..1bb2a13d94337
--- /dev/null
+++ b/lib/services/workload_identity.go
@@ -0,0 +1,91 @@
+// 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 services
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+
+ machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
+ workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
+ "github.com/gravitational/teleport/api/types"
+)
+
+// WorkloadIdentities is an interface over the WorkloadIdentities service. This
+// interface may also be implemented by a client to allow remote and local
+// consumers to access the resource in a similar way.
+type WorkloadIdentities interface {
+ // GetWorkloadIdentity gets a SPIFFE Federation by name.
+ GetWorkloadIdentity(
+ ctx context.Context, name string,
+ ) (*workloadidentityv1pb.WorkloadIdentity, error)
+ // ListWorkloadIdentities lists all WorkloadIdentities using Google style
+ // pagination.
+ ListWorkloadIdentities(
+ ctx context.Context, pageSize int, lastToken string,
+ ) ([]*workloadidentityv1pb.WorkloadIdentity, string, error)
+ // CreateWorkloadIdentity creates a new WorkloadIdentity.
+ CreateWorkloadIdentity(
+ ctx context.Context, workloadIdentity *workloadidentityv1pb.WorkloadIdentity,
+ ) (*workloadidentityv1pb.WorkloadIdentity, error)
+ // DeleteWorkloadIdentity deletes a SPIFFE Federation by name.
+ DeleteWorkloadIdentity(ctx context.Context, name string) error
+ // UpdateWorkloadIdentity updates a WorkloadIdentity. It will not act if the
+ // resource is not found or where the revision does not match.
+ UpdateWorkloadIdentity(
+ ctx context.Context, workloadIdentity *workloadidentityv1pb.WorkloadIdentity,
+ ) (*workloadidentityv1pb.WorkloadIdentity, error)
+}
+
+// MarshalWorkloadIdentity marshals the WorkloadIdentity object into a JSON byte
+// array.
+func MarshalWorkloadIdentity(
+ object *workloadidentityv1pb.WorkloadIdentity, opts ...MarshalOption,
+) ([]byte, error) {
+ return MarshalProtoResource(object, opts...)
+}
+
+// UnmarshalWorkloadIdentity unmarshals the WorkloadIdentity object from a
+// JSON byte array.
+func UnmarshalWorkloadIdentity(
+ data []byte, opts ...MarshalOption,
+) (*workloadidentityv1pb.WorkloadIdentity, error) {
+ return UnmarshalProtoResource[*workloadidentityv1pb.WorkloadIdentity](data, opts...)
+}
+
+// ValidateWorkloadIdentity validates the WorkloadIdentity object. This is
+// performed prior to writing to the backend.
+func ValidateWorkloadIdentity(s *workloadidentityv1pb.WorkloadIdentity) error {
+ switch {
+ case s == nil:
+ return trace.BadParameter("object cannot be nil")
+ case s.Version != types.V1:
+ return trace.BadParameter("version: only %q is supported", types.V1)
+ case s.Kind != types.KindWorkloadIdentity:
+ return trace.BadParameter("kind: must be %q", types.KindWorkloadIdentity)
+ case s.Metadata == nil:
+ return trace.BadParameter("metadata: is required")
+ case s.Metadata.Name == "":
+ return trace.BadParameter("metadata.name: is required")
+ case s.Spec == nil:
+ return trace.BadParameter("spec: is required")
+ }
+
+ // TODO: More validation here!!
+ return nil
+}
diff --git a/lib/services/workload_identity_test.go b/lib/services/workload_identity_test.go
new file mode 100644
index 0000000000000..15ad8316c6bd2
--- /dev/null
+++ b/lib/services/workload_identity_test.go
@@ -0,0 +1,17 @@
+// 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 services