mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
[v18] Backend support for scoped login on web (#69197)
* Backend support for scoped login on web (#68884) * Backend support for scoped login on web * Review * Add support for MFA turned off * Compute device trust mode for scoped sessions * Review * Prevent dropping the scope pin when extending a session * Fix TestNewWebSessionScopedTrustedDeviceRequirement flakiness (#69506)
This commit is contained in:
@@ -27,19 +27,19 @@ const (
|
||||
WebConfigAuthProviderOIDCType = "oidc"
|
||||
// WebConfigAuthProviderOIDCURL is OIDC webapi endpoint.
|
||||
// redirect_url MUST be the last query param, see the comment in parseSSORequestParams for an explanation.
|
||||
WebConfigAuthProviderOIDCURL = "/v1/webapi/oidc/login/web?connector_id=:providerName&login_hint=:loginHint?&redirect_url=:redirect"
|
||||
WebConfigAuthProviderOIDCURL = "/v1/webapi/oidc/login/web?connector_id=:providerName&login_hint=:loginHint?&scope=:scope?&redirect_url=:redirect"
|
||||
|
||||
// WebConfigAuthProviderSAMLType is SAML provider type
|
||||
WebConfigAuthProviderSAMLType = "saml"
|
||||
// WebConfigAuthProviderSAMLURL is SAML webapi endpoint.
|
||||
// redirect_url MUST be the last query param, see the comment in parseSSORequestParams for an explanation.
|
||||
WebConfigAuthProviderSAMLURL = "/v1/webapi/saml/sso?connector_id=:providerName&login_hint=:loginHint?&redirect_url=:redirect"
|
||||
WebConfigAuthProviderSAMLURL = "/v1/webapi/saml/sso?connector_id=:providerName&login_hint=:loginHint?&scope=:scope?&redirect_url=:redirect"
|
||||
|
||||
// WebConfigAuthProviderGitHubType is GitHub provider type
|
||||
WebConfigAuthProviderGitHubType = "github"
|
||||
// WebConfigAuthProviderGitHubURL is GitHub webapi endpoint
|
||||
// redirect_url MUST be the last query param, see the comment in parseSSORequestParams for an explanation.
|
||||
WebConfigAuthProviderGitHubURL = "/v1/webapi/github/login/web?connector_id=:providerName&redirect_url=:redirect"
|
||||
WebConfigAuthProviderGitHubURL = "/v1/webapi/github/login/web?connector_id=:providerName&scope=:scope?&redirect_url=:redirect"
|
||||
)
|
||||
|
||||
// WebConfig is web application configuration served by the backend to be used in frontend apps.
|
||||
|
||||
+168
-4
@@ -93,6 +93,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/modules"
|
||||
"github.com/gravitational/teleport/lib/modules/modulestest"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
|
||||
"github.com/gravitational/teleport/lib/service/servicecfg"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/services/local"
|
||||
@@ -3572,10 +3573,20 @@ func TestGenerateKubernetesUserCert(t *testing.T) {
|
||||
|
||||
func TestNewWebSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
p, err := newTestPack(ctx, testPackOptions{DataDir: t.TempDir()})
|
||||
ctx := t.Context()
|
||||
|
||||
p, err := newTestPack(ctx, testPackOptions{
|
||||
DataDir: t.TempDir(),
|
||||
ScopesFeatures: scopes.Features{Enabled: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
if p.bk != nil {
|
||||
p.bk.Close()
|
||||
}
|
||||
})
|
||||
|
||||
// Set a web idle timeout.
|
||||
duration := time.Duration(5) * time.Minute
|
||||
cfg := types.DefaultClusterNetworkingConfig()
|
||||
@@ -3611,6 +3622,7 @@ func TestNewWebSession(t *testing.T) {
|
||||
require.NotEmpty(t, ws.GetTLSCert())
|
||||
|
||||
t.Run("expands user metadata name in logins", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
user, _, err := authtest.CreateUserAndRole(p.a, "templated-web-user", nil, nil, authtest.WithRoleMutator(func(role types.Role) {
|
||||
role.SetLogins(types.Allow, []string{"{{user.metadata.name}}"})
|
||||
}))
|
||||
@@ -3624,13 +3636,165 @@ func TestNewWebSession(t *testing.T) {
|
||||
SessionTTL: apidefaults.CertDuration,
|
||||
}
|
||||
|
||||
_, checker, err := p.a.NewWebSession(ctx, req, nil /* opts */)
|
||||
_, checkerCtx, err := p.a.NewWebSession(ctx, req, nil /* opts */)
|
||||
require.NoError(t, err)
|
||||
|
||||
logins, err := checker.CheckLoginDuration(0)
|
||||
logins, err := checkerCtx.CertParams().GetSSHLoginsForTTL(ctx, 0)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, logins, user.GetName())
|
||||
})
|
||||
|
||||
t.Run("scoped session", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
scopedService := local.NewScopedAccessService(p.bk)
|
||||
_, err := scopedService.CreateScopedRole(ctx, scopedaccessv1pb.CreateScopedRoleRequest_builder{
|
||||
Role: scopedaccessv1pb.ScopedRole_builder{
|
||||
Kind: "scoped_role",
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "web-session",
|
||||
}.Build(),
|
||||
Scope: "/test",
|
||||
Spec: scopedaccessv1pb.ScopedRoleSpec_builder{
|
||||
AssignableScopes: []string{"/test"},
|
||||
}.Build(),
|
||||
Version: types.V1,
|
||||
}.Build(),
|
||||
}.Build())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = scopedService.CreateScopedRoleAssignment(ctx, scopedaccessv1pb.CreateScopedRoleAssignmentRequest_builder{
|
||||
Assignment: scopedaccessv1pb.ScopedRoleAssignment_builder{
|
||||
Kind: "scoped_role_assignment",
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: uuid.NewString(),
|
||||
}.Build(),
|
||||
Scope: "/test",
|
||||
SubKind: "dynamic",
|
||||
Spec: scopedaccessv1pb.ScopedRoleAssignmentSpec_builder{
|
||||
User: user.GetName(),
|
||||
Assignments: []*scopedaccessv1pb.Assignment{
|
||||
scopedaccessv1pb.Assignment_builder{
|
||||
Role: "/test::web-session",
|
||||
Scope: "/test",
|
||||
}.Build(),
|
||||
},
|
||||
}.Build(),
|
||||
Version: types.V1,
|
||||
}.Build(),
|
||||
}.Build())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for scoped access cache to see the assignment.
|
||||
pin := scopesv1pb.Pin_builder{Kind: scopesv1pb.PinKind_PIN_KIND_USER, Scope: "/test"}.Build()
|
||||
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
err := p.a.ScopedAccessCache.PopulatePinnedAssignmentsForUser(ctx, user.GetName(), pin)
|
||||
assert.NoError(ct, err)
|
||||
assert.NotNil(ct, pin.GetAssignmentTree())
|
||||
}, 15*time.Second, 100*time.Millisecond)
|
||||
|
||||
req := auth.NewWebSessionRequest{
|
||||
User: user.GetName(),
|
||||
Scope: "/test",
|
||||
}
|
||||
|
||||
ws, _, err := p.a.NewWebSession(ctx, req, nil /* opts */)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.GetName(), ws.GetUser())
|
||||
|
||||
_, identity := parseX509PEMAndIdentity(t, ws.GetTLSCert())
|
||||
sp := identity.ScopePin
|
||||
require.NotNil(t, sp)
|
||||
assert.Equal(t, "/test", sp.GetScope())
|
||||
assert.Equal(t, scopesv1pb.PinKind_PIN_KIND_USER, sp.GetKind())
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewWebSessionScopedTrustedDeviceRequirement(t *testing.T) {
|
||||
// Not parallel, because we set test modules.
|
||||
modulestest.SetTestModules(t, *modulestest.EnterpriseModules())
|
||||
ctx := t.Context()
|
||||
|
||||
p, err := newTestPack(ctx, testPackOptions{
|
||||
DataDir: t.TempDir(),
|
||||
ScopesFeatures: scopes.Features{Enabled: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
authPref, err := p.a.GetAuthPreference(ctx)
|
||||
require.NoError(t, err)
|
||||
authPref.SetDeviceTrust(&types.DeviceTrust{
|
||||
Mode: constants.DeviceTrustModeRequired,
|
||||
})
|
||||
_, err = p.a.UpsertAuthPreference(ctx, authPref)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = authtest.CreateUserAndRole(p.a, "test-user", []string{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
scopedService := p.a.ScopedAccess()
|
||||
_, err = scopedService.CreateScopedRole(ctx, scopedaccessv1pb.CreateScopedRoleRequest_builder{
|
||||
Role: scopedaccessv1pb.ScopedRole_builder{
|
||||
Kind: "scoped_role",
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "some-scoped-role",
|
||||
}.Build(),
|
||||
Scope: "/foo",
|
||||
Spec: scopedaccessv1pb.ScopedRoleSpec_builder{
|
||||
AssignableScopes: []string{"/foo/bar"},
|
||||
}.Build(),
|
||||
Version: types.V1,
|
||||
}.Build(),
|
||||
}.Build())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = scopedService.CreateScopedRoleAssignment(ctx, scopedaccessv1pb.CreateScopedRoleAssignmentRequest_builder{
|
||||
Assignment: scopedaccessv1pb.ScopedRoleAssignment_builder{
|
||||
Kind: "scoped_role_assignment",
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "some-scoped-role-assignment",
|
||||
}.Build(),
|
||||
Scope: "/foo",
|
||||
SubKind: scopedaccess.SubKindDynamic,
|
||||
Spec: scopedaccessv1pb.ScopedRoleAssignmentSpec_builder{
|
||||
User: "test-user",
|
||||
Assignments: []*scopedaccessv1pb.Assignment{
|
||||
scopedaccessv1pb.Assignment_builder{
|
||||
Role: "/foo::some-scoped-role",
|
||||
Scope: "/foo/bar",
|
||||
}.Build(),
|
||||
},
|
||||
}.Build(),
|
||||
Version: types.V1,
|
||||
}.Build(),
|
||||
}.Build())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for scoped access cache to see the role and the assignment.
|
||||
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
_, err := p.a.ScopedAccessCache.GetScopedRole(ctx, scopedaccessv1pb.GetScopedRoleRequest_builder{
|
||||
Name: "some-scoped-role",
|
||||
Scope: "/foo",
|
||||
}.Build())
|
||||
require.NoError(ct, err)
|
||||
_, err = p.a.ScopedAccessCache.GetScopedRoleAssignment(ctx, scopedaccessv1pb.GetScopedRoleAssignmentRequest_builder{
|
||||
Name: "some-scoped-role-assignment",
|
||||
Scope: "/foo",
|
||||
SubKind: scopedaccess.SubKindDynamic,
|
||||
}.Build())
|
||||
require.NoError(ct, err)
|
||||
}, 15*time.Second, 100*time.Millisecond)
|
||||
|
||||
ws, _, err := p.a.NewWebSession(ctx, auth.NewWebSessionRequest{
|
||||
User: "test-user",
|
||||
Scope: "/foo/bar",
|
||||
}, nil /* opts */)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, types.TrustedDeviceRequirement_TRUSTED_DEVICE_REQUIREMENT_REQUIRED, ws.GetTrustedDeviceRequirement())
|
||||
|
||||
// Make sure this has actually created a scoped session.
|
||||
_, identity := parseX509PEMAndIdentity(t, ws.GetTLSCert())
|
||||
assert.NotNil(t, identity.ScopePin)
|
||||
}
|
||||
|
||||
func TestDeleteMFADeviceSync(t *testing.T) {
|
||||
|
||||
@@ -3704,6 +3704,32 @@ func (a *ServerWithRoles) SubmitAccessReview(ctx context.Context, submission typ
|
||||
return a.authServer.submitAccessReview(ctx, submission, &identity)
|
||||
}
|
||||
|
||||
func (a *ScopedServerWithRoles) GetAccessCapabilities(ctx context.Context, req types.AccessCapabilitiesRequest) (*types.AccessCapabilities, error) {
|
||||
if unscoped, ok := a.UnscopedServerWithRoles(); ok {
|
||||
return unscoped.GetAccessCapabilities(ctx, req)
|
||||
}
|
||||
|
||||
if a.scopedContext.User == nil {
|
||||
return nil, trace.AccessDenied(
|
||||
"access capabilities can only be retrieved by scoped identities if they are users",
|
||||
)
|
||||
}
|
||||
|
||||
// default to checking the capabilities of the caller
|
||||
if req.User == "" {
|
||||
req.User = a.scopedContext.User.GetName()
|
||||
}
|
||||
|
||||
// Scoped identities can only check their own capabilities
|
||||
if err := a.scopedCurrentUserAction(req.User); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// TODO(bl-nero): Implement once it's supported by the backend. Currently,
|
||||
// there's no concept of scoped access requests.
|
||||
return &types.AccessCapabilities{}, nil
|
||||
}
|
||||
|
||||
func (a *ServerWithRoles) GetAccessCapabilities(ctx context.Context, req types.AccessCapabilitiesRequest) (*types.AccessCapabilities, error) {
|
||||
// default to checking the capabilities of the caller
|
||||
if req.User == "" {
|
||||
|
||||
@@ -14911,3 +14911,160 @@ func TestRoleWindowsDesktopLeastPrivilege(t *testing.T) {
|
||||
require.True(t, trace.IsAccessDenied(err), "expected access denied, got: %v", err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAccessCapabilitiesRBAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
srv, err := authtest.NewAuthServer(authtest.AuthServerConfig{Dir: t.TempDir()})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, srv.Close()) })
|
||||
|
||||
targetUser, err := types.NewUser("target")
|
||||
require.NoError(t, err)
|
||||
_, err = srv.AuthServer.CreateUser(ctx, targetUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
allowRules []types.Rule
|
||||
callerUser string
|
||||
queriedUser string
|
||||
wantAccessDenied bool
|
||||
}{
|
||||
{
|
||||
name: "querying for self doesn't require permissions is allowed",
|
||||
callerUser: "caller-self-default",
|
||||
queriedUser: "",
|
||||
},
|
||||
{
|
||||
name: "querying for self doesn't require permissions is allowed (explicit user name)",
|
||||
callerUser: "caller-self-explicit",
|
||||
queriedUser: "caller-self-explicit",
|
||||
},
|
||||
{
|
||||
name: "querying for other user without permissions is denied",
|
||||
callerUser: "caller-other-denied",
|
||||
queriedUser: "target",
|
||||
wantAccessDenied: true,
|
||||
},
|
||||
{
|
||||
name: "querying for other user with permissions is allowed",
|
||||
allowRules: []types.Rule{
|
||||
types.NewRule(types.KindUser, []string{types.VerbRead}),
|
||||
types.NewRule(types.KindRole, []string{types.VerbList, types.VerbRead}),
|
||||
},
|
||||
callerUser: "caller-other-allowed",
|
||||
queriedUser: "target",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
callerRole, err := types.NewRole(tt.callerUser+"-role", types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{
|
||||
Rules: tt.allowRules,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
callerUser, err := authtest.CreateUser(ctx, srv.AuthServer, tt.callerUser, callerRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
callerIdentity := authz.LocalUser{
|
||||
Username: tt.callerUser,
|
||||
Identity: tlsca.Identity{
|
||||
Username: tt.callerUser,
|
||||
Groups: callerUser.GetRoles(),
|
||||
},
|
||||
}
|
||||
authCtx, err := authz.ContextForLocalUser(
|
||||
ctx, callerIdentity, srv.AuthServer.Services, srv.ClusterName, true, /* disableDeviceAuthz */
|
||||
)
|
||||
require.NoError(t, err)
|
||||
authCtx.AdminActionAuthState = authz.AdminActionAuthMFAVerified
|
||||
|
||||
server := auth.NewServerWithRoles(srv.AuthServer, srv.AuditLog, *authCtx)
|
||||
_, err = server.GetAccessCapabilities(authz.ContextWithUser(ctx, callerIdentity), types.AccessCapabilitiesRequest{
|
||||
User: tt.queriedUser,
|
||||
})
|
||||
if tt.wantAccessDenied {
|
||||
require.True(t, trace.IsAccessDenied(err), "expected access denied, got %T: %v", err, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAccessCapabilitiesScopedRBAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
srv, err := authtest.NewAuthServer(authtest.AuthServerConfig{Dir: t.TempDir()})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, srv.Close()) })
|
||||
|
||||
targetUser, err := types.NewUser("target")
|
||||
require.NoError(t, err)
|
||||
_, err = srv.AuthServer.CreateUser(ctx, targetUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
callerUser string
|
||||
queriedUser string
|
||||
wantAccessDenied bool
|
||||
}{
|
||||
{
|
||||
name: "querying for self is allowed",
|
||||
callerUser: "scoped-caller-self",
|
||||
queriedUser: "",
|
||||
},
|
||||
{
|
||||
name: "querying for other user is denied",
|
||||
callerUser: "scoped-caller-other",
|
||||
queriedUser: "target",
|
||||
wantAccessDenied: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
callerUser, err := types.NewUser(tt.callerUser)
|
||||
require.NoError(t, err)
|
||||
callerUser, err = srv.AuthServer.CreateUser(t.Context(), callerUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
callerIdentity := authz.LocalUser{
|
||||
Username: tt.callerUser,
|
||||
Identity: tlsca.Identity{
|
||||
Username: tt.callerUser,
|
||||
ScopePin: scopesv1.Pin_builder{
|
||||
Kind: scopesv1.PinKind_PIN_KIND_USER,
|
||||
Scope: "/test",
|
||||
}.Build(),
|
||||
},
|
||||
}
|
||||
|
||||
server := auth.NewScopedServerWithRoles(srv.AuthServer, srv.AuditLog, &authz.ScopedContext{
|
||||
User: callerUser,
|
||||
Identity: callerIdentity,
|
||||
})
|
||||
t.Cleanup(func() { server.Close() })
|
||||
|
||||
_, err = server.GetAccessCapabilities(authz.ContextWithUser(t.Context(), callerIdentity), types.AccessCapabilitiesRequest{
|
||||
User: tt.queriedUser,
|
||||
})
|
||||
if tt.wantAccessDenied {
|
||||
require.True(t, trace.IsAccessDenied(err), "expected access denied, got %T: %v", err, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func (a *Server) NewWebSession(
|
||||
ctx context.Context,
|
||||
req NewWebSessionRequest,
|
||||
opts *newWebSessionOpts,
|
||||
) (types.WebSession, services.AccessChecker, error) {
|
||||
) (types.WebSession, *services.ScopedAccessCheckerContext, error) {
|
||||
return a.newWebSession(ctx, req, opts)
|
||||
}
|
||||
|
||||
|
||||
@@ -1274,11 +1274,11 @@ func (g *GRPCServer) GetAccessRequestAllowedPromotions(ctx context.Context, requ
|
||||
}
|
||||
|
||||
func (g *GRPCServer) GetAccessCapabilities(ctx context.Context, req *types.AccessCapabilitiesRequest) (*types.AccessCapabilities, error) {
|
||||
auth, err := g.authenticate(ctx)
|
||||
auth, err := g.scopedAuthenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
caps, err := auth.ServerWithRoles.GetAccessCapabilities(ctx, *req)
|
||||
caps, err := auth.ScopedServerWithRoles.GetAccessCapabilities(ctx, *req)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -6503,7 +6503,7 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {
|
||||
// Initialize and register the user preferences service.
|
||||
userPreferencesSrv, err := userpreferencesv1.NewService(&userpreferencesv1.ServiceConfig{
|
||||
Backend: cfg.AuthServer.Services,
|
||||
Authorizer: cfg.Authorizer,
|
||||
Authorizer: cfg.ScopedAuthorizer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
+40
-19
@@ -55,11 +55,6 @@ import (
|
||||
type NewWebSessionRequest = sessionreq.NewWebSessionRequest
|
||||
|
||||
func (a *Server) CreateWebSessionFromReq(ctx context.Context, req NewWebSessionRequest) (types.WebSession, error) {
|
||||
if req.Scope != "" {
|
||||
// TODO(fspmarshall/scopes): add scoping support for web sessions
|
||||
return nil, trace.BadParameter("web sessions cannot be pinned to a scope")
|
||||
}
|
||||
|
||||
session, _, err := a.newWebSession(ctx, req, nil /* opts */)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
@@ -151,7 +146,16 @@ func (a *Server) newWebSession(
|
||||
ctx context.Context,
|
||||
req NewWebSessionRequest,
|
||||
opts *newWebSessionOpts,
|
||||
) (types.WebSession, services.AccessChecker, error) {
|
||||
) (types.WebSession, *services.ScopedAccessCheckerContext, error) {
|
||||
if req.Scope != "" {
|
||||
if req.DelegationSessionID != "" {
|
||||
return nil, nil, trace.BadParameter("access delegation is only supported for unscoped sessions")
|
||||
}
|
||||
if err := a.scopesFeatures.AssertEnabled(); err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
userState, err := a.GetUserOrLoginState(ctx, req.User)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
@@ -166,15 +170,25 @@ func (a *Server) newWebSession(
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
checker, err := services.NewAccessChecker(&services.AccessInfo{
|
||||
Username: userState.GetName(),
|
||||
Roles: req.Roles,
|
||||
Traits: req.Traits,
|
||||
AllowedResourceAccessIDs: req.RequestedResourceAccessIDs,
|
||||
DelegationSessionID: req.DelegationSessionID,
|
||||
}, clusterName.GetClusterName(), a)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
var checkerCtx *services.ScopedAccessCheckerContext
|
||||
var unscopedChecker services.AccessChecker
|
||||
if req.Scope != "" {
|
||||
checkerCtx, err = a.AccessCheckerForScope(ctx, req.Scope, userState, req.RequestedResourceAccessIDs)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
unscopedChecker, err = services.NewAccessChecker(&services.AccessInfo{
|
||||
Username: userState.GetName(),
|
||||
Roles: req.Roles,
|
||||
Traits: req.Traits,
|
||||
AllowedResourceAccessIDs: req.RequestedResourceAccessIDs,
|
||||
DelegationSessionID: req.DelegationSessionID,
|
||||
}, clusterName.GetClusterName(), a)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
checkerCtx = services.NewScopedAccessCheckerContextFromUnscoped(unscopedChecker)
|
||||
}
|
||||
|
||||
idleTimeout, err := a.getWebIdleTimeout(ctx)
|
||||
@@ -205,7 +219,7 @@ func (a *Server) newWebSession(
|
||||
|
||||
sessionTTL := req.SessionTTL
|
||||
if sessionTTL == 0 {
|
||||
sessionTTL = checker.AdjustSessionTTL(apidefaults.CertDuration)
|
||||
sessionTTL = checkerCtx.CertParams().AdjustSessionTTL(apidefaults.CertDuration)
|
||||
}
|
||||
|
||||
if req.AttestWebSession {
|
||||
@@ -239,7 +253,7 @@ func (a *Server) newWebSession(
|
||||
TTL: sessionTTL,
|
||||
SSHPublicKey: sshAuthorizedKey,
|
||||
TLSPublicKey: tlsPublicKeyPEM,
|
||||
CheckerContext: services.NewScopedAccessCheckerContextFromUnscoped(checker), // TODO(fspmarshall/scopes): add scoping support to newWebSession.
|
||||
CheckerContext: checkerCtx,
|
||||
Traits: req.Traits,
|
||||
ActiveRequests: req.AccessRequests,
|
||||
}
|
||||
@@ -320,7 +334,14 @@ func (a *Server) newWebSession(
|
||||
}
|
||||
|
||||
if tdr, err := a.calculateTrustedDeviceMode(ctx, func() ([]types.Role, error) {
|
||||
return checker.Roles(), nil
|
||||
if unscopedChecker != nil {
|
||||
return unscopedChecker.Roles(), nil
|
||||
}
|
||||
// For scoped sessions, no traditional roles apply, so let's only compute
|
||||
// the trusted device mode from global cluster settings.
|
||||
// TODO(bl-nero): Update this once there's actual support for device trust
|
||||
// in scoped sessions.
|
||||
return nil, nil
|
||||
}); err != nil {
|
||||
a.logger.WarnContext(ctx, "Failed to calculate trusted device mode for session", "error", err)
|
||||
} else {
|
||||
@@ -334,7 +355,7 @@ func (a *Server) newWebSession(
|
||||
}
|
||||
}
|
||||
|
||||
return sess, checker, nil
|
||||
return sess, checkerCtx, nil
|
||||
}
|
||||
|
||||
func (a *Server) getWebIdleTimeout(ctx context.Context) (time.Duration, error) {
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
// ServiceConfig holds configuration options for the user preferences service.
|
||||
type ServiceConfig struct {
|
||||
Backend services.UserPreferences
|
||||
Authorizer authz.Authorizer
|
||||
Authorizer authz.ScopedAuthorizer
|
||||
}
|
||||
|
||||
// Service implements the teleport.userpreferences.v1.UserPreferencesService RPC service.
|
||||
@@ -40,7 +40,7 @@ type Service struct {
|
||||
userpreferences.UnimplementedUserPreferencesServiceServer
|
||||
|
||||
backend services.UserPreferences
|
||||
authorizer authz.Authorizer
|
||||
authorizer authz.ScopedAuthorizer
|
||||
}
|
||||
|
||||
// NewService returns a new user preferences gRPC service.
|
||||
@@ -60,11 +60,14 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
|
||||
|
||||
// GetUserPreferences returns the user preferences for a given user.
|
||||
func (a *Service) GetUserPreferences(ctx context.Context, _ *userpreferences.GetUserPreferencesRequest) (*userpreferences.GetUserPreferencesResponse, error) {
|
||||
authCtx, err := a.authorizer.Authorize(ctx)
|
||||
authCtx, err := a.authorizer.AuthorizeScoped(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if authCtx.User == nil {
|
||||
return nil, trace.AccessDenied("user preferences are only available to users, not scoped agents")
|
||||
}
|
||||
username := authCtx.User.GetName()
|
||||
|
||||
prefs, err := a.backend.GetUserPreferences(ctx, username)
|
||||
@@ -79,11 +82,14 @@ func (a *Service) GetUserPreferences(ctx context.Context, _ *userpreferences.Get
|
||||
|
||||
// UpsertUserPreferences creates or updates user preferences for a given username.
|
||||
func (a *Service) UpsertUserPreferences(ctx context.Context, req *userpreferences.UpsertUserPreferencesRequest) (*emptypb.Empty, error) {
|
||||
authCtx, err := a.authorizer.Authorize(ctx)
|
||||
authCtx, err := a.authorizer.AuthorizeScoped(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if authCtx.User == nil {
|
||||
return nil, trace.AccessDenied("user preferences are only available to users, not scoped agents")
|
||||
}
|
||||
username := authCtx.User.GetName()
|
||||
|
||||
return &emptypb.Empty{}, trace.Wrap(a.backend.UpsertUserPreferences(ctx, username, req.Preferences))
|
||||
|
||||
@@ -193,10 +193,11 @@ func initSvc(t *testing.T) (map[string]context.Context, *Service) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{
|
||||
ClusterName: "test-cluster",
|
||||
AccessPoint: accessPoint,
|
||||
LockWatcher: lockWatcher,
|
||||
authorizer, err := authz.NewScopedAuthorizer(authz.AuthorizerOpts{
|
||||
ClusterName: "test-cluster",
|
||||
AccessPoint: accessPoint,
|
||||
LockWatcher: lockWatcher,
|
||||
ScopedRoleReader: local.NewScopedAccessService(backend),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -229,6 +229,8 @@ type AuthenticateWebUserRequest struct {
|
||||
User string `json:"user"`
|
||||
// WebauthnAssertionResponse is a signed WebAuthn credential assertion.
|
||||
WebauthnAssertionResponse *wantypes.CredentialAssertionResponse `json:"webauthnAssertionResponse,omitempty"`
|
||||
// Scope is a scope for with the user is authenticated. Empty means unscoped.
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
type HeadlessRequest struct {
|
||||
|
||||
+89
-25
@@ -614,6 +614,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) {
|
||||
sessionCache, err := newSessionCache(h.cfg.Context, sessionCacheOptions{
|
||||
proxyClient: cfg.ProxyClient,
|
||||
accessPoint: cfg.AccessPoint,
|
||||
scopedRoleReader: cfg.AccessPoint.ScopedRoleReader(),
|
||||
servers: []utils.NetAddr{cfg.AuthServers},
|
||||
cipherSuites: cfg.CipherSuites,
|
||||
clock: h.clock,
|
||||
@@ -857,7 +858,7 @@ func (h *Handler) authenticateWebSession(w http.ResponseWriter, r *http.Request)
|
||||
if err != nil {
|
||||
return webSession{}, trace.Wrap(err)
|
||||
}
|
||||
resp, err := newSessionResponse(ctx)
|
||||
resp, err := newSessionResponse(r.Context(), ctx)
|
||||
if err != nil {
|
||||
return webSession{}, trace.Wrap(err)
|
||||
}
|
||||
@@ -1512,11 +1513,34 @@ func (h *Handler) getUserContext(w http.ResponseWriter, r *http.Request, p httpr
|
||||
})
|
||||
|
||||
userContext.AvailableScopes = assignedScopes
|
||||
|
||||
userContext.Scope, err = scopeFromSessionContext(c)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return userContext, nil
|
||||
}
|
||||
|
||||
func scopeFromSessionContext(c *SessionContext) (string, error) {
|
||||
id, err := c.GetIdentity()
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
if id == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
pin := id.ScopePin
|
||||
if pin == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return pin.GetScope(), nil
|
||||
}
|
||||
|
||||
// PublicProxyAddr returns the publicly advertised proxy address
|
||||
func (h *Handler) PublicProxyAddr() string {
|
||||
return h.cfg.PublicProxyAddr
|
||||
@@ -2481,7 +2505,6 @@ func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httpr
|
||||
logger.ErrorContext(r.Context(), "Failed to parse request remote address", "error", err)
|
||||
return client.LoginFailedRedirectURL
|
||||
}
|
||||
|
||||
response, err := h.cfg.ProxyClient.CreateGithubAuthRequest(r.Context(), types.GithubAuthRequest{
|
||||
CSRFToken: req.CSRFToken,
|
||||
ConnectorID: req.ConnectorID,
|
||||
@@ -2489,6 +2512,7 @@ func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httpr
|
||||
ClientRedirectURL: req.ClientRedirectURL,
|
||||
ClientLoginIP: remoteAddr,
|
||||
ClientUserAgent: r.UserAgent(),
|
||||
Scope: req.Scope,
|
||||
})
|
||||
if err != nil {
|
||||
logger.ErrorContext(r.Context(), "Error creating auth request", "error", err)
|
||||
@@ -2815,6 +2839,9 @@ type CreateSessionReq struct {
|
||||
Pass string `json:"pass"`
|
||||
// SecondFactorToken is the OTP.
|
||||
SecondFactorToken string `json:"second_factor_token"`
|
||||
// Scope is the scope for which this session is created. Empty means
|
||||
// unscoped.
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// String returns text description of this response
|
||||
@@ -2850,14 +2877,26 @@ type CreateSessionResponse struct {
|
||||
TrustedDeviceRequirement int32 `json:"trustedDeviceRequirement,omitempty"`
|
||||
}
|
||||
|
||||
func newSessionResponse(sctx *SessionContext) (*CreateSessionResponse, error) {
|
||||
func newSessionResponse(ctx context.Context, sctx *SessionContext) (*CreateSessionResponse, error) {
|
||||
accessChecker, err := sctx.GetUserAccessChecker()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
_, err = accessChecker.CheckLoginDuration(0)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
if accessChecker.AccessInfo().ScopePin == nil {
|
||||
_, err = accessChecker.CheckLoginDuration(0)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
checkerContext, err := sctx.GetUserScopedAccessCheckerContext(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
_, err = checkerContext.CertParams().GetSSHLoginsForTTL(ctx, 0)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
token, err := sctx.getToken()
|
||||
@@ -2909,11 +2948,11 @@ func (h *Handler) createWebSession(w http.ResponseWriter, r *http.Request, p htt
|
||||
var webSession types.WebSession
|
||||
switch {
|
||||
case !cap.IsSecondFactorEnforced():
|
||||
webSession, err = h.auth.AuthWithoutOTP(r.Context(), req.User, req.Pass, clientMeta)
|
||||
webSession, err = h.auth.AuthWithoutOTP(r.Context(), req.User, req.Pass, req.Scope, clientMeta)
|
||||
case req.SecondFactorToken == "" && !cap.IsSecondFactorEnforced():
|
||||
webSession, err = h.auth.AuthWithoutOTP(r.Context(), req.User, req.Pass, clientMeta)
|
||||
webSession, err = h.auth.AuthWithoutOTP(r.Context(), req.User, req.Pass, req.Scope, clientMeta)
|
||||
case cap.IsSecondFactorTOTPAllowed():
|
||||
webSession, err = h.auth.AuthWithOTP(r.Context(), req.User, req.Pass, req.SecondFactorToken, clientMeta)
|
||||
webSession, err = h.auth.AuthWithOTP(r.Context(), req.User, req.Pass, req.SecondFactorToken, req.Scope, clientMeta)
|
||||
default:
|
||||
return nil, trace.AccessDenied("direct login with password+otp not supported by this cluster")
|
||||
}
|
||||
@@ -2938,7 +2977,7 @@ func (h *Handler) createWebSession(w http.ResponseWriter, r *http.Request, p htt
|
||||
return nil, trace.AccessDenied("need auth")
|
||||
}
|
||||
|
||||
res, err := newSessionResponse(ctx)
|
||||
res, err := newSessionResponse(r.Context(), ctx)
|
||||
return res, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -3035,6 +3074,19 @@ type renewSessionRequest struct {
|
||||
// - ReloadUser (opt): similar to default but updates user related data (e.g login traits) by retrieving it from the backend
|
||||
// - default (none set): create new session with currently assigned roles
|
||||
func (h *Handler) renewWebSession(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
||||
// TODO(bl-nero): Fix session renewal for scoped sessions. This endpoint
|
||||
// doesn't work for scoped sessions, but currently, the web UI doesn't even
|
||||
// call it in such scenario.
|
||||
identity, err := ctx.GetIdentity()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if identity.ScopePin != nil {
|
||||
// Reject scoped sessions explicitly to prevent removing a scope pin from an existing session without reauthentication.
|
||||
return nil, trace.BadParameter("Scoped sessions are not supported yet")
|
||||
}
|
||||
|
||||
req := renewSessionRequest{}
|
||||
if err := httplib.ReadResourceJSON(r, &req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
@@ -3056,7 +3108,7 @@ func (h *Handler) renewWebSession(w http.ResponseWriter, r *http.Request, params
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
res, err := newSessionResponse(newContext)
|
||||
res, err := newSessionResponse(r.Context(), newContext)
|
||||
return res, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -3134,7 +3186,7 @@ func (h *Handler) changeUserAuthentication(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// Checks for at least one valid login.
|
||||
if _, err := newSessionResponse(ctx); err != nil {
|
||||
if _, err := newSessionResponse(r.Context(), ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -3350,7 +3402,7 @@ func (h *Handler) mfaLoginFinishSession(w http.ResponseWriter, r *http.Request,
|
||||
return nil, trace.AccessDenied("need auth")
|
||||
}
|
||||
|
||||
return newSessionResponse(ctx)
|
||||
return newSessionResponse(r.Context(), ctx)
|
||||
}
|
||||
|
||||
// getClusters returns a list of cluster and its data.
|
||||
@@ -3437,7 +3489,7 @@ func (h *Handler) getSiteNamespaces(w http.ResponseWriter, r *http.Request, _ ht
|
||||
}, nil
|
||||
}
|
||||
|
||||
func makeUnifiedResourceRequest(r *http.Request) (*proto.ListUnifiedResourcesRequest, error) {
|
||||
func makeUnifiedResourceRequest(r *http.Request, scopePin *scopesv1.Pin) (*proto.ListUnifiedResourcesRequest, error) {
|
||||
values := r.URL.Query()
|
||||
|
||||
limit, err := QueryLimitAsInt32(values, "limit", defaults.MaxIterationLimit)
|
||||
@@ -3461,16 +3513,23 @@ func makeUnifiedResourceRequest(r *http.Request) (*proto.ListUnifiedResourcesReq
|
||||
}
|
||||
|
||||
// set default kinds to be requested if none exist in the request
|
||||
scopedIdentity := scopePin.GetScope() != ""
|
||||
if len(kinds) == 0 {
|
||||
kinds = []string{
|
||||
types.KindApp,
|
||||
types.KindDatabase,
|
||||
types.KindNode,
|
||||
types.KindWindowsDesktop,
|
||||
types.KindLinuxDesktop,
|
||||
types.KindKubernetesCluster,
|
||||
types.KindSAMLIdPServiceProvider,
|
||||
types.KindGitServer,
|
||||
if scopedIdentity {
|
||||
// TODO(bl-nero): Support more resource kinds when they are supported for
|
||||
// scoped sessions.
|
||||
kinds = []string{types.KindNode}
|
||||
} else {
|
||||
kinds = []string{
|
||||
types.KindApp,
|
||||
types.KindDatabase,
|
||||
types.KindNode,
|
||||
types.KindWindowsDesktop,
|
||||
types.KindLinuxDesktop,
|
||||
types.KindKubernetesCluster,
|
||||
types.KindSAMLIdPServiceProvider,
|
||||
types.KindGitServer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3487,7 +3546,7 @@ func makeUnifiedResourceRequest(r *http.Request) (*proto.ListUnifiedResourcesReq
|
||||
PredicateExpression: values.Get("query"),
|
||||
SearchKeywords: client.ParseSearchKeywords(values.Get("search"), ' '),
|
||||
UseSearchAsRoles: useSearchAsRoles,
|
||||
IncludeLogins: true,
|
||||
IncludeLogins: !scopedIdentity,
|
||||
IncludeRequestable: includeRequestable,
|
||||
}, nil
|
||||
}
|
||||
@@ -3553,7 +3612,7 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
req, err := makeUnifiedResourceRequest(request)
|
||||
req, err := makeUnifiedResourceRequest(request, identity.ScopePin)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -5745,6 +5804,8 @@ type SSORequestParams struct {
|
||||
CSRFToken string
|
||||
// LoginHint is the user's identifier (email) for identifier-first login.
|
||||
LoginHint string
|
||||
// Scope is the scope that this session will be pinned to.
|
||||
Scope string
|
||||
}
|
||||
|
||||
// ParseSSORequestParams extracts the SSO request parameters from an http.Request,
|
||||
@@ -5786,11 +5847,14 @@ func ParseSSORequestParams(r *http.Request) (*SSORequestParams, error) {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
scope := query.Get("scope")
|
||||
|
||||
return &SSORequestParams{
|
||||
ClientRedirectURL: clientRedirectURL,
|
||||
ConnectorID: connectorID,
|
||||
CSRFToken: csrfToken,
|
||||
LoginHint: loginHint,
|
||||
Scope: scope,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+177
-79
@@ -142,7 +142,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/reversetunnel"
|
||||
"github.com/gravitational/teleport/lib/reversetunnelclient"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
access "github.com/gravitational/teleport/lib/scopes/access"
|
||||
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
|
||||
scopedapp "github.com/gravitational/teleport/lib/scopes/app"
|
||||
"github.com/gravitational/teleport/lib/secret"
|
||||
"github.com/gravitational/teleport/lib/service/servicecfg"
|
||||
@@ -301,6 +301,9 @@ type webSuiteConfig struct {
|
||||
|
||||
// middleware adds optional middleware to the test handler.
|
||||
middleware func(next http.Handler) http.Handler
|
||||
|
||||
// scopesFeatures controls scoped access feature flags.
|
||||
scopesFeatures scopes.Features
|
||||
}
|
||||
|
||||
func newWebSuiteWithConfig(t *testing.T, cfg webSuiteConfig) *WebSuite {
|
||||
@@ -337,6 +340,7 @@ func newWebSuiteWithConfig(t *testing.T, cfg webSuiteConfig) *WebSuite {
|
||||
Clock: s.clock,
|
||||
ClusterNetworkingConfig: networkingConfig,
|
||||
AuthPreferenceSpec: cfg.authPreferenceSpec,
|
||||
ScopesFeatures: cfg.scopesFeatures,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -636,6 +640,7 @@ func newWebSuiteWithConfig(t *testing.T, cfg webSuiteConfig) *WebSuite {
|
||||
ProxySSHAddr: "127.0.0.1",
|
||||
AccessPoint: s.server.Auth(),
|
||||
},
|
||||
ScopesFeatures: cfg.scopesFeatures,
|
||||
SessionControl: SessionControllerFunc(func(ctx context.Context, sctx *SessionContext, login, localAddr, remoteAddr string) (context.Context, error) {
|
||||
controller := srv.WebSessionController(proxySessionController)
|
||||
ctx, err := controller(ctx, sctx, login, localAddr, remoteAddr)
|
||||
@@ -3164,10 +3169,14 @@ func TestLogin_PrivateKeyEnabledError(t *testing.T) {
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newWebSuite(t)
|
||||
ctx := t.Context()
|
||||
|
||||
s := newWebSuiteWithConfig(t, webSuiteConfig{
|
||||
scopesFeatures: scopes.Features{Enabled: true},
|
||||
})
|
||||
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
||||
Type: constants.Local,
|
||||
SecondFactor: constants.SecondFactorOff,
|
||||
SecondFactor: constants.SecondFactorOTP,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = s.server.Auth().UpsertAuthPreference(s.ctx, ap)
|
||||
@@ -3177,63 +3186,130 @@ func TestLogin(t *testing.T) {
|
||||
const user = "user1"
|
||||
const pass = "password1234"
|
||||
s.createUser(t, user, "root", pass, "")
|
||||
otpSecret := newOTPSharedSecret()
|
||||
dev, err := services.NewTOTPDevice("otp-device", otpSecret, s.clock.Now())
|
||||
require.NoError(t, err)
|
||||
err = s.server.Auth().UpsertMFADevice(ctx, user, dev)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.server.Auth().ScopedAccess().CreateScopedRole(
|
||||
ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
|
||||
Role: scopedaccessv1.ScopedRole_builder{
|
||||
Kind: scopedaccess.KindScopedRole,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "prod-role",
|
||||
}.Build(),
|
||||
Scope: "/prod",
|
||||
Spec: scopedaccessv1.ScopedRoleSpec_builder{
|
||||
AssignableScopes: []string{"/prod/east"},
|
||||
}.Build(),
|
||||
}.Build(),
|
||||
}.Build(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.server.Auth().ScopedAccess().CreateScopedRoleAssignment(
|
||||
ctx, scopedaccessv1.CreateScopedRoleAssignmentRequest_builder{
|
||||
Assignment: scopedaccessv1.ScopedRoleAssignment_builder{
|
||||
Kind: scopedaccess.KindScopedRoleAssignment,
|
||||
SubKind: scopedaccess.SubKindDynamic,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "prod-role-assignment",
|
||||
}.Build(),
|
||||
Scope: "/prod",
|
||||
Spec: scopedaccessv1.ScopedRoleAssignmentSpec_builder{
|
||||
User: "user1",
|
||||
Assignments: []*scopedaccessv1.Assignment{
|
||||
scopedaccessv1.Assignment_builder{
|
||||
Role: "/prod::prod-role",
|
||||
Scope: "/prod/east",
|
||||
}.Build(),
|
||||
},
|
||||
}.Build(),
|
||||
}.Build(),
|
||||
}.Build(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
clt := s.client(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const ua = "test-ua"
|
||||
sessionResp, httpResp := loginWebOTP(t, ctx, loginWebOTPParams{
|
||||
webClient: clt,
|
||||
user: user,
|
||||
password: pass,
|
||||
userAgent: ua,
|
||||
})
|
||||
cases := []struct {
|
||||
name string
|
||||
scope string
|
||||
}{
|
||||
{name: "unscoped", scope: ""},
|
||||
{name: "scoped", scope: "/prod/east"},
|
||||
}
|
||||
|
||||
events, _, err := s.server.AuthServer.AuditLog.SearchEvents(ctx, events.SearchEventsRequest{
|
||||
From: s.clock.Now().Add(-time.Hour),
|
||||
To: s.clock.Now().Add(time.Hour),
|
||||
EventTypes: []string{events.UserLoginEvent},
|
||||
Limit: 1,
|
||||
Order: types.EventOrderDescending,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
event := events[0].(*apievents.UserLogin)
|
||||
require.True(t, event.Success)
|
||||
require.Equal(t, ua, event.UserAgent)
|
||||
require.True(t, strings.HasPrefix(event.RemoteAddr, "127.0.0.1:"))
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
const ua = "test-ua"
|
||||
s.clock.Advance(time.Minute) // Prevent reusing old OTP
|
||||
sessionResp, httpResp := loginWebOTP(t, ctx, loginWebOTPParams{
|
||||
webClient: clt,
|
||||
clock: s.clock,
|
||||
user: user,
|
||||
password: pass,
|
||||
otpSecret: otpSecret,
|
||||
scope: tc.scope,
|
||||
userAgent: ua,
|
||||
})
|
||||
|
||||
cookies := httpResp.Cookies()
|
||||
require.Len(t, cookies, 1)
|
||||
require.NotEmpty(t, sessionResp.SessionExpires)
|
||||
events, _, err := s.server.AuthServer.AuditLog.SearchEvents(ctx, events.SearchEventsRequest{
|
||||
From: s.clock.Now().Add(-time.Hour),
|
||||
To: s.clock.Now().Add(time.Hour),
|
||||
EventTypes: []string{events.UserLoginEvent},
|
||||
Limit: 1,
|
||||
Order: types.EventOrderDescending,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
event := events[0].(*apievents.UserLogin)
|
||||
require.True(t, event.Success)
|
||||
require.Equal(t, ua, event.UserAgent)
|
||||
require.True(t, strings.HasPrefix(event.RemoteAddr, "127.0.0.1:"))
|
||||
|
||||
// now make sure we are logged in by calling authenticated method
|
||||
// we need to supply both session cookie and bearer token for
|
||||
// request to succeed
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
cookies := httpResp.Cookies()
|
||||
require.Len(t, cookies, 1)
|
||||
require.NotEmpty(t, sessionResp.SessionExpires)
|
||||
|
||||
clt = s.client(t, roundtrip.BearerAuth(sessionResp.Token), roundtrip.CookieJar(jar))
|
||||
jar.SetCookies(s.url(), cookies)
|
||||
// now make sure we are logged in by calling authenticated method
|
||||
// we need to supply both session cookie and bearer token for
|
||||
// request to succeed
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
re, err := clt.Get(s.ctx, clt.Endpoint("webapi", "sites"), url.Values{})
|
||||
require.NoError(t, err)
|
||||
clt = s.client(t, roundtrip.BearerAuth(sessionResp.Token), roundtrip.CookieJar(jar))
|
||||
jar.SetCookies(s.url(), cookies)
|
||||
|
||||
var clusters []webui.Cluster
|
||||
require.NoError(t, json.Unmarshal(re.Bytes(), &clusters))
|
||||
re, err := clt.Get(
|
||||
s.ctx,
|
||||
clt.Endpoint("webapi", "sites", s.server.ClusterName(), "context"),
|
||||
url.Values{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// in absence of session cookie or bearer auth the same request fill fail
|
||||
var userContext webui.UserContext
|
||||
require.NoError(t, json.Unmarshal(re.Bytes(), &userContext))
|
||||
assert.Equal(t, tc.scope, userContext.Scope)
|
||||
|
||||
// no session cookie:
|
||||
clt = s.client(t, roundtrip.BearerAuth(sessionResp.Token))
|
||||
_, err = clt.Get(s.ctx, clt.Endpoint("webapi", "sites"), url.Values{})
|
||||
require.Error(t, err)
|
||||
require.True(t, trace.IsAccessDenied(err))
|
||||
// in absence of session cookie or bearer auth the same request fill fail
|
||||
|
||||
// no bearer token:
|
||||
clt = s.client(t, roundtrip.CookieJar(jar))
|
||||
_, err = clt.Get(s.ctx, clt.Endpoint("webapi", "sites"), url.Values{})
|
||||
require.Error(t, err)
|
||||
require.True(t, trace.IsAccessDenied(err))
|
||||
// no session cookie:
|
||||
clt = s.client(t, roundtrip.BearerAuth(sessionResp.Token))
|
||||
_, err = clt.Get(s.ctx, clt.Endpoint("webapi", "sites"), url.Values{})
|
||||
require.Error(t, err)
|
||||
require.True(t, trace.IsAccessDenied(err))
|
||||
|
||||
// no bearer token:
|
||||
clt = s.client(t, roundtrip.CookieJar(jar))
|
||||
_, err = clt.Get(s.ctx, clt.Endpoint("webapi", "sites"), url.Values{})
|
||||
require.Error(t, err)
|
||||
require.True(t, trace.IsAccessDenied(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmptyMotD ensures that responses returned by both /webapi/ping and
|
||||
@@ -9436,21 +9512,17 @@ type testProxy struct {
|
||||
webURL url.URL
|
||||
}
|
||||
|
||||
// authPack returns new authenticated package consisting of created valid
|
||||
// user, otp token, created web session and authenticated client.
|
||||
func (r *testProxy) authPack(t *testing.T, teleportUser string, roles []types.Role) *authPack {
|
||||
// authPackWithLoginParams returns new authenticated package consisting of
|
||||
// created valid user, otp token, created web session and authenticated client
|
||||
// with given login params.
|
||||
func (r *testProxy) authPackWithLoginParams(
|
||||
t *testing.T, teleportUser string, loginParams loginWebOTPParams, roles []types.Role,
|
||||
) *authPack {
|
||||
ctx := context.Background()
|
||||
const (
|
||||
pass = "abcdef123456"
|
||||
rawSecret = "def456"
|
||||
)
|
||||
|
||||
u, err := user.Current()
|
||||
require.NoError(t, err)
|
||||
loginUser := u.Username
|
||||
|
||||
otpSecret := newOTPSharedSecret()
|
||||
|
||||
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
||||
Type: constants.Local,
|
||||
SecondFactor: constants.SecondFactorOTP,
|
||||
@@ -9460,15 +9532,11 @@ func (r *testProxy) authPack(t *testing.T, teleportUser string, roles []types.Ro
|
||||
_, err = r.auth.Auth().UpsertAuthPreference(ctx, ap)
|
||||
require.NoError(t, err)
|
||||
|
||||
r.createUser(context.Background(), t, teleportUser, loginUser, pass, otpSecret, roles)
|
||||
r.createUser(
|
||||
context.Background(), t, teleportUser, loginUser, loginParams.password, loginParams.otpSecret, roles,
|
||||
)
|
||||
|
||||
sessionResp, httpResp := loginWebOTP(t, ctx, loginWebOTPParams{
|
||||
webClient: r.newClient(t),
|
||||
clock: r.clock,
|
||||
user: teleportUser,
|
||||
password: pass,
|
||||
otpSecret: otpSecret,
|
||||
})
|
||||
sessionResp, httpResp := loginWebOTP(t, ctx, loginParams)
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
@@ -9477,19 +9545,48 @@ func (r *testProxy) authPack(t *testing.T, teleportUser string, roles []types.Ro
|
||||
jar.SetCookies(&r.webURL, httpResp.Cookies())
|
||||
|
||||
return &authPack{
|
||||
otpSecret: otpSecret,
|
||||
otpSecret: loginParams.otpSecret,
|
||||
user: teleportUser,
|
||||
login: loginUser,
|
||||
session: sessionResp,
|
||||
clt: clt,
|
||||
cookies: httpResp.Cookies(),
|
||||
password: pass,
|
||||
password: loginParams.password,
|
||||
device: &authtest.Device{
|
||||
TOTPSecret: otpSecret,
|
||||
TOTPSecret: loginParams.otpSecret,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *testProxy) authPack(t *testing.T, teleportUser string, roles []types.Role) *authPack {
|
||||
return r.authPackWithLoginParams(
|
||||
t, teleportUser,
|
||||
loginWebOTPParams{
|
||||
webClient: r.newClient(t),
|
||||
clock: r.clock,
|
||||
user: teleportUser,
|
||||
password: "abcdef123456",
|
||||
otpSecret: newOTPSharedSecret(),
|
||||
},
|
||||
roles,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *testProxy) scopedAuthPack(t *testing.T, teleportUser string, scope string, roles []types.Role) *authPack {
|
||||
return r.authPackWithLoginParams(
|
||||
t, teleportUser,
|
||||
loginWebOTPParams{
|
||||
webClient: r.newClient(t),
|
||||
clock: r.clock,
|
||||
user: teleportUser,
|
||||
password: "abcdef123456",
|
||||
otpSecret: newOTPSharedSecret(),
|
||||
scope: scope,
|
||||
},
|
||||
roles,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *testProxy) authPackFromPack(t *testing.T, pack *authPack) *authPack {
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
@@ -9705,7 +9802,7 @@ func TestUserContextWithAccessRequest(t *testing.T) {
|
||||
require.Equal(t, accessRequestID, userContext.ConsumedAccessRequestID)
|
||||
}
|
||||
|
||||
func TestUerContextWithScopesNoAssignments(t *testing.T) {
|
||||
func TestUserContextWithScopesNoAssignments(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newWebPack(t, 1, withScopesFeatures(scopes.Features{Enabled: true}))
|
||||
proxy := env.proxies[0]
|
||||
@@ -9728,7 +9825,7 @@ func TestUerContextWithScopesNoAssignments(t *testing.T) {
|
||||
require.Empty(t, userContext.AvailableScopes)
|
||||
}
|
||||
|
||||
func TestUerContextWithScopes(t *testing.T) {
|
||||
func TestUserContextWithScopes(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newWebPack(t, 1, withScopesFeatures(scopes.Features{Enabled: true}))
|
||||
proxy := env.proxies[0]
|
||||
@@ -9737,7 +9834,7 @@ func TestUerContextWithScopes(t *testing.T) {
|
||||
// Create scoped roles.
|
||||
_, err := env.server.Auth().ScopedAccess().CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
|
||||
Role: scopedaccessv1.ScopedRole_builder{
|
||||
Kind: access.KindScopedRole,
|
||||
Kind: scopedaccess.KindScopedRole,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "role-a",
|
||||
@@ -9751,7 +9848,7 @@ func TestUerContextWithScopes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
_, err = env.server.Auth().ScopedAccess().CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
|
||||
Role: scopedaccessv1.ScopedRole_builder{
|
||||
Kind: access.KindScopedRole,
|
||||
Kind: scopedaccess.KindScopedRole,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "role-b",
|
||||
@@ -9768,8 +9865,8 @@ func TestUerContextWithScopes(t *testing.T) {
|
||||
username := "dave"
|
||||
assignment1, err := env.server.Auth().ScopedAccess().CreateScopedRoleAssignment(ctx, scopedaccessv1.CreateScopedRoleAssignmentRequest_builder{
|
||||
Assignment: scopedaccessv1.ScopedRoleAssignment_builder{
|
||||
Kind: access.KindScopedRoleAssignment,
|
||||
SubKind: access.SubKindDynamic,
|
||||
Kind: scopedaccess.KindScopedRoleAssignment,
|
||||
SubKind: scopedaccess.SubKindDynamic,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "assignment-1",
|
||||
@@ -9795,8 +9892,8 @@ func TestUerContextWithScopes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assignment2, err := env.server.Auth().ScopedAccess().CreateScopedRoleAssignment(ctx, scopedaccessv1.CreateScopedRoleAssignmentRequest_builder{
|
||||
Assignment: scopedaccessv1.ScopedRoleAssignment_builder{
|
||||
Kind: access.KindScopedRoleAssignment,
|
||||
SubKind: access.SubKindDynamic,
|
||||
Kind: scopedaccess.KindScopedRoleAssignment,
|
||||
SubKind: scopedaccess.SubKindDynamic,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: "assignment-2",
|
||||
@@ -9822,7 +9919,7 @@ func TestUerContextWithScopes(t *testing.T) {
|
||||
waitForSRACache(t, env.server.TLS, assignment1, assignment2)
|
||||
|
||||
// Create and authenticate the test user.
|
||||
pack := proxy.authPack(t, username, []types.Role{})
|
||||
pack := proxy.scopedAuthPack(t, username, "/test/a2", []types.Role{})
|
||||
|
||||
// Make a request to fetch the userContext.
|
||||
endpoint := pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "context")
|
||||
@@ -9835,7 +9932,8 @@ func TestUerContextWithScopes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that the userContext returned contains the assigned scopes.
|
||||
require.Equal(t, []string{"/test/a1", "/test/a2", "/test/b1"}, userContext.AvailableScopes)
|
||||
assert.Equal(t, []string{"/test/a1", "/test/a2", "/test/b1"}, userContext.AvailableScopes)
|
||||
assert.Equal(t, "/test/a2", userContext.Scope)
|
||||
}
|
||||
|
||||
func waitForSRACache(t *testing.T, srv *authtest.TLSServer, resps ...*scopedaccessv1.CreateScopedRoleAssignmentResponse) {
|
||||
|
||||
@@ -54,6 +54,7 @@ type loginWebOTPParams struct {
|
||||
// If empty then no OTP is sent in the request.
|
||||
otpSecret string
|
||||
|
||||
scope string // Optional.
|
||||
userAgent string // Optional.
|
||||
overrideContentType string // Optional.
|
||||
}
|
||||
@@ -110,6 +111,7 @@ func rawLoginWebOTP(ctx context.Context, params loginWebOTPParams) (resp *Draine
|
||||
User: params.user,
|
||||
Pass: params.password,
|
||||
SecondFactorToken: code,
|
||||
Scope: params.scope,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err, "request marshal")
|
||||
|
||||
+64
-13
@@ -107,6 +107,14 @@ type SessionContextConfig struct {
|
||||
// "GetNodes".
|
||||
UnsafeCachedAuthClient authclient.ReadProxyAccessPoint
|
||||
|
||||
// UnsafeScoopedRoleReader is a scoped role reader. It's authenticated with
|
||||
// the identity of the node, hence it's "unsafe".
|
||||
//
|
||||
// Only use it where the identity of the caller will not affect the result of
|
||||
// the RPC. For example, never call it to get a list of roles and return it
|
||||
// to the user, as it may return more than the user is allowed to see.
|
||||
UnsafeScopedRoleReader services.ScopedRoleReader
|
||||
|
||||
Parent *sessionCache
|
||||
// Resources is a persistent resource store this context is bound to.
|
||||
// The store maintains a list of resources between session renewals
|
||||
@@ -139,6 +147,10 @@ func (c *SessionContextConfig) CheckAndSetDefaults() error {
|
||||
return trace.BadParameter("Session required")
|
||||
}
|
||||
|
||||
if c.UnsafeCachedAuthClient == nil {
|
||||
return trace.BadParameter("Scoped role reader required")
|
||||
}
|
||||
|
||||
if c.Log == nil {
|
||||
c.Log = slog.With(
|
||||
"user", c.User,
|
||||
@@ -517,6 +529,37 @@ func (c *SessionContext) GetUserAccessChecker() (services.AccessChecker, error)
|
||||
return accessChecker, trace.Wrap(err)
|
||||
}
|
||||
|
||||
func (c *SessionContext) GetUserScopedAccessCheckerContext(ctx context.Context) (*services.ScopedAccessCheckerContext, error) {
|
||||
cert, err := c.GetSSHCertificate()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
ident, err := sshca.DecodeIdentity(cert)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
accessInfo := services.AccessInfoFromLocalSSHIdentity(ident)
|
||||
|
||||
if accessInfo.ScopePin == nil {
|
||||
checker, err := c.GetUserAccessChecker()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return services.NewScopedAccessCheckerContextFromUnscoped(checker), nil
|
||||
}
|
||||
|
||||
checkerCtx, err := services.NewScopedAccessCheckerContext(
|
||||
ctx, accessInfo, c.cfg.RootClusterName, c.cfg.UnsafeScopedRoleReader,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return checkerCtx, err
|
||||
}
|
||||
|
||||
// GetProxyListenerMode returns cluster proxy listener mode form cluster networking config.
|
||||
func (c *SessionContext) GetProxyListenerMode(ctx context.Context) (types.ProxyListenerMode, error) {
|
||||
resp, err := c.cfg.UnsafeCachedAuthClient.GetClusterNetworkingConfig(ctx)
|
||||
@@ -626,11 +669,12 @@ func (c *SessionContext) expired(ctx context.Context) bool {
|
||||
const cachedSessionLingeringThreshold = 2 * time.Minute
|
||||
|
||||
type sessionCacheOptions struct {
|
||||
proxyClient authclient.ClientI
|
||||
accessPoint authclient.ReadProxyAccessPoint
|
||||
servers []utils.NetAddr
|
||||
cipherSuites []uint16
|
||||
clock clockwork.Clock
|
||||
proxyClient authclient.ClientI
|
||||
scopedRoleReader services.ScopedRoleReader
|
||||
accessPoint authclient.ReadProxyAccessPoint
|
||||
servers []utils.NetAddr
|
||||
cipherSuites []uint16
|
||||
clock clockwork.Clock
|
||||
// sessionLingeringThreshold specifies the time the session will linger
|
||||
// in the cache before getting purged after it has expired
|
||||
sessionLingeringThreshold time.Duration
|
||||
@@ -667,6 +711,7 @@ func newSessionCache(ctx context.Context, config sessionCacheOptions) (*sessionC
|
||||
cache := &sessionCache{
|
||||
clusterName: clusterName.GetClusterName(),
|
||||
proxyClient: config.proxyClient,
|
||||
scopedRoleReader: config.scopedRoleReader,
|
||||
accessPoint: config.accessPoint,
|
||||
sessions: make(map[string]*SessionContext),
|
||||
resources: make(map[string]*sessionResources),
|
||||
@@ -701,13 +746,14 @@ func newSessionCache(ctx context.Context, config sessionCacheOptions) (*sessionC
|
||||
// sessionCache handles web session authentication,
|
||||
// and holds in-memory contexts associated with each session
|
||||
type sessionCache struct {
|
||||
log *slog.Logger
|
||||
proxyClient authclient.ClientI
|
||||
authServers []utils.NetAddr
|
||||
accessPoint authclient.ReadProxyAccessPoint
|
||||
closer *utils.CloseBroadcaster
|
||||
clusterName string
|
||||
clock clockwork.Clock
|
||||
log *slog.Logger
|
||||
proxyClient authclient.ClientI
|
||||
authServers []utils.NetAddr
|
||||
accessPoint authclient.ReadProxyAccessPoint
|
||||
scopedRoleReader services.ScopedRoleReader
|
||||
closer *utils.CloseBroadcaster
|
||||
clusterName string
|
||||
clock clockwork.Clock
|
||||
// sessionLingeringThreshold specifies the time the session will linger
|
||||
// in the cache before getting purged after it has expired
|
||||
sessionLingeringThreshold time.Duration
|
||||
@@ -939,6 +985,7 @@ func (s *sessionCache) releaseResourcesIfNoDeviceExtensions(ctx context.Context,
|
||||
func (s *sessionCache) AuthWithOTP(
|
||||
ctx context.Context,
|
||||
user, pass, otpToken string,
|
||||
scope string,
|
||||
clientMeta *authclient.ForwardedClientMetadata,
|
||||
) (types.WebSession, error) {
|
||||
return s.proxyClient.AuthenticateWebUser(ctx, authclient.AuthenticateUserRequest{
|
||||
@@ -949,13 +996,14 @@ func (s *sessionCache) AuthWithOTP(
|
||||
Token: otpToken,
|
||||
},
|
||||
ClientMetadata: clientMeta,
|
||||
Scope: scope,
|
||||
})
|
||||
}
|
||||
|
||||
// AuthWithoutOTP authenticates the specified user with the given password.
|
||||
// Returns a new web session if successful.
|
||||
func (s *sessionCache) AuthWithoutOTP(
|
||||
ctx context.Context, user, pass string, clientMeta *authclient.ForwardedClientMetadata,
|
||||
ctx context.Context, user, pass string, scope string, clientMeta *authclient.ForwardedClientMetadata,
|
||||
) (types.WebSession, error) {
|
||||
return s.proxyClient.AuthenticateWebUser(ctx, authclient.AuthenticateUserRequest{
|
||||
Username: user,
|
||||
@@ -963,6 +1011,7 @@ func (s *sessionCache) AuthWithoutOTP(
|
||||
Password: []byte(pass),
|
||||
},
|
||||
ClientMetadata: clientMeta,
|
||||
Scope: scope,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -972,6 +1021,7 @@ func (s *sessionCache) AuthenticateWebUser(
|
||||
authReq := authclient.AuthenticateUserRequest{
|
||||
Username: req.User,
|
||||
ClientMetadata: clientMeta,
|
||||
Scope: req.Scope,
|
||||
}
|
||||
if req.WebauthnAssertionResponse != nil {
|
||||
authReq.Webauthn = req.WebauthnAssertionResponse
|
||||
@@ -1230,6 +1280,7 @@ func (s *sessionCache) newSessionContextFromSession(ctx context.Context, session
|
||||
User: session.GetUser(),
|
||||
RootClient: userClient,
|
||||
UnsafeCachedAuthClient: s.accessPoint,
|
||||
UnsafeScopedRoleReader: s.scopedRoleReader,
|
||||
Parent: s,
|
||||
Resources: s.upsertSessionContext(session.GetUser()),
|
||||
Session: session,
|
||||
|
||||
@@ -75,6 +75,9 @@ type UserContext struct {
|
||||
// AvailableScopes is a list of scopes available to the user through their
|
||||
// scoped role assignments.
|
||||
AvailableScopes []string `json:"availableScopes"`
|
||||
// Scope contains the scope pinned to the current session. An empty string
|
||||
// indicates an unscoped session.
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
func getAccessStrategy(roleset services.RoleSet) accessStrategy {
|
||||
|
||||
Reference in New Issue
Block a user