Add support for auth.oidc.allowed_user_groups (#667) (#668)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run

Signed-off-by: Michael Tupitsyn <michael.tupitsyn@gmail.com>
This commit is contained in:
Michael Tupitsyn
2026-04-11 09:24:18 -07:00
committed by GitHub
parent 401642701a
commit 9b437205b1
10 changed files with 106 additions and 1 deletions

View File

@@ -65,6 +65,9 @@ type AuthenticatorOauth interface {
RegistrationEnabled() bool
// GetAllowedDomains returns the list of whitelisted domains
GetAllowedDomains() []string
// GetAllowedUserGroups returns the list of whitelisted user groups.
// If non-empty, at least one user group must match.
GetAllowedUserGroups() []string
}
// AuthenticatorLdap is the interface for all LDAP authenticators.
@@ -497,6 +500,33 @@ func isDomainAllowed(email string, allowedDomains []string) bool {
return false
}
func isAnyAllowedUserGroup(userGroups, allowedUserGroups []string) bool {
if len(allowedUserGroups) == 0 {
return true
}
allowed := make(map[string]struct{}, len(allowedUserGroups))
for _, group := range allowedUserGroups {
trimmed := strings.TrimSpace(group)
if trimmed == "" {
continue
}
allowed[trimmed] = struct{}{}
}
if len(allowed) == 0 {
return false
}
for _, group := range userGroups {
if _, ok := allowed[strings.TrimSpace(group)]; ok {
return true
}
}
return false
}
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
// fetching the user information.
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
@@ -524,6 +554,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
return nil, fmt.Errorf("user %s is not in allowed domains", userInfo.Email)
}
if !isAnyAllowedUserGroup(userInfo.UserGroups, oauthProvider.GetAllowedUserGroups()) {
return nil, fmt.Errorf("user %s is not in allowed user groups", userInfo.Identifier)
}
ctx = domain.SetUserInfo(ctx,
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(),

View File

@@ -29,6 +29,7 @@ type PlainOauthAuthenticator struct {
userInfoLogging bool
sensitiveInfoLogging bool
allowedDomains []string
allowedUserGroups []string
}
func newPlainOauthAuthenticator(
@@ -60,6 +61,7 @@ func newPlainOauthAuthenticator(
provider.userInfoLogging = cfg.LogUserInfo
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
provider.allowedDomains = cfg.AllowedDomains
provider.allowedUserGroups = cfg.AllowedUserGroups
return provider, nil
}
@@ -73,6 +75,10 @@ func (p PlainOauthAuthenticator) GetAllowedDomains() []string {
return p.allowedDomains
}
func (p PlainOauthAuthenticator) GetAllowedUserGroups() []string {
return p.allowedUserGroups
}
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
return p.registrationEnabled

View File

@@ -26,6 +26,7 @@ type OidcAuthenticator struct {
userInfoLogging bool
sensitiveInfoLogging bool
allowedDomains []string
allowedUserGroups []string
}
func newOidcAuthenticator(
@@ -61,6 +62,7 @@ func newOidcAuthenticator(
provider.userInfoLogging = cfg.LogUserInfo
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
provider.allowedDomains = cfg.AllowedDomains
provider.allowedUserGroups = cfg.AllowedUserGroups
return provider, nil
}
@@ -74,6 +76,10 @@ func (o OidcAuthenticator) GetAllowedDomains() []string {
return o.allowedDomains
}
func (o OidcAuthenticator) GetAllowedUserGroups() []string {
return o.allowedUserGroups
}
// RegistrationEnabled returns whether registration is enabled for this authenticator.
func (o OidcAuthenticator) RegistrationEnabled() bool {
return o.registrationEnabled

View File

@@ -16,6 +16,7 @@ func parseOauthUserInfo(
) (*domain.AuthenticatorUserInfo, error) {
var isAdmin bool
var adminInfoAvailable bool
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
// first try to match the is_admin field against the given regex
if mapping.IsAdmin != "" {
@@ -29,7 +30,6 @@ func parseOauthUserInfo(
// next try to parse the user's groups
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
adminInfoAvailable = true
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
re := adminMapping.GetAdminGroupRegex()
for _, group := range userGroups {
if re.MatchString(strings.TrimSpace(group)) {
@@ -42,6 +42,7 @@ func parseOauthUserInfo(
userInfo := &domain.AuthenticatorUserInfo{
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
Email: internal.MapDefaultString(raw, mapping.Email, ""),
UserGroups: userGroups,
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),

View File

@@ -96,6 +96,7 @@ func Test_parseOauthUserInfo_admin_group(t *testing.T) {
assert.Equal(t, info.Firstname, "Test User")
assert.Equal(t, info.Lastname, "")
assert.Equal(t, info.Email, "test@mydomain.net")
assert.Equal(t, info.UserGroups, []string{"abuse@mydomain.net", "postmaster@mydomain.net", "wgportal-admins@mydomain.net"})
}
func Test_parseOauthUserInfo_admin_value(t *testing.T) {