mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 05:55:42 +08:00
Add GRPC implementation of RevocationService (#52030)
* Add implementation of RevocationService * Wire into grpc service * Add to preset editor role * TestRevocationService_CreateWorkloadIdentityX509Revocation * TestRevocationService_DeleteWorkloadIdentityX509Revocation * TestRevocationService_GetWorkloadIdentityX509Revocation * TestRevocationService_ListWorkloadIdentityX509Revocations * TestRevocationService_UpdateWorkloadIdentityX509Revocation * TestRevocationService_UpsertWorkloadIdentityX509Revocation * Update preset role dump * Fix tests post rebase
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -5290,6 +5290,17 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {
|
||||
}
|
||||
workloadidentityv1pb.RegisterWorkloadIdentityIssuanceServiceServer(server, workloadIdentityIssuanceService)
|
||||
|
||||
workloadIdentityRevocationService, err := workloadidentityv1.NewRevocationService(&workloadidentityv1.RevocationServiceConfig{
|
||||
Authorizer: cfg.Authorizer,
|
||||
Emitter: cfg.Emitter,
|
||||
Clock: cfg.AuthServer.GetClock(),
|
||||
Store: cfg.AuthServer.Services.WorkloadIdentityX509Revocations,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "creating workload identity issuance service")
|
||||
}
|
||||
workloadidentityv1pb.RegisterWorkloadIdentityRevocationServiceServer(server, workloadIdentityRevocationService)
|
||||
|
||||
dbObjectImportRuleService, err := dbobjectimportrulev1.NewDatabaseObjectImportRuleService(dbobjectimportrulev1.DatabaseObjectImportRuleServiceConfig{
|
||||
Authorizer: cfg.Authorizer,
|
||||
Backend: cfg.AuthServer.Services,
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2025 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 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 workloadIdentityX509RevocationReadWriter interface {
|
||||
GetWorkloadIdentityX509Revocation(ctx context.Context, name string) (*workloadidentityv1pb.WorkloadIdentityX509Revocation, error)
|
||||
ListWorkloadIdentityX509Revocations(ctx context.Context, pageSize int, token string) ([]*workloadidentityv1pb.WorkloadIdentityX509Revocation, string, error)
|
||||
CreateWorkloadIdentityX509Revocation(ctx context.Context, resource *workloadidentityv1pb.WorkloadIdentityX509Revocation) (*workloadidentityv1pb.WorkloadIdentityX509Revocation, error)
|
||||
UpdateWorkloadIdentityX509Revocation(ctx context.Context, resource *workloadidentityv1pb.WorkloadIdentityX509Revocation) (*workloadidentityv1pb.WorkloadIdentityX509Revocation, error)
|
||||
DeleteWorkloadIdentityX509Revocation(ctx context.Context, name string) error
|
||||
UpsertWorkloadIdentityX509Revocation(ctx context.Context, resource *workloadidentityv1pb.WorkloadIdentityX509Revocation) (*workloadidentityv1pb.WorkloadIdentityX509Revocation, error)
|
||||
}
|
||||
|
||||
// RevocationServiceConfig holds configuration options for the RevocationService.
|
||||
type RevocationServiceConfig struct {
|
||||
Authorizer authz.Authorizer
|
||||
Store workloadIdentityX509RevocationReadWriter
|
||||
Clock clockwork.Clock
|
||||
Emitter apievents.Emitter
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
// RevocationService is the gRPC service for managing workload identity
|
||||
// revocations.
|
||||
// It implements the workloadidentityv1pb.WorkloadIdentityRevocationServiceServer
|
||||
type RevocationService struct {
|
||||
workloadidentityv1pb.UnimplementedWorkloadIdentityRevocationServiceServer
|
||||
|
||||
authorizer authz.Authorizer
|
||||
store workloadIdentityX509RevocationReadWriter
|
||||
clock clockwork.Clock
|
||||
emitter apievents.Emitter
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewRevocationService returns a new instance of the RevocationService.
|
||||
func NewRevocationService(cfg *RevocationServiceConfig) (*RevocationService, error) {
|
||||
switch {
|
||||
case cfg.Store == nil:
|
||||
return nil, trace.BadParameter("store 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_revocation.service")
|
||||
}
|
||||
if cfg.Clock == nil {
|
||||
cfg.Clock = clockwork.NewRealClock()
|
||||
}
|
||||
return &RevocationService{
|
||||
authorizer: cfg.Authorizer,
|
||||
store: cfg.Store,
|
||||
clock: cfg.Clock,
|
||||
emitter: cfg.Emitter,
|
||||
logger: cfg.Logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWorkloadIdentityX509Revocation returns a WorkloadIdentityX509Revocation
|
||||
// by name. An error is returned if the resource does not exist.
|
||||
// Implements teleport.workloadidentity.v1.RevocationService/GetWorkloadIdentityX509Revocation
|
||||
func (s *RevocationService) GetWorkloadIdentityX509Revocation(
|
||||
ctx context.Context, req *workloadidentityv1pb.GetWorkloadIdentityX509RevocationRequest,
|
||||
) (*workloadidentityv1pb.WorkloadIdentityX509Revocation, error) {
|
||||
authCtx, err := s.authorizer.Authorize(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentityX509Revocation, types.VerbRead); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
return nil, trace.BadParameter("name: must be non-empty")
|
||||
}
|
||||
|
||||
resource, err := s.store.GetWorkloadIdentityX509Revocation(ctx, req.Name)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
// ListWorkloadIdentityX509Revocations returns a list of
|
||||
// WorkloadIdentityX509Revocation resources. It follows the Google API design
|
||||
// guidelines for list pagination.
|
||||
// Implements teleport.workloadidentity.v1.RevocationService/ListWorkloadIdentityX509Revocations
|
||||
func (s *RevocationService) ListWorkloadIdentityX509Revocations(
|
||||
ctx context.Context, req *workloadidentityv1pb.ListWorkloadIdentityX509RevocationsRequest,
|
||||
) (*workloadidentityv1pb.ListWorkloadIdentityX509RevocationsResponse, error) {
|
||||
authCtx, err := s.authorizer.Authorize(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentityX509Revocation, types.VerbRead, types.VerbList); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
resources, nextToken, err := s.store.ListWorkloadIdentityX509Revocations(
|
||||
ctx,
|
||||
int(req.PageSize),
|
||||
req.PageToken,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &workloadidentityv1pb.ListWorkloadIdentityX509RevocationsResponse{
|
||||
WorkloadIdentityX509Revocations: resources,
|
||||
NextPageToken: nextToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteWorkloadIdentityX509Revocation deletes a WorkloadIdentityX509Revocation
|
||||
// by name. An error is returned if the resource does not exist.
|
||||
// Implements teleport.workloadidentity.v1.RevocationService/DeleteWorkloadIdentityX509Revocation
|
||||
func (s *RevocationService) DeleteWorkloadIdentityX509Revocation(
|
||||
ctx context.Context, req *workloadidentityv1pb.DeleteWorkloadIdentityX509RevocationRequest,
|
||||
) (*emptypb.Empty, error) {
|
||||
authCtx, err := s.authorizer.Authorize(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentityX509Revocation, 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.store.DeleteWorkloadIdentityX509Revocation(ctx, req.Name); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
evt := &apievents.WorkloadIdentityX509RevocationDelete{
|
||||
Metadata: apievents.Metadata{
|
||||
Code: events.WorkloadIdentityX509RevocationDeleteCode,
|
||||
Type: events.WorkloadIdentityX509RevocationDeleteEvent,
|
||||
},
|
||||
UserMetadata: authz.ClientUserMetadata(ctx),
|
||||
ConnectionMetadata: authz.ConnectionMetadata(ctx),
|
||||
ResourceMetadata: apievents.ResourceMetadata{
|
||||
Name: req.Name,
|
||||
},
|
||||
}
|
||||
if err := s.emitter.EmitAuditEvent(ctx, evt); err != nil {
|
||||
s.logger.ErrorContext(
|
||||
ctx, "Failed to emit audit event for UpsertWorkloadIdentityX509Revocation",
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// CreateWorkloadIdentityX509Revocation creates a new WorkloadIdentityX509Revocation.
|
||||
// Implements teleport.workloadidentity.v1.RevocationService/CreateWorkloadIdentityX509Revocation
|
||||
func (s *RevocationService) CreateWorkloadIdentityX509Revocation(
|
||||
ctx context.Context, req *workloadidentityv1pb.CreateWorkloadIdentityX509RevocationRequest,
|
||||
) (*workloadidentityv1pb.WorkloadIdentityX509Revocation, error) {
|
||||
authCtx, err := s.authorizer.Authorize(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentityX509Revocation, types.VerbCreate); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := authCtx.AuthorizeAdminAction(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
created, err := s.store.CreateWorkloadIdentityX509Revocation(ctx, req.WorkloadIdentityX509Revocation)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
evt := &apievents.WorkloadIdentityX509RevocationCreate{
|
||||
Metadata: apievents.Metadata{
|
||||
Code: events.WorkloadIdentityX509RevocationCreateCode,
|
||||
Type: events.WorkloadIdentityX509RevocationCreateEvent,
|
||||
},
|
||||
UserMetadata: authz.ClientUserMetadata(ctx),
|
||||
ConnectionMetadata: authz.ConnectionMetadata(ctx),
|
||||
ResourceMetadata: apievents.ResourceMetadata{
|
||||
Name: created.GetMetadata().GetName(),
|
||||
},
|
||||
Reason: created.GetSpec().GetReason(),
|
||||
}
|
||||
if err := s.emitter.EmitAuditEvent(ctx, evt); err != nil {
|
||||
s.logger.ErrorContext(
|
||||
ctx, "Failed to emit audit event for CreateWorkloadIdentityX509Revocation",
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// UpdateWorkloadIdentityX509Revocation updates an existing
|
||||
// WorkloadIdentityX509Revocation.
|
||||
// Implements teleport.workloadidentity.v1.RevocationService/UpdateWorkloadIdentityX509Revocation
|
||||
func (s *RevocationService) UpdateWorkloadIdentityX509Revocation(
|
||||
ctx context.Context, req *workloadidentityv1pb.UpdateWorkloadIdentityX509RevocationRequest,
|
||||
) (*workloadidentityv1pb.WorkloadIdentityX509Revocation, error) {
|
||||
authCtx, err := s.authorizer.Authorize(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := authCtx.CheckAccessToKind(types.KindWorkloadIdentityX509Revocation, types.VerbUpdate); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := authCtx.AuthorizeAdminAction(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
created, err := s.store.UpdateWorkloadIdentityX509Revocation(ctx, req.WorkloadIdentityX509Revocation)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
evt := &apievents.WorkloadIdentityX509RevocationUpdate{
|
||||
Metadata: apievents.Metadata{
|
||||
Code: events.WorkloadIdentityX509RevocationUpdateCode,
|
||||
Type: events.WorkloadIdentityX509RevocationUpdateEvent,
|
||||
},
|
||||
UserMetadata: authz.ClientUserMetadata(ctx),
|
||||
ConnectionMetadata: authz.ConnectionMetadata(ctx),
|
||||
ResourceMetadata: apievents.ResourceMetadata{
|
||||
Name: created.GetMetadata().GetName(),
|
||||
},
|
||||
Reason: created.GetSpec().GetReason(),
|
||||
}
|
||||
if err := s.emitter.EmitAuditEvent(ctx, evt); err != nil {
|
||||
s.logger.ErrorContext(
|
||||
ctx, "Failed to emit audit event for UpdateWorkloadIdentityX509Revocation",
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// UpsertWorkloadIdentityX509Revocation updates or creates an existing
|
||||
// WorkloadIdentityX509Revocation.
|
||||
// Implements teleport.workloadidentity.v1.RevocationService/UpsertWorkloadIdentityX509Revocation
|
||||
func (s *RevocationService) UpsertWorkloadIdentityX509Revocation(
|
||||
ctx context.Context, req *workloadidentityv1pb.UpsertWorkloadIdentityX509RevocationRequest,
|
||||
) (*workloadidentityv1pb.WorkloadIdentityX509Revocation, error) {
|
||||
authCtx, err := s.authorizer.Authorize(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := authCtx.CheckAccessToKind(
|
||||
types.KindWorkloadIdentityX509Revocation, 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.store.UpsertWorkloadIdentityX509Revocation(ctx, req.WorkloadIdentityX509Revocation)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
evt := &apievents.WorkloadIdentityX509RevocationCreate{
|
||||
Metadata: apievents.Metadata{
|
||||
Code: events.WorkloadIdentityX509RevocationCreateCode,
|
||||
Type: events.WorkloadIdentityX509RevocationCreateEvent,
|
||||
},
|
||||
UserMetadata: authz.ClientUserMetadata(ctx),
|
||||
ConnectionMetadata: authz.ConnectionMetadata(ctx),
|
||||
ResourceMetadata: apievents.ResourceMetadata{
|
||||
Name: created.GetMetadata().GetName(),
|
||||
},
|
||||
Reason: created.GetSpec().GetReason(),
|
||||
}
|
||||
if err := s.emitter.EmitAuditEvent(ctx, evt); err != nil {
|
||||
s.logger.ErrorContext(
|
||||
ctx, "Failed to emit audit event for UpsertWorkloadIdentityX509Revocation",
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
apiproto "github.com/gravitational/teleport/api/client/proto"
|
||||
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
||||
@@ -2084,3 +2085,907 @@ func TestResourceService_UpsertWorkloadIdentity(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevocationService_CreateWorkloadIdentityX509Revocation(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.KindWorkloadIdentityX509Revocation},
|
||||
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 revocation
|
||||
preExisting, err := srv.Auth().CreateWorkloadIdentityX509Revocation(
|
||||
ctx,
|
||||
&workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccdd",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
client *authclient.Client
|
||||
req *workloadidentityv1pb.CreateWorkloadIdentityX509RevocationRequest
|
||||
requireError require.ErrorAssertionFunc
|
||||
checkResultReturned bool
|
||||
requireEvent *events.WorkloadIdentityX509RevocationCreate
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.CreateWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: &workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aa",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
requireError: require.NoError,
|
||||
checkResultReturned: true,
|
||||
requireEvent: &events.WorkloadIdentityX509RevocationCreate{
|
||||
Metadata: events.Metadata{
|
||||
Code: libevents.WorkloadIdentityX509RevocationCreateCode,
|
||||
Type: libevents.WorkloadIdentityX509RevocationCreateEvent,
|
||||
},
|
||||
ResourceMetadata: events.ResourceMetadata{
|
||||
Name: "aa",
|
||||
},
|
||||
UserMetadata: events.UserMetadata{
|
||||
User: authorizedUser.GetName(),
|
||||
UserKind: events.UserKind_USER_KIND_HUMAN,
|
||||
},
|
||||
Reason: "compromised",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pre-existing",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.CreateWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: preExisting,
|
||||
},
|
||||
requireError: func(t require.TestingT, err error, i ...interface{}) {
|
||||
require.True(t, trace.IsAlreadyExists(err))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validation fail",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.CreateWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: &workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "bb",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
requireError: func(t require.TestingT, err error, i ...interface{}) {
|
||||
require.True(t, trace.IsBadParameter(err))
|
||||
require.ErrorContains(t, err, "spec.reason: is required")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unauthorized",
|
||||
client: unauthorizedClient,
|
||||
req: &workloadidentityv1pb.CreateWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: &workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "cc",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
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.NewWorkloadIdentityRevocationServiceClient(
|
||||
tt.client.GetConnection(),
|
||||
)
|
||||
res, err := client.CreateWorkloadIdentityX509Revocation(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.WorkloadIdentityX509Revocation,
|
||||
protocmp.Transform(),
|
||||
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
||||
),
|
||||
)
|
||||
// Expect the value fetched from the store to match returned
|
||||
// item.
|
||||
fetched, err := srv.Auth().GetWorkloadIdentityX509Revocation(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.WorkloadIdentityX509RevocationCreate)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr)
|
||||
require.Empty(t, cmp.Diff(
|
||||
evt,
|
||||
tt.requireEvent,
|
||||
cmpopts.IgnoreFields(events.WorkloadIdentityX509RevocationCreate{}, "ConnectionMetadata"),
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevocationService_DeleteWorkloadIdentityX509Revocation(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.KindWorkloadIdentityX509Revocation},
|
||||
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 revocation
|
||||
preExisting, err := srv.Auth().CreateWorkloadIdentityX509Revocation(
|
||||
ctx,
|
||||
&workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccdd",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
client *authclient.Client
|
||||
req *workloadidentityv1pb.DeleteWorkloadIdentityX509RevocationRequest
|
||||
requireError require.ErrorAssertionFunc
|
||||
checkNonExisting bool
|
||||
requireEvent *events.WorkloadIdentityX509RevocationDelete
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.DeleteWorkloadIdentityX509RevocationRequest{
|
||||
Name: preExisting.GetMetadata().GetName(),
|
||||
},
|
||||
requireError: require.NoError,
|
||||
checkNonExisting: true,
|
||||
requireEvent: &events.WorkloadIdentityX509RevocationDelete{
|
||||
Metadata: events.Metadata{
|
||||
Code: libevents.WorkloadIdentityX509RevocationDeleteCode,
|
||||
Type: libevents.WorkloadIdentityX509RevocationDeleteEvent,
|
||||
},
|
||||
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.DeleteWorkloadIdentityX509RevocationRequest{
|
||||
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.DeleteWorkloadIdentityX509RevocationRequest{
|
||||
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.DeleteWorkloadIdentityX509RevocationRequest{
|
||||
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.NewWorkloadIdentityRevocationServiceClient(
|
||||
tt.client.GetConnection(),
|
||||
)
|
||||
_, err := client.DeleteWorkloadIdentityX509Revocation(ctx, tt.req)
|
||||
tt.requireError(t, err)
|
||||
|
||||
if tt.checkNonExisting {
|
||||
_, err := srv.Auth().GetWorkloadIdentityX509Revocation(ctx, tt.req.Name)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
}
|
||||
if tt.requireEvent != nil {
|
||||
evt, ok := eventRecorder.LastEvent().(*events.WorkloadIdentityX509RevocationDelete)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr)
|
||||
require.Empty(t, cmp.Diff(
|
||||
tt.requireEvent,
|
||||
evt,
|
||||
cmpopts.IgnoreFields(events.WorkloadIdentityX509RevocationDelete{}, "ConnectionMetadata"),
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevocationService_GetWorkloadIdentityX509Revocation(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv, _ := newTestTLSServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
authorizedUser, _, err := auth.CreateUserAndRole(
|
||||
srv.Auth(),
|
||||
"authorized",
|
||||
[]string{},
|
||||
[]types.Rule{
|
||||
{
|
||||
Resources: []string{types.KindWorkloadIdentityX509Revocation},
|
||||
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 revocation
|
||||
preExisting, err := srv.Auth().CreateWorkloadIdentityX509Revocation(
|
||||
ctx,
|
||||
&workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccdd",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
client *authclient.Client
|
||||
req *workloadidentityv1pb.GetWorkloadIdentityX509RevocationRequest
|
||||
wantRes *workloadidentityv1pb.WorkloadIdentityX509Revocation
|
||||
requireError require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.GetWorkloadIdentityX509RevocationRequest{
|
||||
Name: preExisting.GetMetadata().GetName(),
|
||||
},
|
||||
wantRes: preExisting,
|
||||
requireError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "non-existing",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.GetWorkloadIdentityX509RevocationRequest{
|
||||
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.GetWorkloadIdentityX509RevocationRequest{
|
||||
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.GetWorkloadIdentityX509RevocationRequest{
|
||||
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.NewWorkloadIdentityRevocationServiceClient(
|
||||
tt.client.GetConnection(),
|
||||
)
|
||||
got, err := client.GetWorkloadIdentityX509Revocation(ctx, tt.req)
|
||||
tt.requireError(t, err)
|
||||
|
||||
if tt.wantRes != nil {
|
||||
require.Empty(
|
||||
t,
|
||||
cmp.Diff(
|
||||
tt.wantRes,
|
||||
got,
|
||||
protocmp.Transform(),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevocationService_ListWorkloadIdentityX509Revocations(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv, _ := newTestTLSServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
authorizedUser, _, err := auth.CreateUserAndRole(
|
||||
srv.Auth(),
|
||||
"authorized",
|
||||
[]string{},
|
||||
[]types.Rule{
|
||||
{
|
||||
Resources: []string{types.KindWorkloadIdentityX509Revocation},
|
||||
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 identitie revocations
|
||||
// Two complete pages of ten, plus one incomplete page of nine
|
||||
created := []*workloadidentityv1pb.WorkloadIdentityX509Revocation{}
|
||||
for i := 0; i < 29; i++ {
|
||||
r, err := srv.Auth().CreateWorkloadIdentityX509Revocation(
|
||||
ctx,
|
||||
&workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: fmt.Sprintf("%d%d", i, i),
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
created = append(created, r)
|
||||
}
|
||||
|
||||
t.Run("unauthorized", func(t *testing.T) {
|
||||
client := workloadidentityv1pb.NewWorkloadIdentityRevocationServiceClient(
|
||||
unauthorizedClient.GetConnection(),
|
||||
)
|
||||
|
||||
_, err := client.ListWorkloadIdentityX509Revocations(
|
||||
ctx,
|
||||
&workloadidentityv1pb.ListWorkloadIdentityX509RevocationsRequest{},
|
||||
)
|
||||
require.True(t, trace.IsAccessDenied(err))
|
||||
})
|
||||
|
||||
t.Run("success - default page", func(t *testing.T) {
|
||||
client := workloadidentityv1pb.NewWorkloadIdentityRevocationServiceClient(
|
||||
authorizedClient.GetConnection(),
|
||||
)
|
||||
|
||||
// For the default page size, we expect to get all results in one page
|
||||
res, err := client.ListWorkloadIdentityX509Revocations(ctx, &workloadidentityv1pb.ListWorkloadIdentityX509RevocationsRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.WorkloadIdentityX509Revocations, 29)
|
||||
require.Empty(t, res.NextPageToken)
|
||||
for _, created := range created {
|
||||
slices.ContainsFunc(res.WorkloadIdentityX509Revocations, func(resource *workloadidentityv1pb.WorkloadIdentityX509Revocation) bool {
|
||||
return proto.Equal(created, resource)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success - page size 10", func(t *testing.T) {
|
||||
client := workloadidentityv1pb.NewWorkloadIdentityRevocationServiceClient(
|
||||
authorizedClient.GetConnection(),
|
||||
)
|
||||
|
||||
fetched := []*workloadidentityv1pb.WorkloadIdentityX509Revocation{}
|
||||
token := ""
|
||||
iterations := 0
|
||||
for {
|
||||
iterations++
|
||||
res, err := client.ListWorkloadIdentityX509Revocations(ctx, &workloadidentityv1pb.ListWorkloadIdentityX509RevocationsRequest{
|
||||
PageSize: 10,
|
||||
PageToken: token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
fetched = append(fetched, res.WorkloadIdentityX509Revocations...)
|
||||
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.WorkloadIdentityX509Revocation) bool {
|
||||
return proto.Equal(created, resource)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRevocationService_UpdateWorkloadIdentityX509Revocation(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.KindWorkloadIdentityX509Revocation},
|
||||
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 revocation
|
||||
preExisting, err := srv.Auth().CreateWorkloadIdentityX509Revocation(
|
||||
ctx,
|
||||
&workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccdd",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Create a pre-existing workload identity revocation
|
||||
preExisting2, err := srv.Auth().CreateWorkloadIdentityX509Revocation(
|
||||
ctx,
|
||||
&workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccee",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
client *authclient.Client
|
||||
req *workloadidentityv1pb.UpdateWorkloadIdentityX509RevocationRequest
|
||||
requireError require.ErrorAssertionFunc
|
||||
checkResultReturned bool
|
||||
requireEvent *events.WorkloadIdentityX509RevocationUpdate
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.UpdateWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: preExisting,
|
||||
},
|
||||
requireError: require.NoError,
|
||||
checkResultReturned: true,
|
||||
requireEvent: &events.WorkloadIdentityX509RevocationUpdate{
|
||||
Metadata: events.Metadata{
|
||||
Code: libevents.WorkloadIdentityX509RevocationUpdateCode,
|
||||
Type: libevents.WorkloadIdentityX509RevocationUpdateEvent,
|
||||
},
|
||||
ResourceMetadata: events.ResourceMetadata{
|
||||
Name: preExisting.GetMetadata().GetName(),
|
||||
},
|
||||
UserMetadata: events.UserMetadata{
|
||||
User: authorizedUser.GetName(),
|
||||
UserKind: events.UserKind_USER_KIND_HUMAN,
|
||||
},
|
||||
Reason: "compromised",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "incorrect revision",
|
||||
client: authorizedClient,
|
||||
req: (func() *workloadidentityv1pb.UpdateWorkloadIdentityX509RevocationRequest {
|
||||
preExisting2.Metadata.Revision = "incorrect"
|
||||
return &workloadidentityv1pb.UpdateWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: preExisting2,
|
||||
}
|
||||
})(),
|
||||
requireError: func(t require.TestingT, err error, i ...interface{}) {
|
||||
require.True(t, trace.IsCompareFailed(err))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not existing",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.UpdateWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: &workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccdd404",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
requireError: func(t require.TestingT, err error, i ...interface{}) {
|
||||
require.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unauthorized",
|
||||
client: unauthorizedClient,
|
||||
req: &workloadidentityv1pb.UpdateWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: 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.NewWorkloadIdentityRevocationServiceClient(
|
||||
tt.client.GetConnection(),
|
||||
)
|
||||
res, err := client.UpdateWorkloadIdentityX509Revocation(ctx, tt.req)
|
||||
tt.requireError(t, err)
|
||||
|
||||
if tt.checkResultReturned {
|
||||
require.NotEmpty(t, res.Metadata.Revision)
|
||||
require.NotEqual(t, tt.req.WorkloadIdentityX509Revocation.GetMetadata().GetRevision(), res.Metadata.Revision)
|
||||
// Expect returned result to match request, but also have a
|
||||
// revision
|
||||
require.Empty(
|
||||
t,
|
||||
cmp.Diff(
|
||||
res,
|
||||
tt.req.WorkloadIdentityX509Revocation,
|
||||
protocmp.Transform(),
|
||||
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
||||
),
|
||||
)
|
||||
// Expect the value fetched from the store to match returned
|
||||
// item.
|
||||
fetched, err := srv.Auth().GetWorkloadIdentityX509Revocation(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.WorkloadIdentityX509RevocationUpdate)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr)
|
||||
require.Empty(t, cmp.Diff(
|
||||
evt,
|
||||
tt.requireEvent,
|
||||
cmpopts.IgnoreFields(events.WorkloadIdentityX509RevocationUpdate{}, "ConnectionMetadata"),
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevocationService_UpsertWorkloadIdentityX509Revocation(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.KindWorkloadIdentityX509Revocation},
|
||||
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.UpsertWorkloadIdentityX509RevocationRequest
|
||||
requireError require.ErrorAssertionFunc
|
||||
checkResultReturned bool
|
||||
requireEvent *events.WorkloadIdentityX509RevocationCreate
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.UpsertWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: &workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccdd",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
requireError: require.NoError,
|
||||
checkResultReturned: true,
|
||||
requireEvent: &events.WorkloadIdentityX509RevocationCreate{
|
||||
Metadata: events.Metadata{
|
||||
Code: libevents.WorkloadIdentityX509RevocationCreateCode,
|
||||
Type: libevents.WorkloadIdentityX509RevocationCreateEvent,
|
||||
},
|
||||
ResourceMetadata: events.ResourceMetadata{
|
||||
Name: "aabbccdd",
|
||||
},
|
||||
UserMetadata: events.UserMetadata{
|
||||
User: authorizedUser.GetName(),
|
||||
UserKind: events.UserKind_USER_KIND_HUMAN,
|
||||
},
|
||||
Reason: "compromised",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validation fail",
|
||||
client: authorizedClient,
|
||||
req: &workloadidentityv1pb.UpsertWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: &workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccdd",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
requireError: func(t require.TestingT, err error, i ...interface{}) {
|
||||
require.True(t, trace.IsBadParameter(err))
|
||||
require.ErrorContains(t, err, "spec.reason: is required")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unauthorized",
|
||||
client: unauthorizedClient,
|
||||
req: &workloadidentityv1pb.UpsertWorkloadIdentityX509RevocationRequest{
|
||||
WorkloadIdentityX509Revocation: &workloadidentityv1pb.WorkloadIdentityX509Revocation{
|
||||
Kind: types.KindWorkloadIdentityX509Revocation,
|
||||
Version: types.V1,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "aabbccdd",
|
||||
Expires: timestamppb.New(srv.Clock().Now().Add(time.Hour)),
|
||||
},
|
||||
Spec: &workloadidentityv1pb.WorkloadIdentityX509RevocationSpec{
|
||||
Reason: "compromised",
|
||||
RevokedAt: timestamppb.New(srv.Clock().Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
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.NewWorkloadIdentityRevocationServiceClient(
|
||||
tt.client.GetConnection(),
|
||||
)
|
||||
res, err := client.UpsertWorkloadIdentityX509Revocation(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.WorkloadIdentityX509Revocation,
|
||||
protocmp.Transform(),
|
||||
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
||||
),
|
||||
)
|
||||
// Expect the value fetched from the store to match returned
|
||||
// item.
|
||||
fetched, err := srv.Auth().GetWorkloadIdentityX509Revocation(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.WorkloadIdentityX509RevocationCreate)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr)
|
||||
require.Empty(t, cmp.Diff(
|
||||
evt,
|
||||
tt.requireEvent,
|
||||
cmpopts.IgnoreFields(events.WorkloadIdentityX509RevocationCreate{}, "ConnectionMetadata"),
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@ func NewPresetEditorRole() types.Role {
|
||||
types.NewRule(types.KindAutoUpdateConfig, RW()),
|
||||
types.NewRule(types.KindAutoUpdateAgentRollout, RO()),
|
||||
types.NewRule(types.KindGitServer, RW()),
|
||||
types.NewRule(types.KindWorkloadIdentityX509Revocation, RW()),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user