mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
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:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user