mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 18:06:34 +02:00
authenticate/proxy: add user impersonation, refresh, dashboard (#123)
proxy: Add user dashboard. [GH-123] proxy/authenticate: Add manual refresh of their session. [GH-73] authorize: Add administrator (super user) account support. [GH-110] internal/policy: Allow administrators to impersonate other users. [GH-110]
This commit is contained in:
parent
dc2eb9668c
commit
66b4c2d3cd
42 changed files with 1644 additions and 1006 deletions
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -1,5 +1,36 @@
|
|||
# Pomerium Changelog
|
||||
|
||||
## vUNRELEASED
|
||||
|
||||
### NEW
|
||||
|
||||
- Add user dashboard containing information about the current user's session. [GH-123]
|
||||
- Add functionality allowing users to initiate manual refresh of their session. This is helpful when a user's access control details are updated but their session hasn't updated yet. To prevent abuse, manual refresh is gated by a cooldown (`REFRESH_COOLDOWN`) which defaults to five minutes. [GH-73]
|
||||
- Add Administrator (super user) account support (`ADMINISTRATORS`). [GH-110]
|
||||
- Add feature that allows Administrators to impersonate / sign-in as another user from the user dashboard. [GH-110]
|
||||
- Add docker images and builds for ARM. [GH-95]
|
||||
- Add support for public, unauthenticated routes. [GH-129]
|
||||
|
||||
### CHANGED
|
||||
|
||||
- User state is now maintained and scoped at the domain level vs at the route level. [GH-128]
|
||||
- Error pages contain a link to sign out from the current user session. [GH-100]
|
||||
- Removed `LifetimeDeadline` from `sessions.SessionState`.
|
||||
- Removed favicon specific request handling. [GH-131]
|
||||
- Headers are now configurable via the `HEADERS` configuration variable. [GH-108]
|
||||
- Refactored proxy and authenticate services to share the same session state cookie. [GH-131]
|
||||
- Removed instances of extraneous session state saves. [GH-131]
|
||||
- Changed default behavior when no session is found. Users are now redirected to login instead of being shown an error page.[GH-131]
|
||||
- Updated routes such that all http handlers are now wrapped with a standard set of middleware. Headers, request id, loggers, and health checks middleware are now applied to all routes including 4xx and 5xx responses. [GH-116]
|
||||
- Changed docker images to be built from [distroless](https://github.com/GoogleContainerTools/distroless). This fixed an issue with `nsswitch` [GH-97], includes `ca-certificates` and limits the attack surface area of our images. [GH-101]
|
||||
- Changed HTTP to HTTPS redirect server to be user configurable via `HTTP_REDIRECT_ADDR`. [GH-103]
|
||||
- `Content-Security-Policy` hash updated to match new UI assets.
|
||||
|
||||
### FIXED
|
||||
|
||||
- Fixed an issue where policy and routes were being pre-processed incorrectly. [GH-132]
|
||||
- Fixed an issue where `golint` was not being found in our docker image. [GH-121]
|
||||
|
||||
## v0.0.4
|
||||
|
||||
### CHANGED
|
||||
|
|
|
@ -61,22 +61,19 @@ func TestAuthenticate_Refresh(t *testing.T) {
|
|||
{"good",
|
||||
&identity.MockProvider{
|
||||
RefreshResponse: &sessions.SessionState{
|
||||
AccessToken: "updated",
|
||||
LifetimeDeadline: fixedDate,
|
||||
RefreshDeadline: fixedDate,
|
||||
AccessToken: "updated",
|
||||
RefreshDeadline: fixedDate,
|
||||
}},
|
||||
&pb.Session{
|
||||
AccessToken: "original",
|
||||
LifetimeDeadline: fixedProtoTime,
|
||||
RefreshDeadline: fixedProtoTime,
|
||||
AccessToken: "original",
|
||||
RefreshDeadline: fixedProtoTime,
|
||||
},
|
||||
&pb.Session{
|
||||
AccessToken: "updated",
|
||||
LifetimeDeadline: fixedProtoTime,
|
||||
RefreshDeadline: fixedProtoTime,
|
||||
AccessToken: "updated",
|
||||
RefreshDeadline: fixedProtoTime,
|
||||
},
|
||||
false},
|
||||
{"test error", &identity.MockProvider{RefreshError: errors.New("hi")}, &pb.Session{RefreshToken: "refresh token", RefreshDeadline: fixedProtoTime, LifetimeDeadline: fixedProtoTime}, nil, true},
|
||||
{"test error", &identity.MockProvider{RefreshError: errors.New("hi")}, &pb.Session{RefreshToken: "refresh token", RefreshDeadline: fixedProtoTime}, nil, true},
|
||||
{"test catch nil", nil, nil, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@ -105,7 +102,6 @@ func TestAuthenticate_Authenticate(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("expected to be able to create cipher: %v", err)
|
||||
}
|
||||
lt := time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC()
|
||||
rt := time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC()
|
||||
vtProto, err := ptypes.TimestampProto(rt)
|
||||
if err != nil {
|
||||
|
@ -113,22 +109,19 @@ func TestAuthenticate_Authenticate(t *testing.T) {
|
|||
}
|
||||
|
||||
want := &sessions.SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: lt,
|
||||
RefreshDeadline: rt,
|
||||
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: rt,
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}
|
||||
|
||||
goodReply := &pb.Session{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: vtProto,
|
||||
RefreshDeadline: vtProto,
|
||||
Email: "user@domain.com",
|
||||
User: "user"}
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: vtProto,
|
||||
Email: "user@domain.com",
|
||||
User: "user"}
|
||||
ciphertext, err := sessions.MarshalSession(want, c)
|
||||
if err != nil {
|
||||
t.Fatalf("expected to be encode session: %v", err)
|
||||
|
|
|
@ -12,13 +12,16 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// CSPHeaders adds content security headers for authenticate's handlers
|
||||
var CSPHeaders = map[string]string{
|
||||
"Content-Security-Policy": "default-src 'none'; style-src 'self' 'sha256-pSTVzZsFAqd2U3QYu+BoBDtuJWaPM/+qMy/dBRrhb5Y='; img-src 'self';",
|
||||
"Referrer-Policy": "Same-origin",
|
||||
"Content-Security-Policy": "default-src 'none'; style-src 'self'" +
|
||||
" 'sha256-z9MsgkMbQjRSLxzAfN55jB3a9pP0PQ4OHFH8b4iDP6s=' " +
|
||||
" 'sha256-qnVkQSG7pWu17hBhIw0kCpfEB3XGvt0mNRa6+uM6OUU=' " +
|
||||
" 'sha256-qOdRsNZhtR+htazbcy7guQl3Cn1cqOw1FcE4d3llae0='; " +
|
||||
"img-src 'self';",
|
||||
"Referrer-Policy": "Same-origin",
|
||||
}
|
||||
|
||||
// Handler returns the authenticate service's HTTP request multiplexer, and routes.
|
||||
|
@ -35,7 +38,7 @@ func (a *Authenticate) Handler() http.Handler {
|
|||
mux.Handle("/oauth2/callback", c.ThenFunc(a.OAuthCallback))
|
||||
// authenticate-server endpoints
|
||||
mux.Handle("/sign_in", validate.ThenFunc(a.SignIn))
|
||||
mux.Handle("/sign_out", validate.ThenFunc(a.SignOut)) // GET POST
|
||||
mux.Handle("/sign_out", validate.ThenFunc(a.SignOut)) // POST
|
||||
return mux
|
||||
}
|
||||
|
||||
|
@ -53,7 +56,7 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request, sess
|
|||
}
|
||||
err = a.sessionStore.SaveSession(w, r, session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("authenticate: refresh failed : %v", err)
|
||||
return fmt.Errorf("authenticate: failed saving refreshed session : %v", err)
|
||||
}
|
||||
} else {
|
||||
valid, err := a.provider.Validate(r.Context(), session.IDToken)
|
||||
|
@ -86,14 +89,7 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
a.ProxyCallback(w, r, session)
|
||||
}
|
||||
|
||||
// ProxyCallback redirects the user back to proxy service along with an encrypted payload, as
|
||||
// url params, of the user's session state as specified in RFC6749 3.1.2.
|
||||
// https://tools.ietf.org/html/rfc6749#section-3.1.2
|
||||
func (a *Authenticate) ProxyCallback(w http.ResponseWriter, r *http.Request, session *sessions.SessionState) {
|
||||
err := r.ParseForm()
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -104,14 +100,8 @@ func (a *Authenticate) ProxyCallback(w http.ResponseWriter, r *http.Request, ses
|
|||
httputil.ErrorResponse(w, r, "no state parameter supplied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// redirect url of proxy-service
|
||||
redirectURI := r.Form.Get("redirect_uri")
|
||||
if redirectURI == "" {
|
||||
httputil.ErrorResponse(w, r, "no redirect_uri parameter supplied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := url.Parse(redirectURI)
|
||||
redirectURL, err := url.Parse(r.Form.Get("redirect_uri"))
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, "malformed redirect_uri parameter passed", http.StatusBadRequest)
|
||||
return
|
||||
|
@ -145,40 +135,11 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// pretty safe to say that no matter what heppanes here, we want to revoke the local session
|
||||
redirectURI := r.Form.Get("redirect_uri")
|
||||
session, err := a.sessionStore.LoadSession(r)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("authenticate: signout failed to load session")
|
||||
httputil.ErrorResponse(w, r, "No session found to log out", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet {
|
||||
signature := r.Form.Get("sig")
|
||||
timestamp := r.Form.Get("ts")
|
||||
destinationURL, err := url.Parse(redirectURI)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("authenticate: malformed destination url")
|
||||
httputil.ErrorResponse(w, r, "Malformed destination URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t := struct {
|
||||
Redirect string
|
||||
Signature string
|
||||
Timestamp string
|
||||
Destination string
|
||||
Email string
|
||||
Version string
|
||||
}{
|
||||
Redirect: redirectURI,
|
||||
Signature: signature,
|
||||
Timestamp: timestamp,
|
||||
Destination: destinationURL.Host,
|
||||
Email: session.Email,
|
||||
Version: version.FullVersion(),
|
||||
}
|
||||
a.templates.ExecuteTemplate(w, "sign_out.html", t)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
log.Error().Err(err).Msg("authenticate: no session to signout, redirect and clear")
|
||||
http.Redirect(w, r, redirectURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
a.sessionStore.ClearSession(w, r)
|
||||
|
@ -196,6 +157,7 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
|
|||
func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
||||
authRedirectURL := a.RedirectURL.ResolveReference(r.URL)
|
||||
|
||||
// generate a nonce to check following authentication with the IdP
|
||||
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
|
||||
a.csrfStore.SetCSRF(w, r, nonce)
|
||||
|
||||
|
@ -204,6 +166,7 @@ func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, "Invalid redirect parameter: redirect uri not from the root domain", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// verify proxy url is from the root domain
|
||||
proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri"))
|
||||
if err != nil || !middleware.SameSubdomain(proxyRedirectURL, a.RedirectURL) {
|
||||
|
@ -221,6 +184,7 @@ func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// concat base64'd nonce and authenticate url to make state
|
||||
state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String())))
|
||||
|
||||
// build the provider sign in url
|
||||
signInURL := a.provider.GetSignInURL(state)
|
||||
http.Redirect(w, r, signInURL, http.StatusFound)
|
||||
|
@ -242,12 +206,14 @@ func (a *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, "Internal Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// redirect back to the proxy-service
|
||||
// redirect back to the proxy-service via sign_in
|
||||
log.Info().Interface("redirect", redirect).Msg("proxy: OAuthCallback")
|
||||
http.Redirect(w, r, redirect, http.StatusFound)
|
||||
}
|
||||
|
||||
// getOAuthCallback completes the oauth cycle from an identity provider's callback
|
||||
func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
// handle the callback response from the identity provider
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
|
||||
|
@ -263,12 +229,14 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
|
|||
return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Missing Code"}
|
||||
}
|
||||
|
||||
// validate the returned code with the identity provider
|
||||
session, err := a.provider.Authenticate(code)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: error redeeming authenticate code")
|
||||
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
|
||||
}
|
||||
|
||||
// okay, time to go back to the proxy service.
|
||||
bytes, err := base64.URLEncoding.DecodeString(r.Form.Get("state"))
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed decoding state")
|
||||
|
@ -281,30 +249,26 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
|
|||
nonce := s[0]
|
||||
redirect := s[1]
|
||||
c, err := a.csrfStore.GetCSRF(r)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Interface("s", s).Msg("authenticate: bad csrf")
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Missing CSRF token"}
|
||||
}
|
||||
a.csrfStore.ClearCSRF(w, r)
|
||||
if c.Value != nonce {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf mismatch")
|
||||
defer a.csrfStore.ClearCSRF(w, r)
|
||||
if err != nil || c.Value != nonce {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf failure")
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "CSRF failed"}
|
||||
}
|
||||
|
||||
redirectURL, err := url.Parse(redirect)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: couldn't parse redirect url")
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Couldn't parse redirect url"}
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: malformed redirect url")
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Malformed redirect url"}
|
||||
}
|
||||
|
||||
// sanity check, we are redirecting back to the same subdomain right?
|
||||
if !middleware.SameSubdomain(redirectURL, a.RedirectURL) {
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Invalid Redirect URI domain"}
|
||||
}
|
||||
|
||||
err = a.sessionStore.SaveSession(w, r, session)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("internal error")
|
||||
log.Error().Err(err).Msg("authenticate: failed saving new session")
|
||||
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: "Internal Error"}
|
||||
}
|
||||
|
||||
return redirect, nil
|
||||
}
|
||||
|
|
|
@ -183,96 +183,6 @@ func (a mockCipher) Unmarshal(s string, i interface{}) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
func TestAuthenticate_ProxyCallback(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
uri string
|
||||
state string
|
||||
authCode string
|
||||
|
||||
sessionState *sessions.SessionState
|
||||
sessionStore sessions.SessionStore
|
||||
wantCode int
|
||||
wantBody string
|
||||
}{
|
||||
{"good", "https://corp.pomerium.io/", "state", "code",
|
||||
&sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
&sessions.MockSessionStore{},
|
||||
302,
|
||||
"<a href=\"https://corp.pomerium.io/?code=ok&state=state\">Found</a>."},
|
||||
{"no state",
|
||||
"https://corp.pomerium.io/",
|
||||
"",
|
||||
"code",
|
||||
&sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
&sessions.MockSessionStore{},
|
||||
403,
|
||||
"no state parameter supplied"},
|
||||
{"no redirect_url",
|
||||
"",
|
||||
"state",
|
||||
"code",
|
||||
&sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
&sessions.MockSessionStore{},
|
||||
403,
|
||||
"no redirect_uri parameter"},
|
||||
{"malformed redirect_url",
|
||||
"https://pomerium.com%zzzzz",
|
||||
"state",
|
||||
"code",
|
||||
&sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
&sessions.MockSessionStore{},
|
||||
400,
|
||||
"malformed redirect_uri"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &Authenticate{
|
||||
sessionStore: tt.sessionStore,
|
||||
cipher: mockCipher{},
|
||||
}
|
||||
u, _ := url.Parse("https://pomerium.io/redirect")
|
||||
params, _ := url.ParseQuery(u.RawQuery)
|
||||
params.Set("code", tt.authCode)
|
||||
params.Set("state", tt.state)
|
||||
params.Set("redirect_uri", tt.uri)
|
||||
|
||||
u.RawQuery = params.Encode()
|
||||
|
||||
r := httptest.NewRequest("GET", u.String(), nil)
|
||||
w := httptest.NewRecorder()
|
||||
a.ProxyCallback(w, r, tt.sessionState)
|
||||
if status := w.Code; status != tt.wantCode {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
|
||||
}
|
||||
if body := w.Body.String(); !strings.Contains(body, tt.wantBody) {
|
||||
t.Errorf("handler returned wrong body Body: got \n%s \n%s", body, tt.wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getAuthCodeRedirectURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
@ -350,59 +260,6 @@ func TestAuthenticate_SignOut(t *testing.T) {
|
|||
},
|
||||
http.StatusBadRequest,
|
||||
"could not revoke"},
|
||||
|
||||
{"good get",
|
||||
http.MethodGet,
|
||||
"https://corp.pomerium.io/",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{},
|
||||
&sessions.MockSessionStore{
|
||||
Session: &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
Email: "blah@blah.com",
|
||||
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
},
|
||||
http.StatusOK,
|
||||
"This will also sign you out of other internal apps."},
|
||||
{"cannot load session",
|
||||
http.MethodGet,
|
||||
"https://corp.pomerium.io/",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{},
|
||||
&sessions.MockSessionStore{
|
||||
LoadError: errors.New("uh oh"),
|
||||
Session: &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
Email: "blah@blah.com",
|
||||
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
"No session found to log out"},
|
||||
{"bad redirect url get",
|
||||
http.MethodGet,
|
||||
"https://pomerium.com%zzzzz",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{},
|
||||
&sessions.MockSessionStore{
|
||||
Session: &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
Email: "blah@blah.com",
|
||||
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
"Error"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -49,17 +49,17 @@ func New(opts *config.Options) (*Authorize, error) {
|
|||
|
||||
return &Authorize{
|
||||
SharedKey: string(sharedKey),
|
||||
identityAccess: NewIdentityWhitelist(opts.Policies),
|
||||
identityAccess: NewIdentityWhitelist(opts.Policies, opts.Administrators),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewIdentityWhitelist returns an indentity validator.
|
||||
// todo(bdd) : a radix-tree implementation is probably more efficient
|
||||
func NewIdentityWhitelist(policies []policy.Policy, admins []string) IdentityValidator {
|
||||
return newIdentityWhitelistMap(policies, admins)
|
||||
}
|
||||
|
||||
// ValidIdentity returns if an identity is authorized to access a route resource.
|
||||
func (a *Authorize) ValidIdentity(route string, identity *Identity) bool {
|
||||
return a.identityAccess.Valid(route, identity)
|
||||
}
|
||||
|
||||
// NewIdentityWhitelist returns an indentity validator.
|
||||
// todo(bdd) : a radix-tree implementation is probably more efficient
|
||||
func NewIdentityWhitelist(policies []policy.Policy) IdentityValidator {
|
||||
return newIdentityWhitelistMap(policies)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,24 @@ import (
|
|||
|
||||
// Authorize validates the user identity, device, and context of a request for
|
||||
// a given route. Currently only checks identity.
|
||||
func (a *Authorize) Authorize(ctx context.Context, in *pb.AuthorizeRequest) (*pb.AuthorizeReply, error) {
|
||||
ok := a.ValidIdentity(in.Route, &Identity{in.User, in.Email, in.Groups})
|
||||
func (a *Authorize) Authorize(ctx context.Context, in *pb.Identity) (*pb.AuthorizeReply, error) {
|
||||
ok := a.ValidIdentity(in.Route,
|
||||
&Identity{
|
||||
User: in.User,
|
||||
Email: in.Email,
|
||||
Groups: in.Groups,
|
||||
ImpersonateEmail: in.ImpersonateEmail,
|
||||
ImpersonateGroups: in.ImpersonateGroups,
|
||||
})
|
||||
return &pb.AuthorizeReply{IsValid: ok}, nil
|
||||
}
|
||||
|
||||
// IsAdmin validates the user is an administrative user.
|
||||
func (a *Authorize) IsAdmin(ctx context.Context, in *pb.Identity) (*pb.IsAdminReply, error) {
|
||||
ok := a.identityAccess.IsAdmin(
|
||||
&Identity{
|
||||
Email: in.Email,
|
||||
Groups: in.Groups,
|
||||
})
|
||||
return &pb.IsAdminReply{IsAdmin: ok}, nil
|
||||
}
|
||||
|
|
|
@ -16,12 +16,12 @@ func TestAuthorize_Authorize(t *testing.T) {
|
|||
name string
|
||||
SharedKey string
|
||||
identityAccess IdentityValidator
|
||||
in *pb.AuthorizeRequest
|
||||
in *pb.Identity
|
||||
want *pb.AuthorizeReply
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: true}, &pb.AuthorizeRequest{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: true}, false},
|
||||
{"invalid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: false}, &pb.AuthorizeRequest{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: false}, false},
|
||||
{"valid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: true}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: true}, false},
|
||||
{"invalid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: false}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: false}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -37,3 +37,33 @@ func TestAuthorize_Authorize(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize_IsAdmin(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
identityAccess IdentityValidator
|
||||
in *pb.Identity
|
||||
want *pb.IsAdminReply
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid authorization request", &MockIdentityValidator{IsAdminResponse: true}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.IsAdminReply{IsAdmin: true}, false},
|
||||
{"invalid authorization request", &MockIdentityValidator{IsAdminResponse: false}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.IsAdminReply{IsAdmin: false}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &Authorize{
|
||||
SharedKey: "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y",
|
||||
identityAccess: tt.identityAccess,
|
||||
}
|
||||
got, err := a.IsAdmin(context.Background(), tt.in)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Authorize.IsAdmin() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Authorize.IsAdmin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,14 +14,26 @@ type Identity struct {
|
|||
User string
|
||||
Email string
|
||||
Groups []string
|
||||
// Impersonation
|
||||
ImpersonateEmail string
|
||||
ImpersonateGroups []string
|
||||
}
|
||||
|
||||
// EmailDomain returns the domain of the identity's email.
|
||||
func (i *Identity) EmailDomain() string {
|
||||
if i.Email == "" {
|
||||
// IsImpersonating returns whether the user is trying to impersonate another
|
||||
// user email or group.
|
||||
func (i *Identity) IsImpersonating() bool {
|
||||
if i.ImpersonateEmail != "" || len(i.ImpersonateGroups) != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// EmailDomain returns the domain portion of an email.
|
||||
func EmailDomain(email string) string {
|
||||
if email == "" {
|
||||
return ""
|
||||
}
|
||||
comp := strings.Split(i.Email, "@")
|
||||
comp := strings.Split(email, "@")
|
||||
if len(comp) != 2 || comp[0] == "" {
|
||||
return ""
|
||||
}
|
||||
|
@ -32,96 +44,141 @@ func (i *Identity) EmailDomain() string {
|
|||
// to a given route.
|
||||
type IdentityValidator interface {
|
||||
Valid(string, *Identity) bool
|
||||
IsAdmin(*Identity) bool
|
||||
}
|
||||
|
||||
type identityWhitelist struct {
|
||||
type whitelist struct {
|
||||
sync.RWMutex
|
||||
m map[string]bool
|
||||
access map[string]bool
|
||||
admins map[string]bool
|
||||
}
|
||||
|
||||
// newIdentityWhitelistMap takes a slice of policies and creates a hashmap of identity
|
||||
// authorizations per-route for each allowed group, domain, and email.
|
||||
func newIdentityWhitelistMap(policies []policy.Policy) *identityWhitelist {
|
||||
var im identityWhitelist
|
||||
im.m = make(map[string]bool, len(policies)*3)
|
||||
func newIdentityWhitelistMap(policies []policy.Policy, admins []string) *whitelist {
|
||||
var wl whitelist
|
||||
wl.access = make(map[string]bool, len(policies)*3)
|
||||
for _, p := range policies {
|
||||
for _, group := range p.AllowedGroups {
|
||||
wl.PutGroup(p.From, group)
|
||||
log.Debug().Str("route", p.From).Str("group", group).Msg("add group")
|
||||
im.PutGroup(p.From, group)
|
||||
}
|
||||
for _, domain := range p.AllowedDomains {
|
||||
im.PutDomain(p.From, domain)
|
||||
log.Debug().Str("route", p.From).Str("group", domain).Msg("add domain")
|
||||
|
||||
wl.PutDomain(p.From, domain)
|
||||
log.Debug().Str("route", p.From).Str("domain", domain).Msg("add domain")
|
||||
}
|
||||
for _, email := range p.AllowedEmails {
|
||||
im.PutEmail(p.From, email)
|
||||
log.Debug().Str("route", p.From).Str("group", email).Msg("add email")
|
||||
wl.PutEmail(p.From, email)
|
||||
log.Debug().Str("route", p.From).Str("email", email).Msg("add email")
|
||||
}
|
||||
}
|
||||
return &im
|
||||
|
||||
wl.admins = make(map[string]bool, len(admins))
|
||||
for _, admin := range admins {
|
||||
wl.PutAdmin(admin)
|
||||
log.Debug().Str("admin", admin).Msg("add administrator")
|
||||
}
|
||||
return &wl
|
||||
}
|
||||
|
||||
// Valid reports whether an identity has valid access for a given route.
|
||||
func (m *identityWhitelist) Valid(route string, i *Identity) bool {
|
||||
if ok := m.Domain(route, i.EmailDomain()); ok {
|
||||
func (wl *whitelist) Valid(route string, i *Identity) bool {
|
||||
email := i.Email
|
||||
domain := EmailDomain(email)
|
||||
groups := i.Groups
|
||||
|
||||
// if user is admin, and wants to impersonate, override values
|
||||
if wl.IsAdmin(i) && i.IsImpersonating() {
|
||||
email = i.ImpersonateEmail
|
||||
domain = EmailDomain(email)
|
||||
groups = i.ImpersonateGroups
|
||||
}
|
||||
|
||||
if ok := wl.Email(route, email); ok {
|
||||
return ok
|
||||
}
|
||||
if ok := m.Email(route, i.Email); ok {
|
||||
if ok := wl.Domain(route, domain); ok {
|
||||
return ok
|
||||
}
|
||||
for _, group := range i.Groups {
|
||||
if ok := m.Group(route, group); ok {
|
||||
for _, group := range groups {
|
||||
if ok := wl.Group(route, group); ok {
|
||||
return ok
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (wl *whitelist) IsAdmin(i *Identity) bool {
|
||||
if ok := wl.Admin(i.Email); ok {
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Group retrieves per-route access given a group name.
|
||||
func (m *identityWhitelist) Group(route, group string) bool {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.m[fmt.Sprintf("%s|group:%s", route, group)]
|
||||
func (wl *whitelist) Group(route, group string) bool {
|
||||
wl.RLock()
|
||||
defer wl.RUnlock()
|
||||
return wl.access[fmt.Sprintf("%s|group:%s", route, group)]
|
||||
}
|
||||
|
||||
// PutGroup adds an access entry for a route given a group name.
|
||||
func (m *identityWhitelist) PutGroup(route, group string) {
|
||||
m.Lock()
|
||||
m.m[fmt.Sprintf("%s|group:%s", route, group)] = true
|
||||
m.Unlock()
|
||||
func (wl *whitelist) PutGroup(route, group string) {
|
||||
wl.Lock()
|
||||
wl.access[fmt.Sprintf("%s|group:%s", route, group)] = true
|
||||
wl.Unlock()
|
||||
}
|
||||
|
||||
// Domain retrieves per-route access given a domain name.
|
||||
func (m *identityWhitelist) Domain(route, domain string) bool {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.m[fmt.Sprintf("%s|domain:%s", route, domain)]
|
||||
func (wl *whitelist) Domain(route, domain string) bool {
|
||||
wl.RLock()
|
||||
defer wl.RUnlock()
|
||||
return wl.access[fmt.Sprintf("%s|domain:%s", route, domain)]
|
||||
}
|
||||
|
||||
// PutDomain adds an access entry for a route given a domain name.
|
||||
func (m *identityWhitelist) PutDomain(route, domain string) {
|
||||
m.Lock()
|
||||
m.m[fmt.Sprintf("%s|domain:%s", route, domain)] = true
|
||||
m.Unlock()
|
||||
func (wl *whitelist) PutDomain(route, domain string) {
|
||||
wl.Lock()
|
||||
wl.access[fmt.Sprintf("%s|domain:%s", route, domain)] = true
|
||||
wl.Unlock()
|
||||
}
|
||||
|
||||
// Email retrieves per-route access given a user's email.
|
||||
func (m *identityWhitelist) Email(route, email string) bool {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.m[fmt.Sprintf("%s|email:%s", route, email)]
|
||||
func (wl *whitelist) Email(route, email string) bool {
|
||||
wl.RLock()
|
||||
defer wl.RUnlock()
|
||||
return wl.access[fmt.Sprintf("%s|email:%s", route, email)]
|
||||
}
|
||||
|
||||
// PutEmail adds an access entry for a route given a user's email.
|
||||
func (m *identityWhitelist) PutEmail(route, email string) {
|
||||
m.Lock()
|
||||
m.m[fmt.Sprintf("%s|email:%s", route, email)] = true
|
||||
m.Unlock()
|
||||
func (wl *whitelist) PutEmail(route, email string) {
|
||||
wl.Lock()
|
||||
wl.access[fmt.Sprintf("%s|email:%s", route, email)] = true
|
||||
wl.Unlock()
|
||||
}
|
||||
|
||||
// PutEmail adds an admin entry
|
||||
func (wl *whitelist) PutAdmin(admin string) {
|
||||
wl.Lock()
|
||||
wl.admins[admin] = true
|
||||
wl.Unlock()
|
||||
}
|
||||
|
||||
// Admin checks if the email matches an admin
|
||||
func (wl *whitelist) Admin(admin string) bool {
|
||||
wl.RLock()
|
||||
defer wl.RUnlock()
|
||||
return wl.admins[admin]
|
||||
}
|
||||
|
||||
// MockIdentityValidator is a mock implementation of IdentityValidator
|
||||
type MockIdentityValidator struct{ ValidResponse bool }
|
||||
type MockIdentityValidator struct {
|
||||
ValidResponse bool
|
||||
IsAdminResponse bool
|
||||
}
|
||||
|
||||
// Valid is a mock implementation IdentityValidator's Valid method
|
||||
// Valid is a mock implementation IdentityValidator's Valid method
|
||||
func (mv *MockIdentityValidator) Valid(u string, i *Identity) bool { return mv.ValidResponse }
|
||||
|
||||
// IsAdmin is a mock implementation IdentityValidator's IsAdmin method
|
||||
func (mv *MockIdentityValidator) IsAdmin(i *Identity) bool { return mv.IsAdminResponse }
|
||||
|
|
|
@ -21,8 +21,7 @@ func TestIdentity_EmailDomain(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
i := &Identity{Email: tt.Email}
|
||||
if got := i.EmailDomain(); got != tt.want {
|
||||
if got := EmailDomain(tt.Email); got != tt.want {
|
||||
t.Errorf("Identity.EmailDomain() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
@ -36,25 +35,39 @@ func Test_IdentityWhitelistMap(t *testing.T) {
|
|||
policies []policy.Policy
|
||||
route string
|
||||
Identity *Identity
|
||||
admins []string
|
||||
want bool
|
||||
}{
|
||||
{"valid domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, true},
|
||||
{"invalid domain prepend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "a@1example.com"}, false},
|
||||
{"invalid domain postpend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com2"}, false},
|
||||
{"valid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, true},
|
||||
{"invalid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, false},
|
||||
{"invalid empty", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{""}}, false},
|
||||
{"valid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, true},
|
||||
{"invalid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, false},
|
||||
{"valid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, true},
|
||||
{"invalid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user2@example.com"}, false},
|
||||
{"empty everything", []policy.Policy{{From: "example.com"}}, "example.com", &Identity{Email: "user2@example.com"}, false},
|
||||
{"valid domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, nil, true},
|
||||
{"valid domain with admins", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, []string{"admin@example.com"}, true},
|
||||
{"invalid domain prepend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "a@1example.com"}, nil, false},
|
||||
{"invalid domain postpend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com2"}, nil, false},
|
||||
{"valid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, nil, true},
|
||||
{"invalid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, nil, false},
|
||||
{"invalid empty", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{""}}, nil, false},
|
||||
{"valid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, nil, true},
|
||||
{"invalid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, nil, false},
|
||||
{"valid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, nil, true},
|
||||
{"invalid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user2@example.com"}, nil, false},
|
||||
{"empty everything", []policy.Policy{{From: "example.com"}}, "example.com", &Identity{Email: "user2@example.com"}, nil, false},
|
||||
// impersonation related
|
||||
{"admin not impersonating allowed", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@example.com"}, []string{"admin@example.com"}, true},
|
||||
{"admin not impersonating denied", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"support"}}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating match many groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"a", "b", "c", "support"}}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support"}}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating does not match many groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support", "b", "c"}}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating does not match empty groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{""}}, []string{"admin@admin-domain.com"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
wl := NewIdentityWhitelist(tt.policies)
|
||||
wl := NewIdentityWhitelist(tt.policies, tt.admins)
|
||||
if got := wl.Valid(tt.route, tt.Identity); got != tt.want {
|
||||
t.Errorf("IdentityACLMap.Allowed() = %v, want %v", got, tt.want)
|
||||
t.Errorf("wl.Valid() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
})
|
||||
|
|
|
@ -146,16 +146,14 @@ func wrapMiddleware(o *config.Options, mux *http.ServeMux) http.Handler {
|
|||
c = c.Append(middleware.NewHandler(log.Logger))
|
||||
c = c.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
||||
middleware.FromRequest(r).Debug().
|
||||
Str("service", o.Services).
|
||||
Str("method", r.Method).
|
||||
Str("url", r.URL.String()).
|
||||
Int("status", status).
|
||||
Int("size", size).
|
||||
Dur("duration", duration).
|
||||
Str("user", r.Header.Get(proxy.HeaderUserID)).
|
||||
Int("size", size).
|
||||
Int("status", status).
|
||||
Str("email", r.Header.Get(proxy.HeaderEmail)).
|
||||
Str("group", r.Header.Get(proxy.HeaderGroups)).
|
||||
// Str("sig", r.Header.Get(proxy.HeaderJWT)).
|
||||
Str("method", r.Method).
|
||||
Str("service", o.Services).
|
||||
Str("url", r.URL.String()).
|
||||
Msg("http-request")
|
||||
}))
|
||||
if o != nil && len(o.Headers) != 0 {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
# export SERVICE="all" # optional, default is all
|
||||
# export LOG_LEVEL="info" # optional, default is debug
|
||||
|
||||
export AUTHENTICATE_SERVICE_URL=https://authenticate.corp.example.com
|
||||
export AUTHORIZE_SERVICE_URL=https://authorize.corp.example.com
|
||||
export AUTHENTICATE_SERVICE_URL=https://authenticate.corp.beyondperimeter.com
|
||||
export AUTHORIZE_SERVICE_URL=https://authorize.corp.beyondperimeter.com
|
||||
|
||||
# Certificates can be loaded as files or base64 encoded bytes. If neither is set, a
|
||||
# pomerium will attempt to locate a pair in the root directory
|
|
@ -3,7 +3,7 @@
|
|||
# Proxied routes and per-route policies are defined in a policy block
|
||||
policy: # Omit the 'policy' key if you are encoding it into an environment variable
|
||||
- from: httpbin.corp.beyondperimeter.com
|
||||
to: http://httpbin
|
||||
to: http://localhost:8000
|
||||
allowed_domains:
|
||||
- pomerium.io
|
||||
cors_allow_preflight: true
|
||||
|
@ -20,6 +20,6 @@ policy: # Omit the 'policy' key if you are encoding it into an environment varia
|
|||
- admins
|
||||
- developers
|
||||
- from: hello.corp.beyondperimeter.com
|
||||
to: http://hello:8080
|
||||
to: http://localhost:8080
|
||||
allowed_groups:
|
||||
- admins
|
||||
- admins@pomerium.io
|
|
@ -1,21 +1,23 @@
|
|||
# Main configuration flags
|
||||
address: ":8443" # optional, default is 443
|
||||
address: ":8443" # optional, default is 443
|
||||
pomerium_debug: true # optional, default is false
|
||||
service: "all" # optional, default is all
|
||||
# log_level: "info" # optional, default is debug
|
||||
service: "all" # optional, default is all
|
||||
log_level: "info" # optional, default is debug
|
||||
|
||||
authenticate_service_url: https://authenticate.corp.pomerium.io:8443
|
||||
authorize_service_url: https://authorize.corp.pomerium.io:8443
|
||||
|
||||
# Certificates can be loaded as files or base64 encoded bytes. If neither is set, a
|
||||
# pomerium will attempt to locate a pair in the root directory
|
||||
certificate_file: "./cert.pem" # optional, defaults to `./cert.pem`
|
||||
certificate_file: "./cert.pem" # optional, defaults to `./cert.pem`
|
||||
certificate_key_file: "./privkey.pem" # optional, defaults to `./certprivkey.pem`
|
||||
certificate_authority_file: "./cert.pem"
|
||||
|
||||
# base64 encoded cert, eg. `base64 -i cert.pem` / `base64 -i privkey.pem`
|
||||
# certificate: |
|
||||
# "xxxxxx" # base64 encoded cert, eg. `base64 -i cert.pem`
|
||||
# "xxxxxx"
|
||||
# certificate_key: |
|
||||
# "xxxx" # base64 encoded key, eg. `base64 -i privkey.pem`
|
||||
# "xxxx"
|
||||
|
||||
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
|
||||
shared_secret: hsJIQsx9KKx4qVlggg/T3AuLTmVu0uHhwTQgMPlVs7U=
|
||||
|
@ -86,4 +88,4 @@ policy:
|
|||
- admins
|
||||
- from: external-search.corp.beyondperimeter.com
|
||||
to: google.com
|
||||
allow_public_unauthenticated_access: true
|
||||
allow_public_unauthenticated_access: true
|
||||
|
|
|
@ -13,7 +13,7 @@ If you are coming from a kubernetes or docker background this should feel famili
|
|||
- [Kubernetes: Config Maps](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/)
|
||||
- [Docker: Environment variables](https://docs.docker.com/compose/environment-variables/)
|
||||
|
||||
In general, any setting specified by environment variable can also be present in the optional config file as the same name but lower cased. Environment variables take precedence.
|
||||
In general, any setting specified by environment variable can also be present in the optional config file as the same name but lower cased. Environment variables take precedence.
|
||||
|
||||
## Global settings
|
||||
|
||||
|
@ -40,6 +40,15 @@ Service mode sets the pomerium service(s) to run. If testing, you may want to se
|
|||
|
||||
Address specifies the host and port to serve HTTPS and gRPC requests from. If empty, `:https`/`:443` is used.
|
||||
|
||||
### Administrators
|
||||
|
||||
- Environmental Variable: `ADMINISTRATORS`
|
||||
- Config File Key: `administrators`
|
||||
- Type: slice of `string`
|
||||
- Example: `"admin@example.com,admin2@example.com"`
|
||||
|
||||
Administrative users are [super user](https://en.wikipedia.org/wiki/Superuser) that can sign in as another user or group. User impersonation allows administrators to temporarily sign in as a different user.
|
||||
|
||||
### Shared Secret
|
||||
|
||||
- Environmental Variable: `SHARED_SECRET`
|
||||
|
@ -139,7 +148,7 @@ If set, the HTTP Redirect Address specifies the host and port to redirect http t
|
|||
- Type: [base64 encoded] `string` or inline policy structure in config file
|
||||
- Required
|
||||
|
||||
Policy contains route specific settings, and access control details. If you are configuring via POLICY environment variable, just the contents of the policy needs to be passed. If you are configuring via file, the policy should be present under the policy key. For example,
|
||||
Policy contains route specific settings, and access control details. If you are configuring via POLICY environment variable, just the contents of the policy needs to be passed. If you are configuring via file, the policy should be present under the policy key. For example,
|
||||
|
||||
<<< @/config-policy-only.yaml
|
||||
|
||||
|
@ -206,8 +215,7 @@ Allow unauthenticated HTTP OPTIONS requests as [per the CORS spec](https://devel
|
|||
- Optional
|
||||
- Default: `false`
|
||||
|
||||
**Use with caution:** Allow all requests for a given route, bypassing authentication and authorization.
|
||||
Suitable for publicly exposed web services.
|
||||
**Use with caution:** Allow all requests for a given route, bypassing authentication and authorization. Suitable for publicly exposed web services.
|
||||
|
||||
If this setting is enabled, no whitelists (e.g. Allowed Users) should be provided in this route.
|
||||
|
||||
|
@ -374,6 +382,16 @@ By default, conservative [secure HTTP headers](https://www.owasp.org/index.php/O
|
|||
|
||||

|
||||
|
||||
### Refresh Cooldown
|
||||
|
||||
- Environmental Variable: `REFRESH_COOLDOWN`
|
||||
- Config File Key: `refresh_cooldown`
|
||||
- Type: [Duration](https://golang.org/pkg/time/#Duration) `string`
|
||||
- Example: `10m`, `1h45m`
|
||||
- Default: `5m`
|
||||
|
||||
Refresh cooldown is the minimum amount of time between allowed manually refreshed sessions.
|
||||
|
||||
[base64 encoded]: https://en.wikipedia.org/wiki/Base64
|
||||
[environmental variables]: https://en.wikipedia.org/wiki/Environment_variable
|
||||
[identity provider]: ./identity-providers.md
|
||||
|
|
9
go.mod
9
go.mod
|
@ -5,15 +5,16 @@ go 1.12
|
|||
require (
|
||||
github.com/golang/mock v1.2.0
|
||||
github.com/golang/protobuf v1.3.1
|
||||
github.com/pomerium/envconfig v1.5.0
|
||||
github.com/magiconair/properties v1.8.1 // indirect
|
||||
github.com/pomerium/go-oidc v2.0.0+incompatible
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/rs/zerolog v1.12.0
|
||||
github.com/rs/zerolog v1.14.3
|
||||
github.com/spf13/viper v1.3.2
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25
|
||||
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
|
||||
golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
google.golang.org/api v0.1.0
|
||||
google.golang.org/grpc v1.19.1
|
||||
|
|
94
go.sum
94
go.sum
|
@ -4,38 +4,22 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
|||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
|
@ -43,61 +27,35 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
|||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pomerium/envconfig v1.5.0 h1:OeYS/p6AUxKFqCZHM5BG7pUb0m3MkaC1ZhRLPTHbk8g=
|
||||
github.com/pomerium/envconfig v1.5.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
|
||||
github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o=
|
||||
github.com/pomerium/go-oidc v2.0.0+incompatible/go.mod h1:DRsGVw6MOgxbfq4Y57jKOE8lbEfayxeiY0A8/4vxjBM=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
|
||||
github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
|
||||
github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
|
@ -108,41 +66,28 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
|||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
|
||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7 h1:Qe/u+eY379X4He4GBMFZYu3pmh1ML5yT1aL1ndNM1zQ=
|
||||
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
@ -152,24 +97,23 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4 h1:VSJ45BzqrVgR4clSx415y1rHH7QAGhGt71J0ZmhLYrc=
|
||||
golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
|
@ -184,18 +128,12 @@ google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk
|
|||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM=
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
@ -94,6 +94,10 @@ type Options struct {
|
|||
|
||||
Policies []policy.Policy
|
||||
|
||||
// Administrators contains a set of emails with users who have super user
|
||||
// (sudo) access including the ability to impersonate other users' access
|
||||
Administrators []string `mapstructure:"administrators"`
|
||||
|
||||
// AuthenticateInternalAddr is used as an override when using a load balancer
|
||||
// or ingress that does not natively support routing gRPC.
|
||||
AuthenticateInternalAddr string `mapstructure:"authenticate_internal_url"`
|
||||
|
@ -116,12 +120,15 @@ type Options struct {
|
|||
// Headers to set on all proxied requests. Add a 'disable' key map to turn off.
|
||||
Headers map[string]string `mapstructure:"headers"`
|
||||
|
||||
// RefreshCooldown limits the rate a user can refresh her session
|
||||
RefreshCooldown time.Duration `mapstructure:"refresh_cooldown"`
|
||||
|
||||
// Sub-routes
|
||||
Routes map[string]string `mapstructure:"routes"`
|
||||
DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout"`
|
||||
}
|
||||
|
||||
// NewOptions returns a new options struct with default vaules
|
||||
// NewOptions returns a new options struct with default values
|
||||
func NewOptions() *Options {
|
||||
o := &Options{
|
||||
Debug: false,
|
||||
|
@ -148,6 +155,7 @@ func NewOptions() *Options {
|
|||
IdleTimeout: 5 * time.Minute,
|
||||
AuthenticateURL: new(url.URL),
|
||||
AuthorizeURL: new(url.URL),
|
||||
RefreshCooldown: time.Duration(5 * time.Minute),
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
|
28
internal/cryptutil/mock_cipher.go
Normal file
28
internal/cryptutil/mock_cipher.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package cryptutil // import "github.com/pomerium/pomerium/internal/cryptutil"
|
||||
|
||||
// MockCipher MockCSRFStore is a mock implementation of Cipher.
|
||||
type MockCipher struct {
|
||||
EncryptResponse []byte
|
||||
EncryptError error
|
||||
DecryptResponse []byte
|
||||
DecryptError error
|
||||
MarshalResponse string
|
||||
MarshalError error
|
||||
UnmarshalError error
|
||||
}
|
||||
|
||||
// Encrypt is a mock implementation of MockCipher.
|
||||
func (mc MockCipher) Encrypt(b []byte) ([]byte, error) { return mc.EncryptResponse, mc.EncryptError }
|
||||
|
||||
// Decrypt is a mock implementation of MockCipher.
|
||||
func (mc MockCipher) Decrypt(b []byte) ([]byte, error) { return mc.DecryptResponse, mc.DecryptError }
|
||||
|
||||
// Marshal is a mock implementation of MockCipher.
|
||||
func (mc MockCipher) Marshal(i interface{}) (string, error) {
|
||||
return mc.MarshalResponse, mc.MarshalError
|
||||
}
|
||||
|
||||
// Unmarshal is a mock implementation of MockCipher.
|
||||
func (mc MockCipher) Unmarshal(s string, i interface{}) error {
|
||||
return mc.UnmarshalError
|
||||
}
|
|
@ -30,7 +30,11 @@ func CodeForError(err error) int {
|
|||
}
|
||||
|
||||
// ErrorResponse renders an error page for errors given a message and a status code.
|
||||
// If no message is passed, defaults to the text of the status code.
|
||||
func ErrorResponse(rw http.ResponseWriter, req *http.Request, message string, code int) {
|
||||
if message == "" {
|
||||
message = http.StatusText(code)
|
||||
}
|
||||
if req.Header.Get("Accept") == "application/json" {
|
||||
var response struct {
|
||||
Error string `json:"error"`
|
||||
|
|
|
@ -166,14 +166,13 @@ func (p *GoogleProvider) Authenticate(code string) (*sessions.SessionState, erro
|
|||
}
|
||||
|
||||
return &sessions.SessionState{
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: groups,
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -109,14 +109,13 @@ func (p *AzureProvider) Authenticate(code string) (*sessions.SessionState, error
|
|||
}
|
||||
|
||||
return &sessions.SessionState{
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: groups,
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -82,12 +82,11 @@ func New(providerName string, p *Provider) (a Authenticator, err error) {
|
|||
type Provider struct {
|
||||
ProviderName string
|
||||
|
||||
RedirectURL *url.URL
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ProviderURL string
|
||||
Scopes []string
|
||||
SessionLifetimeTTL time.Duration
|
||||
RedirectURL *url.URL
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ProviderURL string
|
||||
Scopes []string
|
||||
|
||||
// Some providers, such as google, require additional remote api calls to retrieve
|
||||
// user details like groups. Provider is responsible for parsing.
|
||||
|
@ -156,14 +155,13 @@ func (p *Provider) Authenticate(code string) (*sessions.SessionState, error) {
|
|||
}
|
||||
|
||||
return &sessions.SessionState{
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: claims.Groups,
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: claims.Groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -208,21 +208,19 @@ func TestCookieStore_SaveSession(t *testing.T) {
|
|||
}{
|
||||
{"good",
|
||||
&SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}, cipher, false, false},
|
||||
{"bad cipher",
|
||||
&SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}, mockCipher{}, true, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package sessions // import "github.com/pomerium/pomerium/internal/sessions"
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
|
@ -14,21 +18,17 @@ var (
|
|||
|
||||
// SessionState is our object that keeps track of a user's session state
|
||||
type SessionState struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
|
||||
RefreshDeadline time.Time `json:"refresh_deadline"`
|
||||
LifetimeDeadline time.Time `json:"lifetime_deadline"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
RefreshDeadline time.Time `json:"refresh_deadline"`
|
||||
|
||||
Email string `json:"email"`
|
||||
User string `json:"user"` // 'sub' in jwt
|
||||
User string `json:"user"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
// LifetimePeriodExpired returns true if the lifetime has expired
|
||||
func (s *SessionState) LifetimePeriodExpired() bool {
|
||||
return isExpired(s.LifetimeDeadline)
|
||||
ImpersonateEmail string
|
||||
ImpersonateGroups []string
|
||||
}
|
||||
|
||||
// RefreshPeriodExpired returns true if the refresh period has expired
|
||||
|
@ -36,6 +36,28 @@ func (s *SessionState) RefreshPeriodExpired() bool {
|
|||
return isExpired(s.RefreshDeadline)
|
||||
}
|
||||
|
||||
type idToken struct {
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Expiry jsonTime `json:"exp"`
|
||||
IssuedAt jsonTime `json:"iat"`
|
||||
Nonce string `json:"nonce"`
|
||||
AtHash string `json:"at_hash"`
|
||||
}
|
||||
|
||||
// IssuedAt parses the IDToken's issue date and returns a valid go time.Time.
|
||||
func (s *SessionState) IssuedAt() (time.Time, error) {
|
||||
payload, err := parseJWT(s.IDToken)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("internal/sessions: malformed jwt: %v", err)
|
||||
}
|
||||
var token idToken
|
||||
if err := json.Unmarshal(payload, &token); err != nil {
|
||||
return time.Time{}, fmt.Errorf("internal/sessions: failed to unmarshal claims: %v", err)
|
||||
}
|
||||
return time.Time(token.IssuedAt), nil
|
||||
}
|
||||
|
||||
func isExpired(t time.Time) bool {
|
||||
return t.Before(time.Now())
|
||||
}
|
||||
|
@ -61,3 +83,37 @@ func UnmarshalSession(value string, c cryptutil.Cipher) (*SessionState, error) {
|
|||
func ExtendDeadline(ttl time.Duration) time.Time {
|
||||
return time.Now().Add(ttl).Truncate(time.Second)
|
||||
}
|
||||
|
||||
func parseJWT(p string) ([]byte, error) {
|
||||
parts := strings.Split(p, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("internal/sessions: malformed jwt, expected 3 parts got %d", len(parts))
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/sessions: malformed jwt payload: %v", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
type jsonTime time.Time
|
||||
|
||||
func (j *jsonTime) UnmarshalJSON(b []byte) error {
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(b, &n); err != nil {
|
||||
return err
|
||||
}
|
||||
var unix int64
|
||||
|
||||
if t, err := n.Int64(); err == nil {
|
||||
unix = t
|
||||
} else {
|
||||
f, err := n.Float64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unix = int64(f)
|
||||
}
|
||||
*j = jsonTime(time.Unix(unix, 0))
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -16,12 +16,11 @@ func TestSessionStateSerialization(t *testing.T) {
|
|||
}
|
||||
|
||||
want := &SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}
|
||||
|
||||
ciphertext, err := MarshalSession(want, c)
|
||||
|
@ -43,18 +42,12 @@ func TestSessionStateSerialization(t *testing.T) {
|
|||
|
||||
func TestSessionStateExpirations(t *testing.T) {
|
||||
session := &SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: time.Now().Add(-1 * time.Hour),
|
||||
RefreshDeadline: time.Now().Add(-1 * time.Hour),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: time.Now().Add(-1 * time.Hour),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}
|
||||
|
||||
if !session.LifetimePeriodExpired() {
|
||||
t.Errorf("expected lifetime period to be expired")
|
||||
}
|
||||
|
||||
if !session.RefreshPeriodExpired() {
|
||||
t.Errorf("expected lifetime period to be expired")
|
||||
}
|
||||
|
@ -80,3 +73,30 @@ func TestExtendDeadline(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionState_IssuedAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
IDToken string
|
||||
want time.Time
|
||||
wantErr bool
|
||||
}{
|
||||
{"simple parse", "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlkzYm1qV3R4US16OW1fM1RLb0dtRWciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY3MjY4NywiZXhwIjoxNTU4Njc2Mjg3fQ.a4g8W94E7iVJhiIUmsNMwJssfx3Evi8sXeiXgXMC7kHNvftQ2CFU_LJ-dqZ5Jf61OXcrp26r7lUcTNENXuen9tyUWAiHvxk6OHTxZusdywTCY5xowpSZBO9PDWYrmmdvfhRbaKO6QVAUMkbKr1Tr8xqfoaYVXNZhERXhcVReDznI0ccbwCGrNx5oeqiL4eRdZY9eqFXi4Yfee0mkef9oyVPc2HvnpwcpM0eckYa_l_ZQChGjXVGBFIus_Ao33GbWDuc9gs-_Vp2ev4KUT2qWb7AXMCGDLx0tWI9umm7mCBi_7xnaanGKUYcVwcSrv45arllAAwzuNxO0BVw3oRWa5Q", time.Unix(1558672687, 0), false},
|
||||
{"bad jwt", "x.x.x-x-x", time.Time{}, true},
|
||||
{"malformed jwt", "x", time.Time{}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SessionState{IDToken: tt.IDToken}
|
||||
got, err := s.IssuedAt()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SessionState.IssuedAt() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SessionState.IssuedAt() = %v, want %v", got.Format(time.RFC3339), tt.want.Format(time.RFC3339))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package templates // import "github.com/pomerium/pomerium/internal/templates"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// New loads html and style resources directly. Panics on failure.
|
||||
|
@ -17,129 +14,240 @@ func New() *template.Template {
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.42857143;
|
||||
color: #333;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
p {
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 40em;
|
||||
display: block;
|
||||
margin: 10% auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,'Helvetica Neue', sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: #F8F8FF;
|
||||
}
|
||||
|
||||
#main {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
.content, .message, button {
|
||||
border: 1px solid rgba(0,0,0,.125);
|
||||
border-bottom-width: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
#info-box {
|
||||
max-width: 480px;
|
||||
width: 480px;
|
||||
margin-top: 200px;
|
||||
margin-right: auto;
|
||||
margin-bottom: 0px;
|
||||
margin-left: auto;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
letter-spacing: 0.3px;
|
||||
text-transform: uppercase;
|
||||
color: #32325d;
|
||||
}
|
||||
|
||||
h1.title {
|
||||
text-align: center;
|
||||
background: #F8F8FF;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 15px 0;
|
||||
color: #32325d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 0 -30px;
|
||||
padding: 20px 30px 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.content, .message {
|
||||
background-color: #fff;
|
||||
padding: 2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.error, .message {
|
||||
border-bottom-color: #c00;
|
||||
}
|
||||
.message {
|
||||
padding: 1.5rem 2rem 1.3rem;
|
||||
}
|
||||
header {
|
||||
border-bottom: 1px solid rgba(0,0,0,.075);
|
||||
margin: -2rem 0 2rem;
|
||||
padding: 2rem 0 1.8rem;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
.error header {
|
||||
color: #c00;
|
||||
}
|
||||
.details {
|
||||
font-size: .85rem;
|
||||
color: #999;
|
||||
}
|
||||
button {
|
||||
color: #fff;
|
||||
background-color: #3B8686;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
padding: 1rem 2.5rem;
|
||||
text-shadow: 0 3px 1px rgba(0,0,0,.2);
|
||||
outline: none;
|
||||
}
|
||||
button:active {
|
||||
border-top-width: 4px;
|
||||
border-bottom-width: 1px;
|
||||
text-shadow: none;
|
||||
}
|
||||
footer {
|
||||
font-size: 0.75em;
|
||||
color: #999;
|
||||
border: 1px solid #e8e8fb;
|
||||
background-color: #F8F8FF;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin-bottom: 20px;
|
||||
background: #FCFCFF;
|
||||
box-shadow: 0 1px 3px 0 rgba(50, 50, 93, 0.15), 0 4px 6px 0 rgba(112, 157, 199, 0.15);
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
fieldset label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 42px;
|
||||
padding: 10px 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
fieldset label:not(:last-child) {
|
||||
border-bottom: 1px solid #f0f5fa;
|
||||
}
|
||||
|
||||
fieldset label span {
|
||||
min-width: 125px;
|
||||
padding: 0 15px;
|
||||
text-align: right;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
#group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
#group::before {
|
||||
display: inline-flex;
|
||||
content: '';
|
||||
height: 15px;
|
||||
background-position: -1000px -1000px;
|
||||
background-repeat: no-repeat;
|
||||
// margin-right: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-table;
|
||||
margin-top: -72px;
|
||||
background: #F8F8FF;
|
||||
text-align: center;
|
||||
width: 75px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 115px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.ok{
|
||||
fill: #6E43E8;
|
||||
}
|
||||
|
||||
.error{
|
||||
fill: #EB292F;
|
||||
}
|
||||
|
||||
p.message {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field {
|
||||
flex: 1;
|
||||
padding: 0 15px;
|
||||
background: transparent;
|
||||
font-weight: 400;
|
||||
color: #31325f;
|
||||
outline: none;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
fieldset .select::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
pointer-events: none;
|
||||
background: #6E43E8 url("data:image/svg+xml;utf8,<svg viewBox='0 0 140 140' width='24' height='24' xmlns='http://www.w3.org/2000/svg'><g><path d='m121.3,34.6c-1.6-1.6-4.2-1.6-5.8,0l-51,51.1-51.1-51.1c-1.6-1.6-4.2-1.6-5.8,0-1.6,1.6-1.6,4.2 0,5.8l53.9,53.9c0.8,0.8 1.8,1.2 2.9,1.2 1,0 2.1-0.4 2.9-1.2l53.9-53.9c1.7-1.6 1.7-4.2 0.1-5.8z' fill='white'/></g></svg>") no-repeat;
|
||||
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border-style: none;
|
||||
outline: none;
|
||||
color: #313b3f;
|
||||
}
|
||||
|
||||
select {
|
||||
flex: 1;
|
||||
border-style: none;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
color: #313b3f;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.flex{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #FCFCFF;
|
||||
background: #6E43E8;
|
||||
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 4px;
|
||||
border: 0;
|
||||
font-weight: 700;
|
||||
width: 50%;
|
||||
height: 40px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button.half{
|
||||
flex-grow:0;
|
||||
flex-shrink:0;
|
||||
flex-basis:calc(50% - 10px);
|
||||
}
|
||||
|
||||
.button.full{
|
||||
flex-grow:1;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 7px 14px 0 rgba(50, 50, 93, 0.1), 0 3px 6px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.off-color{
|
||||
background: #5735B5;
|
||||
}
|
||||
|
||||
</style>
|
||||
{{end}}`))
|
||||
t = template.Must(t.Parse(fmt.Sprintf(`{{define "footer.html"}}Secured by <b>pomerium</b> %s {{end}}`, version.FullVersion())))
|
||||
|
||||
t = template.Must(t.Parse(`
|
||||
{{define "sign_in_message.html"}}
|
||||
{{if eq (len .AllowedDomains) 1}}
|
||||
{{if eq (index .AllowedDomains 0) "@*"}}
|
||||
<p>You may sign in with any {{.ProviderName}} account.</p>
|
||||
{{else}}
|
||||
<p>You may sign in with your <b>{{index .AllowedDomains 0}}</b> {{.ProviderName}} account.</p>
|
||||
{{end}}
|
||||
{{else if gt (len .AllowedDomains) 1}}
|
||||
<p>
|
||||
You may sign in with any of these {{.ProviderName}} accounts:<br>
|
||||
{{range $i, $e := .AllowedDomains}}{{if $i}}, {{end}}<b>{{$e}}</b>{{end}}
|
||||
</p>
|
||||
{{end}}
|
||||
{{end}}`))
|
||||
|
||||
t = template.Must(t.Parse(`
|
||||
{{define "sign_in.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
<head>
|
||||
<title>Sign In</title>
|
||||
{{template "header.html"}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<header>
|
||||
<h1>Sign in to <b>{{.Destination}}</b></h1>
|
||||
</header>
|
||||
|
||||
{{template "sign_in_message.html" .}}
|
||||
|
||||
<form method="GET" action="/start">
|
||||
<input type="hidden" name="redirect_uri" value="{{.Redirect}}">
|
||||
<button type="submit" class="btn">Sign in with {{.ProviderName}}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer>{{template "footer.html"}} </footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}`))
|
||||
|
||||
template.Must(t.Parse(`
|
||||
|
@ -147,51 +255,116 @@ footer {
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
<head>
|
||||
<title>Error</title>
|
||||
<title>{{.Code}} - {{.Title}}</title>
|
||||
{{template "header.html"}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content error">
|
||||
<header>
|
||||
<h1>{{.Title}}</h1>
|
||||
</header>
|
||||
<p>
|
||||
{{.Message}}<br>
|
||||
<span class="details">HTTP {{.Code}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<footer>{{template "footer.html"}} </footer>
|
||||
</div>
|
||||
<div id="main">
|
||||
<div id="info-box">
|
||||
<div class="card">
|
||||
<svg class="icon error" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/></svg>
|
||||
<h1 class="title">{{.Title}}</h1>
|
||||
<section>
|
||||
<p class="message">{{.Message}}.</p>
|
||||
<p class="message">Troubleshoot your <a href="/.pomerium">session</a>.</p>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://www.pomerium.io" style="display: block;">
|
||||
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 139 30"><defs><style>.a {fill: #6e43e8;}.a,.b {fill-rule: evenodd;}.b,.c {fill: #fff;}</style></defs><title>powered-by-pomerium</title><path class="a" d="M10.6,5.5H138.4c3.09,0,5.6,2,5.6,4.39V31.11c0,2.42-2.51,4.39-5.6,4.39H10.6c-3.09,0-5.6-2-5.6-4.39V9.89C5,7.47,7.51,5.5,10.6,5.5Z" transform="translate(-5 -5.5)" /><path class="b" d="M75.4,26.62H73.94l1.13-2.79-2.25-5.69h1.54L75.78,22l1.43-3.87h1.54Zm-5.61-2.44a2.42,2.42,0,0,1-1.5-.55V24H66.78V15.56h1.51v3a2.48,2.48,0,0,1,1.5-.55c1.58,0,2.66,1.28,2.66,3.09S71.37,24.18,69.79,24.18Zm-.32-4.88a1.68,1.68,0,0,0-1.18.53v2.52a1.65,1.65,0,0,0,1.18.54c.85,0,1.44-.73,1.44-1.8S70.32,19.3,69.47,19.3Zm-8.8,4.33a2.38,2.38,0,0,1-1.5.55c-1.57,0-2.66-1.27-2.66-3.09S57.6,18,59.17,18a2.44,2.44,0,0,1,1.5.55v-3h1.52V24H60.67Zm0-3.8a1.63,1.63,0,0,0-1.17-.53c-.86,0-1.45.73-1.45,1.79s.59,1.8,1.45,1.8a1.6,1.6,0,0,0,1.17-.54Zm-9,1.68A1.69,1.69,0,0,0,53.47,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,53.11,18,2.66,2.66,0,0,1,55.7,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34a1.38,1.38,0,0,0-1.37,1.36h2.57A1.28,1.28,0,0,0,53.05,19.17Zm-5.34.93V24H46.2v-5.9h1.51v.59A2,2,0,0,1,49.16,18a1.65,1.65,0,0,1,.49.06v1.35a1.83,1.83,0,0,0-.53-.07A1.87,1.87,0,0,0,47.71,20.1ZM41,21.51A1.69,1.69,0,0,0,42.76,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,42.4,18,2.66,2.66,0,0,1,45,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34A1.38,1.38,0,0,0,41,20.53h2.57A1.28,1.28,0,0,0,42.34,19.17ZM35.7,24l-1.2-4-1.2,4H32l-2-5.9h1.51l1.19,4,1.19-4h1.37l1.19,4,1.19-4h1.51l-2,5.9Zm-9.23.14a2.94,2.94,0,0,1-3-3.09,3,3,0,1,1,6.07,0A2.94,2.94,0,0,1,26.47,24.18Zm0-4.92c-.88,0-1.49.75-1.49,1.83s.61,1.83,1.49,1.83S28,22.18,28,21.09,27.35,19.26,26.47,19.26Zm-6.62,1.87H18.49V24H17V15.93h2.87a2.61,2.61,0,1,1,0,5.2Zm-.22-4H18.49V19.9h1.14a1.38,1.38,0,1,0,0-2.75Z" transform="translate(-5 -5.5)" /><path class="c" d="M132.71,14.9A3.93,3.93,0,0,0,128.78,11H94.59a3.93,3.93,0,0,0-3.93,3.92V31.06h2.71V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2.47ZM93.37,19a5.49,5.49,0,1,1,11,0Zm12.95,0a5.49,5.49,0,1,1,11,0Zm12.94,0a5.49,5.49,0,1,1,11,0Z" transform="translate(-5 -5.5)" /></svg>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>{{end}}`))
|
||||
</html>
|
||||
|
||||
{{end}}`))
|
||||
|
||||
t = template.Must(t.Parse(`
|
||||
{{define "sign_out.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
<head>
|
||||
<title>Sign Out</title>
|
||||
{{template "header.html"}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<header>
|
||||
<h1>Sign out of <b>{{.Destination}}</b></h1>
|
||||
</header>
|
||||
|
||||
<p>You're currently signed in as <b>{{.Email}}</b>. This will also sign you out of other internal apps.</p>
|
||||
<form method="POST" action="/sign_out">
|
||||
<input type="hidden" name="redirect_uri" value="{{.Redirect}}">
|
||||
<input type="hidden" name="sig" value="{{.Signature}}">
|
||||
<input type="hidden" name="ts" value="{{.Timestamp}}">
|
||||
<button type="submit">Sign out</button>
|
||||
</form>
|
||||
</div>
|
||||
<footer>{{template "footer.html"}}</footer>
|
||||
</div>
|
||||
</body>
|
||||
{{define "dashboard.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
|
||||
<head>
|
||||
<title>Pomerium</title>
|
||||
{{template "header.html"}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main">
|
||||
<div id="info-box">
|
||||
<div class="card">
|
||||
<svg class="icon ok" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
||||
</svg>
|
||||
<form method="POST" action="{{.SignoutURL}}">
|
||||
<section>
|
||||
<h2>Session</h2>
|
||||
<p class="message">Your current session details.</p>
|
||||
<fieldset>
|
||||
<label>
|
||||
<span>Email</span>
|
||||
<input name="email" type="email" class="field" value="{{.Email}}" disabled>
|
||||
</label>
|
||||
<label>
|
||||
<span>User</span>
|
||||
<input name="user" type="text" class="field" value="{{.User}}" disabled>
|
||||
</label>
|
||||
<label class="select">
|
||||
<span>Groups</span>
|
||||
<div id="group" class="field">
|
||||
<select name="group">
|
||||
{{range .Groups}}
|
||||
<option value="{{.}}">{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>Expiry</span>
|
||||
<input name="session expiration" type="text" class="field" value="{{.RefreshDeadline}}" disabled>
|
||||
</label>
|
||||
</fieldset>
|
||||
</section>
|
||||
<div class="flex">
|
||||
<button class="button half" type="submit">Sign Out</button>
|
||||
<a href="/.pomerium/refresh" class="button half">Refresh</a>
|
||||
</div>
|
||||
</form>
|
||||
{{if .IsAdmin}}
|
||||
<form method="POST" action="/.pomerium/impersonate">
|
||||
<section>
|
||||
<h2>Sign-in-as</h2>
|
||||
<p class="message">Administrators can temporarily impersonate another a user.</p>
|
||||
<fieldset>
|
||||
<label>
|
||||
<span>Email</span>
|
||||
<input name="email" type="email" class="field" value="{{.ImpersonateEmail}}" placeholder="user@example.com">
|
||||
</label>
|
||||
<label>
|
||||
<span>Group</span>
|
||||
<input name="group" type="text" class="field" value="{{.ImpersonateGroup}}" placeholder="engineering">
|
||||
</label>
|
||||
</fieldset>
|
||||
</section>
|
||||
<div class="flex">
|
||||
<input name="csrf" type="hidden" value="{{.CSRF}}">
|
||||
<button class="button full" type="submit">Impersonate session</button>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://www.pomerium.io" style="display: block;">
|
||||
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 139 30"><defs><style>.a {fill: #6e43e8;}.a,.b {fill-rule: evenodd;}.b,.c {fill: #fff;}</style></defs><title>powered-by-pomerium</title><path class="a" d="M10.6,5.5H138.4c3.09,0,5.6,2,5.6,4.39V31.11c0,2.42-2.51,4.39-5.6,4.39H10.6c-3.09,0-5.6-2-5.6-4.39V9.89C5,7.47,7.51,5.5,10.6,5.5Z" transform="translate(-5 -5.5)" /><path class="b" d="M75.4,26.62H73.94l1.13-2.79-2.25-5.69h1.54L75.78,22l1.43-3.87h1.54Zm-5.61-2.44a2.42,2.42,0,0,1-1.5-.55V24H66.78V15.56h1.51v3a2.48,2.48,0,0,1,1.5-.55c1.58,0,2.66,1.28,2.66,3.09S71.37,24.18,69.79,24.18Zm-.32-4.88a1.68,1.68,0,0,0-1.18.53v2.52a1.65,1.65,0,0,0,1.18.54c.85,0,1.44-.73,1.44-1.8S70.32,19.3,69.47,19.3Zm-8.8,4.33a2.38,2.38,0,0,1-1.5.55c-1.57,0-2.66-1.27-2.66-3.09S57.6,18,59.17,18a2.44,2.44,0,0,1,1.5.55v-3h1.52V24H60.67Zm0-3.8a1.63,1.63,0,0,0-1.17-.53c-.86,0-1.45.73-1.45,1.79s.59,1.8,1.45,1.8a1.6,1.6,0,0,0,1.17-.54Zm-9,1.68A1.69,1.69,0,0,0,53.47,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,53.11,18,2.66,2.66,0,0,1,55.7,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34a1.38,1.38,0,0,0-1.37,1.36h2.57A1.28,1.28,0,0,0,53.05,19.17Zm-5.34.93V24H46.2v-5.9h1.51v.59A2,2,0,0,1,49.16,18a1.65,1.65,0,0,1,.49.06v1.35a1.83,1.83,0,0,0-.53-.07A1.87,1.87,0,0,0,47.71,20.1ZM41,21.51A1.69,1.69,0,0,0,42.76,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,42.4,18,2.66,2.66,0,0,1,45,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34A1.38,1.38,0,0,0,41,20.53h2.57A1.28,1.28,0,0,0,42.34,19.17ZM35.7,24l-1.2-4-1.2,4H32l-2-5.9h1.51l1.19,4,1.19-4h1.37l1.19,4,1.19-4h1.51l-2,5.9Zm-9.23.14a2.94,2.94,0,0,1-3-3.09,3,3,0,1,1,6.07,0A2.94,2.94,0,0,1,26.47,24.18Zm0-4.92c-.88,0-1.49.75-1.49,1.83s.61,1.83,1.49,1.83S28,22.18,28,21.09,27.35,19.26,26.47,19.26Zm-6.62,1.87H18.49V24H17V15.93h2.87a2.61,2.61,0,1,1,0,5.2Zm-.22-4H18.49V19.9h1.14a1.38,1.38,0,1,0,0-2.75Z" transform="translate(-5 -5.5)" /><path class="c" d="M132.71,14.9A3.93,3.93,0,0,0,128.78,11H94.59a3.93,3.93,0,0,0-3.93,3.92V31.06h2.71V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2.47ZM93.37,19a5.49,5.49,0,1,1,11,0Zm12.95,0a5.49,5.49,0,1,1,11,0Zm12.94,0a5.49,5.49,0,1,1,11,0Z" transform="translate(-5 -5.5)" /></svg>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}`))
|
||||
return t
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
- from: httpbin.corp.beyondperimeter.com
|
||||
to: http://httpbin
|
||||
allowed_domains:
|
||||
- pomerium.io
|
||||
cors_allow_preflight: true
|
||||
timeout: 30s
|
||||
- from: external-httpbin.corp.beyondperimeter.com
|
||||
to: httpbin.org
|
||||
allowed_domains:
|
||||
- gmail.com
|
||||
- from: weirdlyssl.corp.beyondperimeter.com
|
||||
to: http://neverssl.com
|
||||
allowed_users:
|
||||
- bdd@pomerium.io
|
||||
allowed_groups:
|
||||
- admins
|
||||
- developers
|
||||
- from: hello.corp.beyondperimeter.com
|
||||
to: http://hello:8080
|
||||
allowed_groups:
|
||||
- admins
|
|
@ -35,7 +35,7 @@ func (m *AuthenticateRequest) Reset() { *m = AuthenticateRequest{} }
|
|||
func (m *AuthenticateRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*AuthenticateRequest) ProtoMessage() {}
|
||||
func (*AuthenticateRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{0}
|
||||
return fileDescriptor_authenticate_d9796afa57ba1f78, []int{0}
|
||||
}
|
||||
func (m *AuthenticateRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_AuthenticateRequest.Unmarshal(m, b)
|
||||
|
@ -73,7 +73,7 @@ func (m *ValidateRequest) Reset() { *m = ValidateRequest{} }
|
|||
func (m *ValidateRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*ValidateRequest) ProtoMessage() {}
|
||||
func (*ValidateRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{1}
|
||||
return fileDescriptor_authenticate_d9796afa57ba1f78, []int{1}
|
||||
}
|
||||
func (m *ValidateRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ValidateRequest.Unmarshal(m, b)
|
||||
|
@ -111,7 +111,7 @@ func (m *ValidateReply) Reset() { *m = ValidateReply{} }
|
|||
func (m *ValidateReply) String() string { return proto.CompactTextString(m) }
|
||||
func (*ValidateReply) ProtoMessage() {}
|
||||
func (*ValidateReply) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{2}
|
||||
return fileDescriptor_authenticate_d9796afa57ba1f78, []int{2}
|
||||
}
|
||||
func (m *ValidateReply) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ValidateReply.Unmarshal(m, b)
|
||||
|
@ -146,7 +146,6 @@ type Session struct {
|
|||
Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
|
||||
Groups []string `protobuf:"bytes,6,rep,name=groups,proto3" json:"groups,omitempty"`
|
||||
RefreshDeadline *timestamp.Timestamp `protobuf:"bytes,7,opt,name=refresh_deadline,json=refreshDeadline,proto3" json:"refresh_deadline,omitempty"`
|
||||
LifetimeDeadline *timestamp.Timestamp `protobuf:"bytes,8,opt,name=lifetime_deadline,json=lifetimeDeadline,proto3" json:"lifetime_deadline,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
|
@ -156,7 +155,7 @@ func (m *Session) Reset() { *m = Session{} }
|
|||
func (m *Session) String() string { return proto.CompactTextString(m) }
|
||||
func (*Session) ProtoMessage() {}
|
||||
func (*Session) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{3}
|
||||
return fileDescriptor_authenticate_d9796afa57ba1f78, []int{3}
|
||||
}
|
||||
func (m *Session) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Session.Unmarshal(m, b)
|
||||
|
@ -225,13 +224,6 @@ func (m *Session) GetRefreshDeadline() *timestamp.Timestamp {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Session) GetLifetimeDeadline() *timestamp.Timestamp {
|
||||
if m != nil {
|
||||
return m.LifetimeDeadline
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*AuthenticateRequest)(nil), "authenticate.AuthenticateRequest")
|
||||
proto.RegisterType((*ValidateRequest)(nil), "authenticate.ValidateRequest")
|
||||
|
@ -377,32 +369,31 @@ var _Authenticator_serviceDesc = grpc.ServiceDesc{
|
|||
Metadata: "authenticate.proto",
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("authenticate.proto", fileDescriptor_authenticate_2c495f1e6e8d5900) }
|
||||
func init() { proto.RegisterFile("authenticate.proto", fileDescriptor_authenticate_d9796afa57ba1f78) }
|
||||
|
||||
var fileDescriptor_authenticate_2c495f1e6e8d5900 = []byte{
|
||||
// 378 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0xcf, 0x6e, 0x9b, 0x40,
|
||||
0x10, 0xc6, 0x8d, 0xff, 0x81, 0xc7, 0x58, 0x76, 0xa7, 0x7f, 0x44, 0xa9, 0xaa, 0xda, 0xf4, 0xe2,
|
||||
0x56, 0x15, 0x96, 0xdc, 0x53, 0x8f, 0x95, 0x5a, 0x25, 0xca, 0x91, 0x58, 0xb9, 0x5a, 0x18, 0xc6,
|
||||
0xf6, 0x2a, 0x98, 0x25, 0xec, 0x12, 0xc9, 0x2f, 0x97, 0x67, 0xc9, 0xa3, 0x44, 0x2c, 0x20, 0x43,
|
||||
0x64, 0x2b, 0x37, 0x66, 0xf6, 0x37, 0x1f, 0xb3, 0xdf, 0xb7, 0x80, 0x7e, 0x26, 0xf7, 0x14, 0x4b,
|
||||
0x16, 0xf8, 0x92, 0xdc, 0x24, 0xe5, 0x92, 0xa3, 0x59, 0xef, 0xd9, 0xdf, 0x76, 0x9c, 0xef, 0x22,
|
||||
0x5a, 0xa8, 0xb3, 0x4d, 0xb6, 0x5d, 0x48, 0x76, 0x20, 0x21, 0xfd, 0x43, 0x52, 0xe0, 0xce, 0x0f,
|
||||
0x78, 0xff, 0xb7, 0x36, 0xe0, 0xd1, 0x43, 0x46, 0x42, 0x22, 0x42, 0x37, 0xe0, 0x21, 0x59, 0xda,
|
||||
0x54, 0x9b, 0x0f, 0x3c, 0xf5, 0xed, 0xfc, 0x82, 0xf1, 0x9d, 0x1f, 0xb1, 0xb0, 0x86, 0x7d, 0x06,
|
||||
0x83, 0x85, 0x6b, 0xc9, 0xef, 0x29, 0x2e, 0x51, 0x9d, 0x85, 0xab, 0xbc, 0x74, 0x7e, 0xc2, 0xe8,
|
||||
0x44, 0x27, 0xd1, 0x51, 0xb1, 0x62, 0xfd, 0x98, 0xf7, 0x14, 0x6b, 0x78, 0x3a, 0x13, 0x0a, 0x71,
|
||||
0x9e, 0xda, 0xa0, 0xdf, 0x92, 0x10, 0x8c, 0xc7, 0x38, 0x03, 0xd3, 0x0f, 0x02, 0x12, 0xa2, 0x21,
|
||||
0x3b, 0x2c, 0x7a, 0x4a, 0x1a, 0xbf, 0xc3, 0x28, 0xa5, 0x6d, 0x4a, 0x62, 0x5f, 0x32, 0x6d, 0xc5,
|
||||
0x98, 0x65, 0xb3, 0x80, 0xea, 0xab, 0x75, 0x1a, 0xab, 0xe5, 0x97, 0xcb, 0x04, 0xa5, 0x56, 0xb7,
|
||||
0xb8, 0x5c, 0xfe, 0x8d, 0x1f, 0xa0, 0x47, 0x07, 0x9f, 0x45, 0x56, 0x4f, 0x35, 0x8b, 0x02, 0x3f,
|
||||
0x41, 0x7f, 0x97, 0xf2, 0x2c, 0x11, 0x56, 0x7f, 0xda, 0x99, 0x0f, 0xbc, 0xb2, 0xc2, 0xff, 0x30,
|
||||
0xa9, 0x36, 0x08, 0xc9, 0x0f, 0x23, 0x16, 0x93, 0xa5, 0x4f, 0xb5, 0xf9, 0x70, 0x69, 0xbb, 0x85,
|
||||
0xe3, 0x6e, 0xe5, 0xb8, 0xbb, 0xaa, 0x1c, 0xf7, 0xc6, 0xe5, 0xcc, 0xbf, 0x72, 0x04, 0xaf, 0xe0,
|
||||
0x5d, 0xc4, 0xb6, 0x94, 0x67, 0x72, 0xd2, 0x31, 0xde, 0xd4, 0x99, 0x54, 0x43, 0x95, 0xd0, 0xf2,
|
||||
0x59, 0x83, 0x51, 0x2d, 0x46, 0x9e, 0xe2, 0x0d, 0x98, 0xf5, 0x5c, 0x71, 0xe6, 0x36, 0xde, 0xca,
|
||||
0x99, 0xcc, 0xed, 0x8f, 0x4d, 0xa4, 0x0c, 0xc4, 0x69, 0xe1, 0x35, 0x18, 0x55, 0x94, 0xf8, 0xb5,
|
||||
0x09, 0xbd, 0x7a, 0x10, 0xf6, 0x97, 0x4b, 0xc7, 0x49, 0x74, 0x74, 0x5a, 0xf8, 0x07, 0x74, 0xaf,
|
||||
0xf0, 0x00, 0xcf, 0xff, 0xed, 0xe2, 0x12, 0x9b, 0xbe, 0x32, 0xe2, 0xf7, 0x4b, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0xcb, 0xcf, 0xe8, 0x63, 0xf4, 0x02, 0x00, 0x00,
|
||||
var fileDescriptor_authenticate_d9796afa57ba1f78 = []byte{
|
||||
// 354 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0x4d, 0x4f, 0xb3, 0x40,
|
||||
0x14, 0x85, 0xcb, 0xdb, 0x0f, 0xda, 0x5b, 0x9a, 0xbe, 0xb9, 0x7e, 0x04, 0x31, 0xc6, 0x16, 0x37,
|
||||
0xd5, 0x18, 0x9a, 0xd4, 0x95, 0x4b, 0x13, 0x4d, 0x8c, 0x4b, 0x6c, 0xdc, 0x36, 0x14, 0x6e, 0xdb,
|
||||
0x89, 0x94, 0x41, 0x66, 0x30, 0xe9, 0xbf, 0xf5, 0x4f, 0xb8, 0x37, 0x0c, 0x10, 0xc1, 0xb4, 0x3b,
|
||||
0xee, 0x99, 0xe7, 0x0e, 0x67, 0xce, 0x01, 0xf4, 0x52, 0xb9, 0xa1, 0x48, 0x32, 0xdf, 0x93, 0xe4,
|
||||
0xc4, 0x09, 0x97, 0x1c, 0x8d, 0xaa, 0x66, 0x5d, 0xae, 0x39, 0x5f, 0x87, 0x34, 0x55, 0x67, 0xcb,
|
||||
0x74, 0x35, 0x95, 0x6c, 0x4b, 0x42, 0x7a, 0xdb, 0x38, 0xc7, 0xed, 0x6b, 0x38, 0x7a, 0xa8, 0x2c,
|
||||
0xb8, 0xf4, 0x91, 0x92, 0x90, 0x88, 0xd0, 0xf2, 0x79, 0x40, 0xa6, 0x36, 0xd2, 0x26, 0x3d, 0x57,
|
||||
0x7d, 0xdb, 0xb7, 0x30, 0x7c, 0xf3, 0x42, 0x16, 0x54, 0xb0, 0x33, 0xe8, 0xb2, 0x60, 0x21, 0xf9,
|
||||
0x3b, 0x45, 0x05, 0xaa, 0xb3, 0x60, 0x9e, 0x8d, 0xf6, 0x0d, 0x0c, 0x7e, 0xe9, 0x38, 0xdc, 0x29,
|
||||
0x56, 0x2c, 0x3e, 0x33, 0x4d, 0xb1, 0x5d, 0x57, 0x67, 0x42, 0x21, 0xf6, 0xb7, 0x06, 0xfa, 0x2b,
|
||||
0x09, 0xc1, 0x78, 0x84, 0x63, 0x30, 0x3c, 0xdf, 0x27, 0x21, 0x6a, 0xd7, 0xf6, 0x73, 0x4d, 0x5d,
|
||||
0x8d, 0x57, 0x30, 0x48, 0x68, 0x95, 0x90, 0xd8, 0x14, 0xcc, 0x3f, 0xc5, 0x18, 0x85, 0x98, 0x43,
|
||||
0x55, 0x6b, 0xcd, 0x9a, 0xb5, 0xec, 0x71, 0xa9, 0xa0, 0xc4, 0x6c, 0xe5, 0x8f, 0xcb, 0xbe, 0xf1,
|
||||
0x18, 0xda, 0xb4, 0xf5, 0x58, 0x68, 0xb6, 0x95, 0x98, 0x0f, 0x78, 0x0a, 0x9d, 0x75, 0xc2, 0xd3,
|
||||
0x58, 0x98, 0x9d, 0x51, 0x73, 0xd2, 0x73, 0x8b, 0x09, 0x9f, 0xe0, 0x7f, 0xe9, 0x20, 0x20, 0x2f,
|
||||
0x08, 0x59, 0x44, 0xa6, 0x3e, 0xd2, 0x26, 0xfd, 0x99, 0xe5, 0xe4, 0x89, 0x3b, 0x65, 0xe2, 0xce,
|
||||
0xbc, 0x4c, 0xdc, 0x1d, 0x16, 0x3b, 0x8f, 0xc5, 0xca, 0xec, 0x4b, 0x83, 0x41, 0x25, 0x7d, 0x9e,
|
||||
0xe0, 0x0b, 0x18, 0xd5, 0x3a, 0x70, 0xec, 0xd4, 0x2a, 0xde, 0x53, 0x95, 0x75, 0x52, 0x47, 0x8a,
|
||||
0x1c, 0xed, 0x06, 0x3e, 0x43, 0xb7, 0x6c, 0x00, 0x2f, 0xea, 0xd0, 0x9f, 0x1e, 0xad, 0xf3, 0x43,
|
||||
0xc7, 0x71, 0xb8, 0xb3, 0x1b, 0x78, 0x0f, 0xba, 0x9b, 0x5b, 0xc7, 0xfd, 0x7f, 0x3b, 0x68, 0x62,
|
||||
0xd9, 0x51, 0x39, 0xdc, 0xfd, 0x04, 0x00, 0x00, 0xff, 0xff, 0xdc, 0x47, 0xff, 0x7e, 0xab, 0x02,
|
||||
0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -23,5 +23,4 @@ message Session {
|
|||
string email = 5;
|
||||
repeated string groups = 6;
|
||||
google.protobuf.Timestamp refresh_deadline = 7;
|
||||
google.protobuf.Timestamp lifetime_deadline = 8;
|
||||
}
|
||||
|
|
|
@ -12,23 +12,19 @@ func SessionFromProto(p *Session) (*sessions.SessionState, error) {
|
|||
if p == nil {
|
||||
return nil, fmt.Errorf("proto/authenticate: SessionFromProto session cannot be nil")
|
||||
}
|
||||
lifetimeDeadline, err := ptypes.Timestamp(p.LifetimeDeadline)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proto/authenticate: couldn't parse lifetime deadline %v", err)
|
||||
}
|
||||
|
||||
refreshDeadline, err := ptypes.Timestamp(p.RefreshDeadline)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proto/authenticate: couldn't parse refresh deadline %v", err)
|
||||
}
|
||||
return &sessions.SessionState{
|
||||
AccessToken: p.AccessToken,
|
||||
RefreshToken: p.RefreshToken,
|
||||
IDToken: p.IdToken,
|
||||
Email: p.Email,
|
||||
User: p.User,
|
||||
Groups: p.Groups,
|
||||
RefreshDeadline: refreshDeadline,
|
||||
LifetimeDeadline: lifetimeDeadline,
|
||||
AccessToken: p.AccessToken,
|
||||
RefreshToken: p.RefreshToken,
|
||||
IDToken: p.IdToken,
|
||||
Email: p.Email,
|
||||
User: p.User,
|
||||
Groups: p.Groups,
|
||||
RefreshDeadline: refreshDeadline,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -37,22 +33,17 @@ func ProtoFromSession(s *sessions.SessionState) (*Session, error) {
|
|||
if s == nil {
|
||||
return nil, fmt.Errorf("proto/authenticate: ProtoFromSession session cannot be nil")
|
||||
}
|
||||
lifetimeDeadline, err := ptypes.TimestampProto(s.LifetimeDeadline)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proto/authenticate: couldn't parse lifetime deadline %v", err)
|
||||
}
|
||||
refreshDeadline, err := ptypes.TimestampProto(s.RefreshDeadline)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proto/authenticate: couldn't parse refresh deadline %v", err)
|
||||
}
|
||||
return &Session{
|
||||
AccessToken: s.AccessToken,
|
||||
RefreshToken: s.RefreshToken,
|
||||
IdToken: s.IDToken,
|
||||
Email: s.Email,
|
||||
User: s.User,
|
||||
Groups: s.Groups,
|
||||
RefreshDeadline: refreshDeadline,
|
||||
LifetimeDeadline: lifetimeDeadline,
|
||||
AccessToken: s.AccessToken,
|
||||
RefreshToken: s.RefreshToken,
|
||||
IdToken: s.IDToken,
|
||||
Email: s.Email,
|
||||
User: s.User,
|
||||
Groups: s.Groups,
|
||||
RefreshDeadline: refreshDeadline,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -23,70 +23,87 @@ var _ = math.Inf
|
|||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type AuthorizeRequest struct {
|
||||
type Identity struct {
|
||||
// request context
|
||||
Route string `protobuf:"bytes,1,opt,name=route,proto3" json:"route,omitempty"`
|
||||
// user context
|
||||
User string `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
|
||||
Groups []string `protobuf:"bytes,4,rep,name=groups,proto3" json:"groups,omitempty"`
|
||||
User string `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
|
||||
Groups []string `protobuf:"bytes,4,rep,name=groups,proto3" json:"groups,omitempty"`
|
||||
// user context
|
||||
ImpersonateEmail string `protobuf:"bytes,5,opt,name=impersonate_email,json=impersonateEmail,proto3" json:"impersonate_email,omitempty"`
|
||||
ImpersonateGroups []string `protobuf:"bytes,6,rep,name=impersonate_groups,json=impersonateGroups,proto3" json:"impersonate_groups,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *AuthorizeRequest) Reset() { *m = AuthorizeRequest{} }
|
||||
func (m *AuthorizeRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*AuthorizeRequest) ProtoMessage() {}
|
||||
func (*AuthorizeRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_authorize_dad4e29706fc340b, []int{0}
|
||||
func (m *Identity) Reset() { *m = Identity{} }
|
||||
func (m *Identity) String() string { return proto.CompactTextString(m) }
|
||||
func (*Identity) ProtoMessage() {}
|
||||
func (*Identity) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_authorize_86f44c7874077551, []int{0}
|
||||
}
|
||||
func (m *AuthorizeRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_AuthorizeRequest.Unmarshal(m, b)
|
||||
func (m *Identity) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Identity.Unmarshal(m, b)
|
||||
}
|
||||
func (m *AuthorizeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_AuthorizeRequest.Marshal(b, m, deterministic)
|
||||
func (m *Identity) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Identity.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *AuthorizeRequest) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_AuthorizeRequest.Merge(dst, src)
|
||||
func (dst *Identity) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Identity.Merge(dst, src)
|
||||
}
|
||||
func (m *AuthorizeRequest) XXX_Size() int {
|
||||
return xxx_messageInfo_AuthorizeRequest.Size(m)
|
||||
func (m *Identity) XXX_Size() int {
|
||||
return xxx_messageInfo_Identity.Size(m)
|
||||
}
|
||||
func (m *AuthorizeRequest) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_AuthorizeRequest.DiscardUnknown(m)
|
||||
func (m *Identity) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Identity.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_AuthorizeRequest proto.InternalMessageInfo
|
||||
var xxx_messageInfo_Identity proto.InternalMessageInfo
|
||||
|
||||
func (m *AuthorizeRequest) GetRoute() string {
|
||||
func (m *Identity) GetRoute() string {
|
||||
if m != nil {
|
||||
return m.Route
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *AuthorizeRequest) GetUser() string {
|
||||
func (m *Identity) GetUser() string {
|
||||
if m != nil {
|
||||
return m.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *AuthorizeRequest) GetEmail() string {
|
||||
func (m *Identity) GetEmail() string {
|
||||
if m != nil {
|
||||
return m.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *AuthorizeRequest) GetGroups() []string {
|
||||
func (m *Identity) GetGroups() []string {
|
||||
if m != nil {
|
||||
return m.Groups
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Identity) GetImpersonateEmail() string {
|
||||
if m != nil {
|
||||
return m.ImpersonateEmail
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Identity) GetImpersonateGroups() []string {
|
||||
if m != nil {
|
||||
return m.ImpersonateGroups
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthorizeReply struct {
|
||||
IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
|
@ -98,7 +115,7 @@ func (m *AuthorizeReply) Reset() { *m = AuthorizeReply{} }
|
|||
func (m *AuthorizeReply) String() string { return proto.CompactTextString(m) }
|
||||
func (*AuthorizeReply) ProtoMessage() {}
|
||||
func (*AuthorizeReply) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_authorize_dad4e29706fc340b, []int{1}
|
||||
return fileDescriptor_authorize_86f44c7874077551, []int{1}
|
||||
}
|
||||
func (m *AuthorizeReply) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_AuthorizeReply.Unmarshal(m, b)
|
||||
|
@ -125,9 +142,48 @@ func (m *AuthorizeReply) GetIsValid() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
type IsAdminReply struct {
|
||||
IsAdmin bool `protobuf:"varint,1,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *IsAdminReply) Reset() { *m = IsAdminReply{} }
|
||||
func (m *IsAdminReply) String() string { return proto.CompactTextString(m) }
|
||||
func (*IsAdminReply) ProtoMessage() {}
|
||||
func (*IsAdminReply) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_authorize_86f44c7874077551, []int{2}
|
||||
}
|
||||
func (m *IsAdminReply) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_IsAdminReply.Unmarshal(m, b)
|
||||
}
|
||||
func (m *IsAdminReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_IsAdminReply.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *IsAdminReply) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_IsAdminReply.Merge(dst, src)
|
||||
}
|
||||
func (m *IsAdminReply) XXX_Size() int {
|
||||
return xxx_messageInfo_IsAdminReply.Size(m)
|
||||
}
|
||||
func (m *IsAdminReply) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_IsAdminReply.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_IsAdminReply proto.InternalMessageInfo
|
||||
|
||||
func (m *IsAdminReply) GetIsAdmin() bool {
|
||||
if m != nil {
|
||||
return m.IsAdmin
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*AuthorizeRequest)(nil), "authorize.AuthorizeRequest")
|
||||
proto.RegisterType((*Identity)(nil), "authorize.Identity")
|
||||
proto.RegisterType((*AuthorizeReply)(nil), "authorize.AuthorizeReply")
|
||||
proto.RegisterType((*IsAdminReply)(nil), "authorize.IsAdminReply")
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
|
@ -142,7 +198,8 @@ const _ = grpc.SupportPackageIsVersion4
|
|||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
||||
type AuthorizerClient interface {
|
||||
Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*AuthorizeReply, error)
|
||||
Authorize(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*AuthorizeReply, error)
|
||||
IsAdmin(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*IsAdminReply, error)
|
||||
}
|
||||
|
||||
type authorizerClient struct {
|
||||
|
@ -153,7 +210,7 @@ func NewAuthorizerClient(cc *grpc.ClientConn) AuthorizerClient {
|
|||
return &authorizerClient{cc}
|
||||
}
|
||||
|
||||
func (c *authorizerClient) Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*AuthorizeReply, error) {
|
||||
func (c *authorizerClient) Authorize(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*AuthorizeReply, error) {
|
||||
out := new(AuthorizeReply)
|
||||
err := c.cc.Invoke(ctx, "/authorize.Authorizer/Authorize", in, out, opts...)
|
||||
if err != nil {
|
||||
|
@ -162,9 +219,19 @@ func (c *authorizerClient) Authorize(ctx context.Context, in *AuthorizeRequest,
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authorizerClient) IsAdmin(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*IsAdminReply, error) {
|
||||
out := new(IsAdminReply)
|
||||
err := c.cc.Invoke(ctx, "/authorize.Authorizer/IsAdmin", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AuthorizerServer is the server API for Authorizer service.
|
||||
type AuthorizerServer interface {
|
||||
Authorize(context.Context, *AuthorizeRequest) (*AuthorizeReply, error)
|
||||
Authorize(context.Context, *Identity) (*AuthorizeReply, error)
|
||||
IsAdmin(context.Context, *Identity) (*IsAdminReply, error)
|
||||
}
|
||||
|
||||
func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) {
|
||||
|
@ -172,7 +239,7 @@ func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) {
|
|||
}
|
||||
|
||||
func _Authorizer_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AuthorizeRequest)
|
||||
in := new(Identity)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -184,7 +251,25 @@ func _Authorizer_Authorize_Handler(srv interface{}, ctx context.Context, dec fun
|
|||
FullMethod: "/authorize.Authorizer/Authorize",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthorizerServer).Authorize(ctx, req.(*AuthorizeRequest))
|
||||
return srv.(AuthorizerServer).Authorize(ctx, req.(*Identity))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Authorizer_IsAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Identity)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthorizerServer).IsAdmin(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/authorize.Authorizer/IsAdmin",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthorizerServer).IsAdmin(ctx, req.(*Identity))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
@ -197,25 +282,34 @@ var _Authorizer_serviceDesc = grpc.ServiceDesc{
|
|||
MethodName: "Authorize",
|
||||
Handler: _Authorizer_Authorize_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "IsAdmin",
|
||||
Handler: _Authorizer_IsAdmin_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "authorize.proto",
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("authorize.proto", fileDescriptor_authorize_dad4e29706fc340b) }
|
||||
func init() { proto.RegisterFile("authorize.proto", fileDescriptor_authorize_86f44c7874077551) }
|
||||
|
||||
var fileDescriptor_authorize_dad4e29706fc340b = []byte{
|
||||
// 187 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4f, 0x2c, 0x2d, 0xc9,
|
||||
0xc8, 0x2f, 0xca, 0xac, 0x4a, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x84, 0x0b, 0x28,
|
||||
0x65, 0x71, 0x09, 0x38, 0xc2, 0x38, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x22, 0x5c,
|
||||
0xac, 0x45, 0xf9, 0xa5, 0x25, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x10, 0x8e, 0x90,
|
||||
0x10, 0x17, 0x4b, 0x69, 0x71, 0x6a, 0x91, 0x04, 0x13, 0x58, 0x10, 0xcc, 0x06, 0xa9, 0x4c, 0xcd,
|
||||
0x4d, 0xcc, 0xcc, 0x91, 0x60, 0x86, 0xa8, 0x04, 0x73, 0x84, 0xc4, 0xb8, 0xd8, 0xd2, 0x8b, 0xf2,
|
||||
0x4b, 0x0b, 0x8a, 0x25, 0x58, 0x14, 0x98, 0x35, 0x38, 0x83, 0xa0, 0x3c, 0x25, 0x6d, 0x2e, 0x3e,
|
||||
0x24, 0xbb, 0x0a, 0x72, 0x2a, 0x85, 0x24, 0xb9, 0x38, 0x32, 0x8b, 0xe3, 0xcb, 0x12, 0x73, 0x32,
|
||||
0x53, 0xc0, 0x96, 0x71, 0x04, 0xb1, 0x67, 0x16, 0x87, 0x81, 0xb8, 0x46, 0xc1, 0x5c, 0x5c, 0x70,
|
||||
0xc5, 0x45, 0x42, 0xae, 0x5c, 0x9c, 0x70, 0x9e, 0x90, 0xb4, 0x1e, 0xc2, 0x43, 0xe8, 0x8e, 0x97,
|
||||
0x92, 0xc4, 0x2e, 0x59, 0x90, 0x53, 0xa9, 0xc4, 0x90, 0xc4, 0x06, 0xf6, 0xbf, 0x31, 0x20, 0x00,
|
||||
0x00, 0xff, 0xff, 0x28, 0xac, 0x76, 0x2d, 0x12, 0x01, 0x00, 0x00,
|
||||
var fileDescriptor_authorize_86f44c7874077551 = []byte{
|
||||
// 264 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x51, 0xbd, 0x4e, 0xc3, 0x30,
|
||||
0x10, 0x6e, 0x68, 0x9b, 0x26, 0x27, 0xc4, 0xcf, 0x81, 0x20, 0x65, 0xaa, 0x3c, 0x15, 0x55, 0x74,
|
||||
0x80, 0x89, 0x81, 0xa1, 0x03, 0x42, 0x5d, 0x33, 0xb0, 0x56, 0x46, 0xb1, 0xe0, 0xa4, 0x24, 0x8e,
|
||||
0x6c, 0x07, 0xa9, 0x3c, 0x00, 0x8f, 0xc5, 0xb3, 0xa1, 0x5c, 0xd2, 0xc4, 0x48, 0x6c, 0xf9, 0x7e,
|
||||
0x73, 0x77, 0x86, 0x53, 0x59, 0xbb, 0x0f, 0x6d, 0xe8, 0x4b, 0xad, 0x2b, 0xa3, 0x9d, 0xc6, 0xb8,
|
||||
0x27, 0xc4, 0x4f, 0x00, 0xd1, 0x36, 0x53, 0xa5, 0x23, 0xb7, 0xc7, 0x4b, 0x98, 0x1a, 0x5d, 0x3b,
|
||||
0x95, 0x04, 0x8b, 0x60, 0x19, 0xa7, 0x2d, 0x40, 0x84, 0x49, 0x6d, 0x95, 0x49, 0x8e, 0x98, 0xe4,
|
||||
0xef, 0xc6, 0xa9, 0x0a, 0x49, 0x79, 0x32, 0x6e, 0x9d, 0x0c, 0xf0, 0x0a, 0xc2, 0x77, 0xa3, 0xeb,
|
||||
0xca, 0x26, 0x93, 0xc5, 0x78, 0x19, 0xa7, 0x1d, 0xc2, 0x15, 0x9c, 0x53, 0x51, 0x29, 0x63, 0x75,
|
||||
0x29, 0x9d, 0xda, 0xb5, 0xc9, 0x29, 0x27, 0xcf, 0x3c, 0xe1, 0x99, 0x4b, 0xee, 0x00, 0x7d, 0x73,
|
||||
0x57, 0x18, 0x72, 0xa1, 0x5f, 0xf3, 0xc2, 0x82, 0x58, 0xc1, 0xc9, 0xe6, 0xb0, 0x4d, 0xaa, 0xaa,
|
||||
0x7c, 0x8f, 0x73, 0x88, 0xc8, 0xee, 0x3e, 0x65, 0x4e, 0x19, 0x2f, 0x12, 0xa5, 0x33, 0xb2, 0xaf,
|
||||
0x0d, 0x14, 0xb7, 0x70, 0xbc, 0xb5, 0x9b, 0xac, 0xa0, 0xd2, 0xb7, 0xca, 0x86, 0x18, 0xac, 0xac,
|
||||
0xdf, 0x7f, 0x07, 0x00, 0x7d, 0xb1, 0xc1, 0x27, 0x88, 0x7b, 0x84, 0x17, 0xeb, 0xe1, 0xa2, 0x87,
|
||||
0xe3, 0xdd, 0xcc, 0x3d, 0xf2, 0xef, 0x44, 0x62, 0x84, 0x8f, 0x30, 0xeb, 0x7e, 0xfc, 0x7f, 0xf8,
|
||||
0xda, 0x27, 0xbd, 0x09, 0xc5, 0xe8, 0x2d, 0xe4, 0x37, 0x7b, 0xf8, 0x0d, 0x00, 0x00, 0xff, 0xff,
|
||||
0x6d, 0x2f, 0xa0, 0x1b, 0xc6, 0x01, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -3,16 +3,23 @@ syntax = "proto3";
|
|||
package authorize;
|
||||
|
||||
service Authorizer {
|
||||
rpc Authorize(AuthorizeRequest) returns (AuthorizeReply) {}
|
||||
rpc Authorize(Identity) returns (AuthorizeReply) {}
|
||||
rpc IsAdmin(Identity) returns (IsAdminReply) {}
|
||||
|
||||
}
|
||||
|
||||
message AuthorizeRequest {
|
||||
message Identity {
|
||||
// request context
|
||||
string route = 1;
|
||||
// user context
|
||||
string user = 2;
|
||||
string email = 3;
|
||||
repeated string groups = 4;
|
||||
// user context
|
||||
string impersonate_email = 5;
|
||||
repeated string impersonate_groups = 6;
|
||||
}
|
||||
|
||||
message AuthorizeReply { bool is_valid = 1; }
|
||||
|
||||
message IsAdminReply { bool is_admin = 1; }
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/pomerium/pomerium/proto/authorize (interfaces: AuthorizerClient)
|
||||
// Source: proto/authorize/authorize.pb.go
|
||||
|
||||
// Package mock_authorize is a generated GoMock package.
|
||||
package mock_authorize
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
authorize "github.com/pomerium/pomerium/proto/authorize"
|
||||
"github.com/pomerium/pomerium/proto/authorize"
|
||||
context "golang.org/x/net/context"
|
||||
grpc "google.golang.org/grpc"
|
||||
)
|
||||
|
||||
|
@ -37,10 +37,10 @@ func (m *MockAuthorizerClient) EXPECT() *MockAuthorizerClientMockRecorder {
|
|||
}
|
||||
|
||||
// Authorize mocks base method
|
||||
func (m *MockAuthorizerClient) Authorize(arg0 context.Context, arg1 *authorize.AuthorizeRequest, arg2 ...grpc.CallOption) (*authorize.AuthorizeReply, error) {
|
||||
func (m *MockAuthorizerClient) Authorize(ctx context.Context, in *authorize.Identity, opts ...grpc.CallOption) (*authorize.AuthorizeReply, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0, arg1}
|
||||
for _, a := range arg2 {
|
||||
varargs := []interface{}{ctx, in}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Authorize", varargs...)
|
||||
|
@ -50,8 +50,81 @@ func (m *MockAuthorizerClient) Authorize(arg0 context.Context, arg1 *authorize.A
|
|||
}
|
||||
|
||||
// Authorize indicates an expected call of Authorize
|
||||
func (mr *MockAuthorizerClientMockRecorder) Authorize(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||
func (mr *MockAuthorizerClientMockRecorder) Authorize(ctx, in interface{}, opts ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0, arg1}, arg2...)
|
||||
varargs := append([]interface{}{ctx, in}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorize", reflect.TypeOf((*MockAuthorizerClient)(nil).Authorize), varargs...)
|
||||
}
|
||||
|
||||
// IsAdmin mocks base method
|
||||
func (m *MockAuthorizerClient) IsAdmin(ctx context.Context, in *authorize.Identity, opts ...grpc.CallOption) (*authorize.IsAdminReply, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{ctx, in}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "IsAdmin", varargs...)
|
||||
ret0, _ := ret[0].(*authorize.IsAdminReply)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsAdmin indicates an expected call of IsAdmin
|
||||
func (mr *MockAuthorizerClientMockRecorder) IsAdmin(ctx, in interface{}, opts ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{ctx, in}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAdmin", reflect.TypeOf((*MockAuthorizerClient)(nil).IsAdmin), varargs...)
|
||||
}
|
||||
|
||||
// MockAuthorizerServer is a mock of AuthorizerServer interface
|
||||
type MockAuthorizerServer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAuthorizerServerMockRecorder
|
||||
}
|
||||
|
||||
// MockAuthorizerServerMockRecorder is the mock recorder for MockAuthorizerServer
|
||||
type MockAuthorizerServerMockRecorder struct {
|
||||
mock *MockAuthorizerServer
|
||||
}
|
||||
|
||||
// NewMockAuthorizerServer creates a new mock instance
|
||||
func NewMockAuthorizerServer(ctrl *gomock.Controller) *MockAuthorizerServer {
|
||||
mock := &MockAuthorizerServer{ctrl: ctrl}
|
||||
mock.recorder = &MockAuthorizerServerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockAuthorizerServer) EXPECT() *MockAuthorizerServerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Authorize mocks base method
|
||||
func (m *MockAuthorizerServer) Authorize(arg0 context.Context, arg1 *authorize.Identity) (*authorize.AuthorizeReply, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Authorize", arg0, arg1)
|
||||
ret0, _ := ret[0].(*authorize.AuthorizeReply)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Authorize indicates an expected call of Authorize
|
||||
func (mr *MockAuthorizerServerMockRecorder) Authorize(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorize", reflect.TypeOf((*MockAuthorizerServer)(nil).Authorize), arg0, arg1)
|
||||
}
|
||||
|
||||
// IsAdmin mocks base method
|
||||
func (m *MockAuthorizerServer) IsAdmin(arg0 context.Context, arg1 *authorize.Identity) (*authorize.IsAdminReply, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsAdmin", arg0, arg1)
|
||||
ret0, _ := ret[0].(*authorize.IsAdminReply)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsAdmin indicates an expected call of IsAdmin
|
||||
func (mr *MockAuthorizerServerMockRecorder) IsAdmin(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAdmin", reflect.TypeOf((*MockAuthorizerServer)(nil).IsAdmin), arg0, arg1)
|
||||
}
|
||||
|
|
|
@ -70,13 +70,12 @@ func TestProxy_Redeem(t *testing.T) {
|
|||
gomock.Any(),
|
||||
&rpcMsg{msg: req},
|
||||
).Return(&pb.Session{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IdToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
LifetimeDeadline: mockExpire,
|
||||
RefreshDeadline: mockExpire,
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IdToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
RefreshDeadline: mockExpire,
|
||||
}, nil)
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -85,13 +84,12 @@ func TestProxy_Redeem(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{"good", "unit_test", &sessions.SessionState{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IDToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
LifetimeDeadline: (fixedDate),
|
||||
RefreshDeadline: (fixedDate),
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IDToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
RefreshDeadline: (fixedDate),
|
||||
}, false},
|
||||
{"empty code", "", nil, true},
|
||||
}
|
||||
|
@ -170,9 +168,8 @@ func TestProxy_AuthenticateRefresh(t *testing.T) {
|
|||
gomock.Any(),
|
||||
gomock.Not(sessions.SessionState{RefreshToken: "fail"}),
|
||||
).Return(&pb.Session{
|
||||
AccessToken: "new access token",
|
||||
RefreshDeadline: mockExpire,
|
||||
LifetimeDeadline: mockExpire,
|
||||
AccessToken: "new access token",
|
||||
RefreshDeadline: mockExpire,
|
||||
}, nil).AnyTimes()
|
||||
|
||||
tests := []struct {
|
||||
|
@ -184,9 +181,8 @@ func TestProxy_AuthenticateRefresh(t *testing.T) {
|
|||
{"good",
|
||||
&sessions.SessionState{RefreshToken: "unit_test"},
|
||||
&sessions.SessionState{
|
||||
AccessToken: "new access token",
|
||||
RefreshDeadline: fixedDate,
|
||||
LifetimeDeadline: fixedDate,
|
||||
AccessToken: "new access token",
|
||||
RefreshDeadline: fixedDate,
|
||||
}, false},
|
||||
{"empty refresh token", &sessions.SessionState{RefreshToken: ""}, nil, true},
|
||||
}
|
||||
|
|
|
@ -13,8 +13,11 @@ import (
|
|||
|
||||
// Authorizer provides the authorize service interface
|
||||
type Authorizer interface {
|
||||
// Authorize takes a code and returns a validated session or an error
|
||||
// Authorize takes a route and user session and returns whether the
|
||||
// request is valid per access policy
|
||||
Authorize(context.Context, string, *sessions.SessionState) (bool, error)
|
||||
// IsAdmin takes a session and returns whether the user is an administrator
|
||||
IsAdmin(context.Context, *sessions.SessionState) (bool, error)
|
||||
// Close closes the auth connection if any.
|
||||
Close() error
|
||||
}
|
||||
|
@ -35,29 +38,42 @@ func NewGRPCAuthorizeClient(opts *Options) (p *AuthorizeGRPC, err error) {
|
|||
return &AuthorizeGRPC{Conn: conn, client: client}, nil
|
||||
}
|
||||
|
||||
// AuthorizeGRPC is a gRPC implementation of an authenticator (authenticate client)
|
||||
// AuthorizeGRPC is a gRPC implementation of an authenticator (authorize client)
|
||||
type AuthorizeGRPC struct {
|
||||
Conn *grpc.ClientConn
|
||||
client pb.AuthorizerClient
|
||||
}
|
||||
|
||||
// Authorize makes an RPC call to the authorize service to creates a session state
|
||||
// from an encrypted code provided as a result of an oauth2 callback process.
|
||||
// Authorize takes a route and user session and returns whether the
|
||||
// request is valid per access policy
|
||||
func (a *AuthorizeGRPC) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
|
||||
if s == nil {
|
||||
return false, errors.New("session cannot be nil")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
response, err := a.client.Authorize(ctx, &pb.AuthorizeRequest{
|
||||
Route: route,
|
||||
User: s.User,
|
||||
Email: s.Email,
|
||||
Groups: s.Groups,
|
||||
response, err := a.client.Authorize(ctx, &pb.Identity{
|
||||
Route: route,
|
||||
User: s.User,
|
||||
Email: s.Email,
|
||||
Groups: s.Groups,
|
||||
ImpersonateEmail: s.ImpersonateEmail,
|
||||
ImpersonateGroups: s.ImpersonateGroups,
|
||||
})
|
||||
return response.GetIsValid(), err
|
||||
}
|
||||
|
||||
// IsAdmin takes a session and returns whether the user is an administrator
|
||||
func (a *AuthorizeGRPC) IsAdmin(ctx context.Context, s *sessions.SessionState) (bool, error) {
|
||||
if s == nil {
|
||||
return false, errors.New("session cannot be nil")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
response, err := a.client.IsAdmin(ctx, &pb.Identity{Email: s.Email, Groups: s.Groups})
|
||||
return response.GetIsAdmin(), err
|
||||
}
|
||||
|
||||
// Close tears down the ClientConn and all underlying connections.
|
||||
func (a *AuthorizeGRPC) Close() error {
|
||||
return a.Conn.Close()
|
||||
|
|
|
@ -43,3 +43,35 @@ func TestAuthorizeGRPC_Authorize(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
func TestAuthorizeGRPC_IsAdmin(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
client := mock.NewMockAuthorizerClient(ctrl)
|
||||
client.EXPECT().IsAdmin(
|
||||
gomock.Any(),
|
||||
gomock.Any(),
|
||||
).Return(&authorize.IsAdminReply{IsAdmin: true}, nil).AnyTimes()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
s *sessions.SessionState
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", &sessions.SessionState{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false},
|
||||
{"session cannot be nil", nil, false, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &AuthorizeGRPC{client: client}
|
||||
got, err := a.IsAdmin(context.Background(), tt.s)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AuthorizeGRPC.IsAdmin() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("AuthorizeGRPC.IsAdmin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,8 @@ func (a MockAuthenticate) Close() error { return a.CloseError }
|
|||
type MockAuthorize struct {
|
||||
AuthorizeResponse bool
|
||||
AuthorizeError error
|
||||
IsAdminResponse bool
|
||||
IsAdminError error
|
||||
CloseError error
|
||||
}
|
||||
|
||||
|
@ -49,3 +51,8 @@ func (a MockAuthorize) Close() error { return a.CloseError }
|
|||
func (a MockAuthorize) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
|
||||
return a.AuthorizeResponse, a.AuthorizeError
|
||||
}
|
||||
|
||||
// IsAdmin is a mocked IsAdmin function.
|
||||
func (a MockAuthorize) IsAdmin(ctx context.Context, s *sessions.SessionState) (bool, error) {
|
||||
return a.IsAdminResponse, a.IsAdminError
|
||||
}
|
||||
|
|
|
@ -5,26 +5,22 @@ import (
|
|||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
)
|
||||
|
||||
func TestMockAuthenticate(t *testing.T) {
|
||||
// Absurd, but I caught a typo this way.
|
||||
fixedDate := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
|
||||
redeemResponse := &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
LifetimeDeadline: fixedDate,
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
}
|
||||
ma := &MockAuthenticate{
|
||||
RedeemError: errors.New("RedeemError"),
|
||||
RedeemResponse: redeemResponse,
|
||||
RefreshResponse: &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
LifetimeDeadline: fixedDate,
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
},
|
||||
RefreshError: errors.New("RefreshError"),
|
||||
ValidateResponse: true,
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -15,6 +14,7 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/templates"
|
||||
)
|
||||
|
||||
// StateParameter holds the redirect id along with the session id.
|
||||
|
@ -33,11 +33,14 @@ func (p *Proxy) Handler() http.Handler {
|
|||
}))
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/robots.txt", p.RobotsTxt)
|
||||
mux.HandleFunc("/.pomerium/sign_out", p.SignOut)
|
||||
mux.HandleFunc("/.pomerium/callback", p.OAuthCallback)
|
||||
// mux.HandleFunc("/.pomerium/refresh", p.Refresh) //todo(bdd): needs DoS protection before inclusion
|
||||
mux.HandleFunc("/", p.Proxy)
|
||||
return validate.Then(mux)
|
||||
mux.HandleFunc("/.pomerium", p.UserDashboard)
|
||||
mux.HandleFunc("/.pomerium/impersonate", p.Impersonate) // POST
|
||||
mux.HandleFunc("/.pomerium/sign_out", p.SignOutCallback)
|
||||
// handlers handlers with validation
|
||||
mux.Handle("/.pomerium/callback", validate.ThenFunc(p.OAuthCallback))
|
||||
mux.Handle("/.pomerium/refresh", validate.ThenFunc(p.Refresh))
|
||||
mux.Handle("/", validate.ThenFunc(p.Proxy))
|
||||
return mux
|
||||
}
|
||||
|
||||
// RobotsTxt sets the User-Agent header in the response to be "Disallow"
|
||||
|
@ -46,17 +49,12 @@ func (p *Proxy) RobotsTxt(w http.ResponseWriter, _ *http.Request) {
|
|||
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
|
||||
}
|
||||
|
||||
// SignOut redirects the request to the sign out url. It's the responsibility
|
||||
// SignOutCallback redirects the request to the sign out url. It's the responsibility
|
||||
// of the authenticate service to revoke the remote session and clear
|
||||
// the local session state.
|
||||
func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
|
||||
redirectURL := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: r.Host,
|
||||
Path: "/",
|
||||
}
|
||||
fullURL := p.GetSignOutURL(p.AuthenticateURL, redirectURL)
|
||||
http.Redirect(w, r, fullURL.String(), http.StatusFound)
|
||||
func (p *Proxy) SignOutCallback(w http.ResponseWriter, r *http.Request) {
|
||||
redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/"}
|
||||
http.Redirect(w, r, redirectURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// OAuthStart begins the authenticate flow, encrypting the redirect url
|
||||
|
@ -65,34 +63,47 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
|||
requestURI := r.URL.String()
|
||||
callbackURL := p.GetRedirectURL(r.Host)
|
||||
|
||||
// state prevents cross site forgery and maintain state across the client and server
|
||||
// CSRF value used to mitigate replay attacks.
|
||||
state := &StateParameter{
|
||||
SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey()), // nonce
|
||||
RedirectURI: requestURI, // where to redirect the user back to
|
||||
SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey()),
|
||||
RedirectURI: requestURI,
|
||||
}
|
||||
|
||||
// we encrypt this value to be opaque the browser cookie
|
||||
// this value will be unique since we always use a randomized nonce as part of marshaling
|
||||
encryptedCSRF, err := p.cipher.Marshal(state)
|
||||
// Encrypt, and save CSRF state. Will be checked on callback.
|
||||
localState, err := p.cipher.Marshal(state)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf")
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
p.csrfStore.SetCSRF(w, r, encryptedCSRF)
|
||||
p.csrfStore.SetCSRF(w, r, localState)
|
||||
|
||||
// we encrypt this value to be opaque the uri query value
|
||||
// this value will be unique since we always use a randomized nonce as part of marshaling
|
||||
encryptedState, err := p.cipher.Marshal(state)
|
||||
// Though the plaintext payload is identical, we re-encrypt which will
|
||||
// create a different cipher text using another nonce
|
||||
remoteState, err := p.cipher.Marshal(state)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to encrypt cookie")
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
signinURL := p.GetSignInURL(p.AuthenticateURL, callbackURL, encryptedState)
|
||||
log.FromRequest(r).Info().Str("SigninURL", signinURL.String()).Msg("proxy: oauth start")
|
||||
// redirect the user to the authenticate provider along with the encrypted state which
|
||||
// contains a redirect uri pointing back to the proxy
|
||||
|
||||
// Sanity check. The encrypted payload of local and remote state should
|
||||
// never match as each encryption round uses a cryptographic nonce.
|
||||
//
|
||||
// todo(bdd): since this should nearly (1/(2^32*2^32)) never happen should
|
||||
// we panic as a failure most likely means the rands entropy source is failing?
|
||||
if remoteState == localState {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
log.FromRequest(r).Error().Msg("proxy: encrypted state should not match")
|
||||
httputil.ErrorResponse(w, r, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
signinURL := p.GetSignInURL(p.AuthenticateURL, callbackURL, remoteState)
|
||||
log.FromRequest(r).Debug().Str("SigninURL", signinURL.String()).Msg("proxy: oauth start")
|
||||
|
||||
// Redirect the user to the authenticate service along with the encrypted
|
||||
// state which contains a redirect uri back to the proxy and a nonce
|
||||
http.Redirect(w, r, signinURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
|
@ -112,23 +123,18 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, errorString, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// We begin the process of redeeming the code for an access token.
|
||||
session, err := p.AuthenticateClient.Redeem(r.Context(), r.Form.Get("code"))
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: error redeeming authorization code")
|
||||
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedState := r.Form.Get("state")
|
||||
stateParameter := &StateParameter{}
|
||||
err = p.cipher.Unmarshal(encryptedState, stateParameter)
|
||||
// Encrypted CSRF passed from authenticate service
|
||||
remoteStateEncrypted := r.Form.Get("state")
|
||||
remoteStatePlain := new(StateParameter)
|
||||
err = p.cipher.Unmarshal(remoteStateEncrypted, remoteStatePlain)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: could not unmarshal state")
|
||||
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Encrypted CSRF from session storage
|
||||
c, err := p.csrfStore.GetCSRF(r)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie")
|
||||
|
@ -136,44 +142,34 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
p.csrfStore.ClearCSRF(w, r)
|
||||
|
||||
encryptedCSRF := c.Value
|
||||
csrfParameter := &StateParameter{}
|
||||
err = p.cipher.Unmarshal(encryptedCSRF, csrfParameter)
|
||||
localStateEncrypted := c.Value
|
||||
localStatePlain := new(StateParameter)
|
||||
err = p.cipher.Unmarshal(localStateEncrypted, localStatePlain)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF")
|
||||
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if encryptedState == encryptedCSRF {
|
||||
log.FromRequest(r).Error().Msg("encrypted state and CSRF should not be equal")
|
||||
httputil.ErrorResponse(w, r, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(stateParameter, csrfParameter) {
|
||||
log.FromRequest(r).Error().Msg("state and CSRF should be equal")
|
||||
httputil.ErrorResponse(w, r, "Bad request", http.StatusBadRequest)
|
||||
|
||||
// If the encrypted value of local and remote state match, reject.
|
||||
// Likely a replay attack or nonce-reuse.
|
||||
if remoteStateEncrypted == localStateEncrypted {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
log.FromRequest(r).Error().Msg("proxy: local and remote state should not match")
|
||||
httputil.ErrorResponse(w, r, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// We store the session in a cookie and redirect the user back to the application
|
||||
err = p.sessionStore.SaveSession(w, r, session)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Msg("error saving session")
|
||||
httputil.ErrorResponse(w, r, "Error saving session", http.StatusInternalServerError)
|
||||
// Decrypted remote and local state struct (inc. nonce) must match
|
||||
if remoteStatePlain.SessionID != localStatePlain.SessionID {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
log.FromRequest(r).Error().Msg("proxy: CSRF mismatch")
|
||||
httputil.ErrorResponse(w, r, "CSRF mismatch", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.FromRequest(r).Debug().
|
||||
Str("code", r.Form.Get("code")).
|
||||
Str("state", r.Form.Get("state")).
|
||||
Str("RefreshToken", session.RefreshToken).
|
||||
Str("session", session.AccessToken).
|
||||
Str("RedirectURI", stateParameter.RedirectURI).
|
||||
Msg("session")
|
||||
|
||||
// This is the redirect back to the original requested application
|
||||
http.Redirect(w, r, stateParameter.RedirectURI, http.StatusFound)
|
||||
http.Redirect(w, r, remoteStatePlain.RedirectURI, http.StatusFound)
|
||||
}
|
||||
|
||||
// shouldSkipAuthentication contains conditions for skipping authentication.
|
||||
|
@ -223,8 +219,7 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
err = p.authenticate(w, r, session)
|
||||
if err != nil {
|
||||
if err = p.authenticate(w, r, session); err != nil {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
log.Debug().Err(err).Msg("proxy: user unauthenticated")
|
||||
httputil.ErrorResponse(w, r, "User unauthenticated", http.StatusForbidden)
|
||||
|
@ -236,10 +231,6 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, "Access unauthorized", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// append
|
||||
r.Header.Set(HeaderUserID, session.User)
|
||||
r.Header.Set(HeaderEmail, session.Email)
|
||||
r.Header.Set(HeaderGroups, strings.Join(session.Groups, ","))
|
||||
}
|
||||
|
||||
// We have validated the users request and now proxy their request to the provided upstream.
|
||||
|
@ -251,33 +242,162 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
|||
route.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Refresh refreshes a user session, validating group, extending timeout period, without requiring
|
||||
// a user to re-authenticate
|
||||
// func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
// session, err := p.sessionStore.LoadSession(r)
|
||||
// if err != nil {
|
||||
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// session, err = p.AuthenticateClient.Refresh(r.Context(), session)
|
||||
// if err != nil {
|
||||
// log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
|
||||
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// err = p.sessionStore.SaveSession(w, r, session)
|
||||
// if err != nil {
|
||||
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// jsonSession, err := json.Marshal(session)
|
||||
// if err != nil {
|
||||
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// fmt.Fprint(w, string(jsonSession))
|
||||
// }
|
||||
// UserDashboard lets users investigate, and refresh their current session.
|
||||
// It also contains certain administrative actions like user impersonation.
|
||||
// Nota bene: This endpoint does authentication, not authorization.
|
||||
func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := p.sessionStore.LoadSession(r)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: load session failed")
|
||||
httputil.ErrorResponse(w, r, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := p.authenticate(w, r, session); err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: authenticate failed")
|
||||
httputil.ErrorResponse(w, r, "", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/.pomerium/sign_out"}
|
||||
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: is admin client")
|
||||
httputil.ErrorResponse(w, r, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// CSRF value used to mitigate replay attacks.
|
||||
csrf := &StateParameter{SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey())}
|
||||
csrfCookie, err := p.cipher.Marshal(csrf)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf")
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
p.csrfStore.SetCSRF(w, r, csrfCookie)
|
||||
|
||||
t := struct {
|
||||
Email string
|
||||
User string
|
||||
Groups []string
|
||||
RefreshDeadline string
|
||||
SignoutURL string
|
||||
|
||||
IsAdmin bool
|
||||
ImpersonateEmail string
|
||||
ImpersonateGroup string
|
||||
CSRF string
|
||||
}{
|
||||
Email: session.Email,
|
||||
User: session.User,
|
||||
Groups: session.Groups,
|
||||
RefreshDeadline: time.Until(session.RefreshDeadline).Round(time.Second).String(),
|
||||
SignoutURL: p.GetSignOutURL(p.AuthenticateURL, redirectURL).String(),
|
||||
IsAdmin: isAdmin,
|
||||
ImpersonateEmail: session.ImpersonateEmail,
|
||||
ImpersonateGroup: strings.Join(session.ImpersonateGroups[:], ","),
|
||||
CSRF: csrf.SessionID,
|
||||
}
|
||||
templates.New().ExecuteTemplate(w, "dashboard.html", t)
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh redeems and extends an existing authenticated oidc session with
|
||||
// the underlying idenity provider. All session details including groups,
|
||||
// timeouts, will be renewed.
|
||||
func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := p.sessionStore.LoadSession(r)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
iss, err := session.IssuedAt()
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't get token's create time")
|
||||
httputil.ErrorResponse(w, r, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// reject a refresh if it's been less than 5 minutes to prevent a bad actor
|
||||
// trying to DOS the identity provider.
|
||||
if time.Since(iss) < p.refreshCooldown {
|
||||
log.FromRequest(r).Error().Dur("cooldown", p.refreshCooldown).Err(err).Msg("proxy: refresh cooldown")
|
||||
httputil.ErrorResponse(w, r,
|
||||
fmt.Sprintf("Session must be %v old before refresh", p.refreshCooldown),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
newSession, err := p.AuthenticateClient.Refresh(r.Context(), session)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err = p.sessionStore.SaveSession(w, r, newSession); err != nil {
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/.pomerium", http.StatusFound)
|
||||
}
|
||||
|
||||
// Impersonate takes the result of a form and adds user impersonation details
|
||||
// to the user's current user sessions state if the user is currently an
|
||||
// administrative user. Requests are redirected back to the user dashboard.
|
||||
func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate form")
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
session, err := p.sessionStore.LoadSession(r)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: load session")
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// authorization check -- is this user an admin?
|
||||
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
|
||||
if err != nil || !isAdmin {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: user must be admin to impersonate")
|
||||
httputil.ErrorResponse(w, r, "user must be admin to impersonate", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// CSRF check -- did this request originate from our form?
|
||||
c, err := p.csrfStore.GetCSRF(r)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie")
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
p.csrfStore.ClearCSRF(w, r)
|
||||
encryptedCSRF := c.Value
|
||||
decryptedCSRF := new(StateParameter)
|
||||
if err = p.cipher.Unmarshal(encryptedCSRF, decryptedCSRF); err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF")
|
||||
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if decryptedCSRF.SessionID != r.FormValue("csrf") {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate CSRF mismatch")
|
||||
httputil.ErrorResponse(w, r, "CSRF mismatch", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// OK to impersonation
|
||||
session.ImpersonateEmail = r.FormValue("email")
|
||||
session.ImpersonateGroups = strings.Split(r.FormValue("group"), ",")
|
||||
|
||||
if err := p.sessionStore.SaveSession(w, r, session); err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("proxy: save session")
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/.pomerium", http.StatusFound)
|
||||
}
|
||||
|
||||
// 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..
|
||||
|
@ -297,6 +417,9 @@ func (p *Proxy) authenticate(w http.ResponseWriter, r *http.Request, session *se
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -12,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/proxy/clients"
|
||||
|
@ -32,7 +34,12 @@ func (a mockCipher) Decrypt(s []byte) ([]byte, error) {
|
|||
}
|
||||
return []byte("OK"), nil
|
||||
}
|
||||
func (a mockCipher) Marshal(s interface{}) (string, error) { return "ok", nil }
|
||||
func (a mockCipher) Marshal(s interface{}) (string, error) {
|
||||
if s == "error" {
|
||||
return "", errors.New("error")
|
||||
}
|
||||
return "ok", nil
|
||||
}
|
||||
func (a mockCipher) Unmarshal(s string, i interface{}) error {
|
||||
if string(s) == "unmarshal error" || string(s) == "error" {
|
||||
return errors.New("error")
|
||||
|
@ -153,9 +160,8 @@ func TestProxy_Signout(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
req := httptest.NewRequest("GET", "/.pomerium/sign_out", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
proxy.SignOut(rr, req)
|
||||
proxy.SignOutCallback(rr, req)
|
||||
if status := rr.Code; status != http.StatusFound {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound)
|
||||
}
|
||||
|
@ -201,78 +207,6 @@ func TestProxy_Handler(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProxy_OAuthCallback(t *testing.T) {
|
||||
normalSession := sessions.MockSessionStore{
|
||||
Session: &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
LifetimeDeadline: time.Now().Add(10 * time.Second),
|
||||
RefreshDeadline: time.Now().Add(-10 * time.Second),
|
||||
},
|
||||
}
|
||||
normalAuth := clients.MockAuthenticate{
|
||||
RedeemResponse: &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
LifetimeDeadline: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
}
|
||||
normalCsrf := sessions.MockCSRFStore{
|
||||
ResponseCSRF: "ok",
|
||||
GetError: nil,
|
||||
Cookie: &http.Cookie{
|
||||
Name: "something_csrf",
|
||||
Value: "csrf_state",
|
||||
}}
|
||||
tests := []struct {
|
||||
name string
|
||||
csrf sessions.MockCSRFStore
|
||||
session sessions.MockSessionStore
|
||||
authenticator clients.MockAuthenticate
|
||||
params map[string]string
|
||||
wantCode int
|
||||
}{
|
||||
{"good", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusFound},
|
||||
{"error", normalCsrf, normalSession, normalAuth, map[string]string{"error": "some error"}, http.StatusForbidden},
|
||||
{"code err", normalCsrf, normalSession, clients.MockAuthenticate{RedeemError: errors.New("error")}, map[string]string{"code": "error"}, http.StatusInternalServerError},
|
||||
{"state err", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError},
|
||||
{"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusBadRequest},
|
||||
{"unmarshal err", sessions.MockCSRFStore{
|
||||
Cookie: &http.Cookie{
|
||||
Name: "something_csrf",
|
||||
Value: "unmarshal error",
|
||||
},
|
||||
}, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
{"encrypted state != CSRF", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "csrf_state"}, http.StatusBadRequest},
|
||||
{"session save err", normalCsrf, sessions.MockSessionStore{SaveError: errors.New("error")}, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
proxy, err := New(testOptions())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
proxy.sessionStore = &tt.session
|
||||
proxy.csrfStore = tt.csrf
|
||||
proxy.AuthenticateClient = tt.authenticator
|
||||
proxy.cipher = mockCipher{}
|
||||
// proxy.Csrf
|
||||
req := httptest.NewRequest(http.MethodPost, "/.pomerium/callback", nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range tt.params {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
w := httptest.NewRecorder()
|
||||
proxy.OAuthCallback(w, req)
|
||||
if status := w.Code; status != tt.wantCode {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
func Test_extendDeadline(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -331,10 +265,9 @@ func TestProxy_router(t *testing.T) {
|
|||
|
||||
func TestProxy_Proxy(t *testing.T) {
|
||||
goodSession := &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
LifetimeDeadline: time.Now().Add(10 * time.Second),
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -374,13 +307,14 @@ func TestProxy_Proxy(t *testing.T) {
|
|||
{"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.StatusForbidden},
|
||||
// authenticate errors
|
||||
{"no session error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie, Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound},
|
||||
{"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},
|
||||
{"cannot resave refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{SaveError: errors.New("weird"), Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
|
||||
{"authenticate validation error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: false}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
|
||||
{"public access", optsPublic, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
|
||||
{"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||
// no session, redirect to login
|
||||
{"no http found (no session)", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -410,3 +344,152 @@ func TestProxy_Proxy(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_UserDashboard(t *testing.T) {
|
||||
opts := testOptions()
|
||||
tests := []struct {
|
||||
name string
|
||||
options *config.Options
|
||||
method string
|
||||
cipher cryptutil.Cipher
|
||||
session sessions.SessionStore
|
||||
authenticator clients.Authenticator
|
||||
authorizer clients.Authorizer
|
||||
|
||||
wantAdminForm bool
|
||||
wantStatus int
|
||||
}{
|
||||
{"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusOK},
|
||||
{"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusBadRequest},
|
||||
{"auth failure, validation error", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthenticate{ValidateError: errors.New("not valid anymore"), ValidateResponse: false}, clients.MockAuthorize{}, false, http.StatusUnauthorized},
|
||||
{"can't save csrf", opts, http.MethodGet, &cryptutil.MockCipher{MarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusInternalServerError},
|
||||
{"want admin form good admin authorization", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, true, http.StatusOK},
|
||||
{"is admin but authorization fails", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminError: errors.New("err")}, false, http.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p, err := New(tt.options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.cipher = tt.cipher
|
||||
p.sessionStore = tt.session
|
||||
p.AuthenticateClient = tt.authenticator
|
||||
p.AuthorizeClient = tt.authorizer
|
||||
|
||||
r := httptest.NewRequest(tt.method, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
p.UserDashboard(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
|
||||
t.Errorf("\n%+v", opts)
|
||||
}
|
||||
if adminForm := strings.Contains(w.Body.String(), "impersonate"); adminForm != tt.wantAdminForm {
|
||||
t.Errorf("wanted admin form got %v want %v", adminForm, tt.wantAdminForm)
|
||||
t.Errorf("\n%+v", w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_Refresh(t *testing.T) {
|
||||
opts := testOptions()
|
||||
opts.RefreshCooldown = 0
|
||||
timeSinceError := testOptions()
|
||||
timeSinceError.RefreshCooldown = time.Duration(int(^uint(0) >> 1))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
options *config.Options
|
||||
method string
|
||||
cipher cryptutil.Cipher
|
||||
session sessions.SessionStore
|
||||
authenticator clients.Authenticator
|
||||
authorizer clients.Authorizer
|
||||
wantStatus int
|
||||
}{
|
||||
{"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusFound},
|
||||
{"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusBadRequest},
|
||||
{"bad id token", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "bad"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError},
|
||||
{"issue date too soon", timeSinceError, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusBadRequest},
|
||||
{"refresh failure", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{RefreshError: errors.New("err")}, clients.MockAuthorize{}, http.StatusInternalServerError},
|
||||
{"can't save refreshed session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p, err := New(tt.options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.cipher = tt.cipher
|
||||
p.sessionStore = tt.session
|
||||
p.AuthenticateClient = tt.authenticator
|
||||
p.AuthorizeClient = tt.authorizer
|
||||
|
||||
r := httptest.NewRequest(tt.method, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
p.Refresh(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
|
||||
// t.Errorf("\n%+v", w.Body.String())
|
||||
t.Errorf("\n%+v", opts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_Impersonate(t *testing.T) {
|
||||
opts := testOptions()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
options *config.Options
|
||||
method string
|
||||
email string
|
||||
groups string
|
||||
csrf string
|
||||
cipher cryptutil.Cipher
|
||||
sessionStore sessions.SessionStore
|
||||
csrfStore sessions.CSRFStore
|
||||
authenticator clients.Authenticator
|
||||
authorizer clients.Authorizer
|
||||
wantStatus int
|
||||
}{
|
||||
{"good", 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.StatusFound},
|
||||
{"session load error", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: 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},
|
||||
{"non admin users rejected", 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: false}, http.StatusForbidden},
|
||||
{"non admin users rejected on error", 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, IsAdminError: errors.New("err")}, http.StatusForbidden},
|
||||
{"csrf from store retrieve failure", 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"}, GetError: errors.New("err")}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest},
|
||||
{"can't decrypt csrf value", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{UnmarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"decrypted csrf mismatch", 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", 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},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p, err := New(tt.options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.cipher = tt.cipher
|
||||
p.sessionStore = tt.sessionStore
|
||||
p.csrfStore = tt.csrfStore
|
||||
p.AuthenticateClient = tt.authenticator
|
||||
p.AuthorizeClient = tt.authorizer
|
||||
postForm := url.Values{}
|
||||
postForm.Add("email", tt.email)
|
||||
postForm.Add("group", tt.groups)
|
||||
postForm.Set("csrf", tt.csrf)
|
||||
|
||||
r := httptest.NewRequest(tt.method, "/", bytes.NewBufferString(postForm.Encode()))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
|
||||
w := httptest.NewRecorder()
|
||||
p.Impersonate(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
|
||||
// t.Errorf("\n%+v", w.Body.String())
|
||||
t.Errorf("\n%+v", opts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
|
||||
|
@ -92,9 +93,10 @@ type Proxy struct {
|
|||
csrfStore sessions.CSRFStore
|
||||
sessionStore sessions.SessionStore
|
||||
|
||||
redirectURL *url.URL
|
||||
templates *template.Template
|
||||
routeConfigs map[string]*routeConfig
|
||||
redirectURL *url.URL
|
||||
templates *template.Template
|
||||
routeConfigs map[string]*routeConfig
|
||||
refreshCooldown time.Duration
|
||||
}
|
||||
|
||||
type routeConfig struct {
|
||||
|
@ -137,12 +139,13 @@ func New(opts *config.Options) (*Proxy, error) {
|
|||
// services
|
||||
AuthenticateURL: opts.AuthenticateURL,
|
||||
// session state
|
||||
cipher: cipher,
|
||||
csrfStore: cookieStore,
|
||||
sessionStore: cookieStore,
|
||||
SharedKey: opts.SharedKey,
|
||||
redirectURL: &url.URL{Path: "/.pomerium/callback"},
|
||||
templates: templates.New(),
|
||||
cipher: cipher,
|
||||
csrfStore: cookieStore,
|
||||
sessionStore: cookieStore,
|
||||
SharedKey: opts.SharedKey,
|
||||
redirectURL: &url.URL{Path: "/.pomerium/callback"},
|
||||
templates: templates.New(),
|
||||
refreshCooldown: opts.RefreshCooldown,
|
||||
}
|
||||
|
||||
for _, route := range opts.Policies {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package proxy // import "github.com/pomerium/pomerium/proxy"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
|
@ -10,6 +11,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/proxy/clients"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
)
|
||||
|
@ -237,3 +240,46 @@ func TestNew(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_OAuthCallback(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
csrf sessions.MockCSRFStore
|
||||
session sessions.MockSessionStore
|
||||
authenticator clients.MockAuthenticate
|
||||
params map[string]string
|
||||
wantCode int
|
||||
}{
|
||||
{"good", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusFound},
|
||||
{"error", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"error": "some error"}, http.StatusForbidden},
|
||||
{"state err", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError},
|
||||
{"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusBadRequest},
|
||||
{"unmarshal err", sessions.MockCSRFStore{Cookie: &http.Cookie{Name: "something_csrf", Value: "unmarshal error"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
proxy, err := New(testOptions())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
proxy.sessionStore = &tt.session
|
||||
proxy.csrfStore = tt.csrf
|
||||
proxy.AuthenticateClient = tt.authenticator
|
||||
proxy.cipher = mockCipher{}
|
||||
// proxy.Csrf
|
||||
req := httptest.NewRequest(http.MethodPost, "/.pomerium/callback", nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range tt.params {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
w := httptest.NewRecorder()
|
||||
proxy.OAuthCallback(w, req)
|
||||
if status := w.Code; status != tt.wantCode {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue