Skip to content

Commit

Permalink
Implment access-request system (workflow API)
Browse files Browse the repository at this point in the history
  • Loading branch information
fspmarshall committed Dec 2, 2019
1 parent a324680 commit ec327b6
Show file tree
Hide file tree
Showing 32 changed files with 4,951 additions and 651 deletions.
3 changes: 3 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ const (
CertExtensionTeleportRouteToCluster = "teleport-route-to-cluster"
// CertExtensionTeleportTraits is used to propagate traits about the user.
CertExtensionTeleportTraits = "teleport-traits"
// CertExtensionTeleportActiveRequests is used to track which privilege
// escalation requests were used to construct the certificate.
CertExtensionTeleportActiveRequests = "teleport-active-requests"
)

const (
Expand Down
80 changes: 80 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ func NewAuthServer(cfg *InitConfig, opts ...AuthServerOption) (*AuthServer, erro
if cfg.Access == nil {
cfg.Access = local.NewAccessService(cfg.Backend)
}
if cfg.DynamicAccess == nil {
cfg.DynamicAccess = local.NewDynamicAccessService(cfg.Backend)
}
if cfg.ClusterConfiguration == nil {
cfg.ClusterConfiguration = local.NewClusterConfigurationService(cfg.Backend)
}
Expand Down Expand Up @@ -111,6 +114,7 @@ func NewAuthServer(cfg *InitConfig, opts ...AuthServerOption) (*AuthServer, erro
Provisioner: cfg.Provisioner,
Identity: cfg.Identity,
Access: cfg.Access,
DynamicAccess: cfg.DynamicAccess,
ClusterConfiguration: cfg.ClusterConfiguration,
IAuditLog: cfg.AuditLog,
Events: cfg.Events,
Expand All @@ -132,6 +136,7 @@ type AuthServices struct {
services.Provisioner
services.Identity
services.Access
services.DynamicAccess
services.ClusterConfiguration
services.Events
events.IAuditLog
Expand Down Expand Up @@ -408,6 +413,9 @@ type certRequest struct {
routeToCluster string
// traits hold claim data used to populate a role at runtime.
traits wrappers.Traits
// activeRequests tracks privilege escalation requests applied
// during the construction of the certificate.
activeRequests services.RequestIDs
}

// GenerateUserTestCerts is used to generate user certificate, used internally for tests
Expand Down Expand Up @@ -509,6 +517,7 @@ func (s *AuthServer) generateUserCert(req certRequest) (*certs, error) {
PermitAgentForwarding: req.checker.CanForwardAgents(),
RouteToCluster: req.routeToCluster,
Traits: req.traits,
ActiveRequests: req.activeRequests,
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -1350,6 +1359,77 @@ func (a *AuthServer) DeleteRole(name string) error {
return a.Access.DeleteRole(name)
}

func (a *AuthServer) CreateAccessRequest(req services.AccessRequest) error {
if err := services.ValidateAccessRequest(a, req); err != nil {
return trace.Wrap(err)
}
ttl, err := a.calculateMaxAccessTTL(req)
if err != nil {
return trace.Wrap(err)
}
now := a.clock.Now().UTC()
req.SetCreationTime(now)
exp := now.Add(ttl)
// Set acccess expiry if an allowable default was not provided.
if req.GetAccessExpiry().Before(now) || req.GetAccessExpiry().After(exp) {
req.SetAccessExpiry(exp)
}
// By default, resource expiry should match access expiry.
req.SetExpiry(req.GetAccessExpiry())
// If the access-request is in a pending state, then the expiry of the underlying resource
// is capped to to PendingAccessDuration in order to limit orphaned access requests.
if req.GetState().IsPending() {
pexp := now.Add(defaults.PendingAccessDuration)
if pexp.Before(req.Expiry()) {
req.SetExpiry(pexp)
}
}
if err := a.DynamicAccess.CreateAccessRequest(req); err != nil {
return trace.Wrap(err)
}
err = a.EmitAuditEvent(events.AccessRequestCreated, events.EventFields{
events.AccessRequestID: req.GetName(),
events.EventUser: req.GetUser(),
events.UserRoles: req.GetRoles(),
events.AccessRequestState: req.GetState().String(),
})
return trace.Wrap(err)
}

func (a *AuthServer) SetAccessRequestState(reqID string, state services.RequestState, updatedBy ...string) error {
if err := a.DynamicAccess.SetAccessRequestState(reqID, state); err != nil {
return trace.Wrap(err)
}
u := "unknown"
if len(updatedBy) == 1 {
u = updatedBy[0]
}
err := a.EmitAuditEvent(events.AccessRequestUpdated, events.EventFields{
events.AccessRequestID: reqID,
events.AccessRequestState: state.String(),
events.AccessRequestUpdateBy: u,
})
return trace.Wrap(err)
}

// calculateMaxAccessTTL determines the maximum allowable TTL for a given access request
// based on the MaxSessionTTLs of the roles being requested (a access request's life cannot
// exceed the smallest allowable MaxSessionTTL value of the roles that it requests).
func (a *AuthServer) calculateMaxAccessTTL(req services.AccessRequest) (time.Duration, error) {
minTTL := defaults.MaxAccessDuration
for _, roleName := range req.GetRoles() {
role, err := a.GetRole(roleName)
if err != nil {
return 0, trace.Wrap(err)
}
roleTTL := time.Duration(role.GetOptions().MaxSessionTTL)
if roleTTL > 0 && roleTTL < minTTL {
minTTL = roleTTL
}
}
return minTTL, nil
}

// NewKeepAliver returns a new instance of keep aliver
func (a *AuthServer) NewKeepAliver(ctx context.Context) (services.KeepAliver, error) {
cancelCtx, cancel := context.WithCancel(ctx)
Expand Down
93 changes: 92 additions & 1 deletion lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,16 @@ func (a *AuthWithRoles) NewWatcher(ctx context.Context, watch services.Watch) (s
return nil, trace.Wrap(err)
}
}

case services.KindAccessRequest:
var filter services.AccessRequestFilter
if err := filter.FromMap(kind.Filter); err != nil {
return nil, trace.Wrap(err)
}
if filter.User == "" || a.currentUserAction(filter.User) != nil {
if err := a.action(defaults.Namespace, services.KindAccessRequest, services.VerbRead); err != nil {
return nil, trace.Wrap(err)
}
}
default:
return nil, trace.AccessDenied("not authorized to watch %v events", kind.Kind)
}
Expand Down Expand Up @@ -778,6 +787,47 @@ func (a *AuthWithRoles) DeleteWebSession(user string, sid string) error {
return a.authServer.DeleteWebSession(user, sid)
}

func (a *AuthWithRoles) GetAccessRequests(filter services.AccessRequestFilter) ([]services.AccessRequest, error) {
// An exception is made to allow users to get their own access requests.
if filter.User == "" || a.currentUserAction(filter.User) != nil {
if err := a.action(defaults.Namespace, services.KindAccessRequest, services.VerbList); err != nil {
return nil, trace.Wrap(err)
}
if err := a.action(defaults.Namespace, services.KindAccessRequest, services.VerbRead); err != nil {
return nil, trace.Wrap(err)
}
}
return a.authServer.GetAccessRequests(filter)
}

func (a *AuthWithRoles) CreateAccessRequest(req services.AccessRequest) error {
// An exception is made to allow users to create access *pending* requests for themselves.
if !req.GetState().IsPending() || a.currentUserAction(req.GetUser()) != nil {
if err := a.action(defaults.Namespace, services.KindAccessRequest, services.VerbCreate); err != nil {
return trace.Wrap(err)
}
}
// Ensure that an access request cannot outlive the identity that creates it.
if req.GetAccessExpiry().Before(a.authServer.GetClock().Now()) || req.GetAccessExpiry().After(a.identity.Expires) {
req.SetAccessExpiry(a.identity.Expires)
}
return a.authServer.CreateAccessRequest(req)
}

func (a *AuthWithRoles) SetAccessRequestState(reqID string, state services.RequestState) error {
if err := a.action(defaults.Namespace, services.KindAccessRequest, services.VerbUpdate); err != nil {
return trace.Wrap(err)
}
return a.authServer.SetAccessRequestState(reqID, state, a.user.GetName())
}

func (a *AuthWithRoles) DeleteAccessRequest(name string) error {
if err := a.action(defaults.Namespace, services.KindAccessRequest, services.VerbUpdate); err != nil {
return trace.Wrap(err)
}
return a.authServer.DeleteAccessRequest(name)
}

func (a *AuthWithRoles) GetUsers(withSecrets bool) ([]services.User, error) {
if withSecrets {
// TODO(fspmarshall): replace admin requirement with VerbReadWithSecrets once we've
Expand Down Expand Up @@ -908,6 +958,44 @@ func (a *AuthWithRoles) GenerateUserCerts(ctx context.Context, req proto.UserCer
return nil, trace.AccessDenied("this request can be only executed by an admin")
}

// TODO(fspmarshall): Move this logic to AuthServer.
if len(req.AccessRequests) > 0 {
// add any applicable access request values.
for _, reqID := range req.AccessRequests {
accessReq, err := a.authServer.GetAccessRequest(reqID)
if err != nil {
if trace.IsNotFound(err) {
return nil, trace.AccessDenied("invalid access request %q", reqID)
}
return nil, trace.Wrap(err)
}
if accessReq.GetUser() != req.Username {
return nil, trace.AccessDenied("invalid access request %q", reqID)
}
if !accessReq.GetState().IsApproved() {
if accessReq.GetState().IsDenied() {
return nil, trace.AccessDenied("access-request %q has been denied", reqID)
}
return nil, trace.AccessDenied("access-request %q is awaiting approval", reqID)
}
if err := services.ValidateAccessRequest(a.authServer, accessReq); err != nil {
return nil, trace.Wrap(err)
}
aexp := accessReq.GetAccessExpiry()
if aexp.Before(a.authServer.GetClock().Now()) {
return nil, trace.AccessDenied("access-request %q is expired", reqID)
}
if aexp.Before(req.Expires) {
// cannot generate a cert that would outlive the access request
req.Expires = aexp
}
roles = append(roles, accessReq.GetRoles()...)
}
// nothing prevents an access-request from including roles already posessed by the
// user, so we must make sure to trim duplicate roles.
roles = utils.Deduplicate(roles)
}

// Extract the user and role set for whom the certificate will be generated.
user, err := a.GetUser(req.Username, false)
if err != nil {
Expand All @@ -929,6 +1017,9 @@ func (a *AuthWithRoles) GenerateUserCerts(ctx context.Context, req proto.UserCer
routeToCluster: req.RouteToCluster,
checker: checker,
traits: traits,
activeRequests: services.RequestIDs{
AccessRequests: req.AccessRequests,
},
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down
72 changes: 71 additions & 1 deletion lib/auth/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func DecodeClusterName(serverName string) (string, error) {
}
const suffix = "." + teleport.APIDomain
if !strings.HasSuffix(serverName, suffix) {
return "", trace.BadParameter("unrecognized name, expected suffix %v, got %q", teleport.APIDomain, serverName)
return "", trace.NotFound("no cluster name is encoded")
}
clusterName := strings.TrimSuffix(serverName, suffix)

Expand Down Expand Up @@ -810,6 +810,7 @@ func (c *Client) NewWatcher(ctx context.Context, watch services.Watch) (services
Name: kind.Name,
Kind: kind.Kind,
LoadSecrets: kind.LoadSecrets,
Filter: kind.Filter,
})
}
stream, err := clt.WatchEvents(cancelCtx, &protoWatch)
Expand Down Expand Up @@ -2530,6 +2531,67 @@ func (c *Client) DeleteTrustedCluster(name string) error {
return trace.Wrap(err)
}

func (c *Client) GetAccessRequests(filter services.AccessRequestFilter) ([]services.AccessRequest, error) {
clt, err := c.grpc()
if err != nil {
return nil, trace.Wrap(err)
}
rsp, err := clt.GetAccessRequests(context.TODO(), &filter)
if err != nil {
return nil, trail.FromGRPC(err)
}
reqs := make([]services.AccessRequest, 0, len(rsp.AccessRequests))
for _, req := range rsp.AccessRequests {
reqs = append(reqs, req)
}
return reqs, nil
}

func (c *Client) CreateAccessRequest(req services.AccessRequest) error {
r, ok := req.(*services.AccessRequestV3)
if !ok {
return trace.BadParameter("unexpected access request type %T", req)
}
clt, err := c.grpc()
if err != nil {
return trace.Wrap(err)
}
_, err = clt.CreateAccessRequest(context.TODO(), r)
if err != nil {
return trail.FromGRPC(err)
}
return nil
}

func (c *Client) DeleteAccessRequest(reqID string) error {
clt, err := c.grpc()
if err != nil {
return trace.Wrap(err)
}
_, err = clt.DeleteAccessRequest(context.TODO(), &proto.RequestID{
ID: reqID,
})
if err != nil {
return trail.FromGRPC(err)
}
return nil
}

func (c *Client) SetAccessRequestState(reqID string, state services.RequestState) error {
clt, err := c.grpc()
if err != nil {
return trace.Wrap(err)
}
_, err = clt.SetAccessRequestState(context.TODO(), &proto.RequestStateSetter{
ID: reqID,
State: state,
})
if err != nil {
return trail.FromGRPC(err)
}
return nil
}

// WebService implements features used by Web UI clients
type WebService interface {
// GetWebSessionInfo checks if a web sesion is valid, returns session id in case if
Expand Down Expand Up @@ -2750,4 +2812,12 @@ type ClientI interface {
// ProcessKubeCSR processes CSR request against Kubernetes CA, returns
// signed certificate if sucessful.
ProcessKubeCSR(req KubeCSR) (*KubeCSRResponse, error)
// GetAccessRequests lists all existing access requests.
GetAccessRequests(services.AccessRequestFilter) ([]services.AccessRequest, error)
// CreateAccessRequest creates a new access request.
CreateAccessRequest(req services.AccessRequest) error
// DeleteAccessRequest deletes an access request.
DeleteAccessRequest(reqID string) error
// SetAccessRequestState updates the state of an existing access request.
SetAccessRequestState(reqID string, state services.RequestState) error
}
Loading

0 comments on commit ec327b6

Please sign in to comment.