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:
@ -15,6 +15,7 @@
|
|||||||
- [#2524](https://github.com/oauth2-proxy/oauth2-proxy/pull/2524) fix: regex substitution for $ signs in upstream path handling before running envsubst (@dashkan / @tuunit)
|
- [#2524](https://github.com/oauth2-proxy/oauth2-proxy/pull/2524) fix: regex substitution for $ signs in upstream path handling before running envsubst (@dashkan / @tuunit)
|
||||||
- [#3104](https://github.com/oauth2-proxy/oauth2-proxy/pull/3104) feat(cookie): add feature support for cookie-secret-file (@sandy2008)
|
- [#3104](https://github.com/oauth2-proxy/oauth2-proxy/pull/3104) feat(cookie): add feature support for cookie-secret-file (@sandy2008)
|
||||||
- [#3055](https://github.com/oauth2-proxy/oauth2-proxy/pull/3055) feat: support non-default authorization request response mode also for OIDC providers (@stieler-it)
|
- [#3055](https://github.com/oauth2-proxy/oauth2-proxy/pull/3055) feat: support non-default authorization request response mode also for OIDC providers (@stieler-it)
|
||||||
|
- [#3138](https://github.com/oauth2-proxy/oauth2-proxy/pull/3138) feat: make google_groups argument optional when using google provider (@sourava01)
|
||||||
|
|
||||||
# V7.10.0
|
# V7.10.0
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ title: Google (default)
|
|||||||
| Flag | Toml Field | Type | Description | Default |
|
| Flag | Toml Field | Type | Description | Default |
|
||||||
| ---------------------------------------------- | -------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
|
| ---------------------------------------------- | -------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
|
||||||
| `--google-admin-email` | `google_admin_email` | string | the google admin to impersonate for api calls | |
|
| `--google-admin-email` | `google_admin_email` | string | the google admin to impersonate for api calls | |
|
||||||
| `--google-group` | `google_groups` | string | restrict logins to members of this google group (may be given multiple times). | |
|
| `--google-group` | `google_groups` | string | restrict logins to members of this google group (may be given multiple times). If not specified and service account or default credentials are configured, all user groups will be allowed. | |
|
||||||
| `--google-service-account-json` | `google_service_account_json` | string | the path to the service account json credentials | |
|
| `--google-service-account-json` | `google_service_account_json` | string | the path to the service account json credentials | |
|
||||||
| `--google-use-application-default-credentials` | `google_use_application_default_credentials` | bool | use application default credentials instead of service account json (i.e. GKE Workload Identity) | |
|
| `--google-use-application-default-credentials` | `google_use_application_default_credentials` | bool | use application default credentials instead of service account json (i.e. GKE Workload Identity) | |
|
||||||
| `--google-target-principal` | `google_target_principal` | bool | the target principal to impersonate when using ADC | defaults to the service account configured for ADC |
|
| `--google-target-principal` | `google_target_principal` | bool | the target principal to impersonate when using ADC | defaults to the service account configured for ADC |
|
||||||
|
@ -55,18 +55,38 @@ func TestNewOptions(t *testing.T) {
|
|||||||
assert.Equal(t, expected, err.Error())
|
assert.Equal(t, expected, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGoogleGroupOptions(t *testing.T) {
|
func TestGoogleGroupOptionsWithoutServiceAccountJSON(t *testing.T) {
|
||||||
o := testOptions()
|
o := testOptions()
|
||||||
o.Providers[0].GoogleConfig.Groups = []string{"googlegroup"}
|
o.Providers[0].GoogleConfig.AdminEmail = "admin@example.com"
|
||||||
err := Validate(o)
|
err := Validate(o)
|
||||||
assert.NotEqual(t, nil, err)
|
assert.NotEqual(t, nil, err)
|
||||||
|
|
||||||
expected := errorMsg([]string{
|
expected := errorMsg([]string{
|
||||||
"missing setting: google-admin-email",
|
|
||||||
"missing setting: google-service-account-json or google-use-application-default-credentials"})
|
"missing setting: google-service-account-json or google-use-application-default-credentials"})
|
||||||
assert.Equal(t, expected, err.Error())
|
assert.Equal(t, expected, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGoogleGroupOptionsWithoutAdminEmail(t *testing.T) {
|
||||||
|
o := testOptions()
|
||||||
|
o.Providers[0].GoogleConfig.UseApplicationDefaultCredentials = true
|
||||||
|
err := Validate(o)
|
||||||
|
assert.NotEqual(t, nil, err)
|
||||||
|
|
||||||
|
expected := errorMsg([]string{
|
||||||
|
"missing setting: google-admin-email"})
|
||||||
|
assert.Equal(t, expected, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoogleGroupOptionsWithoutGroups(t *testing.T) {
|
||||||
|
o := testOptions()
|
||||||
|
// Set admin email and application default credentials but no groups - should still require them
|
||||||
|
o.Providers[0].GoogleConfig.AdminEmail = "admin@example.com"
|
||||||
|
o.Providers[0].GoogleConfig.UseApplicationDefaultCredentials = true
|
||||||
|
err := Validate(o)
|
||||||
|
// Should pass validation since google-group is now optional
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestGoogleGroupInvalidFile(t *testing.T) {
|
func TestGoogleGroupInvalidFile(t *testing.T) {
|
||||||
o := testOptions()
|
o := testOptions()
|
||||||
o.Providers[0].GoogleConfig.Groups = []string{"test_group"}
|
o.Providers[0].GoogleConfig.Groups = []string{"test_group"}
|
||||||
|
@ -94,18 +94,14 @@ func validateClientSecret(provider options.Provider) []string {
|
|||||||
func validateGoogleConfig(provider options.Provider) []string {
|
func validateGoogleConfig(provider options.Provider) []string {
|
||||||
msgs := []string{}
|
msgs := []string{}
|
||||||
|
|
||||||
hasGoogleGroups := len(provider.GoogleConfig.Groups) >= 1
|
|
||||||
hasAdminEmail := provider.GoogleConfig.AdminEmail != ""
|
hasAdminEmail := provider.GoogleConfig.AdminEmail != ""
|
||||||
hasSAJSON := provider.GoogleConfig.ServiceAccountJSON != ""
|
hasSAJSON := provider.GoogleConfig.ServiceAccountJSON != ""
|
||||||
useADC := provider.GoogleConfig.UseApplicationDefaultCredentials
|
useADC := provider.GoogleConfig.UseApplicationDefaultCredentials
|
||||||
|
|
||||||
if !hasGoogleGroups && !hasAdminEmail && !hasSAJSON && !useADC {
|
if !hasAdminEmail && !hasSAJSON && !useADC {
|
||||||
return msgs
|
return msgs
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasGoogleGroups {
|
|
||||||
msgs = append(msgs, "missing setting: google-group")
|
|
||||||
}
|
|
||||||
if !hasAdminEmail {
|
if !hasAdminEmail {
|
||||||
msgs = append(msgs, "missing setting: google-admin-email")
|
msgs = append(msgs, "missing setting: google-admin-email")
|
||||||
}
|
}
|
||||||
|
@ -103,17 +103,24 @@ func NewGoogleProvider(p *ProviderData, opts options.GoogleOptions) (*GoogleProv
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
|
if opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
|
||||||
// Backwards compatibility with `--google-group` option
|
provider.configureGroups(opts)
|
||||||
if len(opts.Groups) > 0 {
|
|
||||||
provider.setAllowedGroups(opts.Groups)
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.setGroupRestriction(opts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider, nil
|
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) {
|
func claimsFromIDToken(idToken string) (*claims, error) {
|
||||||
|
|
||||||
// id_token is a base64 encode ID token payload
|
// 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
|
// SetGroupRestriction configures the GoogleProvider to restrict access to the
|
||||||
// specified group(s). AdminEmail has to be an administrative email on the domain that is
|
// specified group(s).
|
||||||
// checked. CredentialsFile is the path to a json file containing a Google service
|
func (p *GoogleProvider) setGroupRestriction(groups []string, adminService *admin.Service) func(*sessions.SessionState) bool {
|
||||||
// account credentials.
|
return func(s *sessions.SessionState) bool {
|
||||||
//
|
|
||||||
// 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 {
|
|
||||||
// Reset our saved Groups in case membership changed
|
// Reset our saved Groups in case membership changed
|
||||||
// This is used by `Authorize` on every request
|
// This is used by `Authorize` on every request
|
||||||
s.Groups = make([]string, 0, len(opts.Groups))
|
s.Groups = make([]string, 0, len(groups))
|
||||||
for _, group := range opts.Groups {
|
for _, group := range groups {
|
||||||
if userInGroup(adminService, group, s.Email) {
|
if userInGroup(adminService, group, s.Email) {
|
||||||
s.Groups = append(s.Groups, group)
|
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
|
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/hasMember#authorization-scopes
|
||||||
var possibleScopesList = [...]string{
|
var possibleScopesList = [...]string{
|
||||||
admin.AdminDirectoryGroupMemberReadonlyScope,
|
admin.AdminDirectoryGroupMemberReadonlyScope,
|
||||||
@ -269,6 +290,10 @@ func getOauth2TokenSource(ctx context.Context, opts options.GoogleOptions, scope
|
|||||||
return conf.TokenSource(ctx)
|
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 {
|
func getAdminService(opts options.GoogleOptions) *admin.Service {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
var client *http.Client
|
var client *http.Client
|
||||||
@ -339,6 +364,38 @@ func getTargetPrincipal(ctx context.Context, opts options.GoogleOptions) (target
|
|||||||
return targetPrincipal
|
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 {
|
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
|
// Use the HasMember API to checking for the user's presence in each group or nested subgroups
|
||||||
req := service.Members.HasMember(group, email)
|
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")
|
result = userInGroup(service, "group@example.com", "non-member-out-of-domain@otherexample.com")
|
||||||
assert.False(t, result)
|
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