diff --git a/main.go b/main.go index 07c1df51..4217e050 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ func main() { emailDomains := StringArray{} upstreams := StringArray{} skipAuthRegex := StringArray{} - googleGroups := StringArray{} + permittedGroups := StringArray{} config := flagSet.String("config", "", "path to config file") showVersion := flagSet.Bool("version", false, "print version string") @@ -36,6 +36,7 @@ func main() { flagSet.Bool("pass-user-headers", true, "pass X-Forwarded-User and X-Forwarded-Email information to upstream") flagSet.Bool("pass-groups", false, "pass user group information in the X-Forwarded-Groups header to upstream (Azure only)") flagSet.String("filter-groups", "", "exclude groups that do not contain this value in its 'displayName' (Azure only)") + flagSet.Var(&permittedGroups, "permit-groups", "restrict logins to members of this group (may be given multiple times; Azure and Google only).") flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header") flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header") flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream") @@ -47,7 +48,6 @@ func main() { flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") flagSet.String("github-org", "", "restrict logins to members of this organisation") flagSet.String("github-team", "", "restrict logins to members of this team") - flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).") flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls") flagSet.String("google-service-account-json", "", "the path to the service account json credentials") flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"") diff --git a/oauthproxy.go b/oauthproxy.go index 6a8ba3d0..b58edcf8 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -566,7 +566,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { } // set cookie, or deny - if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) { + if p.Validator(session.Email) && p.provider.ValidateGroup(session) { log.Printf("%s authentication complete %s", remoteAddr, session) err := p.SaveSession(rw, req, session) if err != nil { diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 06894244..027ccbe5 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -78,6 +78,7 @@ func TestEncodedSlashes(t *testing.T) { func TestRobotsTxt(t *testing.T) { opts := NewOptions() + opts.Provider = "google" opts.ClientID = "bazquux" opts.ClientSecret = "foobar" opts.CookieSecret = "xyzzyplugh" @@ -151,6 +152,7 @@ func TestBasicAuthPassword(t *testing.T) { opts.Upstreams = append(opts.Upstreams, provider_server.URL) // The CookieSecret must be 32 bytes in order to create the AES // cipher. + opts.Provider = "google" opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" opts.ClientID = "bazquux" opts.ClientSecret = "foobar" @@ -245,6 +247,7 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes t.opts.Upstreams = append(t.opts.Upstreams, t.provider_server.URL) // The CookieSecret must be 32 bytes in order to create the AES // cipher. + t.opts.Provider = "google" t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" t.opts.ClientID = "bazquux" t.opts.ClientSecret = "foobar" @@ -370,6 +373,7 @@ func NewSignInPageTest() *SignInPageTest { var sip_test SignInPageTest sip_test.opts = NewOptions() + sip_test.opts.Provider = "google" sip_test.opts.CookieSecret = "foobar" sip_test.opts.ClientID = "bazquux" sip_test.opts.ClientSecret = "xyzzyplugh" @@ -441,6 +445,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts) *ProcessCookieTest { var pc_test ProcessCookieTest pc_test.opts = NewOptions() + pc_test.opts.Provider = "google" pc_test.opts.ClientID = "bazquux" pc_test.opts.ClientSecret = "xyzzyplugh" pc_test.opts.CookieSecret = "0123456789abcdefabcd" @@ -672,6 +677,7 @@ type SignatureTest struct { func NewSignatureTest() *SignatureTest { opts := NewOptions() + opts.Provider = "google" opts.CookieSecret = "cookie secret" opts.ClientID = "client ID" opts.ClientSecret = "client secret" diff --git a/options.go b/options.go index d3333a9c..40dadf27 100644 --- a/options.go +++ b/options.go @@ -32,7 +32,6 @@ type Options struct { EmailDomains []string `flag:"email-domain" cfg:"email_domains"` GitHubOrg string `flag:"github-org" cfg:"github_org"` GitHubTeam string `flag:"github-team" cfg:"github_team"` - GoogleGroups []string `flag:"google-group" cfg:"google_group"` GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"` GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"` HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"` @@ -53,6 +52,7 @@ type Options struct { PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"` PassGroups bool `flag:"pass-groups" cfg:"pass_groups"` FilterGroups string `flag:"filter-groups" cfg:"filter_groups"` + PermitGroups []string `flag:"permit-groups" cfg:"permit_groups"` BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"` PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"` PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"` @@ -197,15 +197,17 @@ func (o *Options) Validate() error { o.CookieExpire.String())) } - if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" { - if len(o.GoogleGroups) < 1 { - msgs = append(msgs, "missing setting: google-group") - } - if o.GoogleAdminEmail == "" { - msgs = append(msgs, "missing setting: google-admin-email") - } - if o.GoogleServiceAccountJSON == "" { - msgs = append(msgs, "missing setting: google-service-account-json") + if o.Provider == "google" { + if len(o.PermitGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" { + if len(o.PermitGroups) < 1 { + msgs = append(msgs, "missing setting: permit-groups") + } + if o.GoogleAdminEmail == "" { + msgs = append(msgs, "missing setting: google-admin-email") + } + if o.GoogleServiceAccountJSON == "" { + msgs = append(msgs, "missing setting: google-service-account-json") + } } } @@ -239,10 +241,17 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs) p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs) - o.provider = providers.New(o.Provider, p) + provider, err := providers.New(o.Provider, p) + o.provider = provider + if err != nil { + msgs = append(msgs, err.Error()) + } switch p := o.provider.(type) { case *providers.AzureProvider: p.Configure(o.AzureTenant) + if len(o.PermitGroups) > 0 { + p.SetGroupRestriction(o.PermitGroups) + } case *providers.GitHubProvider: p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) case *providers.GoogleProvider: @@ -251,7 +260,7 @@ func parseProviderInfo(o *Options, msgs []string) []string { if err != nil { msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON) } else { - p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file) + p.SetGroupRestriction(o.PermitGroups, o.GoogleAdminEmail, file) } } } diff --git a/options_test.go b/options_test.go index 27dd8524..f3cf1a95 100644 --- a/options_test.go +++ b/options_test.go @@ -14,6 +14,7 @@ import ( func testOptions() *Options { o := NewOptions() o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8080/") + o.Provider = "google" o.CookieSecret = "foobar" o.ClientID = "bazquux" o.ClientSecret = "xyzzyplugh" @@ -38,25 +39,34 @@ func TestNewOptions(t *testing.T) { "missing setting: upstream", "missing setting: cookie-secret", "missing setting: client-id", - "missing setting: client-secret"}) + "missing setting: client-secret", + "missing setting: provider"}) assert.Equal(t, expected, err.Error()) } -func TestGoogleGroupOptions(t *testing.T) { +func TestGooglePermitGroupsOptions(t *testing.T) { o := testOptions() - o.GoogleGroups = []string{"googlegroup"} + o.GoogleAdminEmail = "admin@example.com" err := o.Validate() assert.NotEqual(t, nil, err) expected := errorMsg([]string{ - "missing setting: google-admin-email", + "missing setting: permit-groups", "missing setting: google-service-account-json"}) assert.Equal(t, expected, err.Error()) } -func TestGoogleGroupInvalidFile(t *testing.T) { +func TestPermitGroupsOptions(t *testing.T) { o := testOptions() - o.GoogleGroups = []string{"test_group"} + o.Provider = "azure" + o.PermitGroups = []string{"agoodgroup"} + err := o.Validate() + assert.Equal(t, nil, err) +} + +func TestPermitGroupsInvalidFile(t *testing.T) { + o := testOptions() + o.PermitGroups = []string{"test_group"} o.GoogleAdminEmail = "admin@example.com" o.GoogleServiceAccountJSON = "file_doesnt_exist.json" err := o.Validate() diff --git a/providers/azure.go b/providers/azure.go index bf7b50e1..4097819b 100644 --- a/providers/azure.go +++ b/providers/azure.go @@ -13,7 +13,8 @@ import ( type AzureProvider struct { *ProviderData - Tenant string + Tenant string + PermittedGroups []string } func NewAzureProvider(p *ProviderData) *AzureProvider { @@ -205,3 +206,18 @@ func (p *AzureProvider) GetLoginURL(redirectURI, finalRedirect string) string { a.RawQuery = params.Encode() return a.String() } + +func (p *AzureProvider) SetGroupRestriction(groups []string) { + p.PermittedGroups = groups +} + +func (p *AzureProvider) ValidateGroup(s *SessionState) bool { + if len(p.PermittedGroups) != 0 { + for _, pGroup := range p.PermittedGroups { + if strings.Contains(s.Groups, pGroup) { + return true + } + } + } + return false +} diff --git a/providers/google.go b/providers/google.go index a9cb487a..13962c26 100644 --- a/providers/google.go +++ b/providers/google.go @@ -252,8 +252,8 @@ func fetchGroupMembers(service *admin.Service, group string) ([]*admin.Member, e // ValidateGroup validates that the provided email exists in the configured Google // group(s). -func (p *GoogleProvider) ValidateGroup(email string) bool { - return p.GroupValidator(email) +func (p *GoogleProvider) ValidateGroup(s *SessionState) bool { + return p.GroupValidator(s.Email) } func (p *GoogleProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) { @@ -267,7 +267,7 @@ func (p *GoogleProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) { } // re-check that the user is in the proper google group(s) - if !p.ValidateGroup(s.Email) { + if !p.ValidateGroup(s) { return false, fmt.Errorf("%s is no longer in the group(s)", s.Email) } diff --git a/providers/provider_default.go b/providers/provider_default.go index 755d2925..01874853 100644 --- a/providers/provider_default.go +++ b/providers/provider_default.go @@ -117,7 +117,7 @@ func (p *ProviderData) GetGroups(s *SessionState, f string) (string, error) { // ValidateGroup validates that the provided email exists in the configured provider // email group(s). -func (p *ProviderData) ValidateGroup(email string) bool { +func (p *ProviderData) ValidateGroup(s *SessionState) bool { return true } diff --git a/providers/providers.go b/providers/providers.go index 40cd07e4..aec0a219 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -1,6 +1,7 @@ package providers import ( + "errors" "github.com/bitly/oauth2_proxy/cookie" ) @@ -9,7 +10,7 @@ type Provider interface { GetEmailAddress(*SessionState) (string, error) GetGroups(*SessionState, string) (string, error) Redeem(string, string) (*SessionState, error) - ValidateGroup(string) bool + ValidateGroup(*SessionState) bool ValidateSessionState(*SessionState) bool GetLoginURL(redirectURI, finalRedirect string) string RefreshSessionIfNeeded(*SessionState) (bool, error) @@ -17,21 +18,23 @@ type Provider interface { CookieForSession(*SessionState, *cookie.Cipher) (string, error) } -func New(provider string, p *ProviderData) Provider { +func New(provider string, p *ProviderData) (Provider, error) { switch provider { case "myusa": - return NewMyUsaProvider(p) + return NewMyUsaProvider(p), nil case "linkedin": - return NewLinkedInProvider(p) + return NewLinkedInProvider(p), nil case "facebook": - return NewFacebookProvider(p) + return NewFacebookProvider(p), nil case "github": - return NewGitHubProvider(p) + return NewGitHubProvider(p), nil case "azure": - return NewAzureProvider(p) + return NewAzureProvider(p), nil case "gitlab": - return NewGitLabProvider(p) + return NewGitLabProvider(p), nil + case "google": + return NewGoogleProvider(p), nil default: - return NewGoogleProvider(p) + return nil, errors.New("missing setting: provider") } }