Get additional roles from approved access request when extending web session (#4619)

* Add strategyAccess field in user context
* Enable passing of requestID to renewSession handler and extendWebSession
This commit is contained in:
Lisa Kim 2020-11-05 13:15:57 -08:00 committed by GitHub
parent af05ce3eeb
commit 727c1fae9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 196 additions and 34 deletions

View file

@ -771,7 +771,8 @@ func (s *APIServer) u2fSignRequest(auth ClientI, w http.ResponseWriter, r *http.
}
type createWebSessionReq struct {
PrevSessionID string `json:"prev_session_id"`
PrevSessionID string `json:"prev_session_id"`
AccessRequestID string `json:"access_request_id"`
}
func (s *APIServer) createWebSession(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
@ -781,7 +782,7 @@ func (s *APIServer) createWebSession(auth ClientI, w http.ResponseWriter, r *htt
}
user := p.ByName("user")
if req.PrevSessionID != "" {
sess, err := auth.ExtendWebSession(user, req.PrevSessionID)
sess, err := auth.ExtendWebSession(user, req.PrevSessionID, req.AccessRequestID)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -826,9 +826,9 @@ func (a *Server) CheckU2FSignResponse(user string, response *u2f.SignResponse) e
return nil
}
// ExtendWebSession creates a new web session for a user based on a valid previous sessionID,
// method is used to renew the web session for a user
func (a *Server) ExtendWebSession(user string, prevSessionID string, identity tlsca.Identity) (services.WebSession, error) {
// ExtendWebSession creates a new web session for a user based on a valid previous sessionID.
// Additional roles are appended to initial roles if there is an approved access request.
func (a *Server) ExtendWebSession(user, prevSessionID, accessRequestID string, identity tlsca.Identity) (services.WebSession, error) {
prevSession, err := a.GetWebSession(user, prevSessionID)
if err != nil {
return nil, trace.Wrap(err)
@ -846,6 +846,20 @@ func (a *Server) ExtendWebSession(user string, prevSessionID string, identity tl
if err != nil {
return nil, trace.Wrap(err)
}
if accessRequestID != "" {
newRoles, requestExpiry, err := a.getRolesAndExpiryFromAccessRequest(user, accessRequestID)
if err != nil {
return nil, trace.Wrap(err)
}
roles = append(roles, newRoles...)
roles = utils.Deduplicate(roles)
// Let session expire with access request expiry.
expiresAt = requestExpiry
}
sess, err := a.NewWebSession(user, roles, traits)
if err != nil {
return nil, trace.Wrap(err)
@ -863,6 +877,42 @@ func (a *Server) ExtendWebSession(user string, prevSessionID string, identity tl
return sess, nil
}
func (a *Server) getRolesAndExpiryFromAccessRequest(user, accessRequestID string) ([]string, time.Time, error) {
reqFilter := services.AccessRequestFilter{
User: user,
ID: accessRequestID,
}
reqs, err := a.GetAccessRequests(context.TODO(), reqFilter)
if err != nil {
return nil, time.Time{}, trace.Wrap(err)
}
if len(reqs) < 1 {
return nil, time.Time{}, trace.NotFound("access request %q not found", accessRequestID)
}
req := reqs[0]
if !req.GetState().IsApproved() {
if req.GetState().IsDenied() {
return nil, time.Time{}, trace.AccessDenied("access request %q has been denied", accessRequestID)
}
return nil, time.Time{}, trace.BadParameter("access request %q is awaiting approval", accessRequestID)
}
if err := services.ValidateAccessRequest(a, req); err != nil {
return nil, time.Time{}, trace.Wrap(err)
}
accessExpiry := req.GetAccessExpiry()
if accessExpiry.Before(a.GetClock().Now()) {
return nil, time.Time{}, trace.BadParameter("access request %q has expired", accessRequestID)
}
return req.GetRoles(), accessExpiry, nil
}
// CreateWebSession creates a new web session for user without any
// checks, is used by admins
func (a *Server) CreateWebSession(user string) (services.WebSession, error) {

View file

@ -814,11 +814,11 @@ func (a *ServerWithRoles) CreateWebSession(user string) (services.WebSession, er
return a.authServer.CreateWebSession(user)
}
func (a *ServerWithRoles) ExtendWebSession(user, prevSessionID string) (services.WebSession, error) {
func (a *ServerWithRoles) ExtendWebSession(user, prevSessionID, accessRequestID string) (services.WebSession, error) {
if err := a.currentUserAction(user); err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.ExtendWebSession(user, prevSessionID, a.context.Identity.GetIdentity())
return a.authServer.ExtendWebSession(user, prevSessionID, accessRequestID, a.context.Identity.GetIdentity())
}
func (a *ServerWithRoles) GetWebSessionInfo(user string, sid string) (services.WebSession, error) {

View file

@ -1433,11 +1433,12 @@ func (c *Client) GetU2FSignRequest(user string, password []byte) (*u2f.SignReque
// ExtendWebSession creates a new web session for a user based on another
// valid web session
func (c *Client) ExtendWebSession(user string, prevSessionID string) (services.WebSession, error) {
func (c *Client) ExtendWebSession(user string, prevSessionID string, accessRequestID string) (services.WebSession, error) {
out, err := c.PostJSON(
c.Endpoint("users", user, "web", "sessions"),
createWebSessionReq{
PrevSessionID: prevSessionID,
PrevSessionID: prevSessionID,
AccessRequestID: accessRequestID,
},
)
if err != nil {
@ -3188,7 +3189,7 @@ type WebService interface {
GetWebSessionInfo(user string, sid string) (services.WebSession, error)
// ExtendWebSession creates a new web session for a user based on another
// valid web session
ExtendWebSession(user string, prevSessionID string) (services.WebSession, error)
ExtendWebSession(user string, prevSessionID string, accessRequestID string) (services.WebSession, error)
// CreateWebSession creates a new web session for a user
CreateWebSession(user string) (services.WebSession, error)
// DeleteWebSession deletes a web session for this user by id

View file

@ -1419,7 +1419,7 @@ func (s *TLSSuite) TestOTPCRUD(c *check.C) {
// TestWebSessions tests web sessions flow for web user,
// that logs in, extends web session and tries to perform administratvie action
// but fails
func (s *TLSSuite) TestWebSessions(c *check.C) {
func (s *TLSSuite) TestWebSessionWithoutAccessRequest(c *check.C) {
clt, err := s.server.NewClient(TestAdmin())
c.Assert(err, check.IsNil)
@ -1456,7 +1456,7 @@ func (s *TLSSuite) TestWebSessions(c *check.C) {
_, err = web.GetWebSessionInfo(user, ws.GetName())
c.Assert(err, check.IsNil)
new, err := web.ExtendWebSession(user, ws.GetName())
new, err := web.ExtendWebSession(user, ws.GetName(), "")
c.Assert(err, check.IsNil)
c.Assert(new, check.NotNil)
@ -1470,10 +1470,79 @@ func (s *TLSSuite) TestWebSessions(c *check.C) {
_, err = web.GetWebSessionInfo(user, ws.GetName())
c.Assert(err, check.NotNil)
_, err = web.ExtendWebSession(user, ws.GetName())
_, err = web.ExtendWebSession(user, ws.GetName(), "")
c.Assert(err, check.NotNil)
}
func (s *TLSSuite) TestWebSessionWithApprovedAccessRequest(c *check.C) {
clt, err := s.server.NewClient(TestAdmin())
c.Assert(err, check.IsNil)
user := "user2"
pass := []byte("abc123")
newUser, err := CreateUserRoleAndRequestable(clt, user, "test-request-role")
c.Assert(err, check.IsNil)
c.Assert(newUser.GetRoles(), check.HasLen, 1)
initialRole := newUser.GetRoles()[0]
c.Assert(newUser.GetRoles(), check.DeepEquals, []string{"user:user2"})
proxy, err := s.server.NewClient(TestBuiltin(teleport.RoleProxy))
c.Assert(err, check.IsNil)
// Create a user to create a web session for.
req := AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{
Password: pass,
},
}
err = clt.UpsertPassword(user, pass)
c.Assert(err, check.IsNil)
ws, err := proxy.AuthenticateWebUser(req)
c.Assert(err, check.IsNil)
web, err := s.server.NewClientFromWebSession(ws)
c.Assert(err, check.IsNil)
// Create a approved access request.
accessReq, err := services.NewAccessRequest(user, []string{"test-request-role"}...)
c.Assert(err, check.IsNil)
accessReq.SetState(services.RequestState_APPROVED)
err = clt.CreateAccessRequest(context.Background(), accessReq)
c.Assert(err, check.IsNil)
sess, err := web.ExtendWebSession(user, ws.GetName(), accessReq.GetMetadata().Name)
c.Assert(err, check.IsNil)
pub, _, _, _, err := ssh.ParseAuthorizedKey(sess.GetPub())
c.Assert(err, check.IsNil)
sshcert, ok := pub.(*ssh.Certificate)
c.Assert(ok, check.Equals, true)
// Roles extracted from cert should contain the initial role and the role assigned with access request.
roles, _, err := services.ExtractFromCertificate(clt, sshcert)
c.Assert(err, check.IsNil)
c.Assert(roles, check.HasLen, 2)
mappedRole := map[string]string{
roles[0]: "",
roles[1]: "",
}
_, hasRole := mappedRole[initialRole]
c.Assert(hasRole, check.Equals, true)
_, hasRole = mappedRole["test-request-role"]
c.Assert(hasRole, check.Equals, true)
}
// TestGetCertAuthority tests certificate authority permissions
func (s *TLSSuite) TestGetCertAuthority(c *check.C) {
ctx := context.Background()

View file

@ -224,6 +224,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
h.POST("/webapi/sessions/app", h.WithAuth(h.createAppSession))
h.DELETE("/webapi/sessions", h.WithAuth(h.deleteSession))
h.POST("/webapi/sessions/renew", h.WithAuth(h.renewSession))
h.POST("/webapi/sessions/renew/:requestId", h.WithAuth(h.renewSession))
h.GET("/webapi/users/password/token/:token", httplib.MakeHandler(h.getResetPasswordTokenHandle))
h.PUT("/webapi/users/password/token", httplib.WithCSRFProtection(h.changePasswordWithToken))
@ -1259,19 +1260,16 @@ func (h *Handler) logout(w http.ResponseWriter, ctx *SessionContext) error {
return nil
}
// renewSession is called to renew the session that is about to expire
// it issues the new session and generates new session cookie.
// It's important to understand that the old session becomes effectively invalid.
// renewSession is called in two ways:
// - Without requestId: Creates new session that is about to expire.
// - With requestId: Creates new session that includes additional roles assigned with approving access request.
//
// POST /v1/webapi/sessions/renew
//
// Response
//
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
//
//
func (h *Handler) renewSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
newSess, err := ctx.ExtendWebSession()
// It issues the new session and generates new session cookie.
// It's important to understand that the old session becomes effectively invalid.
func (h *Handler) renewSession(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
requestID := params.ByName("requestId")
newSess, err := ctx.ExtendWebSession(requestID)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -242,8 +242,8 @@ func (c *SessionContext) GetWebSession() services.WebSession {
// ExtendWebSession creates a new web session for this user
// based on the previous session
func (c *SessionContext) ExtendWebSession() (services.WebSession, error) {
sess, err := c.clt.ExtendWebSession(c.user, c.sess.GetName())
func (c *SessionContext) ExtendWebSession(accessRequestID string) (services.WebSession, error) {
sess, err := c.clt.ExtendWebSession(c.user, c.sess.GetName(), accessRequestID)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -30,6 +30,15 @@ type access struct {
Delete bool `json:"remove"`
}
type accessStrategy struct {
// Type determines how a user should access teleport resources.
// ie: does the user require a request to access resources?
Type services.RequestStrategy `json:"type"`
// Prompt is the optional dialogue shown to user,
// when the access strategy type requires a reason.
Prompt string `json:"prompt"`
}
type userACL struct {
// Sessions defines access to recorded sessions
Sessions access `json:"sessions"`
@ -60,15 +69,18 @@ const (
authSSO authType = "sso"
)
// UserContext describes a users settings to various resources.
type UserContext struct {
// AuthType is auth method of this user
// AuthType is auth method of this user.
AuthType authType `json:"authType"`
// Name is this user name
// Name is this user name.
Name string `json:"userName"`
// ACL contains user access control list
// ACL contains user access control list.
ACL userACL `json:"userAcl"`
// Cluster contains cluster detail for this user's context
// Cluster contains cluster detail for this user's context.
Cluster *Cluster `json:"cluster"`
// AccessStrategy describes how a user should access teleport resources.
AccessStrategy accessStrategy `json:"accessStrategy"`
}
func getLogins(roleSet services.RoleSet) []string {
@ -115,6 +127,30 @@ func newAccess(roleSet services.RoleSet, ctx *services.Context, kind string) acc
}
}
func getAccessStrategy(roleset services.RoleSet) accessStrategy {
strategy := services.RequestStrategyOptional
prompt := ""
for _, role := range roleset {
options := role.GetOptions()
if options.RequestAccess == services.RequestStrategyReason {
strategy = services.RequestStrategyReason
prompt = options.RequestPrompt
break
}
if options.RequestAccess == services.RequestStrategyAlways {
strategy = services.RequestStrategyAlways
}
}
return accessStrategy{
Type: strategy,
Prompt: prompt,
}
}
// NewUserContext returns user context
func NewUserContext(user services.User, userRoles services.RoleSet) (*UserContext, error) {
ctx := &services.Context{User: user}
@ -128,6 +164,7 @@ func NewUserContext(user services.User, userRoles services.RoleSet) (*UserContex
nodeAccess := newAccess(userRoles, ctx, services.KindNode)
appServerAccess := newAccess(userRoles, ctx, services.KindAppServer)
logins := getLogins(userRoles)
requestAccess := getAccessStrategy(userRoles)
acl := userACL{
AppServers: appServerAccess,
@ -156,8 +193,9 @@ func NewUserContext(user services.User, userRoles services.RoleSet) (*UserContex
}
return &UserContext{
Name: user.GetName(),
ACL: acl,
AuthType: authType,
Name: user.GetName(),
ACL: acl,
AuthType: authType,
AccessStrategy: requestAccess,
}, nil
}

View file

@ -64,6 +64,7 @@ func (s *UserContextSuite) TestNewUserContext(c *check.C) {
c.Assert(userContext.Name, check.Equals, "root")
c.Assert(userContext.ACL.AuthConnectors, check.DeepEquals, allowed)
c.Assert(userContext.ACL.TrustedClusters, check.DeepEquals, allowed)
c.Assert(userContext.ACL.AppServers, check.DeepEquals, denied)
c.Assert(userContext.ACL.Events, check.DeepEquals, denied)
c.Assert(userContext.ACL.Sessions, check.DeepEquals, denied)
c.Assert(userContext.ACL.Roles, check.DeepEquals, denied)
@ -71,6 +72,10 @@ func (s *UserContextSuite) TestNewUserContext(c *check.C) {
c.Assert(userContext.ACL.Tokens, check.DeepEquals, denied)
c.Assert(userContext.ACL.Nodes, check.DeepEquals, denied)
c.Assert(userContext.ACL.SSHLogins, check.DeepEquals, []string{"a", "b", "d"})
c.Assert(userContext.AccessStrategy, check.DeepEquals, accessStrategy{
Type: services.RequestStrategyOptional,
Prompt: "",
})
// test local auth type
c.Assert(userContext.AuthType, check.Equals, authLocal)