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

[v15] Add SPIFFEFederation gRPC service and client support (#45253) #45692

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -857,6 +857,10 @@ func (c *Client) BotServiceClient() machineidv1pb.BotServiceClient {
return machineidv1pb.NewBotServiceClient(c.conn)
}

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

// WorkloadIdentityServiceClient returns an unadorned client for the workload
// identity service.
func (c *Client) WorkloadIdentityServiceClient() machineidv1pb.WorkloadIdentityServiceClient {
Expand Down
12 changes: 12 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5815,6 +5815,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)

dbObjectService, err := dbobjectv1.NewDatabaseObjectService(dbobjectv1.DatabaseObjectServiceConfig{
Authorizer: cfg.Authorizer,
Backend: cfg.AuthServer.Services,
Expand Down
26 changes: 15 additions & 11 deletions lib/auth/machineid/machineidv1/machineidv1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/gravitational/teleport/api/types/wrappers"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
"github.com/gravitational/teleport/lib/events/eventstest"
)

func TestBotResourceName(t *testing.T) {
Expand All @@ -61,7 +62,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 @@ -532,7 +533,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 @@ -895,7 +896,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 @@ -1323,7 +1324,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 @@ -1434,7 +1435,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 @@ -1545,7 +1546,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 @@ -1730,7 +1731,7 @@ func TestDeleteBot(t *testing.T) {
// TODO(noah): DELETE IN 16.0.0
func TestCreateBotLegacy(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
srv, _ := newTestTLSServer(t)
ctx := context.Background()
testRole := "test-role"
_, err := auth.CreateRole(ctx, srv.Auth(), testRole, types.RoleSpecV6{})
Expand Down Expand Up @@ -1840,7 +1841,7 @@ func TestCreateBotLegacy(t *testing.T) {
// TODO(noah): DELETE IN 16.0.0
func TestGetBotUsersLegacy(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
srv, _ := newTestTLSServer(t)
ctx := context.Background()

getBotsUser, _, err := auth.CreateUserAndRole(
Expand Down Expand Up @@ -1982,14 +1983,17 @@ func TestGetBotUsersLegacy(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 @@ -2000,5 +2004,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