From f9127a4e6b7a8dfbedd5c62f576d4d6ebfa03573 Mon Sep 17 00:00:00 2001
From: Andrew Burke <31974658+atburke@users.noreply.github.com>
Date: Fri, 16 Aug 2024 13:15:21 -0700
Subject: [PATCH] Add static host users gRPC service (#45292)
This change adds the gRPC service for static host users.
---
api/client/client.go | 7 +
api/client/statichostuser/statichostuser.go | 94 +++++
lib/auth/auth.go | 9 +
lib/auth/grpcserver.go | 11 +
lib/auth/init.go | 4 +
lib/auth/statichostuser/service.go | 176 +++++++++
lib/auth/statichostuser/service_test.go | 386 ++++++++++++++++++++
7 files changed, 687 insertions(+)
create mode 100644 api/client/statichostuser/statichostuser.go
create mode 100644 lib/auth/statichostuser/service.go
create mode 100644 lib/auth/statichostuser/service_test.go
diff --git a/api/client/client.go b/api/client/client.go
index e6e8c6d27eef5..20d45bf22a9b7 100644
--- a/api/client/client.go
+++ b/api/client/client.go
@@ -57,6 +57,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/client/scim"
"github.com/gravitational/teleport/api/client/secreport"
+ statichostuserclient "github.com/gravitational/teleport/api/client/statichostuser"
"github.com/gravitational/teleport/api/client/userloginstate"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
@@ -84,6 +85,7 @@ import (
secreportsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/secreports/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userloginstatev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/userloginstate/v1"
+ userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1"
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
userpreferencespb "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1"
@@ -3264,6 +3266,11 @@ func (c *Client) DeleteKubernetesWaitingContainer(ctx context.Context, req *kube
return c.GetKubernetesWaitingContainerClient().DeleteKubernetesWaitingContainer(ctx, req)
}
+// StaticHostUserClient returns a new static host user client.
+func (c *Client) StaticHostUserClient() *statichostuserclient.Client {
+ return statichostuserclient.NewClient(userprovisioningpb.NewStaticHostUsersServiceClient(c.conn))
+}
+
// CreateDatabase creates a new database resource.
func (c *Client) CreateDatabase(ctx context.Context, database types.Database) error {
databaseV3, ok := database.(*types.DatabaseV3)
diff --git a/api/client/statichostuser/statichostuser.go b/api/client/statichostuser/statichostuser.go
new file mode 100644
index 0000000000000..f8100ff9998fc
--- /dev/null
+++ b/api/client/statichostuser/statichostuser.go
@@ -0,0 +1,94 @@
+// Copyright 2024 Gravitational, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package statichostuser
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+
+ userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1"
+)
+
+// Client is a StaticHostUser client.
+type Client struct {
+ grpcClient userprovisioningpb.StaticHostUsersServiceClient
+}
+
+// NewClient creates a new StaticHostUser client.
+func NewClient(grpcClient userprovisioningpb.StaticHostUsersServiceClient) *Client {
+ return &Client{
+ grpcClient: grpcClient,
+ }
+}
+
+// ListStaticHostUsers lists static host users.
+func (c *Client) ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioningpb.StaticHostUser, string, error) {
+ resp, err := c.grpcClient.ListStaticHostUsers(ctx, &userprovisioningpb.ListStaticHostUsersRequest{
+ PageSize: int32(pageSize),
+ PageToken: pageToken,
+ })
+ if err != nil {
+ return nil, "", trace.Wrap(err)
+ }
+ return resp.Users, resp.NextPageToken, nil
+}
+
+// GetStaticHostUser returns a static host user by name.
+func (c *Client) GetStaticHostUser(ctx context.Context, name string) (*userprovisioningpb.StaticHostUser, error) {
+ if name == "" {
+ return nil, trace.BadParameter("missing name")
+ }
+ out, err := c.grpcClient.GetStaticHostUser(ctx, &userprovisioningpb.GetStaticHostUserRequest{
+ Name: name,
+ })
+ return out, trace.Wrap(err)
+}
+
+// CreateStaticHostUser creates a static host user.
+func (c *Client) CreateStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) {
+ out, err := c.grpcClient.CreateStaticHostUser(ctx, &userprovisioningpb.CreateStaticHostUserRequest{
+ User: in,
+ })
+ return out, trace.Wrap(err)
+}
+
+// UpdateStaticHostUser updates a static host user.
+func (c *Client) UpdateStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) {
+ out, err := c.grpcClient.UpdateStaticHostUser(ctx, &userprovisioningpb.UpdateStaticHostUserRequest{
+ User: in,
+ })
+ return out, trace.Wrap(err)
+}
+
+// UpsertStaticHostUser upserts a static host user.
+func (c *Client) UpsertStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) {
+ out, err := c.grpcClient.UpsertStaticHostUser(ctx, &userprovisioningpb.UpsertStaticHostUserRequest{
+ User: in,
+ })
+ return out, trace.Wrap(err)
+}
+
+// DeleteStaticHostUser deletes a static host user. Note that this does not
+// remove any host users created on nodes from the resource.
+func (c *Client) DeleteStaticHostUser(ctx context.Context, name string) error {
+ if name == "" {
+ return trace.BadParameter("missing name")
+ }
+ _, err := c.grpcClient.DeleteStaticHostUser(ctx, &userprovisioningpb.DeleteStaticHostUserRequest{
+ Name: name,
+ })
+ return trace.Wrap(err)
+}
diff --git a/lib/auth/auth.go b/lib/auth/auth.go
index f78cc18683f11..b16071f63bd41 100644
--- a/lib/auth/auth.go
+++ b/lib/auth/auth.go
@@ -391,6 +391,13 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
}
}
+ if cfg.StaticHostUsers == nil {
+ cfg.StaticHostUsers, err = local.NewStaticHostUserService(cfg.Backend)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ }
+
closeCtx, cancelFunc := context.WithCancel(context.TODO())
services := &Services{
TrustInternal: cfg.Trust,
@@ -430,6 +437,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
CrownJewels: cfg.CrownJewels,
BotInstance: cfg.BotInstance,
SPIFFEFederations: cfg.SPIFFEFederations,
+ StaticHostUser: cfg.StaticHostUsers,
}
as := Server{
@@ -627,6 +635,7 @@ type Services struct {
services.AccessGraphSecretsGetter
services.DevicesGetter
services.BotInstance
+ services.StaticHostUser
}
// GetWebSession returns existing web session described by req.
diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go
index 408ce27189130..283ae4245e660 100644
--- a/lib/auth/grpcserver.go
+++ b/lib/auth/grpcserver.go
@@ -66,6 +66,7 @@ import (
presencev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/presence/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userloginstatev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/userloginstate/v1"
+ userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1"
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
userpreferencespb "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1"
@@ -89,6 +90,7 @@ import (
notifications "github.com/gravitational/teleport/lib/auth/notifications/notificationsv1"
"github.com/gravitational/teleport/lib/auth/okta"
"github.com/gravitational/teleport/lib/auth/presence/presencev1"
+ statichostuserv1 "github.com/gravitational/teleport/lib/auth/statichostuser"
"github.com/gravitational/teleport/lib/auth/trust/trustv1"
"github.com/gravitational/teleport/lib/auth/userloginstate"
"github.com/gravitational/teleport/lib/auth/userpreferences/userpreferencesv1"
@@ -5422,6 +5424,15 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {
vnetConfigServiceServer := vnetconfig.NewService(vnetConfigStorage, cfg.Authorizer)
vnet.RegisterVnetConfigServiceServer(server, vnetConfigServiceServer)
+ staticHostUserServer, err := statichostuserv1.NewService(statichostuserv1.ServiceConfig{
+ Authorizer: cfg.Authorizer,
+ Backend: cfg.AuthServer.Services,
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ userprovisioningpb.RegisterStaticHostUsersServiceServer(server, staticHostUserServer)
+
// Only register the service if this is an open source build. Enterprise builds
// register the actual service via an auth plugin, if we register here then all
// Enterprise builds would fail with a duplicate service registered error.
diff --git a/lib/auth/init.go b/lib/auth/init.go
index 64aae856f22e2..974ae19f13cda 100644
--- a/lib/auth/init.go
+++ b/lib/auth/init.go
@@ -309,6 +309,10 @@ type InitConfig struct {
// SPIFFEFederations is a service that manages storing SPIFFE federations.
SPIFFEFederations services.SPIFFEFederations
+
+ // StaticHostUsers is a service that manages host users that should be
+ // created on SSH nodes.
+ StaticHostUsers services.StaticHostUser
}
// Init instantiates and configures an instance of AuthServer
diff --git a/lib/auth/statichostuser/service.go b/lib/auth/statichostuser/service.go
new file mode 100644
index 0000000000000..e216a21f5a74b
--- /dev/null
+++ b/lib/auth/statichostuser/service.go
@@ -0,0 +1,176 @@
+// Teleport
+// Copyright (C) 2024 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package statichostuser
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+ "google.golang.org/protobuf/types/known/emptypb"
+
+ userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/authz"
+ "github.com/gravitational/teleport/lib/services"
+)
+
+// ServiceConfig holds configuration options for the static host user gRPC service.
+type ServiceConfig struct {
+ // Authorizer is the authorizer used to check access to resources.
+ Authorizer authz.Authorizer
+ // Backend is the backend used to store static host users.
+ Backend services.StaticHostUser
+ // TODO(atburke): add cache
+}
+
+// Service implements the static host user RPC service.
+type Service struct {
+ userprovisioningpb.UnimplementedStaticHostUsersServiceServer
+
+ authorizer authz.Authorizer
+ backend services.StaticHostUser
+}
+
+// NewService creates a new static host user gRPC service.
+func NewService(cfg ServiceConfig) (*Service, error) {
+ switch {
+ case cfg.Backend == nil:
+ return nil, trace.BadParameter("backend is required")
+ case cfg.Authorizer == nil:
+ return nil, trace.BadParameter("authorizer is required")
+ }
+
+ return &Service{
+ authorizer: cfg.Authorizer,
+ backend: cfg.Backend,
+ }, nil
+}
+
+// ListStaticHostUsers lists static host users.
+func (s *Service) ListStaticHostUsers(ctx context.Context, req *userprovisioningpb.ListStaticHostUsersRequest) (*userprovisioningpb.ListStaticHostUsersResponse, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindStaticHostUser, types.VerbList, types.VerbRead); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // TODO(atburke): Switch to using the cache after static host users have been added to the cache.
+ users, nextToken, err := s.backend.ListStaticHostUsers(ctx, int(req.PageSize), req.PageToken)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &userprovisioningpb.ListStaticHostUsersResponse{
+ Users: users,
+ NextPageToken: nextToken,
+ }, nil
+}
+
+// GetStaticHostUser returns a static host user by name.
+func (s *Service) GetStaticHostUser(ctx context.Context, req *userprovisioningpb.GetStaticHostUserRequest) (*userprovisioningpb.StaticHostUser, error) {
+ if req.Name == "" {
+ return nil, trace.BadParameter("missing name")
+ }
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindStaticHostUser, types.VerbRead); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // TODO(atburke): Switch to using the cache after static host users have been added to the cache.
+ out, err := s.backend.GetStaticHostUser(ctx, req.Name)
+ return out, trace.Wrap(err)
+}
+
+// CreateStaticHostUser creates a static host user.
+func (s *Service) CreateStaticHostUser(ctx context.Context, req *userprovisioningpb.CreateStaticHostUserRequest) (*userprovisioningpb.StaticHostUser, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindStaticHostUser, types.VerbCreate); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ out, err := s.backend.CreateStaticHostUser(ctx, req.User)
+ return out, trace.Wrap(err)
+}
+
+// UpdateStaticHostUser updates a static host user.
+func (s *Service) UpdateStaticHostUser(ctx context.Context, req *userprovisioningpb.UpdateStaticHostUserRequest) (*userprovisioningpb.StaticHostUser, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindStaticHostUser, types.VerbUpdate); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ out, err := s.backend.UpdateStaticHostUser(ctx, req.User)
+ return out, trace.Wrap(err)
+}
+
+// UpsertStaticHostUser upserts a static host user.
+func (s *Service) UpsertStaticHostUser(ctx context.Context, req *userprovisioningpb.UpsertStaticHostUserRequest) (*userprovisioningpb.StaticHostUser, error) {
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindStaticHostUser, types.VerbCreate, types.VerbUpdate); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ out, err := s.backend.UpsertStaticHostUser(ctx, req.User)
+ return out, trace.Wrap(err)
+}
+
+// DeleteStaticHostUser deletes a static host user. Note that this does not
+// remove any host users created on nodes from the resource.
+func (s *Service) DeleteStaticHostUser(ctx context.Context, req *userprovisioningpb.DeleteStaticHostUserRequest) (*emptypb.Empty, error) {
+ if req.Name == "" {
+ return nil, trace.BadParameter("missing name")
+ }
+ authCtx, err := s.authorizer.Authorize(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.CheckAccessToKind(types.KindStaticHostUser, types.VerbDelete); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := authCtx.AuthorizeAdminAction(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &emptypb.Empty{}, trace.Wrap(s.backend.DeleteStaticHostUser(ctx, req.Name))
+}
diff --git a/lib/auth/statichostuser/service_test.go b/lib/auth/statichostuser/service_test.go
new file mode 100644
index 0000000000000..1d6854ad8d1fc
--- /dev/null
+++ b/lib/auth/statichostuser/service_test.go
@@ -0,0 +1,386 @@
+// Teleport
+// Copyright (C) 2024 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package statichostuser
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/gravitational/trace"
+ "github.com/stretchr/testify/require"
+
+ userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/api/types/userprovisioning"
+ "github.com/gravitational/teleport/api/types/wrappers"
+ "github.com/gravitational/teleport/lib/authz"
+ "github.com/gravitational/teleport/lib/backend/memory"
+ "github.com/gravitational/teleport/lib/services/local"
+ "github.com/gravitational/teleport/lib/tlsca"
+)
+
+type authorizerFactory func(t *testing.T, client localClient) authz.Authorizer
+
+func staticHostUserName(i int) string {
+ return fmt.Sprintf("user-%d", i)
+}
+
+func makeStaticHostUser(i int) *userprovisioningpb.StaticHostUser {
+ name := staticHostUserName(i)
+ return userprovisioning.NewStaticHostUser(name, &userprovisioningpb.StaticHostUserSpec{
+ Login: name,
+ Groups: []string{"foo", "bar"},
+ NodeLabels: &wrappers.LabelValues{
+ Values: map[string]wrappers.StringValues{
+ "foo": {
+ Values: []string{"bar"},
+ },
+ },
+ },
+ })
+}
+
+func authorizeWithVerbs(verbs []string, mfaVerified bool) authorizerFactory {
+ return func(t *testing.T, client localClient) authz.Authorizer {
+ return authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) {
+ authzContext := authorizerForDummyUser(t, ctx, client, verbs)
+ if mfaVerified {
+ authzContext.AdminActionAuthState = authz.AdminActionAuthMFAVerified
+ } else {
+ authzContext.AdminActionAuthState = authz.AdminActionAuthUnauthorized
+ }
+ return authzContext, nil
+ })
+ }
+}
+
+func assertTraceErr(f func(error) bool) require.ErrorAssertionFunc {
+ return func(t require.TestingT, err error, _ ...any) {
+ require.Error(t, err)
+ require.True(t, f(err), "unexpected error: %v", err)
+ }
+}
+
+func TestStaticHostUserCRUD(t *testing.T) {
+ t.Parallel()
+
+ accessTests := []struct {
+ name string
+ request func(ctx context.Context, svc *Service, localSvc *local.StaticHostUserService) error
+ allowVerbs []string
+ }{
+ {
+ name: "list",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.ListStaticHostUsers(ctx, &userprovisioningpb.ListStaticHostUsersRequest{})
+ return err
+ },
+ allowVerbs: []string{types.VerbList, types.VerbRead},
+ },
+ {
+ name: "get",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.GetStaticHostUser(ctx, &userprovisioningpb.GetStaticHostUserRequest{
+ Name: staticHostUserName(0),
+ })
+ return err
+ },
+ allowVerbs: []string{types.VerbRead},
+ },
+ {
+ name: "create",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.CreateStaticHostUser(ctx, &userprovisioningpb.CreateStaticHostUserRequest{
+ User: makeStaticHostUser(10),
+ })
+ return err
+ },
+ allowVerbs: []string{types.VerbCreate},
+ },
+ {
+ name: "update",
+ request: func(ctx context.Context, svc *Service, localSvc *local.StaticHostUserService) error {
+ // Get the initial user from the local service to bypass RBAC.
+ hostUser, err := localSvc.GetStaticHostUser(ctx, staticHostUserName(0))
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ hostUser.Spec.Login = "bob"
+ _, err = svc.UpdateStaticHostUser(ctx, &userprovisioningpb.UpdateStaticHostUserRequest{
+ User: hostUser,
+ })
+ return err
+ },
+ allowVerbs: []string{types.VerbRead, types.VerbUpdate},
+ },
+ {
+ name: "upsert",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.UpsertStaticHostUser(ctx, &userprovisioningpb.UpsertStaticHostUserRequest{
+ User: makeStaticHostUser(10),
+ })
+ return err
+ },
+ allowVerbs: []string{types.VerbCreate, types.VerbUpdate},
+ },
+ {
+ name: "delete",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.DeleteStaticHostUser(ctx, &userprovisioningpb.DeleteStaticHostUserRequest{
+ Name: staticHostUserName(0),
+ })
+ return err
+ },
+ allowVerbs: []string{types.VerbDelete},
+ },
+ }
+
+ for _, tc := range accessTests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+
+ t.Run("allow", func(t *testing.T) {
+ t.Parallel()
+ // Create authorizer with required verbs.
+ authorizer := authorizeWithVerbs(tc.allowVerbs, true)
+ // CRUD action should succeed.
+ testStaticHostUserAccess(t, authorizer, tc.request, require.NoError)
+ })
+
+ t.Run("deny rbac", func(t *testing.T) {
+ t.Parallel()
+ // Create authorizer without required verbs.
+ authorizer := authorizeWithVerbs(nil, true)
+ // CRUD action should fail.
+ testStaticHostUserAccess(t, authorizer, tc.request, assertTraceErr(trace.IsAccessDenied))
+ })
+
+ t.Run("deny mfa", func(t *testing.T) {
+ t.Parallel()
+ // Create authorizer without verified MFA.
+ authorizer := authorizeWithVerbs(tc.allowVerbs, false)
+ // CRUD action should fail.
+ testStaticHostUserAccess(t, authorizer, tc.request, assertTraceErr(trace.IsAccessDenied))
+ })
+ })
+ }
+
+ otherTests := []struct {
+ name string
+ request func(ctx context.Context, svc *Service, localSvc *local.StaticHostUserService) error
+ verbs []string
+ assert require.ErrorAssertionFunc
+ }{
+ {
+ name: "get nonexistent resource",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.GetStaticHostUser(ctx, &userprovisioningpb.GetStaticHostUserRequest{
+ Name: "fake",
+ })
+ return err
+ },
+ verbs: []string{types.VerbRead},
+ assert: assertTraceErr(trace.IsNotFound),
+ },
+ {
+ name: "create resource twice",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.CreateStaticHostUser(ctx, &userprovisioningpb.CreateStaticHostUserRequest{
+ User: makeStaticHostUser(0),
+ })
+ return err
+ },
+ verbs: []string{types.VerbCreate},
+ assert: assertTraceErr(trace.IsAlreadyExists),
+ },
+ {
+ name: "delete nonexisting resource",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.DeleteStaticHostUser(ctx, &userprovisioningpb.DeleteStaticHostUserRequest{
+ Name: staticHostUserName(10),
+ })
+ return err
+ },
+ verbs: []string{types.VerbDelete},
+ assert: assertTraceErr(trace.IsNotFound),
+ },
+ {
+ name: "update with wrong revision",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.UpdateStaticHostUser(ctx, &userprovisioningpb.UpdateStaticHostUserRequest{
+ User: makeStaticHostUser(0),
+ })
+ return err
+ },
+ verbs: []string{types.VerbUpdate},
+ assert: assertTraceErr(trace.IsCompareFailed),
+ },
+ {
+ name: "update nonexistent resource",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.UpdateStaticHostUser(ctx, &userprovisioningpb.UpdateStaticHostUserRequest{
+ User: makeStaticHostUser(10),
+ })
+ return err
+ },
+ verbs: []string{types.VerbUpdate},
+ assert: assertTraceErr(trace.IsCompareFailed),
+ },
+ {
+ name: "upsert with update permission only",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.UpsertStaticHostUser(ctx, &userprovisioningpb.UpsertStaticHostUserRequest{
+ User: makeStaticHostUser(0),
+ })
+ return err
+ },
+ verbs: []string{types.VerbUpdate},
+ assert: assertTraceErr(trace.IsAccessDenied),
+ },
+ {
+ name: "upsert with create permission only",
+ request: func(ctx context.Context, svc *Service, _ *local.StaticHostUserService) error {
+ _, err := svc.UpsertStaticHostUser(ctx, &userprovisioningpb.UpsertStaticHostUserRequest{
+ User: makeStaticHostUser(10),
+ })
+ return err
+ },
+ verbs: []string{types.VerbCreate},
+ assert: assertTraceErr(trace.IsAccessDenied),
+ },
+ }
+ for _, tc := range otherTests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ authorizer := authorizeWithVerbs(tc.verbs, true)
+ testStaticHostUserAccess(t, authorizer, tc.request, tc.assert)
+ })
+ }
+}
+
+func testStaticHostUserAccess(
+ t *testing.T,
+ authorizer func(t *testing.T, client localClient) authz.Authorizer,
+ request func(ctx context.Context, svc *Service, localSvc *local.StaticHostUserService) error,
+ assert require.ErrorAssertionFunc,
+) {
+ ctx, resourceSvc, localSvc := initSvc(t, authorizer)
+ err := request(ctx, resourceSvc, localSvc)
+ assert(t, err)
+}
+
+func authorizerForDummyUser(t *testing.T, ctx context.Context, localClient localClient, roleVerbs []string) *authz.Context {
+ const clusterName = "localhost"
+
+ // Create role
+ roleName := "role-" + uuid.NewString()
+ var allowRules []types.Rule
+ if len(roleVerbs) != 0 {
+ allowRules = []types.Rule{
+ {
+ Resources: []string{types.KindStaticHostUser},
+ Verbs: roleVerbs,
+ },
+ }
+ }
+ role, err := types.NewRole(roleName, types.RoleSpecV6{
+ Allow: types.RoleConditions{Rules: allowRules},
+ })
+ require.NoError(t, err)
+
+ role, err = localClient.CreateRole(ctx, role)
+ require.NoError(t, err)
+
+ // Create user
+ user, err := types.NewUser("user-" + uuid.NewString())
+ require.NoError(t, err)
+ user.AddRole(roleName)
+ user, err = localClient.CreateUser(ctx, user)
+ require.NoError(t, err)
+
+ localUser := authz.LocalUser{
+ Username: user.GetName(),
+ Identity: tlsca.Identity{
+ Username: user.GetName(),
+ Groups: []string{role.GetName()},
+ },
+ }
+ authCtx, err := authz.ContextForLocalUser(ctx, localUser, localClient, clusterName, true)
+ require.NoError(t, err)
+
+ return authCtx
+}
+
+type localClient interface {
+ authz.AuthorizerAccessPoint
+
+ CreateUser(ctx context.Context, user types.User) (types.User, error)
+ CreateRole(ctx context.Context, role types.Role) (types.Role, error)
+}
+
+func initSvc(t *testing.T, authorizerFn func(t *testing.T, client localClient) authz.Authorizer) (context.Context, *Service, *local.StaticHostUserService) {
+ ctx := context.Background()
+ backend, err := memory.New(memory.Config{})
+ require.NoError(t, err)
+
+ roleSvc := local.NewAccessService(backend)
+ userSvc := local.NewTestIdentityService(backend)
+ clusterSrv, err := local.NewClusterConfigurationService(backend)
+ require.NoError(t, err)
+ caSrv := local.NewCAService(backend)
+
+ clusterConfigSvc, err := local.NewClusterConfigurationService(backend)
+ require.NoError(t, err)
+ _, err = clusterConfigSvc.UpsertAuthPreference(ctx, types.DefaultAuthPreference())
+ require.NoError(t, err)
+ _, err = clusterConfigSvc.UpsertClusterAuditConfig(ctx, types.DefaultClusterAuditConfig())
+ require.NoError(t, err)
+ _, err = clusterConfigSvc.UpsertClusterNetworkingConfig(ctx, types.DefaultClusterNetworkingConfig())
+ require.NoError(t, err)
+ _, err = clusterConfigSvc.UpsertSessionRecordingConfig(ctx, types.DefaultSessionRecordingConfig())
+ require.NoError(t, err)
+
+ localResourceService, err := local.NewStaticHostUserService(backend)
+ require.NoError(t, err)
+ for i := 0; i < 10; i++ {
+ _, err := localResourceService.CreateStaticHostUser(ctx, makeStaticHostUser(i))
+ require.NoError(t, err)
+ }
+
+ client := struct {
+ *local.AccessService
+ *local.IdentityService
+ *local.ClusterConfigurationService
+ *local.CA
+ }{
+ AccessService: roleSvc,
+ IdentityService: userSvc,
+ ClusterConfigurationService: clusterSrv,
+ CA: caSrv,
+ }
+
+ resourceSvc, err := NewService(ServiceConfig{
+ Authorizer: authorizerFn(t, client),
+ Backend: localResourceService,
+ })
+ require.NoError(t, err)
+
+ return ctx, resourceSvc, localResourceService
+}