scoped api level authz rework (#67133)

This commit is contained in:
Forrest
2026-06-09 23:58:10 +00:00
committed by GitHub
parent 6b368c0234
commit 6757719beb
6 changed files with 431 additions and 478 deletions
+9 -14
View File
@@ -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 {
File diff suppressed because it is too large Load Diff
+12 -9
View File
@@ -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,
+4 -6
View File
@@ -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,
}
}
+25 -102
View File
@@ -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
}
+1 -2
View File
@@ -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,
}