1
0
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:
Sourav Agrawal
2025-07-24 11:25:54 +05:30
committed by GitHub
parent b905f2cd93
commit e75a258299
6 changed files with 135 additions and 25 deletions

View File

@ -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)

View File

@ -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)
}