Skip to content

Commit

Permalink
Add SPIFFEFederation gRPC service and client support (#45253)
Browse files Browse the repository at this point in the history
* Start adding `lib/services` content for SPIFFEFederation type

* Add SPIFFEFederation resource to cache

* Wire SPIFFEFederation into cache

* Fix NewTestAuthServer

* Start writing tests

* Add more test cases to validation

* Add tests to cache for SPIFFEFederation

* More test coverage

* Add test for DeleteSPIFFEFederations

* Finish off tests for SPIFFEFederation resource

* go mod tidy

* Go mod tidy

* Appease linter

* Avoid forcing kind/version in MarshalSPIFFEFederation

* more linter appeasal

* Add basics of SPIFFEFederation gRPC service

* Add support into tctl for create/delete/get/list

* Rely on default page size

* Prevent configuration of status field

* Add test for TestSPIFFEFederationService_CreateSPIFFEFederation

* Add TestSPIFFEFederationService_DeleteSPIFFEFederation

* Add test for TestSPIFFEFederationService_GetSPIFFEFederation

* Add TestSPIFFEFederationService_ListSPIFFEFederations

* Add mising error asserrtiion

* Add Verbs to default roles for SPIFFEFederation

* Remove test cases that are now unneeded due to adding to implicit roleset

* Tidier if statement

Co-authored-by: rosstimothy <[email protected]>

---------

Co-authored-by: rosstimothy <[email protected]>
  • Loading branch information
strideynet and rosstimothy authored Aug 22, 2024
1 parent 5ed1222 commit 8495398
Show file tree
Hide file tree
Showing 10 changed files with 949 additions and 10 deletions.
4 changes: 4 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,10 @@ func (c *Client) BotInstanceServiceClient() machineidv1pb.BotInstanceServiceClie
return machineidv1pb.NewBotInstanceServiceClient(c.conn)
}

func (c *Client) SPIFFEFederationServiceClient() machineidv1pb.SPIFFEFederationServiceClient {
return machineidv1pb.NewSPIFFEFederationServiceClient(c.conn)
}

// PresenceServiceClient returns an unadorned client for the presence service.
func (c *Client) PresenceServiceClient() presencepb.PresenceServiceClient {
return presencepb.NewPresenceServiceClient(c.conn)
Expand Down
12 changes: 12 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5200,6 +5200,18 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {
}
machineidv1pb.RegisterWorkloadIdentityServiceServer(server, workloadIdentityService)

spiffeFederationService, err := machineidv1.NewSPIFFEFederationService(machineidv1.SPIFFEFederationServiceConfig{
Authorizer: cfg.Authorizer,
Backend: cfg.AuthServer.Services.SPIFFEFederations,
Cache: cfg.AuthServer.Cache,
Clock: cfg.AuthServer.GetClock(),
Emitter: cfg.Emitter,
})
if err != nil {
return nil, trace.Wrap(err, "creating SPIFFE federation service")
}
machineidv1pb.RegisterSPIFFEFederationServiceServer(server, spiffeFederationService)

dbObjectImportRuleService, err := dbobjectimportrulev1.NewDatabaseObjectImportRuleService(dbobjectimportrulev1.DatabaseObjectImportRuleServiceConfig{
Authorizer: cfg.Authorizer,
Backend: cfg.AuthServer.Services,
Expand Down
22 changes: 13 additions & 9 deletions lib/auth/machineid/machineidv1/machineidv1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
"github.com/gravitational/teleport/lib/events/eventstest"
"github.com/gravitational/teleport/lib/modules"
)

Expand All @@ -66,7 +67,7 @@ func TestBotResourceName(t *testing.T) {
// TestCreateBot is an integration test that uses a real gRPC client/server.
func TestCreateBot(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
srv, _ := newTestTLSServer(t)
ctx := context.Background()

botCreator, _, err := auth.CreateUserAndRole(
Expand Down Expand Up @@ -477,7 +478,7 @@ func TestCreateBot(t *testing.T) {
// TestUpdateBot is an integration test that uses a real gRPC client/server.
func TestUpdateBot(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
srv, _ := newTestTLSServer(t)
ctx := context.Background()

botUpdaterUser, _, err := auth.CreateUserAndRole(srv.Auth(), "bot-updater", []string{}, []types.Rule{
Expand Down Expand Up @@ -840,7 +841,7 @@ func TestUpdateBot(t *testing.T) {
// TestUpsertBot is an integration test that uses a real gRPC client/server.
func TestUpsertBot(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
srv, _ := newTestTLSServer(t)
ctx := context.Background()

botCreator, _, err := auth.CreateUserAndRole(srv.Auth(), "bot-creator", []string{}, []types.Rule{
Expand Down Expand Up @@ -1268,7 +1269,7 @@ func TestUpsertBot(t *testing.T) {
// TestGetBot is an integration test that uses a real gRPC client/server.
func TestGetBot(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
srv, _ := newTestTLSServer(t)
ctx := context.Background()

botGetterUser, _, err := auth.CreateUserAndRole(
Expand Down Expand Up @@ -1379,7 +1380,7 @@ func TestGetBot(t *testing.T) {
// TestListBots is an integration test that uses a real gRPC client/server.
func TestListBots(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
srv, _ := newTestTLSServer(t)
ctx := context.Background()

botListerUser, _, err := auth.CreateUserAndRole(
Expand Down Expand Up @@ -1490,7 +1491,7 @@ func TestListBots(t *testing.T) {
// TestDeleteBot is an integration test that uses a real gRPC client/server.
func TestDeleteBot(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
srv, _ := newTestTLSServer(t)
ctx := context.Background()

botDeleterUser, _, err := auth.CreateUserAndRole(
Expand Down Expand Up @@ -1630,14 +1631,17 @@ func TestDeleteBot(t *testing.T) {
}
}

func newTestTLSServer(t testing.TB) *auth.TestTLSServer {
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)

srv, err := as.NewTestTLSServer()
emitter := &eventstest.MockRecorderEmitter{}
srv, err := as.NewTestTLSServer(func(config *auth.TestTLSServerConfig) {
config.APIConfig.Emitter = emitter
})
require.NoError(t, err)

t.Cleanup(func() {
Expand All @@ -1648,5 +1652,5 @@ func newTestTLSServer(t testing.TB) *auth.TestTLSServer {
require.NoError(t, err)
})

return srv
return srv, emitter
}
252 changes: 252 additions & 0 deletions lib/auth/machineid/machineidv1/spiffe_federation_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// Teleport
// Copyright (C) 2024 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package machineidv1

import (
"context"
"log/slog"

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"

"github.com/gravitational/teleport"
machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/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 spiffeFederationReader interface {
ListSPIFFEFederations(ctx context.Context, limit int, token string) ([]*machineidv1.SPIFFEFederation, string, error)
GetSPIFFEFederation(ctx context.Context, name string) (*machineidv1.SPIFFEFederation, error)
}

type spiffeFederationReadWriter interface {
spiffeFederationReader
CreateSPIFFEFederation(ctx context.Context, federation *machineidv1.SPIFFEFederation) (*machineidv1.SPIFFEFederation, error)
DeleteSPIFFEFederation(ctx context.Context, name string) error
}

// SPIFFEFederationServiceConfig holds configuration options for
// NewSPIFFEFederationService
type SPIFFEFederationServiceConfig struct {
// Authorizer is the authorizer service which checks access to resources.
Authorizer authz.Authorizer
// Backend will be used reading and writing the SPIFFE Federation resources.
Backend spiffeFederationReadWriter
// Cache will be used when reading SPIFFE Federation resources.
Cache spiffeFederationReader
// Clock is the clock instance to use. Useful for injecting in tests.
Clock clockwork.Clock
// Emitter is the event emitter to use when emitting audit events.
Emitter apievents.Emitter
// Logger is the logger instance to use.
Logger *slog.Logger
}

// NewSPIFFEFederationService returns a new instance of the SPIFFEFederationService.
func NewSPIFFEFederationService(
cfg SPIFFEFederationServiceConfig,
) (*SPIFFEFederationService, 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, "spiffe_federation.service")
}
if cfg.Clock == nil {
cfg.Clock = clockwork.NewRealClock()
}

return &SPIFFEFederationService{
authorizer: cfg.Authorizer,
backend: cfg.Backend,
cache: cfg.Cache,
clock: cfg.Clock,
emitter: cfg.Emitter,
logger: cfg.Logger,
}, nil
}

// SPIFFEFederationService is an implementation of
// teleport.machineid.v1.SPIFFEFederationService
type SPIFFEFederationService struct {
machineidv1.UnimplementedSPIFFEFederationServiceServer

authorizer authz.Authorizer
backend spiffeFederationReadWriter
cache spiffeFederationReader
clock clockwork.Clock
emitter apievents.Emitter
logger *slog.Logger
}

// GetSPIFFEFederation returns a SPIFFE Federation by name.
// Implements teleport.machineid.v1.SPIFFEFederationService/GetSPIFFEFederation
func (s *SPIFFEFederationService) GetSPIFFEFederation(
ctx context.Context, req *machineidv1.GetSPIFFEFederationRequest,
) (*machineidv1.SPIFFEFederation, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := authCtx.CheckAccessToKind(types.KindSPIFFEFederation, types.VerbRead); err != nil {
return nil, trace.Wrap(err)
}

if req.Name == "" {
return nil, trace.BadParameter("name: must be non-empty")
}

federation, err := s.cache.GetSPIFFEFederation(ctx, req.Name)
if err != nil {
return nil, trace.Wrap(err)
}

return federation, nil
}

// ListSPIFFEFederations returns a list of SPIFFE Federations. It follows the
// Google API design guidelines for list pagination.
// Implements teleport.machineid.v1.SPIFFEFederationService/ListSPIFFEFederations
func (s *SPIFFEFederationService) ListSPIFFEFederations(
ctx context.Context, req *machineidv1.ListSPIFFEFederationsRequest,
) (*machineidv1.ListSPIFFEFederationsResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := authCtx.CheckAccessToKind(types.KindSPIFFEFederation, types.VerbRead, types.VerbList); err != nil {
return nil, trace.Wrap(err)
}

federations, nextToken, err := s.cache.ListSPIFFEFederations(
ctx,
int(req.PageSize),
req.PageToken,
)
if err != nil {
return nil, trace.Wrap(err)
}

return &machineidv1.ListSPIFFEFederationsResponse{
SpiffeFederations: federations,
NextPageToken: nextToken,
}, nil
}

// DeleteSPIFFEFederation deletes a SPIFFE Federation by name.
// Implements teleport.machineid.v1.SPIFFEFederationService/DeleteSPIFFEFederation
func (s *SPIFFEFederationService) DeleteSPIFFEFederation(
ctx context.Context, req *machineidv1.DeleteSPIFFEFederationRequest,
) (*emptypb.Empty, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := authCtx.CheckAccessToKind(types.KindSPIFFEFederation, 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.DeleteSPIFFEFederation(ctx, req.Name); err != nil {
return nil, trace.Wrap(err)
}

if err := s.emitter.EmitAuditEvent(ctx, &apievents.SPIFFEFederationDelete{
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 SPIFFEFederation",
"error", err,
)
}

return &emptypb.Empty{}, nil
}

// CreateSPIFFEFederation creates a new SPIFFE Federation.
// Implements teleport.machineid.v1.SPIFFEFederationService/CreateSPIFFEFederation
func (s *SPIFFEFederationService) CreateSPIFFEFederation(
ctx context.Context, req *machineidv1.CreateSPIFFEFederationRequest,
) (*machineidv1.SPIFFEFederation, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := authCtx.CheckAccessToKind(types.KindSPIFFEFederation, types.VerbCreate); err != nil {
return nil, trace.Wrap(err)
}
if err := authCtx.AuthorizeAdminAction(); err != nil {
return nil, trace.Wrap(err)
}

if status := req.GetSpiffeFederation().GetStatus(); status != nil {
if !proto.Equal(status, &machineidv1.SPIFFEFederationStatus{}) {
return nil, trace.BadParameter("status: cannot be set")
}
}

created, err := s.backend.CreateSPIFFEFederation(ctx, req.SpiffeFederation)
if err != nil {
return nil, trace.Wrap(err)
}

if err := s.emitter.EmitAuditEvent(ctx, &apievents.SPIFFEFederationCreate{
Metadata: apievents.Metadata{
Code: events.SPIFFEFederationCreateCode,
Type: events.SPIFFEFederationCreateEvent,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ConnectionMetadata: authz.ConnectionMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: req.SpiffeFederation.Metadata.Name,
},
}); err != nil {
s.logger.ErrorContext(
ctx, "Failed to emit audit event for creation of SPIFFEFederation",
"error", err,
)
}

return created, nil
}
Loading

0 comments on commit 8495398

Please sign in to comment.