Skip to content

Commit

Permalink
extended dynamic access API
Browse files Browse the repository at this point in the history
Various improvements related to extending the dynamic access
API, including:

- Support for users with no statically defined roles.

- Unify trait mapping logic (e.g. claims_to_roles) across
the connector types.

- Support for matcher syntax and claims_to_roles mappings when
configuring which roles a user is able to request.

- Allow tsh or the web UI to automatically generate wildcard
access requests when dictated by role configuration.

- Allow RBAC configuration to attach annotations to pending
access requests which can be consumed by plugins.

- Allow plugins to attach annotations to approvals/denials
which appear in the audit log, and may also be looked up
later to determine additional info about a resolution.

- Support prompts, request reasons, and approval/denial
reasons for access requests.
  • Loading branch information
fspmarshall committed Nov 5, 2020
1 parent 679941f commit 37bb1bb
Show file tree
Hide file tree
Showing 31 changed files with 3,501 additions and 1,631 deletions.
29 changes: 24 additions & 5 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1550,7 +1550,13 @@ func (a *Server) upsertRole(ctx context.Context, role services.Role) error {
}

func (a *Server) CreateAccessRequest(ctx context.Context, req services.AccessRequest) error {
if err := services.ValidateAccessRequest(a, req); err != nil {
err := services.ValidateAccessRequest(a, req,
// if request is in state pending, role expansion must be applied
services.ExpandRoles(req.GetState().IsPending()),
// always apply system annotations before storing new requests
services.ApplySystemAnnotations(true),
)
if err != nil {
return trace.Wrap(err)
}
ttl, err := a.calculateMaxAccessTTL(req)
Expand Down Expand Up @@ -1588,12 +1594,13 @@ func (a *Server) CreateAccessRequest(ctx context.Context, req services.AccessReq
Roles: req.GetRoles(),
RequestID: req.GetName(),
RequestState: req.GetState().String(),
Reason: req.GetRequestReason(),
})
return trace.Wrap(err)
}

func (a *Server) SetAccessRequestState(ctx context.Context, reqID string, state services.RequestState) error {
if err := a.DynamicAccess.SetAccessRequestState(ctx, reqID, state); err != nil {
func (a *Server) SetAccessRequestState(ctx context.Context, params services.AccessRequestUpdate) error {
if err := a.DynamicAccess.SetAccessRequestState(ctx, params); err != nil {
return trace.Wrap(err)
}
event := &events.AccessRequestCreate{
Expand All @@ -1604,12 +1611,24 @@ func (a *Server) SetAccessRequestState(ctx context.Context, reqID string, state
ResourceMetadata: events.ResourceMetadata{
UpdatedBy: clientUsername(ctx),
},
RequestID: reqID,
RequestState: state.String(),
RequestID: params.RequestID,
RequestState: params.State.String(),
Reason: params.Reason,
Roles: params.Roles,
}

if delegator := getDelegator(ctx); delegator != "" {
event.Delegator = delegator
}

if len(params.Annotations) > 0 {
annotations, err := events.EncodeMapStrings(params.Annotations)
if err != nil {
log.WithError(err).Debugf("Failed to encode access request annotations.")
} else {
event.Annotations = annotations
}
}
err := a.emitter.EmitAuditEvent(a.closeCtx, event)
if err != nil {
log.WithError(err).Warn("Failed to emit access request update event.")
Expand Down
55 changes: 0 additions & 55 deletions lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,61 +479,6 @@ func (s *AuthSuite) TestGenerateTokenEventsEmitted(c *C) {
c.Assert(s.mockEmitter.LastEvent().GetType(), DeepEquals, events.TrustedClusterTokenCreateEvent)
}

func (s *AuthSuite) TestBuildRolesInvalid(c *C) {
// create a connector
oidcConnector := services.NewOIDCConnector("example", services.OIDCConnectorSpecV2{
IssuerURL: "https://www.exmaple.com",
ClientID: "example-client-id",
ClientSecret: "example-client-secret",
RedirectURL: "https://localhost:3080/v1/webapi/oidc/callback",
Display: "sign in with example.com",
Scope: []string{"foo", "bar"},
})

// create some claims
var claims = make(jose.Claims)
claims.Add("roles", "teleport-user")
claims.Add("email", "foo@example.com")
claims.Add("nickname", "foo")
claims.Add("full_name", "foo bar")

// try and build roles should be invalid since we have no mappings
_, err := s.a.buildOIDCRoles(oidcConnector, claims)
c.Assert(err, NotNil)
}

func (s *AuthSuite) TestBuildRolesStatic(c *C) {
// create a connector
oidcConnector := services.NewOIDCConnector("example", services.OIDCConnectorSpecV2{
IssuerURL: "https://www.exmaple.com",
ClientID: "example-client-id",
ClientSecret: "example-client-secret",
RedirectURL: "https://localhost:3080/v1/webapi/oidc/callback",
Display: "sign in with example.com",
Scope: []string{"foo", "bar"},
ClaimsToRoles: []services.ClaimMapping{
services.ClaimMapping{
Claim: "roles",
Value: "teleport-user",
Roles: []string{"user"},
},
},
})

// create some claims
var claims = make(jose.Claims)
claims.Add("roles", "teleport-user")
claims.Add("email", "foo@example.com")
claims.Add("nickname", "foo")
claims.Add("full_name", "foo bar")

// build roles and check that we mapped to "user" role
roles, err := s.a.buildOIDCRoles(oidcConnector, claims)
c.Assert(err, IsNil)
c.Assert(roles, HasLen, 1)
c.Assert(roles[0], Equals, "user")
}

func (s *AuthSuite) TestValidateACRValues(c *C) {

var tests = []struct {
Expand Down
4 changes: 2 additions & 2 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -862,11 +862,11 @@ func (a *ServerWithRoles) CreateAccessRequest(ctx context.Context, req services.
return a.authServer.CreateAccessRequest(ctx, req)
}

func (a *ServerWithRoles) SetAccessRequestState(ctx context.Context, reqID string, state services.RequestState) error {
func (a *ServerWithRoles) SetAccessRequestState(ctx context.Context, params services.AccessRequestUpdate) error {
if err := a.action(defaults.Namespace, services.KindAccessRequest, services.VerbUpdate); err != nil {
return trace.Wrap(err)
}
return a.authServer.SetAccessRequestState(ctx, reqID, state)
return a.authServer.SetAccessRequestState(ctx, params)
}

// GetPluginData loads all plugin data matching the supplied filter.
Expand Down
12 changes: 9 additions & 3 deletions lib/auth/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,9 @@ type streamWatcher struct {
func (w *streamWatcher) Error() error {
w.RLock()
defer w.RUnlock()
if w.err == nil {
return trace.Wrap(w.ctx.Err())
}
return w.err
}

Expand Down Expand Up @@ -2815,14 +2818,17 @@ func (c *Client) DeleteAccessRequest(ctx context.Context, reqID string) error {
return nil
}

func (c *Client) SetAccessRequestState(ctx context.Context, reqID string, state services.RequestState) error {
func (c *Client) SetAccessRequestState(ctx context.Context, params services.AccessRequestUpdate) error {
clt, err := c.grpc()
if err != nil {
return trace.Wrap(err)
}
setter := proto.RequestStateSetter{
ID: reqID,
State: state,
ID: params.RequestID,
State: params.State,
Reason: params.Reason,
Annotations: params.Annotations,
Roles: params.Roles,
}
if d := getDelegator(ctx); d != "" {
setter.Delegator = d
Expand Down
8 changes: 7 additions & 1 deletion lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,13 @@ func (g *GRPCServer) SetAccessRequestState(ctx context.Context, req *proto.Reque
if req.Delegator != "" {
ctx = WithDelegator(ctx, req.Delegator)
}
if err := auth.ServerWithRoles.SetAccessRequestState(ctx, req.ID, req.State); err != nil {
if err := auth.ServerWithRoles.SetAccessRequestState(ctx, services.AccessRequestUpdate{
RequestID: req.ID,
State: req.State,
Reason: req.Reason,
Annotations: req.Annotations,
Roles: req.Roles,
}); err != nil {
return nil, trail.ToGRPC(err)
}
return &empty.Empty{}, nil
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,10 +665,10 @@ func CreateUserRoleAndRequestable(clt clt, username string, rolename string) (se
return nil, trace.Wrap(err)
}
baseRole := services.RoleForUser(user)
baseRole.SetLogins(services.Allow, []string{username})
baseRole.SetAccessRequestConditions(services.Allow, services.AccessRequestConditions{
Roles: []string{rolename},
})
baseRole.SetLogins(services.Allow, nil)
err = clt.UpsertRole(ctx, baseRole)
if err != nil {
return nil, trace.Wrap(err)
Expand Down
38 changes: 5 additions & 33 deletions lib/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,35 +439,6 @@ type OIDCAuthResponse struct {
HostSigners []services.CertAuthority `json:"host_signers"`
}

// buildOIDCRoles takes a connector and claims and returns a slice of roles.
func (a *Server) buildOIDCRoles(connector services.OIDCConnector, claims jose.Claims) ([]string, error) {
roles := connector.MapClaims(claims)
if len(roles) == 0 {
return nil, trace.AccessDenied("unable to map claims to role for connector: %v", connector.GetName())
}

return roles, nil
}

// claimsToTraitMap extracts all string claims and creates a map of traits
// that can be used to populate role variables.
func claimsToTraitMap(claims jose.Claims) map[string][]string {
traits := make(map[string][]string)

for claimName := range claims {
claimValue, ok, _ := claims.StringClaim(claimName)
if ok {
traits[claimName] = []string{claimValue}
}
claimValues, ok, _ := claims.StringsClaim(claimName)
if ok {
traits[claimName] = claimValues
}
}

return traits
}

func (a *Server) calculateOIDCUser(connector services.OIDCConnector, claims jose.Claims, ident *oidc.Identity, request *services.OIDCAuthRequest) (*createUserParams, error) {
var err error

Expand All @@ -476,11 +447,12 @@ func (a *Server) calculateOIDCUser(connector services.OIDCConnector, claims jose
username: ident.Email,
}

p.roles, err = a.buildOIDCRoles(connector, claims)
if err != nil {
return nil, trace.Wrap(err)
p.traits = services.OIDCClaimsToTraits(claims)

p.roles = connector.GetTraitMappings().TraitsToRoles(p.traits)
if len(p.roles) == 0 {
return nil, trace.AccessDenied("unable to map claims to role for connector: %v", connector.GetName())
}
p.traits = claimsToTraitMap(claims)

// Pick smaller for role: session TTL from role or requested TTL.
roles, err := services.FetchRoles(p.roles, a.Access, p.traits)
Expand Down
Loading

0 comments on commit 37bb1bb

Please sign in to comment.