Skip to content

Commit

Permalink
Add support for requiring reason for access requests
Browse files Browse the repository at this point in the history
  • Loading branch information
kopiczko committed Nov 25, 2024
1 parent 4102fa5 commit 033826f
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 42 deletions.
106 changes: 90 additions & 16 deletions lib/services/access_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,20 @@ func CalculateAccessCapabilities(ctx context.Context, clock clockwork.Clock, clt
caps.SuggestedReviewers = v.SuggestedReviewers
}

caps.RequireReason = v.requireReason
if v.requireReasonForAllRoles {
caps.RequireReason = true
} else if req.RequestableRoles {
caps.RequireReason, _, err = v.isReasonRequired(ctx, caps.RequestableRoles, nil)
if err != nil {
return nil, trace.Wrap(err)
}
} else {
caps.RequireReason, _, err = v.isReasonRequired(ctx, caps.ApplicableRolesForResources, nil)
if err != nil {
return nil, trace.Wrap(err)
}
}

caps.RequestPrompt = v.prompt
caps.AutoRequest = v.autoRequest

Expand All @@ -302,6 +315,8 @@ func (m *RequestValidator) allowedSearchAsRoles() ([]string, error) {

// applicableSearchAsRoles prunes the search_as_roles and only returns those
// applicable for the given list of resourceIDs.
//
// If loginHint is provided, it will attempt to prune the list to a single role.
func (m *RequestValidator) applicableSearchAsRoles(ctx context.Context, resourceIDs []types.ResourceID, loginHint string) ([]string, error) {
rolesToRequest, err := m.allowedSearchAsRoles()
if err != nil {
Expand Down Expand Up @@ -1041,10 +1056,23 @@ func (c *ReviewPermissionChecker) push(role types.Role) error {
// a set of simple Allow/Deny datastructures. These, in turn,
// are used to validate and expand the access request.
type RequestValidator struct {
clock clockwork.Clock
getter RequestValidatorGetter
userState UserState
requireReason bool
clock clockwork.Clock
getter RequestValidatorGetter
userState UserState
// requireReasonForAllRoles indicates that non-empty reason is required for all access
// requests. This happens if any of the user roles has options.request_access "always" or
// "reason".
requireReasonForAllRoles bool
// requiringReasonRoles is a set of role names, which require non-empty reason to be
// specified when requested. The same applies to all requested resources allowed by those
// roles. Such roles are all requestable roles and search_as_roles allowed by a role
// assigned to a user and having spec.allow.request.reason.mode="required" set.
//
// Please note this means, roles having spec.allow.request.reason.mode="required" don't
// necessarily require reason when they are requested themselves. Instead they mark roles
// in spec.allow.request.roles and spec.allow.request.search_as_roles as roles requiring
// reason.
requiringReasonRoles map[string]struct{}
// Used to enforce that the configuration found in the static
// role that defined the search_as_role, is respected.
// An empty map or list means nothing was configured.
Expand Down Expand Up @@ -1097,6 +1125,8 @@ func NewRequestValidator(ctx context.Context, clock clockwork.Clock, getter Requ
getter: getter,
userState: uls,
logger: slog.With(teleport.ComponentKey, "request.validator"),

requiringReasonRoles: make(map[string]struct{}),
}
for _, opt := range opts {
opt(&m)
Expand Down Expand Up @@ -1132,8 +1162,14 @@ func (m *RequestValidator) Validate(ctx context.Context, req types.AccessRequest
return trace.BadParameter("request validator configured for different user (this is a bug)")
}

if m.requireReason && req.GetRequestReason() == "" {
return trace.BadParameter("request reason must be specified (required by static role configuration)")
if len(req.GetRequestReason()) == 0 {
required, explanation, err := m.isReasonRequired(ctx, req.GetRoles(), req.GetRequestedResourceIDs())
if err != nil {
return trace.Wrap(err)
}
if required {
return trace.BadParameter(explanation)
}
}

if !req.GetState().IsPromoted() && req.GetPromotedAccessListTitle() != "" {
Expand Down Expand Up @@ -1313,6 +1349,36 @@ func (m *RequestValidator) Validate(ctx context.Context, req types.AccessRequest
return nil
}

// isReasonValid checks if the reason is valid. If it isn't valid it also returns an explanation
// why.
func (m *RequestValidator) isReasonRequired(ctx context.Context, requestedRoles []string, requestedResourceIDs []types.ResourceID) (required bool, explanation string, err error) {
if m.requireReasonForAllRoles {
return true, "request reason must be specified (required request_access option in one of the roles)", nil
}

allApplicableRoles := requestedRoles
if len(requestedResourceIDs) > 0 {
// Do not provide loginHint. We want all matching search_as_roles for those resources.
roles, err := m.applicableSearchAsRoles(ctx, requestedResourceIDs, "")
if err != nil {
return false, "", trace.Wrap(err)
}
if len(allApplicableRoles) == 0 {
allApplicableRoles = roles
} else {
allApplicableRoles = append(allApplicableRoles, roles...)
}
}

for _, r := range allApplicableRoles {
if _, ok := m.requiringReasonRoles[r]; ok {
return true, fmt.Sprintf("request reason must be specified (required for role %q)", r), nil
}
}

return false, "", nil
}

// calculateMaxAccessDuration calculates the maximum time for the access request.
// The max duration time is the minimum of the max_duration time set on the request
// and the max_duration time set on the request role.
Expand Down Expand Up @@ -1518,7 +1584,7 @@ func (m *RequestValidator) GetRequestableRoles(ctx context.Context, identity tls

roleAllowsAccess := true
for _, resource := range filteredResources {
access, err := m.roleAllowsResource(ctx, role, resource, loginHint)
access, err := m.roleAllowsResource(role, resource, loginHint)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -1564,14 +1630,23 @@ func setAllowRequestKubeResourceLookup(allowKubernetesResources []types.RequestK
func (m *RequestValidator) push(ctx context.Context, role types.Role) error {
var err error

m.requireReason = m.requireReason || role.GetOptions().RequestAccess.RequireReason()
m.requireReasonForAllRoles = m.requireReasonForAllRoles || role.GetOptions().RequestAccess.RequireReason()
m.autoRequest = m.autoRequest || role.GetOptions().RequestAccess.ShouldAutoRequest()
if m.prompt == "" {
m.prompt = role.GetOptions().RequestPrompt
}

allow, deny := role.GetAccessRequestConditions(types.Allow), role.GetAccessRequestConditions(types.Deny)

if allow.Reason != nil && allow.Reason.Mode.Required() {
for _, r := range allow.Roles {
m.requiringReasonRoles[r] = struct{}{}
}
for _, r := range allow.SearchAsRoles {
m.requiringReasonRoles[r] = struct{}{}
}
}

setAllowRequestKubeResourceLookup(allow.KubernetesResources, allow.SearchAsRoles, m.kubernetesResource.allow)

if len(deny.KubernetesResources) > 0 {
Expand Down Expand Up @@ -1951,18 +2026,18 @@ func (m *RequestValidator) SystemAnnotations(req types.AccessRequest) (map[strin

type ValidateRequestOption func(*RequestValidator)

// ExpandVars toggles variable expansion during request validation. Variable expansion
// includes expanding wildcard requests, setting system annotations, and gathering
// threshold information. Variable expansion should be run by the auth server prior
// to storing an access request for the first time.
// ExpandVars toggles variable expansion during request validation. Variable expansion includes
// expanding wildcard requests, setting system annotations, finding applicable roles for
// resource-based requests and gathering threshold information. Variable expansion should be run
// by the auth server prior to storing an access request for the first time.
func ExpandVars(expand bool) ValidateRequestOption {
return func(v *RequestValidator) {
v.opts.expandVars = expand
}
}

// ValidateAccessRequestForUser validates an access request against the associated users's
// *statically assigned* roles. If expandRoles is true, it will also expand wildcard
// *statically assigned* roles. If [[ExpandVars]] is set to true, it will also expand wildcard
// requests, setting their role list to include all roles the user is allowed to request.
// Expansion should be performed before an access request is initially placed in the backend.
func ValidateAccessRequestForUser(ctx context.Context, clock clockwork.Clock, getter RequestValidatorGetter, req types.AccessRequest, identity tlsca.Identity, opts ...ValidateRequestOption) error {
Expand Down Expand Up @@ -2121,7 +2196,7 @@ func (m *RequestValidator) pruneResourceRequestRoles(
}

for _, role := range allRoles {
roleAllowsAccess, err := m.roleAllowsResource(ctx, role, resource, loginHint, resourceMatcherToMatcherSlice(resourceMatcher)...)
roleAllowsAccess, err := m.roleAllowsResource(role, resource, loginHint, resourceMatcherToMatcherSlice(resourceMatcher)...)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -2228,7 +2303,6 @@ func getAllowedKubeResourceKinds(allowedKinds []string, deniedKinds []string) []
}

func (m *RequestValidator) roleAllowsResource(
ctx context.Context,
role types.Role,
resource types.ResourceWithLabels,
loginHint string,
Expand Down
Loading

0 comments on commit 033826f

Please sign in to comment.