Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SPIFFEFederation gRPC service and client support #45253

Merged
merged 30 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f62799f
Start adding `lib/services` content for SPIFFEFederation type
strideynet Aug 5, 2024
d11924c
Add SPIFFEFederation resource to cache
strideynet Aug 5, 2024
61db4eb
Wire SPIFFEFederation into cache
strideynet Aug 5, 2024
f126adb
Fix NewTestAuthServer
strideynet Aug 5, 2024
7c8a4b8
Start writing tests
strideynet Aug 5, 2024
49ab510
Add more test cases to validation
strideynet Aug 6, 2024
859e88c
Add tests to cache for SPIFFEFederation
strideynet Aug 6, 2024
718e533
More test coverage
strideynet Aug 6, 2024
43732f4
Merge remote-tracking branch 'origin/master' into strideynet/add-back…
strideynet Aug 7, 2024
441c532
Add test for DeleteSPIFFEFederations
strideynet Aug 7, 2024
037fa63
Finish off tests for SPIFFEFederation resource
strideynet Aug 8, 2024
826d8f3
go mod tidy
strideynet Aug 8, 2024
3c4e0ee
Go mod tidy
strideynet Aug 8, 2024
651867f
Appease linter
strideynet Aug 8, 2024
a610cbb
Avoid forcing kind/version in MarshalSPIFFEFederation
strideynet Aug 8, 2024
e9b258d
more linter appeasal
strideynet Aug 8, 2024
6fd541d
Add basics of SPIFFEFederation gRPC service
strideynet Aug 8, 2024
6fd0d44
Add support into tctl for create/delete/get/list
strideynet Aug 8, 2024
c9ea40b
Rely on default page size
strideynet Aug 8, 2024
41f0673
Prevent configuration of status field
strideynet Aug 8, 2024
70d3143
Add test for TestSPIFFEFederationService_CreateSPIFFEFederation
strideynet Aug 9, 2024
17dffdb
Add TestSPIFFEFederationService_DeleteSPIFFEFederation
strideynet Aug 9, 2024
1d53301
Add test for TestSPIFFEFederationService_GetSPIFFEFederation
strideynet Aug 9, 2024
67fb15a
Add TestSPIFFEFederationService_ListSPIFFEFederations
strideynet Aug 9, 2024
9e3be3c
Add mising error asserrtiion
strideynet Aug 9, 2024
8e543f7
Add Verbs to default roles for SPIFFEFederation
strideynet Aug 12, 2024
adad74a
Merge branch 'master' into strideynet/spiffe-federation-svc
strideynet Aug 13, 2024
35d258a
Merge branch 'master' into strideynet/spiffe-federation-svc
strideynet Aug 13, 2024
9587351
Remove test cases that are now unneeded due to adding to implicit rol…
strideynet Aug 14, 2024
293f95e
Tidier if statement
strideynet Aug 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,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 @@ -5192,6 +5192,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
Loading