mirror of
https://github.com/h44z/wg-portal.git
synced 2026-04-11 18:06:21 +00:00
Signed-off-by: Michael Tupitsyn <michael.tupitsyn@gmail.com>
This commit is contained in:
@@ -144,6 +144,9 @@ auth:
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
allowed_user_groups:
|
||||
- the-admin-group
|
||||
- vpn-users
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
@@ -201,6 +204,9 @@ auth:
|
||||
- email
|
||||
- profile
|
||||
- i-want-some-groups
|
||||
allowed_user_groups:
|
||||
- admin-group-name
|
||||
- vpn-users
|
||||
field_map:
|
||||
email: email
|
||||
firstname: name
|
||||
|
||||
@@ -561,6 +561,10 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||
|
||||
#### `allowed_user_groups`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of allowlisted user groups. If configured, at least one entry in the mapped `user_groups` claim must match one of these values.
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
||||
@@ -639,6 +643,10 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||
|
||||
#### `allowed_user_groups`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of allowlisted user groups. If configured, at least one entry in the mapped `user_groups` claim must match one of these values.
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
||||
|
||||
@@ -66,6 +66,40 @@ auth:
|
||||
- "outlook.com"
|
||||
```
|
||||
|
||||
#### Limiting Login to Specific User Groups
|
||||
|
||||
You can limit the login to specific user groups by setting the `allowed_user_groups` property for OAuth2 or OIDC providers.
|
||||
If this property is not empty, the user's `user_groups` claim must contain at least one matching group.
|
||||
|
||||
To use this feature, ensure your group claim is mapped via `field_map.user_groups`.
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
oidc:
|
||||
- provider_name: "oidc1"
|
||||
# ... other settings
|
||||
allowed_user_groups:
|
||||
- "wg-users"
|
||||
- "wg-admins"
|
||||
field_map:
|
||||
user_groups: "groups"
|
||||
```
|
||||
|
||||
If `allowed_user_groups` is configured and the authenticated user has no matching group in `user_groups`, login is denied.
|
||||
|
||||
Minimal deny-by-group example:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
oauth:
|
||||
- provider_name: "oauth1"
|
||||
# ... other settings
|
||||
allowed_user_groups:
|
||||
- "vpn-users"
|
||||
field_map:
|
||||
user_groups: "groups"
|
||||
```
|
||||
|
||||
#### Limit Login to Existing Users
|
||||
|
||||
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth2 or OIDC providers.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ""),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -258,6 +258,10 @@ type OpenIDConnectProvider struct {
|
||||
// AllowedDomains defines the list of allowed domains
|
||||
AllowedDomains []string `yaml:"allowed_domains"`
|
||||
|
||||
// AllowedUserGroups defines the list of allowed user groups.
|
||||
// If not empty, at least one group from the user's group claim must match.
|
||||
AllowedUserGroups []string `yaml:"allowed_user_groups"`
|
||||
|
||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||
FieldMap OauthFields `yaml:"field_map"`
|
||||
|
||||
@@ -303,6 +307,10 @@ type OAuthProvider struct {
|
||||
// AllowedDomains defines the list of allowed domains
|
||||
AllowedDomains []string `yaml:"allowed_domains"`
|
||||
|
||||
// AllowedUserGroups defines the list of allowed user groups.
|
||||
// If not empty, at least one group from the user's group claim must match.
|
||||
AllowedUserGroups []string `yaml:"allowed_user_groups"`
|
||||
|
||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||
FieldMap OauthFields `yaml:"field_map"`
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ type LoginProviderInfo struct {
|
||||
type AuthenticatorUserInfo struct {
|
||||
Identifier UserIdentifier
|
||||
Email string
|
||||
UserGroups []string
|
||||
Firstname string
|
||||
Lastname string
|
||||
Phone string
|
||||
|
||||
Reference in New Issue
Block a user