You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-08-06 22:42:56 +02:00
feat: make google-groups argument optional (#3138)
add test cases update documentation refactor code and some cleanup update changelog Signed-off-by: Jan Larwig <jan@larwig.com>
This commit is contained in:
@ -103,17 +103,24 @@ func NewGoogleProvider(p *ProviderData, opts options.GoogleOptions) (*GoogleProv
|
||||
}
|
||||
|
||||
if opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
|
||||
// Backwards compatibility with `--google-group` option
|
||||
if len(opts.Groups) > 0 {
|
||||
provider.setAllowedGroups(opts.Groups)
|
||||
}
|
||||
|
||||
provider.setGroupRestriction(opts)
|
||||
provider.configureGroups(opts)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (p *GoogleProvider) configureGroups(opts options.GoogleOptions) {
|
||||
adminService := getAdminService(opts)
|
||||
// Backwards compatibility with `--google-group` option
|
||||
if len(opts.Groups) > 0 {
|
||||
p.setAllowedGroups(opts.Groups)
|
||||
p.groupValidator = p.setGroupRestriction(opts.Groups, adminService)
|
||||
return
|
||||
}
|
||||
|
||||
p.groupValidator = p.populateAllGroups(adminService)
|
||||
}
|
||||
|
||||
func claimsFromIDToken(idToken string) (*claims, error) {
|
||||
|
||||
// id_token is a base64 encode ID token payload
|
||||
@ -209,18 +216,13 @@ func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionSta
|
||||
}
|
||||
|
||||
// SetGroupRestriction configures the GoogleProvider to restrict access to the
|
||||
// specified group(s). AdminEmail has to be an administrative email on the domain that is
|
||||
// checked. CredentialsFile is the path to a json file containing a Google service
|
||||
// account credentials.
|
||||
//
|
||||
// TODO (@NickMeves) - Unit Test this OR refactor away from groupValidator func
|
||||
func (p *GoogleProvider) setGroupRestriction(opts options.GoogleOptions) {
|
||||
adminService := getAdminService(opts)
|
||||
p.groupValidator = func(s *sessions.SessionState) bool {
|
||||
// specified group(s).
|
||||
func (p *GoogleProvider) setGroupRestriction(groups []string, adminService *admin.Service) func(*sessions.SessionState) bool {
|
||||
return func(s *sessions.SessionState) bool {
|
||||
// Reset our saved Groups in case membership changed
|
||||
// This is used by `Authorize` on every request
|
||||
s.Groups = make([]string, 0, len(opts.Groups))
|
||||
for _, group := range opts.Groups {
|
||||
s.Groups = make([]string, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
if userInGroup(adminService, group, s.Email) {
|
||||
s.Groups = append(s.Groups, group)
|
||||
}
|
||||
@ -229,6 +231,25 @@ func (p *GoogleProvider) setGroupRestriction(opts options.GoogleOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// populateAllGroups configures the GoogleProvider to allow access with all
|
||||
// groups and populate session with all groups of the user when no specific
|
||||
// groups are configured.
|
||||
func (p *GoogleProvider) populateAllGroups(adminService *admin.Service) func(s *sessions.SessionState) bool {
|
||||
return func(s *sessions.SessionState) bool {
|
||||
// Get all groups of the user
|
||||
groups, err := getUserGroups(adminService, s.Email)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get user groups for %s: %v", s.Email, err)
|
||||
s.Groups = []string{}
|
||||
return true // Allow access even if we can't get groups
|
||||
}
|
||||
|
||||
// Populate session with all user groups
|
||||
s.Groups = groups
|
||||
return true // Always allow access when no specific groups are configured
|
||||
}
|
||||
}
|
||||
|
||||
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/hasMember#authorization-scopes
|
||||
var possibleScopesList = [...]string{
|
||||
admin.AdminDirectoryGroupMemberReadonlyScope,
|
||||
@ -269,6 +290,10 @@ func getOauth2TokenSource(ctx context.Context, opts options.GoogleOptions, scope
|
||||
return conf.TokenSource(ctx)
|
||||
}
|
||||
|
||||
// getAdminService retrieves an oauth token for the admin api of Google
|
||||
// AdminEmail has to be an administrative email on the domain that is
|
||||
// checked. CredentialsFile is the path to a json file containing a Google service
|
||||
// account credentials.
|
||||
func getAdminService(opts options.GoogleOptions) *admin.Service {
|
||||
ctx := context.Background()
|
||||
var client *http.Client
|
||||
@ -339,6 +364,38 @@ func getTargetPrincipal(ctx context.Context, opts options.GoogleOptions) (target
|
||||
return targetPrincipal
|
||||
}
|
||||
|
||||
// getUserGroups retrieves all groups that a user is a member of using the Google Admin Directory API
|
||||
func getUserGroups(service *admin.Service, email string) ([]string, error) {
|
||||
var allGroups []string
|
||||
var pageToken string
|
||||
|
||||
for {
|
||||
req := service.Groups.List().UserKey(email).MaxResults(200)
|
||||
if pageToken != "" {
|
||||
req = req.PageToken(pageToken)
|
||||
}
|
||||
|
||||
groupsResp, err := req.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list groups for user %s: %v", email, err)
|
||||
}
|
||||
|
||||
for _, group := range groupsResp.Groups {
|
||||
if group.Email != "" {
|
||||
allGroups = append(allGroups, group.Email)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are more pages
|
||||
if groupsResp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = groupsResp.NextPageToken
|
||||
}
|
||||
|
||||
return allGroups, nil
|
||||
}
|
||||
|
||||
func userInGroup(service *admin.Service, group string, email string) bool {
|
||||
// Use the HasMember API to checking for the user's presence in each group or nested subgroups
|
||||
req := service.Members.HasMember(group, email)
|
||||
|
@ -289,3 +289,39 @@ func TestGoogleProvider_userInGroup(t *testing.T) {
|
||||
result = userInGroup(service, "group@example.com", "non-member-out-of-domain@otherexample.com")
|
||||
assert.False(t, result)
|
||||
}
|
||||
|
||||
func TestGoogleProvider_getUserGroups(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/admin/directory/v1/groups" && r.URL.Query().Get("userKey") == "test@example.com" {
|
||||
response := `{
|
||||
"kind": "admin#directory#groups",
|
||||
"groups": [
|
||||
{
|
||||
"kind": "admin#directory#group",
|
||||
"id": "1",
|
||||
"email": "group1@example.com",
|
||||
"name": "Group 1"
|
||||
},
|
||||
{
|
||||
"kind": "admin#directory#group",
|
||||
"id": "2",
|
||||
"email": "group2@example.com",
|
||||
"name": "Group 2"
|
||||
}
|
||||
]
|
||||
}`
|
||||
fmt.Fprintln(w, response)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := &http.Client{}
|
||||
adminService, err := admin.NewService(context.Background(), option.WithHTTPClient(client), option.WithEndpoint(ts.URL))
|
||||
assert.NoError(t, err)
|
||||
|
||||
groups, err := getUserGroups(adminService, "test@example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"group1@example.com", "group2@example.com"}, groups)
|
||||
}
|
||||
|
Reference in New Issue
Block a user