Implement users service (#33315)

Adds an implementation of the teleport.users.v1.UsersService RPC
service. Logic was copied from lib/auth/auth.go and
lib/auth/auth_with_roles.go but not removed in favor of the new
service yet. Follow up work will consolidate the legacy code to
call into the new users service.
This commit is contained in:
rosstimothy
2023-10-28 00:06:30 +00:00
committed by GitHub
parent 0c88cafc44
commit 8370b3afda
2 changed files with 1110 additions and 0 deletions
+418
View File
@@ -0,0 +1,418 @@
// Copyright 2023 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 usersv1
import (
"context"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/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"
"github.com/gravitational/teleport/lib/services"
usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport"
)
// Cache is the subset of the cached resources that the Service queries.
type Cache interface {
// GetUser returns a user by name.
GetUser(ctx context.Context, user string, withSecrets bool) (types.User, error)
// GetRole returns a role by name.
GetRole(ctx context.Context, name string) (types.Role, error)
}
// Backend is the subset of the backend resources that the Service modifies.
type Backend interface {
// CreateUser creates user, only if the user entry does not exist
CreateUser(ctx context.Context, user types.User) (types.User, error)
// UpdateUser updates an existing user if revisions match.
UpdateUser(ctx context.Context, user types.User) (types.User, error)
// UpsertUser creates a new user or forcefully updates an existing user.
UpsertUser(ctx context.Context, user types.User) (types.User, error)
// DeleteRole deletes a role by name.
DeleteRole(ctx context.Context, name string) error
// DeleteUser deletes a user and all associated objects.
DeleteUser(ctx context.Context, user string) error
}
// ServiceConfig holds configuration options for
// the users gRPC service.
type ServiceConfig struct {
Authorizer authz.Authorizer
Cache Cache
Backend Backend
Logger logrus.FieldLogger
Emitter apievents.Emitter
Reporter usagereporter.UsageReporter
Clock clockwork.Clock
}
// Service implements the teleport.users.v1.UsersService RPC service.
type Service struct {
userspb.UnimplementedUsersServiceServer
authorizer authz.Authorizer
cache Cache
backend Backend
logger logrus.FieldLogger
emitter apievents.Emitter
reporter usagereporter.UsageReporter
clock clockwork.Clock
}
// NewService returns a new users gRPC service.
func NewService(cfg ServiceConfig) (*Service, error) {
switch {
case cfg.Cache == nil:
return nil, trace.BadParameter("cache service is required")
case cfg.Backend == nil:
return nil, trace.BadParameter("backend 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")
case cfg.Reporter == nil:
return nil, trace.BadParameter("reporter is required")
}
if cfg.Logger == nil {
cfg.Logger = logrus.WithField(trace.Component, "users.service")
}
if cfg.Clock == nil {
cfg.Clock = clockwork.NewRealClock()
}
return &Service{
logger: cfg.Logger,
authorizer: cfg.Authorizer,
cache: cfg.Cache,
backend: cfg.Backend,
emitter: cfg.Emitter,
reporter: cfg.Reporter,
clock: cfg.Clock,
}, nil
}
// currentUserAction is a special checker that allows certain actions for users
// even if they are not admins, e.g. update their own passwords,
// or generate certificates, otherwise it will require admin privileges
func currentUserAction(authzContext authz.Context, username string) error {
if authz.IsLocalUser(authzContext) && username == authzContext.User.GetName() {
return nil
}
return authzContext.Checker.CheckAccessToRule(&services.Context{User: authzContext.User},
apidefaults.Namespace, types.KindUser, types.VerbCreate, true)
}
func (s *Service) getCurrentUser(ctx context.Context, authCtx *authz.Context) (*types.UserV2, error) {
// check access to roles
for _, role := range authCtx.User.GetRoles() {
_, err := s.cache.GetRole(ctx, role)
if err != nil {
return nil, trace.Wrap(err)
}
}
withoutSecrets := authCtx.User.WithoutSecrets()
user, ok := withoutSecrets.(types.User)
if !ok {
return nil, trace.BadParameter("expected types.User when fetching current user information, got %T", withoutSecrets)
}
v2, ok := user.(*types.UserV2)
if !ok {
return nil, trace.BadParameter("encountered unexpected user type")
}
return v2, nil
}
func (s *Service) GetUser(ctx context.Context, req *userspb.GetUserRequest) (*types.UserV2, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if req.Name == "" && req.CurrentUser {
user, err := s.getCurrentUser(ctx, authCtx)
return user, trace.Wrap(err)
}
if req.WithSecrets {
// TODO(fspmarshall): replace admin requirement with VerbReadWithSecrets once we've
// migrated to that model.
if !authz.HasBuiltinRole(*authCtx, string(types.RoleAdmin)) {
err := trace.AccessDenied("user %q requested access to user %q with secrets", authCtx.User.GetName(), req.Name)
s.logger.Warn(err)
if err := s.emitter.EmitAuditEvent(ctx, &apievents.UserLogin{
Metadata: apievents.Metadata{
Type: events.UserLoginEvent,
Code: events.UserLocalLoginFailureCode,
},
Method: events.LoginMethodClientCert,
Status: apievents.Status{
Success: false,
Error: trace.Unwrap(err).Error(),
UserMessage: err.Error(),
},
}); err != nil {
s.logger.WithError(err).Warn("Failed to emit local login failure event.")
}
return nil, trace.AccessDenied("this request can be only executed by an admin")
}
} else {
// if secrets are not being accessed, let users always read
// their own info.
if err := currentUserAction(*authCtx, req.Name); err != nil {
// not current user, perform normal permission check.
if _, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindUser, types.VerbRead); err != nil {
return nil, trace.Wrap(err)
}
}
}
user, err := s.cache.GetUser(ctx, req.Name, req.WithSecrets)
if err != nil {
return nil, trace.Wrap(err)
}
v2, ok := user.(*types.UserV2)
if !ok {
s.logger.Warnf("expected type services.UserV2, got %T for user %q", user, user.GetName())
return nil, trace.BadParameter("encountered unexpected user type")
}
return v2, nil
}
func (s *Service) CreateUser(ctx context.Context, req *userspb.CreateUserRequest) (*types.UserV2, error) {
if _, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindUser, types.VerbCreate); err != nil {
return nil, trace.Wrap(err)
}
if req.User.GetCreatedBy().IsEmpty() {
req.User.SetCreatedBy(types.CreatedBy{
User: types.UserRef{Name: authz.ClientUsername(ctx)},
Time: s.clock.Now().UTC(),
})
}
created, err := s.backend.CreateUser(ctx, req.User)
if err != nil {
return nil, trace.Wrap(err)
}
connectorName := constants.LocalConnector
if created.GetCreatedBy().Connector != nil {
connectorName = created.GetCreatedBy().Connector.ID
}
if err := s.emitter.EmitAuditEvent(ctx, &apievents.UserCreate{
Metadata: apievents.Metadata{
Type: events.UserCreateEvent,
Code: events.UserCreateCode,
},
UserMetadata: authz.ClientUserMetadataWithUser(ctx, created.GetCreatedBy().User.Name),
ResourceMetadata: apievents.ResourceMetadata{
Name: created.GetName(),
Expires: created.Expiry(),
},
Connector: connectorName,
Roles: created.GetRoles(),
}); err != nil {
s.logger.WithError(err).Warn("Failed to emit user create event.")
}
usagereporter.EmitEditorChangeEvent(created.GetName(), nil, created.GetRoles(), s.reporter.AnonymizeAndSubmit)
v2, ok := created.(*types.UserV2)
if !ok {
s.logger.Warnf("expected type services.UserV2, got %T for user %q", created, created.GetName())
return nil, trace.BadParameter("encountered unexpected user type")
}
return v2, nil
}
func (s *Service) UpdateUser(ctx context.Context, req *userspb.UpdateUserRequest) (*types.UserV2, error) {
if _, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindUser, types.VerbUpdate); err != nil {
return nil, trace.Wrap(err)
}
prevUser, err := s.cache.GetUser(ctx, req.User.GetName(), false)
var omitEditorEvent bool
if err != nil {
// don't return error here since this call is for event emitting purposes only
s.logger.WithError(err).Warn("Failed getting previous user during update")
omitEditorEvent = true
}
updated, err := s.backend.UpdateUser(ctx, req.User)
if err != nil {
return nil, trace.Wrap(err)
}
connectorName := constants.LocalConnector
if updated.GetCreatedBy().Connector != nil {
connectorName = updated.GetCreatedBy().Connector.ID
}
if err := s.emitter.EmitAuditEvent(ctx, &apievents.UserCreate{
Metadata: apievents.Metadata{
Type: events.UserUpdatedEvent,
Code: events.UserUpdateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: updated.GetName(),
Expires: updated.Expiry(),
},
Connector: connectorName,
Roles: updated.GetRoles(),
}); err != nil {
s.logger.WithError(err).Warn("Failed to emit user update event.")
}
if !omitEditorEvent {
usagereporter.EmitEditorChangeEvent(updated.GetName(), prevUser.GetRoles(), updated.GetRoles(), s.reporter.AnonymizeAndSubmit)
}
v2, ok := updated.(*types.UserV2)
if !ok {
s.logger.Warnf("expected type services.UserV2, got %T for user %q", updated, updated.GetName())
return nil, trace.BadParameter("encountered unexpected user type")
}
return v2, nil
}
func (s *Service) UpsertUser(ctx context.Context, req *userspb.UpsertUserRequest) (*types.UserV2, error) {
authzCtx, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindUser, types.VerbCreate, types.VerbUpdate)
if err != nil {
return nil, trace.Wrap(err)
}
if createdBy := req.User.GetCreatedBy(); createdBy.IsEmpty() {
req.User.SetCreatedBy(types.CreatedBy{
User: types.UserRef{Name: authzCtx.User.GetName()},
})
}
prevUser, err := s.cache.GetUser(ctx, req.User.GetName(), false)
var omitEditorEvent bool
if err != nil {
// don't return error here since this call is for event emitting purposes only
s.logger.WithError(err).Warn("Failed getting previous user during update")
omitEditorEvent = true
}
upserted, err := s.backend.UpsertUser(ctx, req.User)
if err != nil {
return nil, trace.Wrap(err)
}
connectorName := constants.LocalConnector
if upserted.GetCreatedBy().Connector != nil {
connectorName = upserted.GetCreatedBy().Connector.ID
}
if err := s.emitter.EmitAuditEvent(ctx, &apievents.UserCreate{
Metadata: apievents.Metadata{
Type: events.UserCreateEvent,
Code: events.UserCreateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: upserted.GetName(),
Expires: upserted.Expiry(),
},
Connector: connectorName,
Roles: upserted.GetRoles(),
}); err != nil {
s.logger.WithError(err).Warn("Failed to emit user upsert event.")
}
if !omitEditorEvent {
usagereporter.EmitEditorChangeEvent(upserted.GetName(), prevUser.GetRoles(), upserted.GetRoles(), s.reporter.AnonymizeAndSubmit)
}
v2, ok := upserted.(*types.UserV2)
if !ok {
s.logger.Warnf("expected type services.UserV2, got %T for user %q", upserted, upserted.GetName())
return nil, trace.BadParameter("encountered unexpected user type")
}
return v2, nil
}
func (s *Service) DeleteUser(ctx context.Context, req *userspb.DeleteUserRequest) (*emptypb.Empty, error) {
if _, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindUser, types.VerbDelete); err != nil {
return nil, trace.Wrap(err)
}
prevUser, err := s.cache.GetUser(ctx, req.Name, false)
var omitEditorEvent bool
if err != nil && !trace.IsNotFound(err) {
// don't return error here, delete may still succeed
s.logger.WithError(err).Warn("Failed getting previous user during delete operation")
prevUser = nil
omitEditorEvent = true
}
role, err := s.cache.GetRole(ctx, services.RoleNameForUser(req.Name))
if err != nil {
if !trace.IsNotFound(err) {
return &emptypb.Empty{}, trace.Wrap(err)
}
} else {
if err := s.backend.DeleteRole(ctx, role.GetName()); err != nil {
if !trace.IsNotFound(err) {
return &emptypb.Empty{}, trace.Wrap(err)
}
}
}
if err := s.backend.DeleteUser(ctx, req.Name); err != nil {
return &emptypb.Empty{}, trace.Wrap(err)
}
// If the user was successfully deleted, emit an event.
if err := s.emitter.EmitAuditEvent(ctx, &apievents.UserDelete{
Metadata: apievents.Metadata{
Type: events.UserDeleteEvent,
Code: events.UserDeleteCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: req.Name,
},
}); err != nil {
s.logger.WithError(err).Warn("Failed to emit user delete event.")
}
if !omitEditorEvent {
usagereporter.EmitEditorChangeEvent(req.Name, prevUser.GetRoles(), nil, s.reporter.AnonymizeAndSubmit)
}
return &emptypb.Empty{}, nil
}
+692
View File
@@ -0,0 +1,692 @@
// Copyright 2023 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 usersv1
import (
"context"
"encoding/base32"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/backend/memory"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/events/eventstest"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/tlsca"
usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport"
)
type fakeAuthorizer struct {
authorize bool
authzContext *authz.Context
}
// Authorize implements authz.Authorizer
func (a fakeAuthorizer) Authorize(ctx context.Context) (*authz.Context, error) {
identity, err := authz.UserFromContext(ctx)
if err == nil {
user, err := types.NewUser("alice")
if err != nil {
return nil, trace.Wrap(err)
}
return &authz.Context{
User: user,
Checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbList, types.VerbRead, types.VerbUpdate, types.VerbCreate, types.VerbDelete},
},
},
},
Identity: identity,
}, nil
}
if a.authzContext != nil {
return a.authzContext, nil
}
user, err := types.NewUser("alice")
if err != nil {
return nil, trace.Wrap(err)
}
return &authz.Context{
User: user,
Checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbList, types.VerbRead, types.VerbUpdate, types.VerbCreate, types.VerbDelete},
},
},
},
Identity: &authz.LocalUser{
Username: "alice",
Identity: tlsca.Identity{
Groups: []string{"dev"},
},
},
}, nil
}
type fakeChecker struct {
services.AccessChecker
rules []types.Rule
roles []string
checks []check
}
type check struct {
kind, verb string
}
func (f *fakeChecker) CheckAccessToRule(context services.RuleContext, namespace string, kind string, verb string, silent bool) error {
c := check{kind, verb}
f.checks = append(f.checks, c)
for _, r := range f.rules {
if r.HasResource(kind) && r.HasVerb(verb) {
return nil
}
}
return trace.AccessDenied("access to %s with verb %s is not allowed", kind, verb)
}
// HasRole checks if the checker includes the role
func (f *fakeChecker) HasRole(target string) bool {
for _, role := range f.roles {
if role == target {
return true
}
}
return false
}
type serviceOpt = func(*Service)
func withAuthorizer(authz authz.Authorizer) serviceOpt {
return func(service *Service) {
service.authorizer = authz
}
}
type env struct {
*Service
emitter *eventstest.ChannelEmitter
backend Backend
}
func newTestEnv(opts ...serviceOpt) (*env, error) {
bk, err := memory.New(memory.Config{})
if err != nil {
return nil, trace.Wrap(err, "creating memory backend")
}
service := struct {
services.Identity
services.Access
}{
Identity: local.NewIdentityService(bk),
Access: local.NewAccessService(bk),
}
emitter := eventstest.NewChannelEmitter(10)
svc, err := NewService(ServiceConfig{
Authorizer: fakeAuthorizer{authorize: true},
Cache: service,
Backend: service,
Emitter: emitter,
Reporter: usagereporter.DiscardUsageReporter{},
})
if err != nil {
return nil, trace.Wrap(err, "creating users service")
}
for _, opt := range opts {
opt(svc)
}
return &env{
Service: svc,
emitter: emitter,
backend: service,
}, nil
}
func TestCreateUser(t *testing.T) {
t.Parallel()
env, err := newTestEnv()
require.NoError(t, err, "creating test service")
ctx := context.Background()
llama, err := types.NewUser("llama")
require.NoError(t, err, "creating new user llama")
// Create a new user.
created, err := env.CreateUser(ctx, &userspb.CreateUserRequest{User: llama.(*types.UserV2)})
require.NoError(t, err, "creating user llama")
// Validate that the user now exists.
resp, err := env.GetUser(ctx, &userspb.GetUserRequest{Name: created.GetName()})
require.NoError(t, err, "failed getting created user")
require.Empty(t, cmp.Diff(created, resp, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
// Attempt to create a duplicate user
created2, err := env.CreateUser(ctx, &userspb.CreateUserRequest{User: llama.(*types.UserV2)})
assert.Error(t, err, "duplicate user was created successfully")
assert.Nil(t, created2, "received unexpected user ")
require.True(t, trace.IsAlreadyExists(err), "creating duplicate user allowed")
event := <-env.emitter.C()
assert.Equal(t, events.UserCreateEvent, event.GetType(), "unexpected event type")
assert.Equal(t, events.UserCreateCode, event.GetCode(), "unexpected event code")
}
func TestDeleteUser(t *testing.T) {
t.Parallel()
env, err := newTestEnv()
require.NoError(t, err, "creating test service")
ctx := context.Background()
llama, err := types.NewUser("llama")
require.NoError(t, err, "creating new user llama")
// Create the user which will be deleted.
created, err := env.CreateUser(ctx, &userspb.CreateUserRequest{User: llama.(*types.UserV2)})
require.NoError(t, err, "creating user llama")
event := <-env.emitter.C()
assert.Equal(t, events.UserCreateEvent, event.GetType(), "unexpected event type")
assert.Equal(t, events.UserCreateCode, event.GetCode(), "unexpected event code")
// Delete the user.
_, err = env.DeleteUser(ctx, &userspb.DeleteUserRequest{Name: created.GetName()})
require.NoError(t, err)
event = <-env.emitter.C()
assert.Equal(t, events.UserDeleteEvent, event.GetType(), "unexpected event type")
assert.Equal(t, events.UserDeleteCode, event.GetCode(), "unexpected event code")
// Attempt to delete the user again, this time deletion should fail because
// the user no longer exists.
_, err = env.DeleteUser(ctx, &userspb.DeleteUserRequest{Name: created.GetName()})
assert.Error(t, err, "deleting nonexistent user succeeded")
require.True(t, trace.IsNotFound(err), "expected a not found error deleting nonexistent user got %T", err)
}
func TestGetUser(t *testing.T) {
t.Parallel()
// create an admin authz context to test listing users with secrets
authzContext, err := authz.ContextForBuiltinRole(authz.BuiltinRole{
Role: types.RoleAdmin,
Username: string(types.RoleAdmin),
}, &types.SessionRecordingConfigV2{})
require.NoError(t, err, "creating authorization context")
env, err := newTestEnv(withAuthorizer(fakeAuthorizer{authzContext: authzContext}))
require.NoError(t, err, "creating test service")
ctx := context.Background()
llama, err := types.NewUser("llama")
require.NoError(t, err, "creating new user llama")
require.NoError(t, generateUserSecrets(llama), "generating user secrets")
// Validate that the user does not exist.
resp, err := env.GetUser(ctx, &userspb.GetUserRequest{Name: llama.GetName()})
assert.Error(t, err, "expected retrieving nonexistent user to fail")
assert.Nil(t, resp, "non-nil response returned from error")
assert.True(t, trace.IsNotFound(err), "expected not found error got %T", err)
// Create a new user.
created, err := env.CreateUser(ctx, &userspb.CreateUserRequest{User: llama.(*types.UserV2)})
require.NoError(t, err, "creating user llama")
// Validate that the user now exists and that querying by name takes precedence over
// retrieving the current user.
resp, err = env.GetUser(ctx, &userspb.GetUserRequest{Name: created.GetName(), CurrentUser: true})
assert.NoError(t, err, "failed getting created user")
assert.Empty(t, cmp.Diff(created, resp, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision"), cmpopts.IgnoreFields(types.UserSpecV2{}, "LocalAuth")))
assert.Nil(t, resp.GetLocalAuth(), "user secrets were provided when not requested")
// Validate that getting the current user returns "alice" and not "llama".
resp, err = env.GetUser(authz.ContextWithUser(ctx, &authz.LocalUser{
Username: "alice",
Identity: tlsca.Identity{
Groups: []string{"dev"},
},
}), &userspb.GetUserRequest{CurrentUser: true})
assert.NoError(t, err, "failed getting created user")
assert.NotEmpty(t, cmp.Diff(created, resp, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
assert.Equal(t, "alice", resp.GetName(), "expected current user to return alice")
assert.Nil(t, resp.GetLocalAuth(), "secrets returned with current user")
// Validate that requesting a users secrets returns them.
resp, err = env.GetUser(ctx, &userspb.GetUserRequest{Name: created.GetName(), WithSecrets: true})
assert.NoError(t, err, "failed getting created user")
assert.Empty(t, cmp.Diff(created, resp, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
assert.NotNil(t, resp.GetLocalAuth(), "user secrets were not provided requested")
assert.Empty(t, cmp.Diff(llama.GetLocalAuth(), resp.GetLocalAuth()), "user secrets do not match")
// Validate that getting the current user never returns secrets
resp, err = env.GetUser(authz.ContextWithUser(ctx, &authz.LocalUser{
Username: "alice",
Identity: tlsca.Identity{
Groups: []string{"dev"},
},
}), &userspb.GetUserRequest{CurrentUser: true, WithSecrets: true})
assert.NoError(t, err, "failed getting created user")
assert.NotEmpty(t, cmp.Diff(created, resp, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
assert.Equal(t, "alice", resp.GetName(), "expected current user to return alice")
assert.Nil(t, resp.GetLocalAuth(), "secrets returned with current user")
}
func TestUpdateUser(t *testing.T) {
t.Parallel()
env, err := newTestEnv()
require.NoError(t, err, "creating test service")
ctx := context.Background()
llama, err := types.NewUser("llama")
require.NoError(t, err, "creating new user llama")
// Attempt to update a nonexistent user.
updated, err := env.UpdateUser(ctx, &userspb.UpdateUserRequest{User: llama.(*types.UserV2)})
assert.Error(t, err, "duplicate user was created successfully")
assert.Nil(t, updated, "received unexpected user")
require.True(t, trace.IsNotFound(err), "updated nonexistent user")
// Create a new user.
created, err := env.CreateUser(ctx, &userspb.CreateUserRequest{User: llama.(*types.UserV2)})
require.NoError(t, err, "creating user llama")
event := <-env.emitter.C()
assert.Equal(t, events.UserCreateEvent, event.GetType(), "unexpected event type")
assert.Equal(t, events.UserCreateCode, event.GetCode(), "unexpected event code")
// Attempt to update the user again.
created.SetLogins([]string{"alpaca"})
updated, err = env.UpdateUser(ctx, &userspb.UpdateUserRequest{User: created})
require.NoError(t, err, "failed updating user")
require.Empty(t, cmp.Diff(created, updated, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
require.Equal(t, []string{"alpaca"}, updated.GetLogins(), "logins were not updated")
event = <-env.emitter.C()
assert.Equal(t, events.UserUpdatedEvent, event.GetType(), "unexpected event type")
assert.Equal(t, events.UserUpdateCode, event.GetCode(), "unexpected event code")
}
func TestUpsertUser(t *testing.T) {
t.Parallel()
env, err := newTestEnv()
require.NoError(t, err, "creating test service")
ctx := context.Background()
llama, err := types.NewUser("llama")
require.NoError(t, err, "creating new user llama")
// Create a user via upsert.
upserted, err := env.UpsertUser(ctx, &userspb.UpsertUserRequest{User: llama.(*types.UserV2)})
require.NoError(t, err, "failed upserting user")
// Validate that the user was created.
created, err := env.CreateUser(ctx, &userspb.CreateUserRequest{User: llama.(*types.UserV2)})
assert.Error(t, err, "duplicate user was created successfully")
assert.Nil(t, created, "received unexpected user ")
require.True(t, trace.IsAlreadyExists(err), "creating duplicate user allowed")
event := <-env.emitter.C()
assert.Equal(t, events.UserCreateEvent, event.GetType(), "unexpected event type")
assert.Equal(t, events.UserCreateCode, event.GetCode(), "unexpected event code")
// Attempt to update the user again.
upserted.SetLogins([]string{"alpaca"})
updated, err := env.UpsertUser(ctx, &userspb.UpsertUserRequest{User: upserted})
require.NoError(t, err, "failed upserting user")
require.Empty(t, cmp.Diff(upserted, updated, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
require.Equal(t, []string{"alpaca"}, updated.GetLogins(), "logins were not updated")
event = <-env.emitter.C()
assert.Equal(t, events.UserCreateEvent, event.GetType(), "unexpected event type")
assert.Equal(t, events.UserCreateCode, event.GetCode(), "unexpected event code")
}
func generateUserSecrets(u types.User) error {
hash, err := bcrypt.GenerateFromPassword([]byte("insecure"), bcrypt.MinCost)
if err != nil {
return trace.Wrap(err)
}
dev, err := services.NewTOTPDevice("otp", base32.StdEncoding.EncodeToString([]byte("abc123")), time.Now())
if err != nil {
return trace.Wrap(err)
}
u.SetLocalAuth(&types.LocalAuthSecrets{
PasswordHash: hash,
MFA: []*types.MFADevice{dev},
})
return nil
}
func TestRBAC(t *testing.T) {
t.Parallel()
ctx := context.Background()
llama, err := types.NewUser("llama")
require.NoError(t, err, "creating new user llama")
tests := []struct {
desc string
f func(t *testing.T, service *Service)
checker *fakeChecker
expectChecks []check
}{
{
desc: "get no access",
f: func(t *testing.T, service *Service) {
_, err := service.GetUser(ctx, &userspb.GetUserRequest{Name: "alice"})
assert.Error(t, err, "expected RBAC to prevent getting user")
assert.True(t, trace.IsAccessDenied(err), "expected access denied error got %T", err)
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbCreate},
{kind: types.KindUser, verb: types.VerbRead},
},
},
{
desc: "get current users when no access",
f: func(t *testing.T, service *Service) {
user, err := service.GetUser(ctx, &userspb.GetUserRequest{CurrentUser: true})
assert.NoError(t, err, "expected RBAC to allow getting the current user")
assert.Empty(t, cmp.Diff(llama, user, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
},
},
},
},
{
desc: "get with secrets no access",
f: func(t *testing.T, service *Service) {
_, err := service.GetUser(ctx, &userspb.GetUserRequest{Name: "alice", WithSecrets: true})
assert.Error(t, err, "expected RBAC to prevent getting user")
assert.True(t, trace.IsAccessDenied(err), "expected access denied error got %T", err)
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbRead, types.VerbCreate, types.VerbList},
},
},
},
expectChecks: []check{},
},
{
desc: "create no access",
f: func(t *testing.T, service *Service) {
_, err := service.CreateUser(ctx, &userspb.CreateUserRequest{User: llama.(*types.UserV2)})
assert.Error(t, err, "expected RBAC to prevent creating user")
assert.True(t, trace.IsAccessDenied(err), "expected access denied error got %T", err)
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbCreate},
},
},
{
desc: "create",
f: func(t *testing.T, service *Service) {
u := utils.CloneProtoMsg(llama.(*types.UserV2))
u.SetName("alpaca")
created, err := service.CreateUser(ctx, &userspb.CreateUserRequest{User: u})
assert.NoError(t, err, "expected RBAC to allow creating user")
assert.Empty(t, cmp.Diff(u, created, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbCreate},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbCreate},
},
},
{
desc: "update no access",
f: func(t *testing.T, service *Service) {
_, err := service.UpdateUser(ctx, &userspb.UpdateUserRequest{User: llama.(*types.UserV2)})
assert.Error(t, err, "expected RBAC to prevent updating user")
assert.True(t, trace.IsAccessDenied(err), "expected access denied error got %T", err)
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbUpdate},
},
},
{
desc: "update",
f: func(t *testing.T, service *Service) {
u := utils.CloneProtoMsg(llama.(*types.UserV2))
u.SetLogins([]string{"alpaca"})
updated, err := service.UpdateUser(ctx, &userspb.UpdateUserRequest{User: u})
assert.NoError(t, err, "expected RBAC to allow updating user")
assert.Empty(t, cmp.Diff(u, updated, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbUpdate},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbUpdate},
},
},
{
desc: "upsert no access",
f: func(t *testing.T, service *Service) {
_, err := service.UpsertUser(ctx, &userspb.UpsertUserRequest{User: llama.(*types.UserV2)})
assert.Error(t, err, "expected RBAC to prevent upserting user")
assert.True(t, trace.IsAccessDenied(err), "expected access denied error got %T", err)
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbCreate},
{kind: types.KindUser, verb: types.VerbUpdate},
},
},
{
desc: "upsert without create",
f: func(t *testing.T, service *Service) {
_, err := service.UpsertUser(ctx, &userspb.UpsertUserRequest{User: llama.(*types.UserV2)})
assert.Error(t, err, "expected RBAC to prevent upserting user")
assert.True(t, trace.IsAccessDenied(err), "expected access denied error got %T", err)
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbUpdate},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbCreate},
{kind: types.KindUser, verb: types.VerbUpdate},
},
},
{
desc: "upsert without update",
f: func(t *testing.T, service *Service) {
_, err := service.UpsertUser(ctx, &userspb.UpsertUserRequest{User: llama.(*types.UserV2)})
assert.Error(t, err, "expected RBAC to prevent upserting user")
assert.True(t, trace.IsAccessDenied(err), "expected access denied error got %T", err)
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbCreate},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbCreate},
{kind: types.KindUser, verb: types.VerbUpdate},
},
},
{
desc: "upsert",
f: func(t *testing.T, service *Service) {
upserted, err := service.UpsertUser(ctx, &userspb.UpsertUserRequest{User: llama.(*types.UserV2)})
assert.NoError(t, err, "expected RBAC to allow updating user")
assert.Empty(t, cmp.Diff(llama, upserted, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbCreate, types.VerbUpdate},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbCreate},
{kind: types.KindUser, verb: types.VerbUpdate},
},
},
{
desc: "delete no access",
f: func(t *testing.T, service *Service) {
_, err := service.DeleteUser(ctx, &userspb.DeleteUserRequest{Name: llama.GetName()})
assert.Error(t, err, "expected RBAC to prevent deleting user")
assert.True(t, trace.IsAccessDenied(err), "expected access denied error got %T", err)
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbDelete},
},
},
{
desc: "delete",
f: func(t *testing.T, service *Service) {
_, err := service.DeleteUser(ctx, &userspb.DeleteUserRequest{Name: llama.GetName()})
assert.NoError(t, err, "expected RBAC to allow deleting user")
},
checker: &fakeChecker{
rules: []types.Rule{
{
Resources: []string{types.KindUser},
Verbs: []string{types.VerbDelete},
},
},
},
expectChecks: []check{
{kind: types.KindUser, verb: types.VerbDelete},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
env, err := newTestEnv(withAuthorizer(&fakeAuthorizer{authzContext: &authz.Context{
User: llama,
Checker: test.checker,
Identity: authz.LocalUser{
Username: "alice",
Identity: tlsca.Identity{
Groups: []string{"dev"},
},
},
}}))
require.NoError(t, err, "creating test service")
// Create the user directly on the backend to bypass RBAC enforced by the test cases.
_, err = env.backend.CreateUser(ctx, llama.(*types.UserV2))
require.NoError(t, err, "creating test user")
// Validate RBAC is enforced.
test.f(t, env.Service)
require.ElementsMatch(t, test.expectChecks, test.checker.checks)
})
}
}