From 2fd6f02cd851682ae482e2c508db4d93454ec75f Mon Sep 17 00:00:00 2001 From: Brandon Matthews Date: Thu, 12 Jan 2017 12:15:00 -0800 Subject: [PATCH] Add support for a simple group filter --- main.go | 3 +- oauthproxy.go | 4 +- options.go | 2 + providers/azure.go | 74 +++++++++++++++++++++-------------- providers/provider_default.go | 2 +- providers/providers.go | 2 +- 6 files changed, 54 insertions(+), 33 deletions(-) diff --git a/main.go b/main.go index dad45fc7..07c1df51 100644 --- a/main.go +++ b/main.go @@ -34,7 +34,8 @@ func main() { flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path") flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream") 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 along with basic auth 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.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") diff --git a/oauthproxy.go b/oauthproxy.go index 259aa38d..6a8ba3d0 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -64,6 +64,7 @@ type OAuthProxy struct { SetXAuthRequest bool PassBasicAuth bool PassGroups bool + FilterGroups string SkipProviderButton bool PassUserHeaders bool BasicAuthPassword string @@ -208,6 +209,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { PassBasicAuth: opts.PassBasicAuth, PassUserHeaders: opts.PassUserHeaders, PassGroups: opts.PassGroups, + FilterGroups: opts.FilterGroups, BasicAuthPassword: opts.BasicAuthPassword, PassAccessToken: opts.PassAccessToken, SkipProviderButton: opts.SkipProviderButton, @@ -551,7 +553,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { session.IDToken = req.Form.Get("id_token") if p.PassGroups && session.IDToken != "" { - session.Groups, err = p.provider.GetGroups(session) + session.Groups, err = p.provider.GetGroups(session, p.FilterGroups) if err != nil { p.ErrorPage(rw, 500, "Internal Error", "Internal Error") return diff --git a/options.go b/options.go index 0970f631..d3333a9c 100644 --- a/options.go +++ b/options.go @@ -52,6 +52,7 @@ type Options struct { SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` 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"` 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"` @@ -103,6 +104,7 @@ func NewOptions() *Options { PassBasicAuth: true, PassUserHeaders: true, PassGroups: false, + FilterGroups: "", PassAccessToken: false, PassHostHeader: true, ApprovalPrompt: "force", diff --git a/providers/azure.go b/providers/azure.go index 17cf5ea9..bf7b50e1 100644 --- a/providers/azure.go +++ b/providers/azure.go @@ -127,7 +127,8 @@ func (p *AzureProvider) GetEmailAddress(s *SessionState) (string, error) { return email, err } -func (p *AzureProvider) GetGroups(s *SessionState) (string, error) { +// Get list of groups user belong to. Filter the desired names of groups (in case of huge group set) +func (p *AzureProvider) GetGroups(s *SessionState, f string) (string, error) { if s.AccessToken == "" { return "", errors.New("missing access token") } @@ -136,36 +137,51 @@ func (p *AzureProvider) GetGroups(s *SessionState) (string, error) { return "", errors.New("missing id token") } - // parse token, check if it has groups - // if groups, return "|".join(groups_list) - // else - // look for claim source - // GET source - // parse response - // return "|".join(response.groups_list) + // For future use. Right now microsoft graph don't support filter + // http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part2-url-conventions/odata-v4.0-errata02-os-part2-url-conventions-complete.html#_Toc406398116 - req, err := http.NewRequest("GET", "https://graph.microsoft.com/v1.0/me/memberOf/", nil) - //req, err := http.NewRequest("POST", "https://graph.microsoft.com/v1.0/me/getMemberGroups", strings.NewReader("{\"securityEnabledOnly\":true}")) - if err != nil { - return "", err - } - req.Header = getAzureHeader(s.AccessToken) - req.Header.Add("Content-Type", "application/json") - - groupData, err := api.Request(req) - - groups := make([]string, 0) - for _, groupInfo := range groupData.Get("value").MustArray() { - //v, ok := groupInfo.(string) - v, ok := groupInfo.(map[string]interface{}) - if !ok { - continue + /* + var request string = "https://graph.microsoft.com/v1.0/me/memberOf?$select=id,displayName,groupTypes,securityEnabled,description,mailEnabled&$top=999" + if f != "" { + request += "?$filter=contains(displayName, '"+f+"')" } - dname := v["displayName"].(string) - secen := v["securityEnabled"].(bool) - mailen := v["mailEnabled"].(bool) - if secen == true && mailen == false { - groups = append(groups, dname) + */ + // + // Filters that will be possible to use: + // contains - unknown function | "https://graph.microsoft.com/v1.0/me/memberOf?$filter=contains(displayName,%27olm%27)" + // startswith - not supported | "https://graph.microsoft.com/v1.0/me/memberOf?$filter=startswith(displayName,%27olm%27)" + // substring - not supported | "https://graph.microsoft.com/v1.0/me/memberOf?$filter=substring(displayName,0,2)%20eq%20%27olm%27" + + requestUrl := "https://graph.microsoft.com/v1.0/me/memberOf?$select=displayName" + groups := make([]string, 0) + + for { + req, err := http.NewRequest("GET", requestUrl, nil) + + if err != nil { + return "", err + } + req.Header = getAzureHeader(s.AccessToken) + req.Header.Add("Content-Type", "application/json") + + groupData, err := api.Request(req) + + for _, groupInfo := range groupData.Get("value").MustArray() { + v, ok := groupInfo.(map[string]interface{}) + if !ok { + continue + } + dname := v["displayName"].(string) + if strings.Contains(dname, f) { + groups = append(groups, dname) + } + + } + + if nextlink := groupData.Get("@odata.nextLink").MustString(); nextlink != "" { + requestUrl = nextlink + } else { + break } } diff --git a/providers/provider_default.go b/providers/provider_default.go index 25e27827..755d2925 100644 --- a/providers/provider_default.go +++ b/providers/provider_default.go @@ -111,7 +111,7 @@ func (p *ProviderData) GetEmailAddress(s *SessionState) (string, error) { return "", errors.New("not implemented") } -func (p *ProviderData) GetGroups(s *SessionState) (string, error) { +func (p *ProviderData) GetGroups(s *SessionState, f string) (string, error) { return "", errors.New("not implemented") } diff --git a/providers/providers.go b/providers/providers.go index 2634d197..40cd07e4 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -7,7 +7,7 @@ import ( type Provider interface { Data() *ProviderData GetEmailAddress(*SessionState) (string, error) - GetGroups(*SessionState) (string, error) + GetGroups(*SessionState, string) (string, error) Redeem(string, string) (*SessionState, error) ValidateGroup(string) bool ValidateSessionState(*SessionState) bool