diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index ba7eec34d22..8e95b786f19 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -215,9 +215,8 @@ func (s *APIServer) WithAuth(handler HandlerWithAuthFunc) httprouter.Handle { } auth := &ServerWithRoles{ - authServer: s.AuthServer, + serverBase: serverBase{authServer: s.AuthServer, alog: s.AuthServer}, context: *authContext, - alog: s.AuthServer, } version := p.ByName("version") if version == "" { @@ -227,23 +226,19 @@ func (s *APIServer) WithAuth(handler HandlerWithAuthFunc) httprouter.Handle { }) } -func (s *APIServer) WithScopedAuth(handler HandlerWithAuthFunc) httprouter.Handle { +// ScopedHandlerWithAuthFunc is an HTTP handler with a scoped auth context. +type ScopedHandlerWithAuthFunc func(auth *ScopedServerWithRoles, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (any, error) + +func (s *APIServer) WithScopedAuth(handler ScopedHandlerWithAuthFunc) httprouter.Handle { return httplib.MakeHandler(func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (any, error) { // HTTPS server expects auth context to be set by the auth middleware scopedContext, err := s.ScopedAuthorizer.AuthorizeScoped(r.Context()) if err != nil { return nil, trace.Wrap(err) } - authContext, ok := scopedContext.UnscopedContext() - if !ok { - authContext = &authz.Context{} - } - - auth := &ServerWithRoles{ - authServer: s.AuthServer, - context: *authContext, + auth := &ScopedServerWithRoles{ + serverBase: serverBase{authServer: s.AuthServer, alog: s.AuthServer}, scopedContext: scopedContext, - alog: s.AuthServer, } version := p.ByName("version") if version == "" { @@ -327,7 +322,7 @@ func (s *APIServer) upsertProxy(auth *ServerWithRoles, w http.ResponseWriter, r // getProxies returns registered proxies // // TODO(kiosion) DELETE IN 21.0.0 -func (s *APIServer) getProxies(auth *ServerWithRoles, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (any, error) { +func (s *APIServer) getProxies(auth *ScopedServerWithRoles, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (any, error) { //nolint:staticcheck // TODO(kiosion) DELETE IN 21.0.0 servers, err := auth.GetProxies() if err != nil { @@ -354,7 +349,7 @@ func (s *APIServer) deleteProxy(auth *ServerWithRoles, w http.ResponseWriter, r // getAuthServers returns registered auth servers // // TODO(kiosion) DELETE IN 21.0.0 -func (s *APIServer) getAuthServers(auth *ServerWithRoles, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (any, error) { +func (s *APIServer) getAuthServers(auth *ScopedServerWithRoles, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (any, error) { //nolint:staticcheck // TODO(kiosion) DELETE IN 21.0.0 servers, err := auth.GetAuthServers() if err != nil { diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 713eeeac9b0..8b2e6f535e7 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -22,7 +22,6 @@ import ( "bytes" "cmp" "context" - "errors" "fmt" "iter" "log/slog" @@ -80,23 +79,69 @@ import ( logutils "github.com/gravitational/teleport/lib/utils/log" ) +// serverBase holds the fields common to [ServerWithRoles] and [ScopedServerWithRoles]. +type serverBase struct { + authServer *Server + alog events.AuditLogSessionStreamer +} + // ServerWithRoles is a wrapper around auth service // methods that focuses on authorizing every request type ServerWithRoles struct { - authServer *Server - alog events.AuditLogSessionStreamer + serverBase // context holds authorization context context authz.Context +} - // scopedContext is an authz context that may or may not be scoped, some methods which have been - // converted to support scoped identities will be supplied with this context instead of the standard - // context field above. Only one of the two can be set at a time, so care must be taken to ensure - // that the correct one is used for a given method. +// ScopedServerWithRoles is the equivalent of [ServerWithRoles] for APIs that support scoped callers. +// Like [ServerWithRoles], this type wraps the auth service and enforces authorization checks. Methods +// on ScopedServerWithRoles support scoped *and* unscoped callers. There are no APIs that *only* support +// scoped callers. +// +// NOTE: Many of the methods on this type are only partially migrated to support scopes. Some still invoke +// unscoped logic in [ServerWithRoles] when called by an unscoped identity. Others have carve-outs where the +// underlying unscoped access checker is unwrapped in order to leverage specific features not yet ported over +// to scopes. +type ScopedServerWithRoles struct { + serverBase scopedContext *authz.ScopedContext } +// ScopedServerWithRoles returns a ScopedServerWithRoles instance with a scoped authz context derived from +// this instance's unscoped authz context. Unscoped permissions can always be represented as if they were +// scoped permissions. This method may be useful if an unscoped API needs to leverage functionality that +// has already been ported to support scopes. +func (a *ServerWithRoles) ScopedServerWithRoles() *ScopedServerWithRoles { + return &ScopedServerWithRoles{ + serverBase: a.serverBase, + scopedContext: authz.ScopedContextFromUnscopedContext(&a.context), + } +} + +// UnscopedServerWithRoles attempts to unwrap the underlying identity and authz context into an unscoped +// context and returns a ServerWithRoles if successful. This only succeeds if the calling identity is +// unscoped. Scoped permissions cannot be represented as if they were unscoped. This method may be useful +// if an API that has been partially ported over to support scoped callers still has substantive unscoped +// behavior which has no scoped equivalent. In that case, the unscoped logic can be conditionally invoked +// via this method if/when the caller is unscoped. +func (a *ScopedServerWithRoles) UnscopedServerWithRoles() (*ServerWithRoles, bool) { + unscopedCtx, ok := a.scopedContext.UnscopedContext() + if !ok { + return nil, false + } + return &ServerWithRoles{ + serverBase: a.serverBase, + context: *unscopedCtx, + }, true +} + // CloseContext is closed when the auth server shuts down func (a *ServerWithRoles) CloseContext() context.Context { + return a.ScopedServerWithRoles().CloseContext() +} + +// CloseContext is closed when the auth server shuts down. +func (a *ScopedServerWithRoles) CloseContext() context.Context { return a.authServer.closeCtx } @@ -126,22 +171,16 @@ func (a *ServerWithRoles) actionWithContext(ctx *services.Context, resource stri } // actionNamspace will determine if a user has access to the given verbs against the given resource kind in the given -// namespace. It will return [services.ErrScopedIdentity] when called with a scoped identity. Scoped authorization paths -// should instead use the resource-specific ScopedAccessChecker.CheckAccessToRules. +// namespace. func (a *ServerWithRoles) actionNamespace(namespace, resource string, verb string, extraVerbs ...string) error { - _, unscopedCtx, isScoped := a.resolveAuthContext() - if isScoped { - return trace.Wrap(services.ErrScopedIdentity) - } - var errs []error - if err := unscopedCtx.Checker.CheckAccessToRule(&services.Context{User: unscopedCtx.User}, namespace, resource, verb); err != nil { + if err := a.context.Checker.CheckAccessToRule(&services.Context{User: a.context.User}, namespace, resource, verb); err != nil { errs = append(errs, err) } for _, verb := range extraVerbs { - if err := unscopedCtx.Checker.CheckAccessToRule(&services.Context{User: unscopedCtx.User}, namespace, resource, verb); err != nil { + if err := a.context.Checker.CheckAccessToRule(&services.Context{User: a.context.User}, namespace, resource, verb); err != nil { errs = append(errs, err) } } @@ -172,24 +211,18 @@ func (a *ServerWithRoles) currentUserAction(username string) error { apidefaults.Namespace, types.KindUser, types.VerbCreate) } -// scopedCurrentUserAction is the same as currentUserAction but it supports both scoped -// and unscoped auth contexts. -func (a *ServerWithRoles) scopedCurrentUserAction(username string) error { - if a.scopedContext == nil { - return trace.Wrap(a.currentUserAction(username)) +// scopedCurrentUserAction checks whether username matches the current scoped identity. +func (a *ScopedServerWithRoles) scopedCurrentUserAction(username string) error { + if unscoped, ok := a.UnscopedServerWithRoles(); ok { + // unscoped current user action is substantively different because it provides an exception + // that allows global admins to also benefit from the "current user" exception, something which + // does not currently have a scoped equivalent. + return unscoped.currentUserAction(username) } - scopedCtx, unscopedCtx, isScoped := a.resolveAuthContext() - if isScoped { - if authz.ScopedIsCurrentUser(scopedCtx, username) { - return nil - } - return trace.Wrap(services.ErrScopedIdentity, "checking create access for users") - } - if authz.IsCurrentUser(*unscopedCtx, username) { + if authz.ScopedIsCurrentUser(a.scopedContext, username) { return nil } - return unscopedCtx.Checker.CheckAccessToRule(&services.Context{User: unscopedCtx.User}, - apidefaults.Namespace, types.KindUser, types.VerbCreate) + return trace.AccessDenied("access denied") } // authConnectorAction is a special checker that grants access to auth @@ -302,8 +335,7 @@ func (a *ServerWithRoles) actionForKindSession(ctx context.Context, sid session. // localServerAction returns an access denied error if the role is not one of the builtin server roles. func (a *ServerWithRoles) localServerAction() error { - ident := a.getIdentityGetter() - role, ok := ident.(authz.BuiltinRole) + role, ok := a.context.Identity.(authz.BuiltinRole) if !ok || !role.IsServer() { return trace.AccessDenied("this request can be only executed by a teleport built-in server") } @@ -312,12 +344,7 @@ func (a *ServerWithRoles) localServerAction() error { // remoteServerAction returns an access denied error if the role is not one of the remote builtin server roles. func (a *ServerWithRoles) remoteServerAction() error { - _, unscopedCtx, isScoped := a.resolveAuthContext() - if isScoped { - return trace.Wrap(services.ErrScopedIdentity, "checking remote server action") - } - - role, ok := unscopedCtx.UnmappedIdentity.(authz.RemoteBuiltinRole) + role, ok := a.context.UnmappedIdentity.(authz.RemoteBuiltinRole) if !ok || !role.IsRemoteServer() { return trace.AccessDenied("this request can be only executed by a teleport remote server") } @@ -658,13 +685,10 @@ func (a *ServerWithRoles) GetDomainName(ctx context.Context) (string, error) { return a.authServer.GetDomainName() } -// getClusterCACert returns the PEM-encoded TLS certs for the local cluster +// GetClusterCACert returns the PEM-encoded TLS certs for the local cluster // without signing keys. If the cluster has multiple TLS certs, they will all // be concatenated. -func (a *ServerWithRoles) GetClusterCACert( - ctx context.Context, -) (*proto.GetClusterCACertResponse, error) { - // Allow all roles to get the CA certs. +func (a *ScopedServerWithRoles) GetClusterCACert(ctx context.Context) (*proto.GetClusterCACertResponse, error) { return a.authServer.GetClusterCACert(ctx) } @@ -1174,6 +1198,18 @@ func (a *ServerWithRoles) NewStream(ctx context.Context, watch types.Watch) (str return a.authServer.NewStream(ctx, watch) } +// NewStream is the scoped equivalent of [ServerWithRoles.NewStream]. This method currently only supports a +// small subset of the functionality of its unscoped counterpart when invoked by scoped callers. +func (a *ScopedServerWithRoles) NewStream(ctx context.Context, watch types.Watch) (stream.Stream[types.Event], error) { + if unscoped, ok := a.UnscopedServerWithRoles(); ok { + return unscoped.NewStream(ctx, watch) + } + if err := a.authorizeWatchRequest(ctx, &watch); err != nil { + return nil, trace.Wrap(err) + } + return a.authServer.NewStream(ctx, watch) +} + // NewWatcher returns a new event watcher func (a *ServerWithRoles) NewWatcher(ctx context.Context, watch types.Watch) (types.Watcher, error) { if err := a.authorizeWatchRequest(ctx, &watch); err != nil { @@ -1182,6 +1218,18 @@ func (a *ServerWithRoles) NewWatcher(ctx context.Context, watch types.Watch) (ty return a.authServer.NewWatcher(ctx, watch) } +// NewWatcher is the scoped equivalent of [ServerWithRoles.NewWatcher]. This method currently only supports a +// small subset of the functionality of its unscoped counterpart when invoked by scoped callers. +func (a *ScopedServerWithRoles) NewWatcher(ctx context.Context, watch types.Watch) (types.Watcher, error) { + if unscoped, ok := a.UnscopedServerWithRoles(); ok { + return unscoped.NewWatcher(ctx, watch) + } + if err := a.authorizeWatchRequest(ctx, &watch); err != nil { + return nil, trace.Wrap(err) + } + return a.authServer.NewWatcher(ctx, watch) +} + // authorizeWatchRequest performs permission checks and filtering on incoming watch requests. func (a *ServerWithRoles) authorizeWatchRequest(ctx context.Context, watch *types.Watch) error { if len(watch.Kinds) == 0 { @@ -1216,25 +1264,27 @@ func (a *ServerWithRoles) authorizeWatchRequest(ctx context.Context, watch *type return nil } -// hasWatchPermissionForKindScoped evaluates whether an identity can watch a -// specified kind. Must only be called when a.scopedContext != nil - -// i.e. scopedAuthenticate produced the ServerWithRoles. -func (a *ServerWithRoles) hasWatchPermissionForKindScoped( - ctx context.Context, kind types.WatchKind, -) error { - // Scoped identities currently receive "special" handling. For now, we only - // support watching the cert_authority kind, with load_secrets=false. - // - // For this, we use RiskyAuthorizeUnpinnedRead to permit scoped identities to - // read an unscoped resource. - if kind.Kind != types.KindCertAuthority { - return trace.AccessDenied("scoped identities are not permitted to watch kind %q", kind.Kind) +// authorizeWatchRequest is the scoped equivalent of [ServerWithRoles.authorizeWatchRequest]. +// Queue-size tuning for built-in roles is omitted; kind support is restricted (see hasWatchPermissionForKind). +func (a *ScopedServerWithRoles) authorizeWatchRequest(ctx context.Context, watch *types.Watch) error { + if len(watch.Kinds) == 0 { + return trace.AccessDenied("can't setup global watch") } - if kind.LoadSecrets { - return trace.AccessDenied("scoped identities are not permitted to watch cert_authority with load_secrets=true") + validKinds := make([]types.WatchKind, 0, len(watch.Kinds)) + for _, kind := range watch.Kinds { + if err := a.hasWatchPermissionForKind(ctx, kind); err != nil { + if watch.AllowPartialSuccess { + continue + } + return trace.Wrap(err) + } + validKinds = append(validKinds, kind) } - ruleCtx := a.scopedContext.RuleContext() - return a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(ctx, services.UnpinnedReadCertAuthority, &ruleCtx) + if len(validKinds) == 0 { + return trace.BadParameter("none of the requested kinds can be watched") + } + watch.Kinds = validKinds + return nil } // hasWatchPermissionForKind checks the permissions for data of each kind. @@ -1244,10 +1294,6 @@ func (a *ServerWithRoles) hasWatchPermissionForKind( ctx context.Context, kind types.WatchKind, ) error { - // For scoped identities, we perform a different authz check. - if a.scopedContext != nil { - return trace.Wrap(a.hasWatchPermissionForKindScoped(ctx, kind)) - } verb := types.VerbRead switch kind.Kind { @@ -1305,6 +1351,19 @@ func (a *ServerWithRoles) hasWatchPermissionForKind( return trace.Wrap(a.authorizeAction(kind.Kind, verb)) } +// hasWatchPermissionForKind is the scoped equivalent of [ServerWithRoles.hasWatchPermissionForKind]. +// Currently only cert_authority with load_secrets=false is permitted for scoped identities. +func (a *ScopedServerWithRoles) hasWatchPermissionForKind(ctx context.Context, kind types.WatchKind) error { + if kind.Kind != types.KindCertAuthority { + return trace.AccessDenied("scoped identities are not permitted to watch kind %q", kind.Kind) + } + if kind.LoadSecrets { + return trace.AccessDenied("scoped identities are not permitted to watch cert_authority with load_secrets=true") + } + ruleCtx := a.scopedContext.RuleContext() + return a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(ctx, services.UnpinnedReadCertAuthority, &ruleCtx) +} + // DeleteAllNodes deletes all nodes in a given namespace func (a *ServerWithRoles) DeleteAllNodes(ctx context.Context, namespace string) error { if err := a.actionNamespace(namespace, types.KindNode, types.VerbDelete); err != nil { @@ -1670,7 +1729,13 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L }, nil } -func (a *ServerWithRoles) scopedListUnifiedResources(ctx context.Context, req *proto.ListUnifiedResourcesRequest) (*proto.ListUnifiedResourcesResponse, error) { +// ListUnifiedResources is the scoped equivalent of [ServerWithRoles.ListUnifiedResources]. +// For scoped callers only node kind is currently supported. Advanced features such as search-as-roles, +// preview-as-roles, requestable resources, and login inclusion are not yet available for scoped identities. +func (a *ScopedServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.ListUnifiedResourcesRequest) (*proto.ListUnifiedResourcesResponse, error) { + if unscoped, ok := a.UnscopedServerWithRoles(); ok { + return unscoped.ListUnifiedResources(ctx, req) + } // most advanced features don't work for scoped identities yet switch { case req.UseSearchAsRoles: @@ -1898,6 +1963,17 @@ var disableUnqualifiedLookups = os.Getenv("TELEPORT_UNSTABLE_DISABLE_UNQUALIFIED // which is what we want when handling things like ambiguous host errors and resource-based access requests, // but may result in confusing behavior if it is used outside of those contexts. func (a *ServerWithRoles) GetSSHTargets(ctx context.Context, req *proto.GetSSHTargetsRequest) (*proto.GetSSHTargetsResponse, error) { + return a.ScopedServerWithRoles().GetSSHTargets(ctx, req) +} + +func (a *ServerWithRoles) sshTargets(ctx context.Context, host, port string) iterstream.Stream[*types.ServerV2] { + return a.ScopedServerWithRoles().sshTargets(ctx, host, port) +} + +// GetSSHTargets handles both scoped and unscoped callers. The only behavioral difference is +// search_as_roles: unscoped callers have it enabled (full access semantics); scoped callers do +// not (not yet supported). Everything else is uniform. +func (a *ScopedServerWithRoles) GetSSHTargets(ctx context.Context, req *proto.GetSSHTargetsRequest) (*proto.GetSSHTargetsResponse, error) { servers, err := iterstream.Collect(a.sshTargets(ctx, req.GetHost(), req.GetPort())) if err != nil { return nil, err @@ -1905,7 +1981,7 @@ func (a *ServerWithRoles) GetSSHTargets(ctx context.Context, req *proto.GetSSHTa return &proto.GetSSHTargetsResponse{Servers: servers}, nil } -func (a *ServerWithRoles) sshTargets(ctx context.Context, host, port string) iterstream.Stream[*types.ServerV2] { +func (a *ScopedServerWithRoles) sshTargets(ctx context.Context, host, port string) iterstream.Stream[*types.ServerV2] { // try to detect case-insensitive routing setting, but default to false if we can't load // networking config (equivalent to proxy routing behavior). var caseInsensitiveRouting bool @@ -1923,45 +1999,24 @@ func (a *ServerWithRoles) sshTargets(ctx context.Context, host, port string) ite return iterstream.Fail[*types.ServerV2](trace.Wrap(err)) } - // note that we're using a ServerWithRoles level method here rather than some internal method. We are - // delegating all RBAC filtering to the lister and then performing additional filtering on top of that. - // Until we unify scoped/unscoped resource listing this bifurcation is necessary to ensure that search_as_roles - // results are available for unscoped identities. - var pageFunc func(ctx context.Context, pageSize int, pageToken string) ([]*proto.PaginatedResource, string, error) - if a.scopedContext == nil { - pageFunc = func(ctx context.Context, pageSize int, pageToken string) ([]*proto.PaginatedResource, string, error) { - resp, err := a.ListUnifiedResources(ctx, &proto.ListUnifiedResourcesRequest{ - Kinds: []string{types.KindNode}, - SortBy: types.SortBy{Field: types.ResourceMetadataName}, - UseSearchAsRoles: true, + // search_as_roles is not yet supported for scoped identities. + _, isUnscoped := a.UnscopedServerWithRoles() - StartKey: pageToken, - Limit: int32(pageSize), - }) - if err != nil { - return nil, "", trace.Wrap(err) - } - return resp.GetResources(), resp.GetNextKey(), nil + resources := clientutils.Resources(ctx, func(ctx context.Context, pageSize int, pageToken string) ([]*proto.PaginatedResource, string, error) { + resp, err := a.ListUnifiedResources(ctx, &proto.ListUnifiedResourcesRequest{ + Kinds: []string{types.KindNode}, + SortBy: types.SortBy{Field: types.ResourceMetadataName}, + UseSearchAsRoles: isUnscoped, + StartKey: pageToken, + Limit: int32(pageSize), + }) + if err != nil { + return nil, "", trace.Wrap(err) } - } else { - pageFunc = func(ctx context.Context, pageSize int, pageToken string) ([]*proto.PaginatedResource, string, error) { - resp, err := a.scopedListUnifiedResources(ctx, &proto.ListUnifiedResourcesRequest{ - Kinds: []string{types.KindNode}, - SortBy: types.SortBy{Field: types.ResourceMetadataName}, - UseSearchAsRoles: false, // TODO(fspmarshall/scopes): switch this to true once we support search_as_roles with scoped identities + return resp.GetResources(), resp.GetNextKey(), nil + }) - StartKey: pageToken, - Limit: int32(pageSize), - }) - if err != nil { - return nil, "", trace.Wrap(err) - } - return resp.GetResources(), resp.GetNextKey(), nil - } - } - - resources := clientutils.Resources(ctx, pageFunc) - servers := iterstream.FilterMap(resources, func(rsc *proto.PaginatedResource) (*types.ServerV2, bool) { + return iterstream.FilterMap(resources, func(rsc *proto.PaginatedResource) (*types.ServerV2, bool) { srv := rsc.GetNode() if srv == nil { a.authServer.logger.WarnContext(ctx, "Skipping unexpected resource type, expected *types.ServerV2", @@ -1974,8 +2029,6 @@ func (a *ServerWithRoles) sshTargets(ctx context.Context, host, port string) ite } return srv, true }) - - return servers } // ResolveSSHTarget gets a server that would match an equivalent ssh dial request. @@ -2071,8 +2124,13 @@ func (a *ServerWithRoles) ResolveSSHTarget(ctx context.Context, req *proto.Resol return &proto.ResolveSSHTargetResponse{Server: bestServer}, nil } -func (a *ServerWithRoles) scopedListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) { - // most advanced features don't work for scoped identities yet +// ListResources is the scoped equivalent of [ServerWithRoles.ListResources]. Only kube_server and +// kube_cluster resource types are currently supported for scoped identities. Advanced features like +// search_as_roles, preview_as_roles, and login inclusion are not yet available for scoped identities. +func (a *ScopedServerWithRoles) ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) { + if unscoped, ok := a.UnscopedServerWithRoles(); ok { + return unscoped.ListResources(ctx, req) + } switch { case req.UseSearchAsRoles: return nil, trace.AccessDenied("search_as_roles is not supported for scoped identities") @@ -2095,17 +2153,36 @@ func (a *ServerWithRoles) scopedListResources(ctx context.Context, req proto.Lis // listResourcesWithSort with a kind that does not properly support scoped access. return nil, trace.BadParameter("scoped resource kind %q does not support fake pagination", req.ResourceType) } - // listResourcesWithSort is unsafe to call with req.IncludeLogins in a scoped context. We guard against it at - // the top of this function but it is worth repeating explicitly here. - if req.IncludeLogins { - return nil, trace.AccessDenied("include_logins is not supported for scoped identities") + if err := req.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) } - resp, err := a.listResourcesWithSort(ctx, req) + kubeServers, err := a.GetKubernetesServers(ctx) if err != nil { return nil, trace.Wrap(err) } - - return resp, nil + var clusters []types.KubeCluster + for _, svc := range kubeServers { + clusters = append(clusters, svc.GetCluster()) + } + sortedClusters := types.KubeClusters(clusters) + if err := sortedClusters.SortByCustom(req.SortBy); err != nil { + return nil, trace.Wrap(err) + } + params := local.FakePaginateParams{ + ResourceType: req.ResourceType, + Limit: req.Limit, + Labels: req.Labels, + SearchKeywords: req.SearchKeywords, + StartKey: req.StartKey, + } + if req.PredicateExpression != "" { + expression, err := services.NewResourceExpression(req.PredicateExpression) + if err != nil { + return nil, trace.Wrap(err) + } + params.PredicateExpression = expression + } + return local.FakePaginate(sortedClusters.AsResources(), params) } if err := req.CheckAndSetDefaults(); err != nil { @@ -2188,9 +2265,6 @@ func (a *ServerWithRoles) ListResources(ctx context.Context, req proto.ListResou return nil, trace.Wrap(err) } - if a.scopedContext != nil { - return a.scopedListResources(ctx, req) - } // Apply any requested additional search_as_roles and/or preview_as_roles // for the duration of the search. if req.UseSearchAsRoles || req.UsePreviewAsRoles { @@ -2650,21 +2724,20 @@ func (a *ServerWithRoles) listResourcesWithSort(ctx context.Context, req proto.L // // TODO(kiosion) DELETE IN 21.0.0 func (a *ServerWithRoles) GetAuthServers() ([]types.Server, error) { - if a.scopedContext != nil { - ruleCtx := a.scopedContext.RuleContext() - // For auth server reads we do not enforce scope pinning. This ensures that auths are readable for - // all scoped identities regardless of their current scope pinning. This pattern should not - // be used for any checks save essential global configuration reads that are necessary for basic - // teleport functionality. - if err := a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(a.CloseContext(), services.UnpinnedReadAuthServers, &ruleCtx); err != nil { - return nil, trace.Wrap(err) - } + // Retained to support the deprecated APIServer.getAuthServers HTTP handler (apiserver.go). + return a.ScopedServerWithRoles().GetAuthServers() +} - out, err := iterstream.Collect(clientutils.Resources(context.TODO(), a.authServer.ListAuthServers)) - return out, trace.Wrap(err) - } - - if err := a.authorizeAction(types.KindAuthServer, types.VerbList, types.VerbRead); err != nil { +// Deprecated: Prefer paginated variant [ScopedServerWithRoles.ListAuthServers]. +// +// TODO(kiosion) DELETE IN 21.0.0 +func (a *ScopedServerWithRoles) GetAuthServers() ([]types.Server, error) { + // For auth server reads we do not enforce scope pinning. This ensures that auths are readable for + // all scoped identities regardless of their current scope pinning. This pattern should not + // be used for any checks save essential global configuration reads that are necessary for basic + // teleport functionality. + ruleCtx := a.scopedContext.RuleContext() + if err := a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(a.CloseContext(), services.UnpinnedReadAuthServers, &ruleCtx); err != nil { return nil, trace.Wrap(err) } @@ -2672,21 +2745,14 @@ func (a *ServerWithRoles) GetAuthServers() ([]types.Server, error) { return out, trace.Wrap(err) } -func (a *ServerWithRoles) ListAuthServers(ctx context.Context, pageSize int, pageToken string) ([]types.Server, string, error) { - if a.scopedContext != nil { - ruleCtx := a.scopedContext.RuleContext() - // For auth server reads we do not enforce scope pinning. This ensures that auths are readable for - // all scoped identities regardless of their current scope pinning. This pattern should not - // be used for any checks save essential global configuration reads that are necessary for basic - // teleport functionality. - if err := a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(a.CloseContext(), services.UnpinnedReadAuthServers, &ruleCtx); err != nil { - return nil, "", trace.Wrap(err) - } - - return a.authServer.ListAuthServers(ctx, pageSize, pageToken) - } - - if err := a.authorizeAction(types.KindAuthServer, types.VerbList, types.VerbRead); err != nil { +// ListAuthServers reads a page of auth servers. +func (a *ScopedServerWithRoles) ListAuthServers(ctx context.Context, pageSize int, pageToken string) ([]types.Server, string, error) { + // For auth server reads we do not enforce scope pinning. This ensures that auths are readable for + // all scoped identities regardless of their current scope pinning. This pattern should not + // be used for any checks save essential global configuration reads that are necessary for basic + // teleport functionality. + ruleCtx := a.scopedContext.RuleContext() + if err := a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(ctx, services.UnpinnedReadAuthServers, &ruleCtx); err != nil { return nil, "", trace.Wrap(err) } @@ -2713,21 +2779,20 @@ func (a *ServerWithRoles) UpsertProxyServer(ctx context.Context, s types.Server) // // TODO(kiosion) DELETE IN 21.0.0 func (a *ServerWithRoles) GetProxies() ([]types.Server, error) { - if a.scopedContext != nil { - ruleCtx := a.scopedContext.RuleContext() - // For proxy reads we do not enforce scope pinning. This ensures that proxies are readable for - // all scoped identities regardless of their current scope pinning. This pattern should not - // be used for any checks save essential global configuration reads that are necessary for basic - // teleport functionality. - if err := a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(a.CloseContext(), services.UnpinnedReadProxies, &ruleCtx); err != nil { - return nil, trace.Wrap(err) - } + // Retained to support the deprecated APIServer.getProxies HTTP handler (apiserver.go). + return a.ScopedServerWithRoles().GetProxies() +} - out, err := iterstream.Collect(clientutils.Resources(context.TODO(), a.authServer.ListProxyServers)) - return out, trace.Wrap(err) - } - - if err := a.authorizeAction(types.KindProxy, types.VerbList, types.VerbRead); err != nil { +// Deprecated: Prefer paginated variant [ScopedServerWithRoles.ListProxyServers]. +// +// TODO(kiosion) DELETE IN 21.0.0 +func (a *ScopedServerWithRoles) GetProxies() ([]types.Server, error) { + // For proxy reads we do not enforce scope pinning. This ensures that proxies are readable for + // all scoped identities regardless of their current scope pinning. This pattern should not + // be used for any checks save essential global configuration reads that are necessary for basic + // teleport functionality. + ruleCtx := a.scopedContext.RuleContext() + if err := a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(a.CloseContext(), services.UnpinnedReadProxies, &ruleCtx); err != nil { return nil, trace.Wrap(err) } @@ -2736,20 +2801,18 @@ func (a *ServerWithRoles) GetProxies() ([]types.Server, error) { } func (a *ServerWithRoles) ListProxyServers(ctx context.Context, pageSize int, pageToken string) ([]types.Server, string, error) { - if a.scopedContext != nil { - ruleCtx := a.scopedContext.RuleContext() - // For proxy reads we do not enforce scope pinning. This ensures that proxies are readable for - // all scoped identities regardless of their current scope pinning. This pattern should not - // be used for any checks save essential global configuration reads that are necessary for basic - // teleport functionality. - if err := a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(ctx, services.UnpinnedReadProxies, &ruleCtx); err != nil { - return nil, "", trace.Wrap(err) - } + // Retained to satisfy the [services.ProxyGetter] interface on *ServerWithRoles/*grpcContext. + return a.ScopedServerWithRoles().ListProxyServers(ctx, pageSize, pageToken) +} - return a.authServer.ListProxyServers(ctx, pageSize, pageToken) - } - - if err := a.authorizeAction(types.KindProxy, types.VerbList, types.VerbRead); err != nil { +// ListProxyServers reads a page of proxy servers. +func (a *ScopedServerWithRoles) ListProxyServers(ctx context.Context, pageSize int, pageToken string) ([]types.Server, string, error) { + // For proxy reads we do not enforce scope pinning. This ensures that proxies are readable for + // all scoped identities regardless of their current scope pinning. This pattern should not + // be used for any checks save essential global configuration reads that are necessary for basic + // teleport functionality. + ruleCtx := a.scopedContext.RuleContext() + if err := a.scopedContext.CheckerContext.RiskyAuthorizeUnpinnedRead(ctx, services.UnpinnedReadProxies, &ruleCtx); err != nil { return nil, "", trace.Wrap(err) } @@ -3527,6 +3590,11 @@ func (a *ServerWithRoles) UpdatePluginData(ctx context.Context, params types.Plu // Ping gets basic info about the auth server. func (a *ServerWithRoles) Ping(ctx context.Context) (proto.PingResponse, error) { + return a.ScopedServerWithRoles().Ping(ctx) +} + +// Ping gets basic info about the auth server. +func (a *ScopedServerWithRoles) Ping(ctx context.Context) (proto.PingResponse, error) { // The Ping method does not require special permissions since it only returns // basic status information. This is an intentional design choice. Alternative // methods should be used for relaying any sensitive information. @@ -3565,28 +3633,28 @@ func (a *ServerWithRoles) GetCurrentUserRoles(ctx context.Context) ([]types.Role // determine if the user is allowed to assume the returned roles. Will set // `req.AccessRequests` and potentially shorten `req.Expires` based on the // access request expirations. -func (a *ServerWithRoles) desiredAccessInfo(ctx context.Context, req *proto.UserCertsRequest, user services.UserState) (*services.AccessInfo, error) { - if req.Username != a.getUser().GetName() { +func (a *ScopedServerWithRoles) desiredAccessInfo(ctx context.Context, req *proto.UserCertsRequest, user services.UserState) (*services.AccessInfo, error) { + if req.Username != a.scopedContext.User.GetName() { if isRoleImpersonation(*req) { a.authServer.logger.WarnContext(ctx, "User tried to issue a cert for another user while adding role requests", - "user", a.getUser().GetName(), + "user", a.scopedContext.User.GetName(), "requested_user", req.Username, ) - return nil, trace.AccessDenied("User %v tried to issue a cert for %v and added role requests. This is not supported.", a.getUser().GetName(), req.Username) + return nil, trace.AccessDenied("User %v tried to issue a cert for %v and added role requests. This is not supported.", a.scopedContext.User.GetName(), req.Username) } if len(req.AccessRequests) > 0 { a.authServer.logger.WarnContext(ctx, "User tried to issue a cert for another user while adding access requests", - "user", a.getUser().GetName(), + "user", a.scopedContext.User.GetName(), "requested_user", req.Username, ) - return nil, trace.AccessDenied("User %v tried to issue a cert for %v and added access requests. This is not supported.", a.getUser().GetName(), req.Username) + return nil, trace.AccessDenied("User %v tried to issue a cert for %v and added access requests. This is not supported.", a.scopedContext.User.GetName(), req.Username) } return a.desiredAccessInfoForImpersonation(user) } if isRoleImpersonation(*req) { if len(req.AccessRequests) > 0 { - a.authServer.logger.WarnContext(ctx, "User tried to issue a cert with both role and access requests", "user", a.getUser().GetName()) - return nil, trace.AccessDenied("User %v tried to issue a cert with both role and access requests. This is not supported.", a.getUser().GetName()) + a.authServer.logger.WarnContext(ctx, "User tried to issue a cert with both role and access requests", "user", a.scopedContext.User.GetName()) + return nil, trace.AccessDenied("User %v tried to issue a cert with both role and access requests. This is not supported.", a.scopedContext.User.GetName()) } return a.desiredAccessInfoForRoleRequest(req, user) } @@ -3595,7 +3663,7 @@ func (a *ServerWithRoles) desiredAccessInfo(ctx context.Context, req *proto.User // desiredAccessInfoForImpersonation returns the desired AccessInfo for an // impersonation request. -func (a *ServerWithRoles) desiredAccessInfoForImpersonation(user services.UserState) (*services.AccessInfo, error) { +func (a *ScopedServerWithRoles) desiredAccessInfoForImpersonation(user services.UserState) (*services.AccessInfo, error) { return &services.AccessInfo{ Username: user.GetName(), Roles: user.GetRoles(), @@ -3604,7 +3672,7 @@ func (a *ServerWithRoles) desiredAccessInfoForImpersonation(user services.UserSt } // desiredAccessInfoForRoleRequest returns the desired roles for a role request. -func (a *ServerWithRoles) desiredAccessInfoForRoleRequest(req *proto.UserCertsRequest, user services.UserState) (*services.AccessInfo, error) { +func (a *ScopedServerWithRoles) desiredAccessInfoForRoleRequest(req *proto.UserCertsRequest, user services.UserState) (*services.AccessInfo, error) { // If UseRoleRequests is set, make sure we don't return unusable certs: an // identity without roles can't be parsed. if len(req.RoleRequests) == 0 { @@ -3628,8 +3696,8 @@ func (a *ServerWithRoles) desiredAccessInfoForRoleRequest(req *proto.UserCertsRe // desiredAccessInfoForUser returns the desired AccessInfo // cert request which may contain access requests. -func (a *ServerWithRoles) desiredAccessInfoForUser(ctx context.Context, req *proto.UserCertsRequest, user services.UserState) (*services.AccessInfo, error) { - currentIdentity := a.getIdentity() +func (a *ScopedServerWithRoles) desiredAccessInfoForUser(ctx context.Context, req *proto.UserCertsRequest, user services.UserState) (*services.AccessInfo, error) { + currentIdentity := a.scopedContext.Identity.GetIdentity() // Start with the base AccessInfo for current logged-in identity, before // considering new or dropped access requests. This will include roles from @@ -3708,11 +3776,21 @@ func (a *ServerWithRoles) desiredAccessInfoForUser(ctx context.Context, req *pro // GenerateUserCerts generates users certificates func (a *ServerWithRoles) GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) { - identity := a.getIdentity() - return a.generateUserCerts( - ctx, req, - certRequestDeviceExtensions(identity.DeviceExtensions), - ) + // Delegate to ScopedServerWithRoles, which holds the source of truth for generateUserCerts. + return a.ScopedServerWithRoles().GenerateUserCerts(ctx, req) +} + +// GenerateUserCerts generates users certificates. Scoped identities are currently limited to +// generating kubernetes certificates. +func (a *ScopedServerWithRoles) GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) { + if _, isUnscoped := a.UnscopedServerWithRoles(); !isUnscoped { + // Scoped identities may only generate Kubernetes certificates. + if req.Usage != proto.UserCertsRequest_Kubernetes || req.KubernetesCluster == "" { + return nil, trace.Wrap(services.ErrScopedIdentity, "generating scoped user cert for non-kubernetes usage") + } + } + identity := a.scopedContext.Identity.GetIdentity() + return a.generateUserCerts(ctx, req, certRequestDeviceExtensions(identity.DeviceExtensions)) } func isRoleImpersonation(req proto.UserCertsRequest) bool { @@ -3736,13 +3814,13 @@ func getBotName(user services.UserState) string { return "" } -func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserCertsRequest, opts ...certRequestOption) (*proto.Certs, error) { +func (a *ScopedServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserCertsRequest, opts ...certRequestOption) (*proto.Certs, error) { // Device trust: authorize device before issuing certificates. readOnlyAuthPref, err := a.authServer.GetReadOnlyAuthPreference(ctx) if err != nil { return nil, trace.Wrap(err) } - if err := a.verifyUserDeviceForCertIssuance(ctx, req.Usage, readOnlyAuthPref.GetDeviceTrust()); err != nil { + if err := verifyUserDeviceForCertIssuance(ctx, a.scopedContext.Identity.GetIdentity(), req.Usage, readOnlyAuthPref.GetDeviceTrust()); err != nil { return nil, trace.Wrap(err) } @@ -3762,7 +3840,14 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC verifiedMFADeviceID = mfaData.Device.Id } - scopedCtx, unscopedCtx, isScoped := a.resolveAuthContext() + var unscopedCtx *authz.Context + isScoped := true + var unscopedSWR *ServerWithRoles + if uc, ok := a.scopedContext.UnscopedContext(); ok { + unscopedCtx = uc + isScoped = false + unscopedSWR, _ = a.UnscopedServerWithRoles() + } isKubeCert := req.Usage == proto.UserCertsRequest_Kubernetes && req.KubernetesCluster != "" if isScoped && !isKubeCert { // TODO (eriktate/scopes): Remove this restriction once we have more thorough support for scopes with other usages. @@ -3777,11 +3862,11 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // this prevents clients who have no chance at getting a cert and impersonating anyone // from enumerating local users and hitting database - if !canImpersonate && req.Username != a.getUser().GetName() { + if !canImpersonate && req.Username != a.scopedContext.User.GetName() { return nil, trace.AccessDenied("access denied: impersonation is not allowed") } - if a.getIdentity().DisallowReissue { + if a.scopedContext.Identity.GetIdentity().DisallowReissue { return nil, trace.AccessDenied("access denied: identity is not allowed to reissue certificates") } @@ -3800,7 +3885,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // and `ci`, however these impersonated identities, Alice(access) and // Alice(ci), should not be able to issue any new certificates. // - if a.getIdentity().Impersonator != "" { + if a.scopedContext.Identity.GetIdentity().Impersonator != "" { if len(req.AccessRequests) > 0 { return nil, trace.AccessDenied("access denied: impersonated user can not request new roles") } @@ -3809,7 +3894,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // impersonation, to reduce the risk of privilege escalation. return nil, trace.AccessDenied("access denied: impersonated roles can not request other roles") } - if req.Username != a.getUser().GetName() { + if req.Username != a.scopedContext.User.GetName() { return nil, trace.AccessDenied("access denied: impersonated user can not impersonate anyone else") } } @@ -3846,9 +3931,9 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC } // Do not allow SSO users to be impersonated. - if req.Username != a.getUser().GetName() && user.GetUserType() == types.UserTypeSSO { + if req.Username != a.scopedContext.User.GetName() && user.GetUserType() == types.UserTypeSSO { a.authServer.logger.WarnContext(ctx, "User tried to issue a cert for externally managed user", - "user", a.getUser().GetName(), + "user", a.scopedContext.User.GetName(), "external_user", req.Username, ) return nil, trace.AccessDenied("access denied") @@ -3856,8 +3941,8 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // For users renewing certificates limit the TTL to the duration of the session, to prevent // users renewing certificates forever. - if req.Username == a.getUser().GetName() { - identity := a.getIdentity() + if req.Username == a.scopedContext.User.GetName() { + identity := a.scopedContext.Identity.GetIdentity() sessionExpires := identity.Expires if sessionExpires.IsZero() { a.authServer.logger.WarnContext(ctx, "Denied cert issuance for identity with no expiry", @@ -3885,7 +3970,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // it is limited by max session ttl or mfa_verification_interval or req.Expires. // Calculate the expiration time. - roleSet, err := services.FetchRolesForUser(user, a) + roleSet, err := services.FetchRolesForUser(user, unscopedSWR) if err != nil { return nil, trace.Wrap(err) } @@ -3910,7 +3995,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // If the user is not a user cert renewal (impersonation, etc.), this is an // admin action and requires MFA. We would have returned already if this was // a scoped identity, so we can assume unscopedCtx. - if req.Username != a.getUser().GetName() { + if req.Username != a.scopedContext.User.GetName() { // Admin action MFA is not used to create mfa verified certs. if err := unscopedCtx.AuthorizeAdminAction(); err != nil { return nil, trace.Wrap(err) @@ -3920,12 +4005,12 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // we're going to extend the roles list based on the access requests, so we // ensure that all the current requests are added to the new certificate // (and are checked again) - req.AccessRequests = append(req.AccessRequests, a.getIdentity().ActiveRequests...) + req.AccessRequests = append(req.AccessRequests, a.scopedContext.Identity.GetIdentity().ActiveRequests...) if isScoped && len(req.AccessRequests) > 0 { return nil, trace.Wrap(services.ErrScopedIdentity, "requesting access requests") } - if req.Username != a.getUser().GetName() && len(req.AccessRequests) > 0 { - return nil, trace.AccessDenied("user %q requested cert for %q and included access requests, this is not supported.", a.getUser().GetName(), req.Username) + if req.Username != a.scopedContext.User.GetName() && len(req.AccessRequests) > 0 { + return nil, trace.AccessDenied("user %q requested cert for %q and included access requests, this is not supported.", a.scopedContext.User.GetName(), req.Username) } accessInfo, err := a.desiredAccessInfo(ctx, &req, user) @@ -3952,7 +4037,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC case !isScoped && authz.HasBuiltinRole(*unscopedCtx, string(types.RoleAdmin)): // builtin admins can impersonate anyone // this is required for local tctl commands to work - case req.Username == a.getUser().GetName(): + case req.Username == a.scopedContext.User.GetName(): // users can impersonate themselves, but role impersonation requests // must be checked. if isRoleImpersonation(req) { @@ -3964,13 +4049,13 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // to the current identity. If not explicitly denied (as above), // this could allow a role-impersonated certificate to request new // certificates with alternate RoleRequests. - err = unscopedCtx.Checker.CheckImpersonateRoles(a.getUser(), parsedRoles) + err = unscopedCtx.Checker.CheckImpersonateRoles(a.scopedContext.User, parsedRoles) if err != nil { a.authServer.logger.WarnContext(ctx, "user request for role impersonation denied", - "user", a.getUser().GetName(), + "user", a.scopedContext.User.GetName(), "error", err, ) - err := trace.AccessDenied("user %q has requested role impersonation for %q", a.getUser().GetName(), accessInfo.Roles) + err := trace.AccessDenied("user %q has requested role impersonation for %q", a.scopedContext.User.GetName(), accessInfo.Roles) if err := a.authServer.emitter.EmitAuditEvent(a.CloseContext(), &apievents.UserLogin{ Metadata: apievents.Metadata{ Type: events.UserLoginEvent, @@ -3993,17 +4078,17 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC return nil, trace.Wrap(services.ErrScopedIdentity, "impersonation not permitted") } // check if this user is allowed to impersonate other users - err = unscopedCtx.Checker.CheckImpersonate(a.getUser(), user, parsedRoles) + err = unscopedCtx.Checker.CheckImpersonate(a.scopedContext.User, user, parsedRoles) // adjust session TTL based on the impersonated role set limit ttl := req.Expires.Sub(a.authServer.GetClock().Now()) ttl = checker.AdjustSessionTTL(ttl) req.Expires = a.authServer.GetClock().Now().Add(ttl) if err != nil { a.authServer.logger.WarnContext(ctx, "user request for user impersonation denied", - "user", a.getUser().GetName(), + "user", a.scopedContext.User.GetName(), "error", err, ) - err := trace.AccessDenied("user %q has requested to generate certs for %q.", a.getUser().GetName(), accessInfo.Roles) + err := trace.AccessDenied("user %q has requested to generate certs for %q.", a.scopedContext.User.GetName(), accessInfo.Roles) if err := a.authServer.emitter.EmitAuditEvent(a.CloseContext(), &apievents.UserLogin{ Metadata: apievents.Metadata{ Type: events.UserLoginEvent, @@ -4055,7 +4140,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC ws, err := a.authServer.CreateAppSessionFromReq(ctx, sessionreq.NewAppSessionRequest{ NewWebSessionRequest: sessionreq.NewWebSessionRequest{ User: req.Username, - LoginIP: a.getIdentity().LoginIP, + LoginIP: a.scopedContext.Identity.GetIdentity().LoginIP, SessionTTL: req.Expires.Sub(a.authServer.GetClock().Now()), Traits: accessInfo.Traits, Roles: accessInfo.Roles, @@ -4077,7 +4162,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC AzureIdentity: req.RouteToApp.AzureIdentity, GCPServiceAccount: req.RouteToApp.GCPServiceAccount, MFAVerified: verifiedMFADeviceID, - DeviceExtensions: a.getIdentity().DeviceExtensions, + DeviceExtensions: a.scopedContext.Identity.GetIdentity().DeviceExtensions, AppName: req.RouteToApp.Name, AppURI: req.RouteToApp.URI, AppTargetPort: int(req.RouteToApp.TargetPort), @@ -4087,7 +4172,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // joining without an instance ID may have one generated when // `updateBotInstance()` is called below, and this (empty) value will be // overridden. - BotInstanceID: a.getIdentity().BotInstanceID, + BotInstanceID: a.scopedContext.Identity.GetIdentity().BotInstanceID, }) if err != nil { return nil, trace.Wrap(err) @@ -4100,7 +4185,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC wsSession, err := a.authServer.CreateWebSessionFromReq(ctx, NewWebSessionRequest{ User: req.Username, SessionTTL: req.Expires.Sub(a.authServer.GetClock().Now()), - LoginIP: a.getIdentity().LoginIP, + LoginIP: a.scopedContext.Identity.GetIdentity().LoginIP, Roles: accessInfo.Roles, Traits: accessInfo.Traits, LoginTime: a.authServer.clock.Now().UTC(), @@ -4118,7 +4203,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC if !isScoped { checkerCtx = services.NewScopedAccessCheckerContextFromUnscoped(checker) } else { - checkerCtx = scopedCtx.CheckerContext + checkerCtx = a.scopedContext.CheckerContext } // Generate certificate, note that the roles TTL will be ignored because // the request is coming from "tctl auth sign" itself. @@ -4152,7 +4237,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC CheckerContext: checkerCtx, // Copy IP from current identity to the generated certificate, if present, // to avoid generateUserCerts() being used to drop IP pinning in the new certificates. - LoginIP: a.getIdentity().LoginIP, + LoginIP: a.scopedContext.Identity.GetIdentity().LoginIP, Traits: accessInfo.Traits, ActiveRequests: req.AccessRequests, ConnectionDiagnosticID: req.ConnectionDiagnosticID, @@ -4162,19 +4247,19 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // joining without an instance ID may have one generated when // `updateBotInstance()` is called below, and this (empty) value will be // overridden. - BotInstanceID: a.getIdentity().BotInstanceID, - JoinToken: a.getIdentity().JoinToken, + BotInstanceID: a.scopedContext.Identity.GetIdentity().BotInstanceID, + JoinToken: a.scopedContext.Identity.GetIdentity().JoinToken, // Propagate any join attributes from the current identity to the new // identity. - JoinAttributes: a.getIdentity().JoinAttributes, + JoinAttributes: a.scopedContext.Identity.GetIdentity().JoinAttributes, WebSessionID: webSessionID, } - if user.GetName() != a.getUser().GetName() { - certReq.Impersonator = a.getUser().GetName() + if user.GetName() != a.scopedContext.User.GetName() { + certReq.Impersonator = a.scopedContext.User.GetName() } else if isRoleImpersonation(req) { // Role impersonation uses the user's own name as the impersonator value. - certReq.Impersonator = a.getUser().GetName() + certReq.Impersonator = a.scopedContext.User.GetName() // By default, deny reissuing certs to prevent privilege re-escalation. // (E.g a cert generated intended for use for Kubernetes Access against @@ -4186,9 +4271,9 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC if req.ReissuableRoleImpersonation { certReq.DisallowReissue = false } - } else if a.getIdentity().Impersonator != "" { + } else if a.scopedContext.Identity.GetIdentity().Impersonator != "" { // impersonating users can receive new certs - certReq.Impersonator = a.getIdentity().Impersonator + certReq.Impersonator = a.scopedContext.Identity.GetIdentity().Impersonator } switch req.Usage { case proto.UserCertsRequest_Database: @@ -4218,15 +4303,15 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // remains for subsequent requests of the primary certificate. The // renewable flag should never be carried over for impersonation, role // requests, or when the disallow-reissue flag has already been set. - if a.getIdentity().Renewable && - req.Username == a.getUser().GetName() && + if a.scopedContext.Identity.GetIdentity().Renewable && + req.Username == a.scopedContext.User.GetName() && !isRoleImpersonation(req) && !certReq.DisallowReissue { certReq.Renewable = true // We've established this is an internal cert renewal, so pass through // the BotInternal flag if set. - if a.getIdentity().BotInternal { + if a.scopedContext.Identity.GetIdentity().BotInternal { certReq.BotInternal = true } } @@ -4235,7 +4320,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC // counter, auth records, etc). `updateBotInstance()` may modify certain // `certReq` attributes (generation, botInstanceID). if certReq.Renewable { - currentIdentityGeneration := a.getIdentity().Generation + currentIdentityGeneration := a.scopedContext.Identity.GetIdentity().Generation // If we're handling a renewal for a bot, we want to return the // Host CAs as well as the User CAs. @@ -4281,13 +4366,11 @@ func (a *ServerWithRoles) GetAccessRequestAllowedPromotions(ctx context.Context, // is not paramount to the access system itself, but it stops bad attempts from // progressing further and provides better feedback than other protocol-specific // failures. -func (a *ServerWithRoles) verifyUserDeviceForCertIssuance(ctx context.Context, usage proto.UserCertsRequest_CertUsage, dt *types.DeviceTrust) error { - // Ignore App or WindowsDeskop requests, they do not support device trust. +func verifyUserDeviceForCertIssuance(ctx context.Context, identity tlsca.Identity, usage proto.UserCertsRequest_CertUsage, dt *types.DeviceTrust) error { + // Ignore App or WindowsDesktop requests, they do not support device trust. if usage == proto.UserCertsRequest_App || usage == proto.UserCertsRequest_WindowsDesktop { return nil } - - identity := a.getIdentity() return trace.Wrap(dtauthz.VerifyTLSUser(ctx, dt, identity)) } @@ -5433,16 +5516,10 @@ func checkRoleFeatureSupport(mod modules.Modules, role types.Role) error { // GetRole returns role by name func (a *ServerWithRoles) GetRole(ctx context.Context, name string) (types.Role, error) { - if a.scopedContext != nil { - // scoped identities should not be able to fetch unscoped roles for any reason - if _, isUnscoped := a.scopedContext.UnscopedContext(); !isUnscoped { - return nil, trace.Wrap(services.ErrScopedIdentity, "fetching unscoped role") - } - } // Current-user exception: we always allow users to read roles // that they hold. This requirement is checked first to avoid // misleading denial messages in the logs. - if slices.Contains(a.getUser().GetRoles(), name) { + if slices.Contains(a.context.User.GetRoles(), name) { role, err := a.authServer.GetRole(ctx, name) if err != nil && trace.IsNotFound(err) { // Add the UserSessionRoleNotFoundErrorMsg message to indicate this role not found error was @@ -6555,11 +6632,7 @@ func (a *ServerWithRoles) checkAccessToKubeCluster(cluster types.KubeCluster) er services.AccessState{MFAVerified: true}) } -func (a *ServerWithRoles) scopedCheckAccessToKubeClusterWithVerbs(ctx context.Context, cluster types.KubeCluster, verbs ...string) error { - if a.scopedContext == nil { - return trace.AccessDenied("must use scoped credentials when checking scoped access to kube cluster") - } - +func (a *ScopedServerWithRoles) checkAccessToKubeClusterWithVerbs(ctx context.Context, cluster types.KubeCluster, verbs ...string) error { ruleCtx := a.scopedContext.RuleContext() return a.scopedContext.CheckerContext.Decision(ctx, cmp.Or(cluster.GetScope(), scopes.Root), func(checker *services.ScopedAccessChecker) error { if err := checker.CheckAccessToRules(&ruleCtx, types.KindKubernetesCluster, verbs...); err != nil { @@ -6570,35 +6643,11 @@ func (a *ServerWithRoles) scopedCheckAccessToKubeClusterWithVerbs(ctx context.Co }) } -func (a *ServerWithRoles) getScopedKubeServers(ctx context.Context) ([]types.KubeServer, error) { - servers, err := a.authServer.GetKubernetesServers(ctx) - if err != nil { - return nil, trace.Wrap(err) - } - // Filter out kube servers the caller doesn't have access to. - var filtered []types.KubeServer - for _, server := range servers { - err := a.scopedCheckAccessToKubeClusterWithVerbs(ctx, server.GetCluster(), types.VerbList, types.VerbRead) - if err != nil && !trace.IsAccessDenied(err) { - return nil, trace.Wrap(err) - } else if err == nil { - - filtered = append(filtered, server) - } - } - - return filtered, nil - -} - // GetKubernetesServers returns all registered kubernetes servers. func (a *ServerWithRoles) GetKubernetesServers(ctx context.Context) ([]types.KubeServer, error) { if err := a.authorizeAction(types.KindKubeServer, types.VerbList, types.VerbRead); err != nil { - if !errors.Is(err, services.ErrScopedIdentity) { - return nil, trace.Wrap(err) - } + return nil, trace.Wrap(err) - return a.getScopedKubeServers(ctx) } servers, err := a.authServer.GetKubernetesServers(ctx) @@ -6619,6 +6668,26 @@ func (a *ServerWithRoles) GetKubernetesServers(ctx context.Context) ([]types.Kub return filtered, nil } +// GetKubernetesServers is the scoped equivalent of [ServerWithRoles.GetKubernetesServers], returning +// only servers the scoped identity has access to. +func (a *ScopedServerWithRoles) GetKubernetesServers(ctx context.Context) ([]types.KubeServer, error) { + servers, err := a.authServer.GetKubernetesServers(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + var filtered []types.KubeServer + for _, server := range servers { + err := a.checkAccessToKubeClusterWithVerbs(ctx, server.GetCluster(), types.VerbList, types.VerbRead) + if err != nil && !trace.IsAccessDenied(err) { + return nil, trace.Wrap(err) + } else if err == nil { + filtered = append(filtered, server) + } + } + + return filtered, nil +} + // UpsertKubernetesServer creates or updates a Server representing a teleport // kubernetes server. func (a *ServerWithRoles) UpsertKubernetesServer(ctx context.Context, s types.KubeServer) (*types.KeepAlive, error) { @@ -6723,46 +6792,29 @@ func (a *ServerWithRoles) DeleteMFADeviceSync(ctx context.Context, req *proto.De // IsMFARequired queries whether MFA is required for the specified target. func (a *ServerWithRoles) IsMFARequired(ctx context.Context, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error) { - unscopedCtx := &a.context - isUnscoped := true - if a.scopedContext != nil { - unscopedCtx, isUnscoped = a.scopedContext.UnscopedContext() - } // Check if MFA is required for admin actions. We don't currently have // a reason to check the name of the admin action in question. if _, ok := req.Target.(*proto.IsMFARequiredRequest_AdminAction); ok { - if !isUnscoped { - return nil, trace.AccessDenied("admin actions are not supported for scoped identities") - } - if unscopedCtx.AdminActionAuthState == authz.AdminActionAuthNotRequired { + if a.context.AdminActionAuthState == authz.AdminActionAuthNotRequired { return &proto.IsMFARequiredResponse{ Required: false, MFARequired: proto.MFARequired_MFA_REQUIRED_NO, }, nil - } else { - return &proto.IsMFARequiredResponse{ - Required: true, - MFARequired: proto.MFARequired_MFA_REQUIRED_YES, - }, nil } + return &proto.IsMFARequiredResponse{ + Required: true, + MFARequired: proto.MFARequired_MFA_REQUIRED_YES, + }, nil } - if isUnscoped { - // Other than for admin action targets, IsMFARequired should only be called by users. - if !authz.IsLocalOrRemoteUser(*unscopedCtx) { - return nil, trace.AccessDenied("only a user role can call IsMFARequired, got %T", unscopedCtx.Checker) - } - } else { - // Scoped identities do not support remote users, so we do a simpler check here - _, isLocal := a.scopedContext.Identity.(authz.LocalUser) - if !isLocal { - return nil, trace.AccessDenied("only a user role can call IsMFARequired, got %T", a.scopedContext.Identity) - } + // Other than for admin action targets, IsMFARequired should only be called by users. + if !authz.IsLocalOrRemoteUser(a.context) { + return nil, trace.AccessDenied("only a user role can call IsMFARequired, got %T", a.context.Checker) } // Certain hardware-key based private key policies are treated as MFA verification, // except for app sessions which can only be attested with the key policy "web_session". - if a.getIdentity().PrivateKeyPolicy.MFAVerified() { + if a.context.Identity.GetIdentity().PrivateKeyPolicy.MFAVerified() { if _, isAppReq := req.Target.(*proto.IsMFARequiredRequest_App); !isAppReq { return &proto.IsMFARequiredResponse{ Required: false, @@ -6771,11 +6823,37 @@ func (a *ServerWithRoles) IsMFARequired(ctx context.Context, req *proto.IsMFAReq } } - scopedCtx := a.scopedContext - if isUnscoped { - scopedCtx = authz.ScopedContextFromUnscopedContext(unscopedCtx) + return a.authServer.isMFARequired(ctx, authz.ScopedContextFromUnscopedContext(&a.context), req) +} + +// IsMFARequired is the scoped equivalent of [ServerWithRoles.IsMFARequired]. +func (a *ScopedServerWithRoles) IsMFARequired(ctx context.Context, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error) { + if unscoped, ok := a.UnscopedServerWithRoles(); ok { + return unscoped.IsMFARequired(ctx, req) } - return a.authServer.isMFARequired(ctx, scopedCtx, req) + + // Admin actions are not supported for scoped identities. + if _, ok := req.Target.(*proto.IsMFARequiredRequest_AdminAction); ok { + return nil, trace.AccessDenied("admin actions are not supported for scoped identities") + } + + // Scoped identities do not support remote users, so we do a simpler check here. + if _, isLocal := a.scopedContext.Identity.(authz.LocalUser); !isLocal { + return nil, trace.AccessDenied("only a user role can call IsMFARequired, got %T", a.scopedContext.Identity) + } + + // Certain hardware-key based private key policies are treated as MFA verification, + // except for app sessions which can only be attested with the key policy "web_session". + if a.scopedContext.Identity.GetIdentity().PrivateKeyPolicy.MFAVerified() { + if _, isAppReq := req.Target.(*proto.IsMFARequiredRequest_App); !isAppReq { + return &proto.IsMFARequiredResponse{ + Required: false, + MFARequired: proto.MFARequired_MFA_REQUIRED_NO, + }, nil + } + } + + return a.authServer.isMFARequired(ctx, a.scopedContext, req) } // SearchEvents allows searching audit events with pagination support. @@ -8721,46 +8799,3 @@ func checkOktaLockAccess(ctx context.Context, authzCtx *authz.Context, locks ser return okta.CheckAccess(authzCtx, existingLock, verb) } - -// resolveAuthContext returns either a scoped or unscoped auth context with a bool -// representing whether the context is scoped. -// -// There are three possible scenarios: -// - An authz.Context generated by an unscoped authorizer (fully unscoped path) -// - An authz.Context wrapped in an authz.ScopedContext (scoped path with unscoped identity) -// - An authz.ScopedContext (scoped path with scoped identity) -// This function returns the currently active auth context for all three scenarios. -func (a *ServerWithRoles) resolveAuthContext() (*authz.ScopedContext, *authz.Context, bool) { - if a.scopedContext == nil { - return nil, &a.context, false - } - - if unscopedCtx, isUnscoped := a.scopedContext.UnscopedContext(); isUnscoped { - return nil, unscopedCtx, false - } - - return a.scopedContext, nil, true -} - -// getIdentityGetter returns the [authz.IdentityGetter] for the current auth context, regardless of -// whether it's scoped or not. -func (a *ServerWithRoles) getIdentityGetter() authz.IdentityGetter { - if a.scopedContext != nil { - return a.scopedContext.Identity - } - return a.context.Identity -} - -// getIdentity returns the [tlsca.Identity] for the current auth context, regardless of -// whether it's scoped or not. -func (a *ServerWithRoles) getIdentity() tlsca.Identity { - return a.getIdentityGetter().GetIdentity() -} - -// getUser returns the User for the current context regardless of whether or not it's scoped. -func (a *ServerWithRoles) getUser() types.User { - if a.scopedContext != nil { - return a.scopedContext.User - } - return a.context.User -} diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index daceca76d26..48d0c460ec7 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -5369,14 +5369,17 @@ func TestIsLocalOrRemoteServerAction(t *testing.T) { }) require.NoError(t, err) + type listResourcesSrv interface { + ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) + } tts := []struct { name string - getSrvFn func(t *testing.T) *auth.ServerWithRoles + getSrvFn func(t *testing.T) listResourcesSrv expectErr bool }{ { name: "local server builtin", - getSrvFn: func(t *testing.T) *auth.ServerWithRoles { + getSrvFn: func(t *testing.T) listResourcesSrv { authCtx, err := srv.Authorizer.Authorize(authz.ContextWithUser(ctx, authtest.TestBuiltin(types.RoleProxy).I)) require.NoError(t, err) return auth.NewServerWithRoles(srv.AuthServer, srv.AuditLog, *authCtx) @@ -5384,7 +5387,7 @@ func TestIsLocalOrRemoteServerAction(t *testing.T) { }, { name: "remote server builtin", - getSrvFn: func(t *testing.T) *auth.ServerWithRoles { + getSrvFn: func(t *testing.T) listResourcesSrv { authCtx, err := srv.Authorizer.Authorize(authz.ContextWithUser(ctx, authtest.TestRemoteBuiltin(types.RoleProxy, "remote-cluster").I)) require.NoError(t, err) return auth.NewServerWithRoles(srv.AuthServer, srv.AuditLog, *authCtx) @@ -5392,7 +5395,7 @@ func TestIsLocalOrRemoteServerAction(t *testing.T) { }, { name: "local wrapped server builtin", - getSrvFn: func(t *testing.T) *auth.ServerWithRoles { + getSrvFn: func(t *testing.T) listResourcesSrv { authCtx, err := srv.Authorizer.Authorize(authz.ContextWithUser(ctx, authtest.TestBuiltin(types.RoleProxy).I)) require.NoError(t, err) return auth.NewScopedServerWithRoles(srv.AuthServer, srv.AuditLog, authz.ScopedContextFromUnscopedContext(authCtx)) @@ -5400,7 +5403,7 @@ func TestIsLocalOrRemoteServerAction(t *testing.T) { }, { name: "remote wrapped server builtin", - getSrvFn: func(t *testing.T) *auth.ServerWithRoles { + getSrvFn: func(t *testing.T) listResourcesSrv { authCtx, err := srv.Authorizer.Authorize(authz.ContextWithUser(ctx, authtest.TestRemoteBuiltin(types.RoleProxy, "remote-cluster").I)) require.NoError(t, err) return auth.NewScopedServerWithRoles(srv.AuthServer, srv.AuditLog, authz.ScopedContextFromUnscopedContext(authCtx)) @@ -5408,7 +5411,7 @@ func TestIsLocalOrRemoteServerAction(t *testing.T) { }, { name: "non-server builtin", - getSrvFn: func(t *testing.T) *auth.ServerWithRoles { + getSrvFn: func(t *testing.T) listResourcesSrv { authCtx, err := srv.Authorizer.Authorize(authz.ContextWithUser(ctx, authtest.TestAdmin().I)) require.NoError(t, err) return auth.NewServerWithRoles(srv.AuthServer, srv.AuditLog, *authCtx) @@ -5417,7 +5420,7 @@ func TestIsLocalOrRemoteServerAction(t *testing.T) { }, { name: "wrapped non-server builtin", - getSrvFn: func(t *testing.T) *auth.ServerWithRoles { + getSrvFn: func(t *testing.T) listResourcesSrv { authCtx, err := srv.Authorizer.Authorize(authz.ContextWithUser(ctx, authtest.TestAdmin().I)) require.NoError(t, err) return auth.NewScopedServerWithRoles(srv.AuthServer, srv.AuditLog, authz.ScopedContextFromUnscopedContext(authCtx)) @@ -5426,7 +5429,7 @@ func TestIsLocalOrRemoteServerAction(t *testing.T) { }, { name: "remote non-builtin", - getSrvFn: func(t *testing.T) *auth.ServerWithRoles { + getSrvFn: func(t *testing.T) listResourcesSrv { authCtx := authz.Context{ Identity: authz.RemoteBuiltinRole{ Role: types.RoleProxy, @@ -5440,7 +5443,7 @@ func TestIsLocalOrRemoteServerAction(t *testing.T) { }, { name: "remote wrapped non-builtin", - getSrvFn: func(t *testing.T) *auth.ServerWithRoles { + getSrvFn: func(t *testing.T) listResourcesSrv { authCtx := authz.Context{ Identity: authz.RemoteBuiltinRole{ Role: types.RoleProxy, diff --git a/lib/auth/export_test.go b/lib/auth/export_test.go index c7ed04715ec..5b81a338d17 100644 --- a/lib/auth/export_test.go +++ b/lib/auth/export_test.go @@ -272,16 +272,14 @@ func UpsertServer(srv *APIServer, auth presenceForAPIServer, role types.SystemRo func NewServerWithRoles(srv *Server, alog events.AuditLogSessionStreamer, authzContext authz.Context) *ServerWithRoles { return &ServerWithRoles{ - authServer: srv, - alog: alog, + serverBase: serverBase{authServer: srv, alog: alog}, context: authzContext, } } -func NewScopedServerWithRoles(srv *Server, alog events.AuditLogSessionStreamer, scopedContext *authz.ScopedContext) *ServerWithRoles { - return &ServerWithRoles{ - authServer: srv, - alog: alog, +func NewScopedServerWithRoles(srv *Server, alog events.AuditLogSessionStreamer, scopedContext *authz.ScopedContext) *ScopedServerWithRoles { + return &ScopedServerWithRoles{ + serverBase: serverBase{authServer: srv, alog: alog}, scopedContext: scopedContext, } } diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index 82d7af08fb4..443b03538cb 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -551,27 +551,11 @@ const logInterval = 10000 // WatchEvents returns a new stream of cluster events func (g *GRPCServer) WatchEvents(watch *authpb.Watch, stream authpb.AuthService_WatchEventsServer) (err error) { - auth, err := g.authenticate(stream.Context()) + auth, err := g.scopedAuthenticate(stream.Context()) if err != nil { - // Support scoped identities calling the RPC by falling back to scoped - // authorizer check. - if errors.Is(err, services.ErrScopedIdentity) { - scopedAuth, err := g.scopedAuthenticate(stream.Context()) - if err != nil { - return trace.Wrap(err) - } - return trace.Wrap(WatchEvents( - watch, - stream, - scopedAuth.scopedContext.User.GetName(), - scopedAuth, - g.AuthServer.modules, - )) - } return trace.Wrap(err) } - - return trace.Wrap(WatchEvents(watch, stream, auth.User.GetName(), auth, g.AuthServer.modules)) + return trace.Wrap(WatchEvents(watch, stream, auth.scopedContext.User.GetName(), auth, g.AuthServer.modules)) } // WatchEvent is a stream interface for sending events. @@ -709,28 +693,28 @@ func (g *GRPCServer) GenerateUserCerts(ctx context.Context, req *authpb.UserCert } // deny access to scoped bots - if ident := auth.getIdentity(); ident.IsBot() && ident.ScopePin != nil { + if ident := auth.scopedContext.Identity.GetIdentity(); ident.IsBot() && ident.ScopePin != nil { return nil, trace.AccessDenied("scoped bots can not generate user certs") } - if err := validateUserCertsRequest(auth.ServerWithRoles, req); err != nil { + if err := validateUserCertsRequest(auth.ScopedServerWithRoles, req); err != nil { g.logger.DebugContext(ctx, "Validation of user certs request failed", "error", err) return nil, trace.Wrap(err) } if req.Purpose == authpb.UserCertsRequest_CERT_PURPOSE_SINGLE_USE_CERTS { - certs, err := g.generateUserSingleUseCerts(ctx, auth.ServerWithRoles, req) + certs, err := g.generateUserSingleUseCerts(ctx, auth.ScopedServerWithRoles, req) return certs, trace.Wrap(err) } - certs, err := auth.ServerWithRoles.GenerateUserCerts(ctx, *req) + certs, err := auth.GenerateUserCerts(ctx, *req) if err != nil { return nil, trace.Wrap(err) } return certs, nil } -func validateUserCertsRequest(srv *ServerWithRoles, req *authpb.UserCertsRequest) error { +func validateUserCertsRequest(srv *ScopedServerWithRoles, req *authpb.UserCertsRequest) error { if err := validateCertUsage(req); err != nil { return trace.Wrap(err) } @@ -815,7 +799,7 @@ func validateAccessGraphcertificateReq(req *authpb.UserCertsRequest) error { } // generateUserSingleUseCerts issues single-use user certificates. -func (g *GRPCServer) generateUserSingleUseCerts(ctx context.Context, srv *ServerWithRoles, req *authpb.UserCertsRequest) (*authpb.Certs, error) { +func (g *GRPCServer) generateUserSingleUseCerts(ctx context.Context, srv *ScopedServerWithRoles, req *authpb.UserCertsRequest) (*authpb.Certs, error) { setUserSingleUseCertsTTL(srv, req) // We don't do MFA requirement validations here. @@ -2871,7 +2855,7 @@ func (g *GRPCServer) GenerateUserSingleUseCerts(stream authpb.AuthService_Genera return trace.NotImplemented("method GenerateUserSingleUseCerts is deprecated, use GenerateUserCerts instead") } -func setUserSingleUseCertsTTL(srv *ServerWithRoles, req *authpb.UserCertsRequest) { +func setUserSingleUseCertsTTL(srv *ScopedServerWithRoles, req *authpb.UserCertsRequest) { if !isCertWrittenToDiskFlow(req) { // Don't limit the cert expiry to 1 minute for certs that are not written to disk. // When MFA is required, cert expiration time is bounded by the lifetime of the local proxy process @@ -2912,7 +2896,7 @@ func isCertWrittenToDiskFlow(req *authpb.UserCertsRequest) bool { return !isInMemoryCertRequest(req) && !isCredentialsStdoutCertRequest(req) } -func userSingleUseCertsGenerate(ctx context.Context, srv *ServerWithRoles, req authpb.UserCertsRequest) (*authpb.Certs, error) { +func userSingleUseCertsGenerate(ctx context.Context, srv *ScopedServerWithRoles, req authpb.UserCertsRequest) (*authpb.Certs, error) { // Get the client IP. clientPeer, ok := peer.FromContext(ctx) if !ok { @@ -2926,9 +2910,9 @@ func userSingleUseCertsGenerate(ctx context.Context, srv *ServerWithRoles, req a // MFA certificates are supposed to be always pinned to IP, but it was decided to turn this off until // IP pinning comes out of preview. Here we would add option to pin the cert, see commit of this comment for restoring. opts := []certRequestOption{ - certRequestPreviousIdentityExpires(srv.getIdentity().Expires), + certRequestPreviousIdentityExpires(srv.scopedContext.Identity.GetIdentity().Expires), certRequestLoginIP(clientIP), - certRequestDeviceExtensions(srv.getIdentity().DeviceExtensions), + certRequestDeviceExtensions(srv.scopedContext.Identity.GetIdentity().DeviceExtensions), } // Generate the cert. @@ -3797,7 +3781,7 @@ func (g *GRPCServer) DeleteToken(ctx context.Context, req *types.ResourceRequest // ListAuthServers returns a paginated list of auth servers. func (g *GRPCServer) ListAuthServers(ctx context.Context, req *presencev1pb.ListAuthServersRequest) (*presencev1pb.ListAuthServersResponse, error) { - auth, err := g.authenticate(ctx) + auth, err := g.scopedAuthenticate(ctx) if err != nil { return nil, trace.Wrap(err) } @@ -3824,7 +3808,7 @@ func (g *GRPCServer) ListAuthServers(ctx context.Context, req *presencev1pb.List // ListProxyServers returns a paginated list of proxy servers. func (g *GRPCServer) ListProxyServers(ctx context.Context, req *presencev1pb.ListProxyServersRequest) (*presencev1pb.ListProxyServersResponse, error) { - auth, err := g.authenticate(ctx) + auth, err := g.scopedAuthenticate(ctx) if err != nil { return nil, trace.Wrap(err) } @@ -4994,51 +4978,21 @@ func (g *GRPCServer) GenerateCertAuthorityCRL(ctx context.Context, req *authpb.C // ListUnifiedResources retrieves a paginated list of unified resources. func (g *GRPCServer) ListUnifiedResources(ctx context.Context, req *authpb.ListUnifiedResourcesRequest) (*authpb.ListUnifiedResourcesResponse, error) { - auth, err := g.authenticate(ctx) + auth, err := g.scopedAuthenticate(ctx) if err != nil { - if errors.Is(err, services.ErrScopedIdentity) { - // TODO(fspmarshall/scopes): Do away with this bifurcated implementation in favor of making ListUnifiedResources - // able to support scoped callers directly. - return g.scopedListUnifiedResources(ctx, req) - } return nil, trace.Wrap(err) } - return auth.ListUnifiedResources(ctx, req) } -// scopedListUnifiedResources handles ListUnifiedResources requests for scoped identities. eventually we want to do away with -// this in favor of fully porting over the ListUnifiedResources API to support scopes natively. for the time being, we use -// this method to specifically make it possible to use 'tsh ls' with scoped credentials. usecases other than that are not -// guaranteed to work properly at this time. -func (g *GRPCServer) scopedListUnifiedResources(ctx context.Context, req *authpb.ListUnifiedResourcesRequest) (*authpb.ListUnifiedResourcesResponse, error) { +// ListResources retrieves a paginated list of resources. +func (g *GRPCServer) ListResources(ctx context.Context, req *authpb.ListResourcesRequest) (*authpb.ListResourcesResponse, error) { auth, err := g.scopedAuthenticate(ctx) if err != nil { return nil, trace.Wrap(err) } - return auth.scopedListUnifiedResources(ctx, req) -} - -// ListResources retrieves a paginated list of resources. -func (g *GRPCServer) ListResources(ctx context.Context, req *authpb.ListResourcesRequest) (*authpb.ListResourcesResponse, error) { - - var swr *ServerWithRoles - auth, err := g.authenticate(ctx) - if err != nil { - if !errors.Is(err, services.ErrScopedIdentity) { - return nil, trace.Wrap(err) - } - scopedAuth, err := g.scopedAuthenticate(ctx) - if err != nil { - return nil, trace.Wrap(err) - } - swr = scopedAuth.ServerWithRoles - } else { - swr = auth.ServerWithRoles - } - - resp, err := swr.ListResources(ctx, *req) + resp, err := auth.ListResources(ctx, *req) if err != nil { return nil, trace.Wrap(err) } @@ -5057,38 +5011,11 @@ func (g *GRPCServer) ListResources(ctx context.Context, req *authpb.ListResource } func (g *GRPCServer) GetSSHTargets(ctx context.Context, req *authpb.GetSSHTargetsRequest) (*authpb.GetSSHTargetsResponse, error) { - auth, err := g.authenticate(ctx) - if err != nil { - if errors.Is(err, services.ErrScopedIdentity) { - // NOTE: this pattern of having separate code paths for scoped identities is temporary. GetSSHTargets - // relies upon ListUnifiedResources internally, which hasn't yet been fully ported to support scopes. - // until that work is done, we need to have this separate code path to ensure that search_as_roles - // functions as expected for unscoped callers. - return g.scopedGetSSHTargets(ctx, req) - } - return nil, trace.Wrap(err) - } - - rsp, err := auth.ServerWithRoles.GetSSHTargets(ctx, req) - if err != nil { - return nil, trace.Wrap(err) - } - - return rsp, nil -} - -func (g *GRPCServer) scopedGetSSHTargets(ctx context.Context, req *authpb.GetSSHTargetsRequest) (*authpb.GetSSHTargetsResponse, error) { auth, err := g.scopedAuthenticate(ctx) if err != nil { return nil, trace.Wrap(err) } - - rsp, err := auth.GetSSHTargets(ctx, req) - if err != nil { - return nil, trace.Wrap(err) - } - - return rsp, nil + return auth.GetSSHTargets(ctx, req) } // ResolveSSHTarget gets a server that would match an equivalent ssh dial request. @@ -5261,7 +5188,7 @@ func (g *GRPCServer) GetClusterCACert( if err != nil { return nil, trace.Wrap(err) } - return auth.ServerWithRoles.GetClusterCACert(ctx) + return auth.GetClusterCACert(ctx) } // GetConnectionDiagnostic reads a connection diagnostic. @@ -6823,18 +6750,15 @@ func (g *GRPCServer) authenticate(ctx context.Context) (*grpcContext, error) { return &grpcContext{ Context: authContext, ServerWithRoles: &ServerWithRoles{ - authServer: g.AuthServer, + serverBase: serverBase{authServer: g.AuthServer, alog: g.AuthServer}, context: *authContext, - alog: g.AuthServer, }, }, nil } -// scopedGRPCContext servers the same role as [grpcContext] but for scoped identities. It is currently -// a thin wrapper around [ServerWithRoles] since we don't have any need to embed an [authz.Context] yet, -// but we may embed one in the future. +// scopedGRPCContext serves the same role as [grpcContext] but for scoped identities. type scopedGRPCContext struct { - *ServerWithRoles + *ScopedServerWithRoles } // scopedAuthenticate functions similarly to authenticate but supports scoped identities. It returns an initialized auth server @@ -6845,10 +6769,9 @@ func (g *GRPCServer) scopedAuthenticate(ctx context.Context) (*scopedGRPCContext return nil, trace.Wrap(err) } return &scopedGRPCContext{ - ServerWithRoles: &ServerWithRoles{ - authServer: g.AuthServer, + ScopedServerWithRoles: &ScopedServerWithRoles{ + serverBase: serverBase{authServer: g.AuthServer, alog: g.AuthServer}, scopedContext: authContext, - alog: g.AuthServer, }, }, nil } diff --git a/lib/auth/recording_authorizer.go b/lib/auth/recording_authorizer.go index fb457cbe8dc..6d49ad355b8 100644 --- a/lib/auth/recording_authorizer.go +++ b/lib/auth/recording_authorizer.go @@ -51,8 +51,7 @@ func (a *SessionRecordingAuthorizer) Authorize(ctx context.Context, sessionID st } serverWithRoles := &ServerWithRoles{ - authServer: a.authServer, - alog: a.authServer, + serverBase: serverBase{authServer: a.authServer, alog: a.authServer}, context: *userCtx, }