authorize: fix headers when impersonating

- Add user impersonation docs.
- Add navbar link to v0.0.5 docs.
This commit is contained in:
Bobby DeSimone 2019-06-11 15:40:28 -07:00
parent fb92466f45
commit 554e62108f
No known key found for this signature in database
GPG key ID: AEE4CF12FE86D07E
8 changed files with 134 additions and 21 deletions

View file

@ -24,6 +24,7 @@ module.exports = {
{ {
text: 'Versions', text: 'Versions',
items: [ items: [
{ text: 'v0.0.5', link: 'https://v0-0-5.docs.pomerium.io/' },
{ text: 'v0.0.4', link: 'https://v0-0-4.docs.pomerium.io/' }, { text: 'v0.0.4', link: 'https://v0-0-4.docs.pomerium.io/' },
] ]
}, },
@ -42,7 +43,7 @@ function guideSidebar(title) {
{ {
title, title,
collapsable: false, collapsable: false,
children: ["", "binary","from-source","helm", "kubernetes", "synology"] children: ["", "binary", "from-source", "helm", "kubernetes", "synology"]
} }
]; ];
} }
@ -52,7 +53,7 @@ function docsSidebar(title) {
{ {
title, title,
collapsable: false, collapsable: false,
children: ["", "identity-providers", "signed-headers", "certificates", "examples", "upgrading"] children: ["", "identity-providers", "signed-headers", "certificates", "examples", "impersonation", "upgrading"]
} }
]; ];
} }

View file

@ -0,0 +1,43 @@
---
title: User impersonation
description: >-
This article describes how to configure Pomerium to allow an administrative
user to impersonate another user or group.
---
# User impersonation
## What
User impersonation allows administrative users to temporarily "sign in as" another user in pomerium. Users with impersonation permissions can impersonate all other users and groups. The impersonating user will be subject to the authorization and access policies of the impersonated user.
## Why
In certain circumstances, it's useful for an administrative user to impersonate another user. For example:
- To help a user troubleshoot an issue. If your downstream authorization policies are configured differently, it's possible that your UI will look different from theirs and you'll need to impersonate the other user to be able to see what they see.
- You want to make changes on behalf of another user (for example, the other user is away on vacation and you want to manage their orders or run a report).
- You're an administrator who's setting up authorization policies, and you want to preview what other users will be able to see depending on the permissions you grant them.
## How
1. Add an administrator to your [configuration settings].
2. Navigate to user dashboard for any proxied route. (e.g. `https://{your-domain}/.pomerium`)
3. Add the `email` and `user groups` you want to impersonate.
4. That's it!
::: warning
**Note!** On session refresh, impersonation will be reset.
:::
## Example
Here's what it looks like.
<video width="100%" height="600" controls=""><source src="./impersonation/pomerium-user-impersonation.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
[configuration settings]: ../reference/#administrators

View file

@ -36,6 +36,28 @@ func (s *SessionState) RefreshPeriodExpired() bool {
return isExpired(s.RefreshDeadline) return isExpired(s.RefreshDeadline)
} }
// Impersonating returns if the request is impersonating.
func (s *SessionState) Impersonating() bool {
return s.ImpersonateEmail != "" || len(s.ImpersonateGroups) != 0
}
// RequestEmail is the email to make the request as.
func (s *SessionState) RequestEmail() string {
if s.ImpersonateEmail != "" {
return s.ImpersonateEmail
}
return s.Email
}
// RequestGroups returns the groups of the Groups making the request; uses
// impersonating user if set.
func (s *SessionState) RequestGroups() string {
if len(s.ImpersonateGroups) != 0 {
return strings.Join(s.ImpersonateGroups, ",")
}
return strings.Join(s.Groups, ",")
}
type idToken struct { type idToken struct {
Issuer string `json:"iss"` Issuer string `json:"iss"`
Subject string `json:"sub"` Subject string `json:"sub"`

View file

@ -100,3 +100,41 @@ func TestSessionState_IssuedAt(t *testing.T) {
}) })
} }
} }
func TestSessionState_Impersonating(t *testing.T) {
t.Parallel()
tests := []struct {
name string
Email string
Groups []string
ImpersonateEmail string
ImpersonateGroups []string
want bool
wantResponseEmail string
wantResponseGroups string
}{
{"impersonating", "actual@user.com", []string{"actual-group"}, "impersonating@user.com", []string{"impersonating-group"}, true, "impersonating@user.com", "impersonating-group"},
{"not impersonating", "actual@user.com", []string{"actual-group"}, "", []string{}, false, "actual@user.com", "actual-group"},
{"impersonating user only", "actual@user.com", []string{"actual-group"}, "impersonating@user.com", []string{}, true, "impersonating@user.com", "actual-group"},
{"impersonating group only", "actual@user.com", []string{"actual-group"}, "", []string{"impersonating-group"}, true, "actual@user.com", "impersonating-group"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SessionState{
Email: tt.Email,
Groups: tt.Groups,
ImpersonateEmail: tt.ImpersonateEmail,
ImpersonateGroups: tt.ImpersonateGroups,
}
if got := s.Impersonating(); got != tt.want {
t.Errorf("SessionState.Impersonating() = %v, want %v", got, tt.want)
}
if gotEmail := s.RequestEmail(); gotEmail != tt.wantResponseEmail {
t.Errorf("SessionState.RequestEmail() = %v, want %v", gotEmail, tt.wantResponseEmail)
}
if gotGroups := s.RequestGroups(); gotGroups != tt.wantResponseGroups {
t.Errorf("SessionState.v() = %v, want %v", gotGroups, tt.wantResponseGroups)
}
})
}
}

View file

@ -27,6 +27,7 @@ func TestAuthorizeGRPC_Authorize(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{"good", "hello.pomerium.io", &sessions.SessionState{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false}, {"good", "hello.pomerium.io", &sessions.SessionState{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false},
{"impersonate request", "hello.pomerium.io", &sessions.SessionState{User: "admin@pomerium.io", Email: "admin@pomerium.io", ImpersonateEmail: "other@other.example"}, true, false},
{"session cannot be nil", "hello.pomerium.io", nil, false, true}, {"session cannot be nil", "hello.pomerium.io", nil, false, true},
} }
for _, tt := range tests { for _, tt := range tests {

View file

@ -214,7 +214,7 @@ func isCORSPreflight(r *http.Request) bool {
// or starting the authenticate service for validation if not. // or starting the authenticate service for validation if not.
func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) { func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
if !p.shouldSkipAuthentication(r) { if !p.shouldSkipAuthentication(r) {
session, err := p.sessionStore.LoadSession(r) s, err := p.sessionStore.LoadSession(r)
if err != nil { if err != nil {
switch err { switch err {
case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
@ -230,23 +230,30 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
} }
} }
if err = p.authenticate(w, r, session); err != nil { if err = p.authenticate(w, r, s); err != nil {
p.sessionStore.ClearSession(w, r) p.sessionStore.ClearSession(w, r)
log.Debug().Err(err).Msg("proxy: user unauthenticated") log.FromRequest(r).Debug().Err(err).Msg("proxy: user unauthenticated")
httpErr := &httputil.Error{ httpErr := &httputil.Error{Message: "User unauthenticated", Code: http.StatusForbidden, CanDebug: true}
Message: "User unauthenticated",
Code: http.StatusForbidden,
CanDebug: true}
httputil.ErrorResponse(w, r, httpErr) httputil.ErrorResponse(w, r, httpErr)
return return
} }
authorized, err := p.AuthorizeClient.Authorize(r.Context(), r.Host, session) authorized, err := p.AuthorizeClient.Authorize(r.Context(), r.Host, s)
if err != nil || !authorized { if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: failed authorization")
httpErr := &httputil.Error{Code: http.StatusInternalServerError}
httputil.ErrorResponse(w, r, httpErr)
return
}
if !authorized {
log.FromRequest(r).Warn().Err(err).Msg("proxy: user unauthorized") log.FromRequest(r).Warn().Err(err).Msg("proxy: user unauthorized")
httpErr := &httputil.Error{Code: http.StatusUnauthorized, CanDebug: true} httpErr := &httputil.Error{Code: http.StatusUnauthorized, CanDebug: true}
httputil.ErrorResponse(w, r, httpErr) httputil.ErrorResponse(w, r, httpErr)
return return
} }
r.Header.Set(HeaderUserID, s.User)
r.Header.Set(HeaderEmail, s.RequestEmail())
r.Header.Set(HeaderGroups, s.RequestGroups())
} }
// We have validated the users request and now proxy their request to the provided upstream. // We have validated the users request and now proxy their request to the provided upstream.
@ -324,7 +331,7 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
} }
// Refresh redeems and extends an existing authenticated oidc session with // Refresh redeems and extends an existing authenticated oidc session with
// the underlying idenity provider. All session details including groups, // the underlying identity provider. All session details including groups,
// timeouts, will be renewed. // timeouts, will be renewed.
func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) { func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
session, err := p.sessionStore.LoadSession(r) session, err := p.sessionStore.LoadSession(r)
@ -436,25 +443,22 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
// Authenticate authenticates a request by checking for a session cookie, and validating its expiration, // Authenticate authenticates a request by checking for a session cookie, and validating its expiration,
// clearing the session cookie if it's invalid and returning an error if necessary.. // clearing the session cookie if it's invalid and returning an error if necessary..
func (p *Proxy) authenticate(w http.ResponseWriter, r *http.Request, session *sessions.SessionState) error { func (p *Proxy) authenticate(w http.ResponseWriter, r *http.Request, s *sessions.SessionState) error {
if session.RefreshPeriodExpired() { if s.RefreshPeriodExpired() {
session, err := p.AuthenticateClient.Refresh(r.Context(), session) s, err := p.AuthenticateClient.Refresh(r.Context(), s)
if err != nil { if err != nil {
return fmt.Errorf("proxy: session refresh failed : %v", err) return fmt.Errorf("proxy: session refresh failed : %v", err)
} }
err = p.sessionStore.SaveSession(w, r, session) err = p.sessionStore.SaveSession(w, r, s)
if err != nil { if err != nil {
return fmt.Errorf("proxy: refresh failed : %v", err) return fmt.Errorf("proxy: refresh failed : %v", err)
} }
} else { } else {
valid, err := p.AuthenticateClient.Validate(r.Context(), session.IDToken) valid, err := p.AuthenticateClient.Validate(r.Context(), s.IDToken)
if err != nil || !valid { if err != nil || !valid {
return fmt.Errorf("proxy: session valid: %v : %v", valid, err) return fmt.Errorf("proxy: session valid: %v : %v", valid, err)
} }
} }
r.Header.Set(HeaderUserID, session.User)
r.Header.Set(HeaderEmail, session.Email)
r.Header.Set(HeaderGroups, strings.Join(session.Groups, ","))
return nil return nil
} }

View file

@ -301,6 +301,8 @@ func TestProxy_Proxy(t *testing.T) {
}{ }{
{"good", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK}, {"good", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
{"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK}, {"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
{"good email impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateEmail: "test@user.example"}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
{"good group impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateGroups: []string{"group1", "group2"}}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
// same request as above, but with cors_allow_preflight=false in the policy // same request as above, but with cors_allow_preflight=false in the policy
{"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized}, {"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized},
// cors allowed, but the request is missing proper headers // cors allowed, but the request is missing proper headers
@ -308,7 +310,8 @@ func TestProxy_Proxy(t *testing.T) {
{"unexpected error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("ok")}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError}, {"unexpected error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("ok")}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError},
// redirect to start auth process // redirect to start auth process
{"unknown host", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound}, {"unknown host", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
{"user forbidden", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized}, {"user not authorized", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized},
{"authorization call failed", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeError: errors.New("error")}, http.StatusInternalServerError},
// authenticate errors // authenticate errors
{"weird load session error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("weird"), Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError}, {"weird load session error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("weird"), Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError},
{"failed refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RefreshError: errors.New("refresh error")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden}, {"failed refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RefreshError: errors.New("refresh error")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
@ -473,6 +476,7 @@ func TestProxy_Impersonate(t *testing.T) {
{"decrypted csrf mismatch", false, opts, http.MethodPost, "user@blah.com", "", "CSRF!", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusForbidden}, {"decrypted csrf mismatch", false, opts, http.MethodPost, "user@blah.com", "", "CSRF!", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusForbidden},
{"save session failure", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, {"save session failure", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
{"malformed", true, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest}, {"malformed", true, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest},
{"groups", false, opts, http.MethodPost, "user@blah.com", "group1,group2", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {