mirror of
https://github.com/gravitational/teleport
synced 2024-10-22 02:03:24 +00:00
af05ce3eeb
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.
899 lines
25 KiB
Go
899 lines
25 KiB
Go
/*
|
|
Copyright 2019 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/teleport/lib/utils/parse"
|
|
|
|
"github.com/gravitational/trace"
|
|
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/pborman/uuid"
|
|
)
|
|
|
|
// RequestStrategy is an indicator of how access requests
|
|
// should be handled for holders of a given role.
|
|
type RequestStrategy string
|
|
|
|
const (
|
|
// RequestStrategyOptional is the default request strategy,
|
|
// indicating that no special actions/requirements exist.
|
|
RequestStrategyOptional RequestStrategy = "optional"
|
|
|
|
// RequestStrategyReason indicates that client implementations
|
|
// should automatically generate wildcard requests on login, and
|
|
// users should be prompted for a reason.
|
|
RequestStrategyReason RequestStrategy = "reason"
|
|
|
|
// RequestStrategyAlways indicates that client implementations
|
|
// should automatically generate wildcard requests on login, but
|
|
// that reasons are not required.
|
|
RequestStrategyAlways RequestStrategy = "always"
|
|
)
|
|
|
|
// ShouldAutoRequest checks if the request strategy
|
|
// indicates that a request should be automatically
|
|
// generated on login.
|
|
func (s RequestStrategy) ShouldAutoRequest() bool {
|
|
switch s {
|
|
case RequestStrategyReason, RequestStrategyAlways:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// RequireReason checks if the request strategy
|
|
// is one that requires users to always supply
|
|
// reasons with their requests.
|
|
func (s RequestStrategy) RequireReason() bool {
|
|
return s == RequestStrategyReason
|
|
}
|
|
|
|
// RequestIDs is a collection of IDs for privilege escalation requests.
|
|
type RequestIDs struct {
|
|
AccessRequests []string `json:"access_requests,omitempty"`
|
|
}
|
|
|
|
func (r *RequestIDs) Marshal() ([]byte, error) {
|
|
data, err := utils.FastMarshal(r)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (r *RequestIDs) Unmarshal(data []byte) error {
|
|
if err := utils.FastUnmarshal(data, r); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return trace.Wrap(r.Check())
|
|
}
|
|
|
|
func (r *RequestIDs) Check() error {
|
|
for _, id := range r.AccessRequests {
|
|
if uuid.Parse(id) == nil {
|
|
return trace.BadParameter("invalid request id %q", id)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *RequestIDs) IsEmpty() bool {
|
|
return len(r.AccessRequests) < 1
|
|
}
|
|
|
|
// stateVariants allows iteration of the expected variants
|
|
// of RequestState.
|
|
var stateVariants = [4]RequestState{
|
|
RequestState_NONE,
|
|
RequestState_PENDING,
|
|
RequestState_APPROVED,
|
|
RequestState_DENIED,
|
|
}
|
|
|
|
// Parse attempts to interpret a value as a string representation
|
|
// of a RequestState.
|
|
func (s *RequestState) Parse(val string) error {
|
|
for _, state := range stateVariants {
|
|
if state.String() == val {
|
|
*s = state
|
|
return nil
|
|
}
|
|
}
|
|
return trace.BadParameter("unknown request state: %q", val)
|
|
}
|
|
|
|
// key values for map encoding of request filter
|
|
const (
|
|
keyID = "id"
|
|
keyUser = "user"
|
|
keyState = "state"
|
|
)
|
|
|
|
func (f *AccessRequestFilter) IntoMap() map[string]string {
|
|
m := make(map[string]string)
|
|
if f.ID != "" {
|
|
m[keyID] = f.ID
|
|
}
|
|
if f.User != "" {
|
|
m[keyUser] = f.User
|
|
}
|
|
if !f.State.IsNone() {
|
|
m[keyState] = f.State.String()
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (f *AccessRequestFilter) FromMap(m map[string]string) error {
|
|
for key, val := range m {
|
|
switch key {
|
|
case keyID:
|
|
f.ID = val
|
|
case keyUser:
|
|
f.User = val
|
|
case keyState:
|
|
if err := f.State.Parse(val); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
default:
|
|
return trace.BadParameter("unknown filter key %s", key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Match checks if a given access request matches this filter.
|
|
func (f *AccessRequestFilter) Match(req AccessRequest) bool {
|
|
if f.ID != "" && req.GetName() != f.ID {
|
|
return false
|
|
}
|
|
if f.User != "" && req.GetUser() != f.User {
|
|
return false
|
|
}
|
|
if !f.State.IsNone() && req.GetState() != f.State {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (f *AccessRequestFilter) Equals(o AccessRequestFilter) bool {
|
|
return f.ID == o.ID && f.User == o.User && f.State == o.State
|
|
}
|
|
|
|
// AccessRequestUpdate encompasses the parameters of a
|
|
// SetAccessRequestState call.
|
|
type AccessRequestUpdate struct {
|
|
// RequestID is the ID of the request to be updated.
|
|
RequestID string
|
|
// State is the state that the target request
|
|
// should resolve to.
|
|
State RequestState
|
|
// Reason is an optional description of *why* the
|
|
// the request is being resolved.
|
|
Reason string
|
|
// Annotations supplies extra data associated with
|
|
// the resolution; primarily for audit purposes.
|
|
Annotations map[string][]string
|
|
// Roles, if non-empty declares a list of roles
|
|
// that should override the role list of the request.
|
|
// This parameter is only accepted on approvals
|
|
// and must be a subset of the role list originally
|
|
// present on the request.
|
|
Roles []string
|
|
}
|
|
|
|
func (u *AccessRequestUpdate) Check() error {
|
|
if u.RequestID == "" {
|
|
return trace.BadParameter("missing request id")
|
|
}
|
|
if u.State.IsNone() {
|
|
return trace.BadParameter("missing request state")
|
|
}
|
|
if len(u.Roles) > 0 && !u.State.IsApproved() {
|
|
return trace.BadParameter("cannot override roles when setting state: %s", u.State)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DynamicAccess is a service which manages dynamic RBAC.
|
|
type DynamicAccess interface {
|
|
// CreateAccessRequest stores a new access request.
|
|
CreateAccessRequest(ctx context.Context, req AccessRequest) error
|
|
// SetAccessRequestState updates the state of an existing access request.
|
|
SetAccessRequestState(ctx context.Context, params AccessRequestUpdate) error
|
|
// GetAccessRequests gets all currently active access requests.
|
|
GetAccessRequests(ctx context.Context, filter AccessRequestFilter) ([]AccessRequest, error)
|
|
// DeleteAccessRequest deletes an access request.
|
|
DeleteAccessRequest(ctx context.Context, reqID string) error
|
|
// GetPluginData loads all plugin data matching the supplied filter.
|
|
GetPluginData(ctx context.Context, filter PluginDataFilter) ([]PluginData, error)
|
|
// UpdatePluginData updates a per-resource PluginData entry.
|
|
UpdatePluginData(ctx context.Context, params PluginDataUpdateParams) error
|
|
}
|
|
|
|
// DynamicAccessExt is an extended dynamic access interface
|
|
// used to implement some auth server internals.
|
|
type DynamicAccessExt interface {
|
|
DynamicAccess
|
|
// UpsertAccessRequest creates or updates an access request.
|
|
UpsertAccessRequest(ctx context.Context, req AccessRequest) error
|
|
// DeleteAllAccessRequests deletes all existent access requests.
|
|
DeleteAllAccessRequests(ctx context.Context) error
|
|
}
|
|
|
|
// AccessRequest is a request for temporarily granted roles
|
|
type AccessRequest interface {
|
|
Resource
|
|
// GetUser gets the name of the requesting user
|
|
GetUser() string
|
|
// GetRoles gets the roles being requested by the user
|
|
GetRoles() []string
|
|
// SetRoles overrides the roles being requested by the user
|
|
SetRoles([]string)
|
|
// GetState gets the current state of the request
|
|
GetState() RequestState
|
|
// SetState sets the approval state of the request
|
|
SetState(RequestState) error
|
|
// GetCreationTime gets the time at which the request was
|
|
// originally registered with the auth server.
|
|
GetCreationTime() time.Time
|
|
// SetCreationTime sets the creation time of the request.
|
|
SetCreationTime(time.Time)
|
|
// GetAccessExpiry gets the upper limit for which this request
|
|
// may be considered active.
|
|
GetAccessExpiry() time.Time
|
|
// SetAccessExpiry sets the upper limit for which this request
|
|
// may be considered active.
|
|
SetAccessExpiry(time.Time)
|
|
// GetRequestReason gets the reason for the request's creation.
|
|
GetRequestReason() string
|
|
// SetRequestReason sets the reason for the request's creation.
|
|
SetRequestReason(string)
|
|
// GetResolveReason gets the reasson for the request's resolution.
|
|
GetResolveReason() string
|
|
// SetResolveReason sets the reason for the request's resolution.
|
|
SetResolveReason(string)
|
|
// GetResolveAnnotations gets the annotations associated with
|
|
// the request's resolution.
|
|
GetResolveAnnotations() map[string][]string
|
|
// SetResolveAnnotations sets the annotations associated with
|
|
// the request's resolution.
|
|
SetResolveAnnotations(map[string][]string)
|
|
// GetSystemAnnotations gets the teleport-applied annotations.
|
|
GetSystemAnnotations() map[string][]string
|
|
// SetSystemAnnotations sets the teleport-applied annotations.
|
|
SetSystemAnnotations(map[string][]string)
|
|
// CheckAndSetDefaults validates the access request and
|
|
// supplies default values where appropriate.
|
|
CheckAndSetDefaults() error
|
|
// Equals checks equality between access request values.
|
|
Equals(AccessRequest) bool
|
|
}
|
|
|
|
// GetAccessRequest is a helper function assists with loading a specific request by ID.
|
|
func GetAccessRequest(ctx context.Context, acc DynamicAccess, reqID string) (AccessRequest, error) {
|
|
reqs, err := acc.GetAccessRequests(ctx, AccessRequestFilter{
|
|
ID: reqID,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if len(reqs) < 1 {
|
|
return nil, trace.NotFound("no access request matching %q", reqID)
|
|
}
|
|
return reqs[0], nil
|
|
}
|
|
|
|
func (s RequestState) IsNone() bool {
|
|
return s == RequestState_NONE
|
|
}
|
|
|
|
func (s RequestState) IsPending() bool {
|
|
return s == RequestState_PENDING
|
|
}
|
|
|
|
func (s RequestState) IsApproved() bool {
|
|
return s == RequestState_APPROVED
|
|
}
|
|
|
|
func (s RequestState) IsDenied() bool {
|
|
return s == RequestState_DENIED
|
|
}
|
|
|
|
func (s RequestState) IsResolved() bool {
|
|
return s.IsApproved() || s.IsDenied()
|
|
}
|
|
|
|
// NewAccessRequest assembled an AccessReqeust resource.
|
|
func NewAccessRequest(user string, roles ...string) (AccessRequest, error) {
|
|
req := AccessRequestV3{
|
|
Kind: KindAccessRequest,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: uuid.New(),
|
|
},
|
|
Spec: AccessRequestSpecV3{
|
|
User: user,
|
|
Roles: roles,
|
|
State: RequestState_PENDING,
|
|
},
|
|
}
|
|
if err := req.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return &req, nil
|
|
}
|
|
|
|
func (c AccessRequestConditions) GetTraitMappings() TraitMappingSet {
|
|
tm := make([]TraitMapping, 0, len(c.ClaimsToRoles))
|
|
for _, mapping := range c.ClaimsToRoles {
|
|
tm = append(tm, TraitMapping{
|
|
Trait: mapping.Claim,
|
|
Value: mapping.Value,
|
|
Roles: mapping.Roles,
|
|
})
|
|
}
|
|
return TraitMappingSet(tm)
|
|
}
|
|
|
|
type UserAndRoleGetter interface {
|
|
UserGetter
|
|
RoleGetter
|
|
GetRoles() ([]Role, error)
|
|
}
|
|
|
|
// 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) {
|
|
// build matchers for the role list
|
|
for _, r := range conditions.Roles {
|
|
m, err := parse.NewMatcher(r)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
matchers = append(matchers, m)
|
|
}
|
|
|
|
// build matchers for all role mappings
|
|
ms, err := conditions.GetTraitMappings().TraitsToRoleMatchers(traits)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return append(matchers, ms...), nil
|
|
}
|
|
|
|
// insertAnnotations constructs all annotations for a given
|
|
// AccessRequestConditions instance and adds them to the
|
|
// supplied annotations mapping.
|
|
func insertAnnotations(annotations map[string][]string, conditions AccessRequestConditions, traits map[string][]string) {
|
|
for key, vals := range conditions.Annotations {
|
|
// get any previous values at key
|
|
allVals := annotations[key]
|
|
|
|
// iterate through all new values and expand any
|
|
// variable interpolation syntax they contain.
|
|
ApplyTraits:
|
|
for _, v := range vals {
|
|
applied, err := applyValueTraits(v, traits)
|
|
if err != nil {
|
|
// skip values that failed variable expansion
|
|
continue ApplyTraits
|
|
}
|
|
allVals = append(allVals, applied...)
|
|
}
|
|
|
|
annotations[key] = allVals
|
|
}
|
|
}
|
|
|
|
// requestValidator a helper for validating access requests.
|
|
// a user's statically assigned roles are are "added" to the
|
|
// validator via the push() method, which extracts all the
|
|
// relevant rules, peforms variable substitutions, and builds
|
|
// a set of simple Allow/Deny datastructures. These, in turn,
|
|
// are used to validate and expand the access request.
|
|
type requestValidator struct {
|
|
traits map[string][]string
|
|
requireReason bool
|
|
opts struct {
|
|
expandRoles, annotate bool
|
|
}
|
|
Roles struct {
|
|
Allow, Deny []parse.Matcher
|
|
}
|
|
Annotations struct {
|
|
Allow, Deny map[string][]string
|
|
}
|
|
}
|
|
|
|
func newRequestValidator(traits map[string][]string, opts ...ValidateRequestOption) requestValidator {
|
|
m := requestValidator{
|
|
traits: traits,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(&m)
|
|
}
|
|
if m.opts.annotate {
|
|
// validation process for incoming access requests requires
|
|
// generating system annotations to be attached to the request
|
|
// before it is inserted into the backend.
|
|
m.Annotations.Allow = make(map[string][]string)
|
|
m.Annotations.Deny = make(map[string][]string)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// push compiles a role's configuration into the request validator.
|
|
// All of the requesint user's statically assigned roles must be pushed
|
|
// before validation begins.
|
|
func (m *requestValidator) push(role Role) error {
|
|
var err error
|
|
|
|
m.requireReason = m.requireReason || role.GetOptions().RequestAccess.RequireReason()
|
|
|
|
allow, deny := role.GetAccessRequestConditions(Allow), role.GetAccessRequestConditions(Deny)
|
|
|
|
m.Roles.Deny, err = appendRoleMatchers(m.Roles.Deny, deny, m.traits)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
m.Roles.Allow, err = appendRoleMatchers(m.Roles.Allow, allow, m.traits)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if m.opts.annotate {
|
|
// validation process for incoming access requests requires
|
|
// generating system annotations to be attached to the request
|
|
// before it is inserted into the backend.
|
|
insertAnnotations(m.Annotations.Deny, deny, m.traits)
|
|
insertAnnotations(m.Annotations.Allow, allow, m.traits)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CanRequestRole checks if a given role can be requested.
|
|
func (m *requestValidator) CanRequestRole(name string) bool {
|
|
for _, deny := range m.Roles.Deny {
|
|
if deny.Match(name) {
|
|
return false
|
|
}
|
|
}
|
|
for _, allow := range m.Roles.Allow {
|
|
if allow.Match(name) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SystemAnnotations calculates the system annotations for a pending
|
|
// access request.
|
|
func (m *requestValidator) SystemAnnotations() map[string][]string {
|
|
annotations := make(map[string][]string)
|
|
for k, va := range m.Annotations.Allow {
|
|
var filtered []string
|
|
for _, v := range va {
|
|
if !utils.SliceContainsStr(m.Annotations.Deny[k], v) {
|
|
filtered = append(filtered, v)
|
|
}
|
|
}
|
|
if len(filtered) == 0 {
|
|
continue
|
|
}
|
|
annotations[k] = filtered
|
|
}
|
|
return annotations
|
|
}
|
|
|
|
type ValidateRequestOption func(*requestValidator)
|
|
|
|
// ExpandRoles activates expansion of wildcard role lists
|
|
// (`[]string{"*"}`) when true.
|
|
func ExpandRoles(expand bool) ValidateRequestOption {
|
|
return func(v *requestValidator) {
|
|
v.opts.expandRoles = expand
|
|
}
|
|
}
|
|
|
|
// ApplySystemAnnotations causes system annotations to be computed
|
|
// and attached during validation when true.
|
|
func ApplySystemAnnotations(annotate bool) ValidateRequestOption {
|
|
return func(v *requestValidator) {
|
|
v.opts.annotate = annotate
|
|
}
|
|
}
|
|
|
|
// ValidateAccessRequest validates an access request against the associated users's
|
|
// *statically assigned* roles. If expandRoles is 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 ValidateAccessRequest(getter UserAndRoleGetter, req AccessRequest, opts ...ValidateRequestOption) error {
|
|
user, err := getter.GetUser(req.GetUser(), false)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
validator := newRequestValidator(user.GetTraits(), opts...)
|
|
|
|
// load all statically assigned roles for the user and
|
|
// use them to build our validation state.
|
|
for _, roleName := range user.GetRoles() {
|
|
role, err := getter.GetRole(roleName)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if err := validator.push(role); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if validator.requireReason && req.GetRequestReason() == "" {
|
|
return trace.BadParameter("request reason must be specified (required by static role configuration)")
|
|
}
|
|
|
|
// check for "wildcard request" (`roles=*`). wildcard requests
|
|
// need to be expanded into a list consisting of all existing roles
|
|
// that the user does not hold and is allowed to request.
|
|
if r := req.GetRoles(); len(r) == 1 && r[0] == Wildcard {
|
|
|
|
if !req.GetState().IsPending() {
|
|
// expansion is only permitted in pending requests. once resolved,
|
|
// a request's role list must be immutable.
|
|
return trace.BadParameter("wildcard requests are not permitted in state %s", req.GetState())
|
|
}
|
|
|
|
if !validator.opts.expandRoles {
|
|
// teleport always validates new incoming pending access requests
|
|
// with ExpandRoles(true). after that, it should be impossible to
|
|
// add new values to the role list.
|
|
return trace.BadParameter("unexpected wildcard request (this is a bug)")
|
|
}
|
|
|
|
allRoles, err := getter.GetRoles()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
var expanded []string
|
|
for _, role := range allRoles {
|
|
if n := role.GetName(); !utils.SliceContainsStr(user.GetRoles(), n) && validator.CanRequestRole(n) {
|
|
// user does not currently hold this role, and is allowed to request it.
|
|
expanded = append(expanded, n)
|
|
}
|
|
}
|
|
if len(expanded) == 0 {
|
|
return trace.BadParameter("no requestable roles, please verify static RBAC configuration")
|
|
}
|
|
req.SetRoles(expanded)
|
|
}
|
|
|
|
// verify that all requested roles are permissible
|
|
for _, roleName := range req.GetRoles() {
|
|
if !validator.CanRequestRole(roleName) {
|
|
return trace.BadParameter("user %q can not request role %q", req.GetUser(), roleName)
|
|
}
|
|
}
|
|
|
|
if validator.opts.annotate {
|
|
// incoming requests must have system annotations attached before
|
|
// before being inserted into the backend. this is how the
|
|
// RBAC system propagates sideband information to plugins.
|
|
req.SetSystemAnnotations(validator.SystemAnnotations())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetUser() string {
|
|
return r.Spec.User
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetRoles() []string {
|
|
return r.Spec.Roles
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetRoles(roles []string) {
|
|
r.Spec.Roles = roles
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetState() RequestState {
|
|
return r.Spec.State
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetState(state RequestState) error {
|
|
if r.Spec.State.IsDenied() {
|
|
if state.IsDenied() {
|
|
return nil
|
|
}
|
|
return trace.BadParameter("cannot set request-state %q (already denied)", state.String())
|
|
}
|
|
r.Spec.State = state
|
|
return nil
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetCreationTime() time.Time {
|
|
return r.Spec.Created
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetCreationTime(t time.Time) {
|
|
r.Spec.Created = t
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetAccessExpiry() time.Time {
|
|
return r.Spec.Expires
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetAccessExpiry(expiry time.Time) {
|
|
r.Spec.Expires = expiry
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetRequestReason() string {
|
|
return r.Spec.RequestReason
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetRequestReason(reason string) {
|
|
r.Spec.RequestReason = reason
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetResolveReason() string {
|
|
return r.Spec.ResolveReason
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetResolveReason(reason string) {
|
|
r.Spec.ResolveReason = reason
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetResolveAnnotations() map[string][]string {
|
|
return r.Spec.ResolveAnnotations
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetResolveAnnotations(annotations map[string][]string) {
|
|
r.Spec.ResolveAnnotations = annotations
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetSystemAnnotations() map[string][]string {
|
|
return r.Spec.SystemAnnotations
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetSystemAnnotations(annotations map[string][]string) {
|
|
r.Spec.SystemAnnotations = annotations
|
|
}
|
|
|
|
func (r *AccessRequestV3) CheckAndSetDefaults() error {
|
|
if err := r.Metadata.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if r.GetState().IsNone() {
|
|
if err := r.SetState(RequestState_PENDING); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if err := r.Check(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *AccessRequestV3) Check() error {
|
|
if r.Kind == "" {
|
|
return trace.BadParameter("access request kind not set")
|
|
}
|
|
if r.Version == "" {
|
|
return trace.BadParameter("access request version not set")
|
|
}
|
|
if r.GetName() == "" {
|
|
return trace.BadParameter("access request id not set")
|
|
}
|
|
if uuid.Parse(r.GetName()) == nil {
|
|
return trace.BadParameter("invalid access request id %q", r.GetName())
|
|
}
|
|
if r.GetUser() == "" {
|
|
return trace.BadParameter("access request user name not set")
|
|
}
|
|
if len(r.GetRoles()) < 1 {
|
|
return trace.BadParameter("access request does not specify any roles")
|
|
}
|
|
if r.GetState().IsPending() {
|
|
if r.GetResolveReason() != "" {
|
|
return trace.BadParameter("pending requests cannot include resolve reason")
|
|
}
|
|
if len(r.GetResolveAnnotations()) != 0 {
|
|
return trace.BadParameter("pending requests cannot include resolve annotations")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *AccessRequestV3) Equals(other AccessRequest) bool {
|
|
o, ok := other.(*AccessRequestV3)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if r.GetName() != o.GetName() {
|
|
return false
|
|
}
|
|
return r.Spec.Equals(&o.Spec)
|
|
}
|
|
|
|
func (s *AccessRequestSpecV3) Equals(other *AccessRequestSpecV3) bool {
|
|
if s.User != other.User {
|
|
return false
|
|
}
|
|
if len(s.Roles) != len(other.Roles) {
|
|
return false
|
|
}
|
|
for i, role := range s.Roles {
|
|
if role != other.Roles[i] {
|
|
return false
|
|
}
|
|
}
|
|
if s.Created != other.Created {
|
|
return false
|
|
}
|
|
if s.Expires != other.Expires {
|
|
return false
|
|
}
|
|
return s.State == other.State
|
|
}
|
|
|
|
type AccessRequestMarshaler interface {
|
|
MarshalAccessRequest(req AccessRequest, opts ...MarshalOption) ([]byte, error)
|
|
UnmarshalAccessRequest(bytes []byte, opts ...MarshalOption) (AccessRequest, error)
|
|
}
|
|
|
|
type accessRequestMarshaler struct{}
|
|
|
|
func (r *accessRequestMarshaler) MarshalAccessRequest(req AccessRequest, opts ...MarshalOption) ([]byte, error) {
|
|
cfg, err := collectOptions(opts)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
switch r := req.(type) {
|
|
case *AccessRequestV3:
|
|
if !cfg.PreserveResourceID {
|
|
// avoid modifying the original object
|
|
// to prevent unexpected data races
|
|
cp := *r
|
|
cp.SetResourceID(0)
|
|
r = &cp
|
|
}
|
|
return utils.FastMarshal(r)
|
|
default:
|
|
return nil, trace.BadParameter("unrecognized access request type: %T", req)
|
|
}
|
|
}
|
|
|
|
func (r *accessRequestMarshaler) UnmarshalAccessRequest(data []byte, opts ...MarshalOption) (AccessRequest, error) {
|
|
cfg, err := collectOptions(opts)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
var req AccessRequestV3
|
|
if cfg.SkipValidation {
|
|
if err := utils.FastUnmarshal(data, &req); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
} else {
|
|
if err := utils.UnmarshalWithSchema(GetAccessRequestSchema(), &req, data); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
if err := req.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if cfg.ID != 0 {
|
|
req.SetResourceID(cfg.ID)
|
|
}
|
|
if !cfg.Expires.IsZero() {
|
|
req.SetExpiry(cfg.Expires)
|
|
}
|
|
return &req, nil
|
|
}
|
|
|
|
var accessRequestMarshalerInstance AccessRequestMarshaler = &accessRequestMarshaler{}
|
|
|
|
func GetAccessRequestMarshaler() AccessRequestMarshaler {
|
|
marshalerMutex.Lock()
|
|
defer marshalerMutex.Unlock()
|
|
return accessRequestMarshalerInstance
|
|
}
|
|
|
|
const AccessRequestSpecSchema = `{
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"user": { "type": "string" },
|
|
"roles": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
},
|
|
"state": { "type": "integer" },
|
|
"created": { "type": "string" },
|
|
"expires": { "type": "string" },
|
|
"request_reason": { "type": "string" },
|
|
"resolve_reason": { "type": "string" },
|
|
"resolve_annotations": { "type": "object" },
|
|
"system_annotations": { "type": "object" }
|
|
}
|
|
}`
|
|
|
|
func GetAccessRequestSchema() string {
|
|
return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, AccessRequestSpecSchema, DefaultDefinitions)
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetKind() string {
|
|
return r.Kind
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetSubKind() string {
|
|
return r.SubKind
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetSubKind(subKind string) {
|
|
r.SubKind = subKind
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetVersion() string {
|
|
return r.Version
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetName() string {
|
|
return r.Metadata.Name
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetName(name string) {
|
|
r.Metadata.Name = name
|
|
}
|
|
|
|
func (r *AccessRequestV3) Expiry() time.Time {
|
|
return r.Metadata.Expiry()
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetExpiry(expiry time.Time) {
|
|
r.Metadata.SetExpiry(expiry)
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetTTL(clock clockwork.Clock, ttl time.Duration) {
|
|
r.Metadata.SetTTL(clock, ttl)
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetMetadata() Metadata {
|
|
return r.Metadata
|
|
}
|
|
|
|
func (r *AccessRequestV3) GetResourceID() int64 {
|
|
return r.Metadata.GetID()
|
|
}
|
|
|
|
func (r *AccessRequestV3) SetResourceID(id int64) {
|
|
r.Metadata.SetID(id)
|
|
}
|
|
|
|
func (r *AccessRequestV3) String() string {
|
|
return fmt.Sprintf("AccessRequest(user=%v,roles=%+v)", r.Spec.User, r.Spec.Roles)
|
|
}
|