-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
extended dynamic access #4573
extended dynamic access #4573
Conversation
@fspmarshall the reason I've proposed to have
In your design for roles use slightly different config. But in your example above - this combination where both
@fspmarshall ping on my question above |
f29bd6e
to
31095b0
Compare
That is not the intent. The intent is that if both are true, the dialogue for adding a note atomatically appears (i.e. if both are true it is equivalent to That being said, this was just an experiment. Its possible that requiring a note when not auto-requesting is too niche, we can drop it in favor of the single field. |
@fspmarshall I got it. I prefer encapsulating this logic in one option, # allow user to request access if `can_access` is setup for the role (default value for all roles)
request_access: 'optional'
# create access request after asking for note if `can_access` is setup for the role
request_access: 'reason'
# always create access request after login without asking for note if `can_access` is setup for the role
request_access: 'always' We can always add more options and combinations in the future. |
@fspmarshall thoughts for tomorrow conversation: Add claims to access_requestAdding traits to access_request allows plugins to map users to roles dynamically.
access_request:
spec:
claims: ['gravitational/devc'] In enterprise, contains OIDC claims or SAML attribute statements: access_request:
spec:
claims: {'groups': ['dictators', 'populists']} Add mapped_roles to access_requestThis field contains the roles user is already assigned by Teleport after login. access_request:
spec:
mapped_roles: ['dictator', 'populist'] Imagine a user already is assigned to roles role:
name: requestor
spec:
options:
request_access: 'note'
allow:
request:
roles: ['.*'] and modifies the connector mapping: connector:
spec:
claims_to_roles:
- claim: "group"
value: ".*"
roles: [ ".*", "requestor"] Because of the 'requestor access_request:
spec:
claims: {'groups': 'dictator'}
mapped_roles: ['dictator']
# in this case we don't need to populate roles with any matching roles
# what makes the system more resilient
roles: [] The plugin can take a look at claims and see if likes the roles user is already mapped and then simply: request.Roles = request.MappedRoles
request.State = Approved In this case we won't need to populate |
@klizhentas Still mulling this over, but my immediate impression it that the above is going to require some tricky plumbing. kind: oidc
metadata:
name: "connector"
spec:
claims_to_roles:
- claim: "group"
value: "employee"
roles: [ "employee" ]
# ...
---
kind: role
metadata:
name: employee
spec:
allow:
request:
roles: ['{{external.groups}}']
options:
request_access: 'note' The plugin would see a request for all roles matching the groups held by the user (minus We could also provide a simple mechanism for propagating side-band information directly to the plugin via the existing variable interpolation system (again, without much effort, and with no special cases in the login system): kind: role
metadata:
name: employee
spec:
allow:
request:
roles: ['{{external.groups}}']
# pass some extra metadata to the plugin; nothing special, just a mapping that obeys
# our existing variable interpolation system.
meta:
tickets: '{{external.tickets}}'
options:
request_access: 'note' |
@fspmarshall your suggestion is better, let's use it! |
kind: oidc
metadata:
name: "connector"
spec:
claims_to_roles:
- claim: "group"
value: ".*"
roles: [ "catchall" ] kind: role
metadata:
name: catchall
spec:
allow:
request:
roles: ['{{external.groups}}']
# pass some extra metadata to the plugin; nothing special, just a mapping that obeys
claims_to_roles:
- claim: "group"
value: ".*"
roles: [ "admins" ]
options:
request_access: 'note'
request_prompt: |
Hey, Enter the Ticker number from https://ibm.com/tickets
.... Usual role kind: role
metadata:
name: admins
spec:
allow:
logins: ['root'] access_request:
spec:
reason: 'ticket #1234`
roles: ['admins', 'devops']
Plugin processes the request: access_request:
spec:
note: 'ticket #1234`
roles: ['admins', 'devops']
state: approved |
@fspmarshall @benarent @kimlisa After giving our last idea some thought, pushing It duplicates the logic of the connector and introduces ambiguity. What happens if there are two roles with What if we do a reverse, move In some cases, there is no need for claims_to_roles mapping, could be done by the plugin: connector:
spec:
request:
mode: always
prompt: |
Enter the ticket number. In other cases, one change will start requesting reason for everyone after doing the mapping if the role allows it: connector:
spec:
claims_to_roles:
- claim: group
value: .*
roles: ['requestor']
request:
mode: reason
prompt: |
Enter the ticket number or free form reason |
@fspmarshall how about this:
connector:
spec:
claims_to_roles:
- claim: groups
value: '.*'
roles: ['requestor']
role:
name: requestor
spec:
options:
request_access: 'note'
allow:
request:
# used claims to roles generator
# instead of static role list
claims_to_roles:
- claim: 'groups'
value: '.*'
roles: ['dictator', 'populist'] Generates access request: access_request:
spec:
roles: ['dictator', 'populist'] |
58f7526
to
a6d9bea
Compare
UpdateRole conditions have been updated to allow mapping claims (traits) to construct the role list dynamically. This allows the access request system to determine which roles a user should be allowed to request based on information passed in from the identity provider. In order to simplify the upgrade process, we've kept the mapping syntax identical to that found in the kind: role
metadata:
name: employee
spec:
allow:
request:
claims_to_roles: # same as it would appear in an oidc connector
- claim: groups
value: dev
roles: ["dev-prod", "dev-staging"] Because the kind: role
metadata:
name: employee
spec:
allow:
request:
claims_to_roles:
# allow admins to request all roles which do not end in '-restricted'
- claim: groups
value: admin
roles: ['{{regexp.not_match(".*-restricted")}}'] note: the above roles are all minimal examples and don't represent all options needed to create a secure entrypoint role. See the restricted role UX improvements notes in the 'further work' section below. The kind: role
metadata:
name: contractor
spec:
options:
request_access: reason
Request resolution attributes have been changed from Audit events now support Request creation event:
Request approval event:
Deferred work
Further work needed
|
@fspmarshall I did a quick review and it looks good. As I think though the docs / setting this up. I thinking about these things.
|
Good question. The best way to do this is to test with the real configuration in a staging env. However, if you don't have the ability to do this, I think I have an idea for a fairly simple way to make testing easier... Just pushed an experimental commit that adds a $ cat > rscs.yaml <<EOF
kind: user
metadata:
name: test-user
spec:
roles: ['employee']
traits:
# put the claims/traits that you'd like to test against here
groups: ['admins']
version: v2
---
kind: role
metadata:
name: employee
spec:
allow:
request:
claims_to_roles:
- claim: groups
value: admins
roles: ['*-admin']
version: v3
EOF
$ tctl create rscs.yaml
# ...
$ tctl request create --dry-run test-user | jq -r '.[].spec.roles[]'
staging-admin
prod-admin
# ... Thoughts?
Same process as passing data to teleport's variable interpolation system (e.g. kind: oidc
metadata:
name: my-odic
spec:
scope: ['some-custom-scope', 'some-other-custom-scope']
Login and access request are separate. If you are generating an access request, you are already logged in. That being said, if the role is configured to automatically generate an access request after login (e.g. |
Couple of things:
|
This is super helpful and is another step to helping with #3845, and will make debugging this much easier. I take that I can use this with any user in the system ( if they have been created / logged in via SSO ) |
Thanks for the comments @fspmarshall once we are the 'beta' stage of this PR, I'll setup a staging site so we can fully test with an IDP |
ea0d4d5
to
5f133b4
Compare
1510796
to
0a523db
Compare
Updated PR DescriptionThis PR includes various changes designed to significantly increase the power of the dynamic access (workflow) API. Most of these changes revolve around supporting two key features:
RBACRoles now support a number of new or updated configuration fields: kind: role
metadata:
name: employee
spec:
allow:
request:
# the `roles` list can now be a mixture of literals and matchers
roles: ['common', 'dev-*']
# the `claims_to_roles` mapping works the same as it does in
# the oidc connector, with the added benefit that the mapped to roles
# can also be matchers. the below mapping says that users with
# the claims `groups: admins` can request any role in the system.
claims_to_roles:
- claim: groups
value: admins
roles: ['*']
# teleport can attach annotations to pending access requests. these
# annotations may be literals, or be variable interpolation expressions,
# effectively creating a means for propagating selected claims from an
# external identity provider to the plugin system.
annotations:
foo: ['bar']
groups: ['{{external.groups}}']
options:
# the `request_access` field can be set to 'always' or 'reason' to tell
# tsh of the web UI to always create an access request on login. If it is
# set to 'reason', the user will be required to indicate *why* they are
# generating the access request.
request_access: reason
# the `request_prompt` field can be used to tell the user what should
# be supplied in the request reason field.
request_prompt: Please provide your ticket ID
version: v3 Notice that the above role does not specify any logins. This is OK. If a users's roles specify no logins, teleport will now generate the user's initial SSH certificates with an invalid dummy login of the form APIA number of new parameters are now available that allow the plugin or administrator to grant greater insight into approvals/denials: $ tctl request deny --reason='you seem sus' --annotations=method=cli,unix-user=${USER} 28a3fb86-0230-439d-ad88-11cfcb213193 Because automatically generated requests always include all roles that the user is allowed to request, approvers can now specify a smaller subset of the requested roles that should actually be applied, allowing for subselection in cases where full escalation is not a desirable default: $ tctl request approve --roles=role-1,role-3 --reason='thats cool, but no role-2 right now' 28a3fb86-0230-439d-ad88-11cfcb213193 EventsThe Request creation event:
Request approval event:
|
8d21f45
to
31095b0
Compare
44519a2
to
d0507b7
Compare
@@ -1569,6 +1578,14 @@ func (set RoleSet) CheckLoginDuration(ttl time.Duration) ([]string, error) { | |||
if !matchedTTL { | |||
return nil, trace.AccessDenied("this user cannot request a certificate for %v", ttl) | |||
} | |||
if len(logins) == 0 && !set.hasPossibleLogins() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this make this check always pass? I'd either look into making sure services.RoleSet
has a role with a (dummy) login or maybe passing a flag to this function that control this behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the logins
list there has been filtered down. This check is used to distinguish between the case where the user does have logins, but none match, and the case where the user actually has no logins. We're only generating the dummy login for the case that wasn't previously allowed (actually having no logins), which keeps us backwards-compatible.
// RequestAccess defines the access request stategy (optional|note|always) | ||
// where optional is the default. | ||
string RequestAccess = 11 [ | ||
(gogoproto.jsontag) = "request_access,omitempty", | ||
(gogoproto.casttype) = "RequestStrategy" | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Protobufs support a enum type.
https://github.com/gravitational/teleport/blob/master/lib/services/types.proto#L25-L32
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those serialize to integers by default, and this field needs to be a string when in json or yaml format since users are expected to manually modify the field. I could write a custom marshaler for it, but IMO the benefit is negligible. Especially since go doesn't have enums, so we end up with a bunch of constants under the hood anyhow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, makes sense.
// appendRoleMatchers constructs all role matchers for a given | ||
// AccessRequestConditions instance and appends them to the | ||
// supplied matcher slice. | ||
func appendRoleMatchers(matchers []parse.Matcher, conditions AccessRequestConditions, traits map[string][]string) ([]parse.Matcher, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style nit (optional), not sure how much append
simplifies the logic, if you have collectMatchers
that has no side effects you can always do:
matchers = append(matchers, collectMatchers(allow, traits)...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks solid. I haven't found logical flaws. I'd encourage to add calls to actions to make API callers and developers life easier. Otherwise, 👍
tm: TraitMapping{ | ||
Trait: "sketchy-pfx", | ||
Value: "pfx-*", | ||
Roles: []string{"${1}-prod", "${1}-staging", "${1}"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good thinking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me pending applying remaining comments.
|
||
// iterate through all new values and expand any | ||
// variable interpolation syntax they contain. | ||
ApplyTraits: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need this label here, continue
should continue to the next value in a inner loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I try to always use loop labels if using continue
within nested loops. IMO its more readable and less prone subtle bugs due to later changes or bad merges.
// RequestAccess defines the access request stategy (optional|note|always) | ||
// where optional is the default. | ||
string RequestAccess = 11 [ | ||
(gogoproto.jsontag) = "request_access,omitempty", | ||
(gogoproto.casttype) = "RequestStrategy" | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, makes sense.
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.
cab1cb7
to
37bb1bb
Compare
See the Updated PR Description
Old PR description (hidden)
This is a WIP implementation of part of various improvements to the dynamic access (workflow) API, based primarily off of [rfd/0003](https://github.com//pull/4305). Field/option naming is subject to change.
Overview
Pattern-based allow/deny
Allow/deny blocks for role requests now support regex and blob patterns:
Request and resolution reasons
Requests can now be generated with a
RequestReason
field:When a request is "resolved" (approved/denied) a reason can be given for that too:
note: the default formatter has not been updated yet, so use
tctl request ls --format=json
to view the reason fields associated with requests.Resolution attributes
Arbitrary attributes can now be associated with a resolution for automated bookkeeping:
Automatic role selection and overrides
An access request can now have its roles auto-populated by the auth server:
This will cause the access request to automatically include all roles that are permissible for the user to assume. This is also going to be the default behavior used by the web UI's "waiting room" (see #4384).
If the approving party does not wish the user to assume all of the specified roles, the role list can now be overridden at the time of resolution like so:
note: Role list overrides don't circumvent the requesting user's
allow
/deny
directives. The user must be allowed to assume the specified roles in order for the override to succeed.New role options
Two new boolean role options have also been added: