mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-30 06:02:22 +00:00
Currently, Forgejo supports configuring static group team mappings for
an OIDC authentication source that map OIDC groups to Forgejo
organizations and teams. For example, the following mapping
```json
{"Developer": {"MyForgejoOrganization": ["MyForgejoTeam1", "MyForgejoTeam2"]}}
```
automatically adds a user in the OIDC group `Developer` to the teams
`MyForgejoTeam1` and `MyForgejoTeam2` in organization
`MyForgejoOrganization`.
In order to support more dynamic mappings and to avoid having to update
the mappings for new organizations and teams, add an additional
configuration option that supports mappings with placeholders like in
the following example:
```json
["group-{org}-{team}", "other:{org}/{team}"]
```
In this example, the mappings add a user in OIDC groups
`group-org1-team1`, `group-org2-team2`, and `other:org3/team3` to team
`team1` in organization `org1`, team `team2` in organization `org2`, and
to team `team3` in organization `org3`.
Additionally, this adds a configuration option to dynamically remove
users from organization teams. If enabled, a user is removed from all
teams that are not added via a static or dynamic mapping. Thus, users
are only in teams that are added via such a mapping and no other teams.
Docs: forgejo/docs!1950
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11656
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
368 lines
10 KiB
Go
368 lines
10 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
auth_model "forgejo.org/models/auth"
|
|
"forgejo.org/services/auth/source/oauth2"
|
|
|
|
"github.com/urfave/cli/v3"
|
|
)
|
|
|
|
func oauthCLIFlags() []cli.Flag {
|
|
return []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "name",
|
|
Value: "",
|
|
Usage: "Application Name",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "provider",
|
|
Value: "",
|
|
Usage: "OAuth2 Provider",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "key",
|
|
Value: "",
|
|
Usage: "Client ID (Key)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "secret",
|
|
Value: "",
|
|
Usage: "Client Secret",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "auto-discover-url",
|
|
Value: "",
|
|
Usage: "OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "use-custom-urls",
|
|
Value: "false",
|
|
Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "custom-tenant-id",
|
|
Value: "",
|
|
Usage: "Use custom Tenant ID for OAuth endpoints",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "custom-auth-url",
|
|
Value: "",
|
|
Usage: "Use a custom Authorization URL (option for GitLab/GitHub)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "custom-token-url",
|
|
Value: "",
|
|
Usage: "Use a custom Token URL (option for GitLab/GitHub)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "custom-profile-url",
|
|
Value: "",
|
|
Usage: "Use a custom Profile URL (option for GitLab/GitHub)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "custom-email-url",
|
|
Value: "",
|
|
Usage: "Use a custom Email URL (option for GitHub)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "icon-url",
|
|
Value: "",
|
|
Usage: "Custom icon URL for OAuth2 login source",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "skip-local-2fa",
|
|
Usage: "Set to true to skip local 2fa for users authenticated by this source",
|
|
},
|
|
&cli.StringSliceFlag{
|
|
Name: "scopes",
|
|
Value: nil,
|
|
Usage: "Scopes to request when to authenticate against this OAuth2 source",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "attribute-ssh-public-key",
|
|
Value: "",
|
|
Usage: "Claim name providing SSH public keys for this source",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "required-claim-name",
|
|
Value: "",
|
|
Usage: "Claim name that has to be set to allow users to login with this source",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "required-claim-value",
|
|
Value: "",
|
|
Usage: "Claim value that has to be set to allow users to login with this source",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "group-claim-name",
|
|
Value: "",
|
|
Usage: "Claim name providing group names for this source",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "admin-group",
|
|
Value: "",
|
|
Usage: "Group Claim value for administrator users",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "restricted-group",
|
|
Value: "",
|
|
Usage: "Group Claim value for restricted users",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "group-team-map",
|
|
Value: "",
|
|
Usage: "JSON mapping between groups and org teams",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "group-team-map-removal",
|
|
Usage: "Activate automatic team membership removal depending on groups",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "dyn-group-maps",
|
|
Value: "",
|
|
Usage: "Dynamic mappings between groups and org teams",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "dyn-group-maps-removal",
|
|
Usage: "Activate automatic team membership removal of org teams not automatically added",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "allow-username-change",
|
|
Usage: "Allow users to change their username",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "quota-group-claim-name",
|
|
Value: "",
|
|
Usage: "Claim name providing quota group names for this source",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "quota-group-map",
|
|
Value: "",
|
|
Usage: "JSON mapping between groups and quota groups",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "quota-group-map-removal",
|
|
Usage: "Activate automatic quota group removal depending on groups",
|
|
},
|
|
}
|
|
}
|
|
|
|
func microcmdAuthAddOauth() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "add-oauth",
|
|
Usage: "Add new Oauth authentication source",
|
|
Before: noDanglingArgs,
|
|
Action: newAuthService().addOauth,
|
|
Flags: oauthCLIFlags(),
|
|
}
|
|
}
|
|
|
|
func microcmdAuthUpdateOauth() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "update-oauth",
|
|
Usage: "Update existing Oauth authentication source",
|
|
Before: noDanglingArgs,
|
|
Action: newAuthService().updateOauth,
|
|
Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{idFlag()}, oauthCLIFlags()[1:]...)...),
|
|
}
|
|
}
|
|
|
|
func parseOAuth2Config(_ context.Context, c *cli.Command) *oauth2.Source {
|
|
var customURLMapping *oauth2.CustomURLMapping
|
|
if c.IsSet("use-custom-urls") {
|
|
customURLMapping = &oauth2.CustomURLMapping{
|
|
TokenURL: c.String("custom-token-url"),
|
|
AuthURL: c.String("custom-auth-url"),
|
|
ProfileURL: c.String("custom-profile-url"),
|
|
EmailURL: c.String("custom-email-url"),
|
|
Tenant: c.String("custom-tenant-id"),
|
|
}
|
|
} else {
|
|
customURLMapping = nil
|
|
}
|
|
return &oauth2.Source{
|
|
Provider: c.String("provider"),
|
|
ClientID: c.String("key"),
|
|
ClientSecret: c.String("secret"),
|
|
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
|
|
CustomURLMapping: customURLMapping,
|
|
IconURL: c.String("icon-url"),
|
|
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
|
|
Scopes: c.StringSlice("scopes"),
|
|
AttributeSSHPublicKey: c.String("attribute-ssh-public-key"),
|
|
RequiredClaimName: c.String("required-claim-name"),
|
|
RequiredClaimValue: c.String("required-claim-value"),
|
|
GroupClaimName: c.String("group-claim-name"),
|
|
AdminGroup: c.String("admin-group"),
|
|
RestrictedGroup: c.String("restricted-group"),
|
|
GroupTeamMap: c.String("group-team-map"),
|
|
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
|
|
DynGroupMaps: c.String("dyn-group-maps"),
|
|
DynGroupMapsRemoval: c.Bool("dyn-group-maps-removal"),
|
|
AllowUsernameChange: c.Bool("allow-username-change"),
|
|
QuotaGroupClaimName: c.String("quota-group-claim-name"),
|
|
QuotaGroupMap: c.String("quota-group-map"),
|
|
QuotaGroupMapRemoval: c.Bool("quota-group-map-removal"),
|
|
}
|
|
}
|
|
|
|
func (a *authService) addOauth(ctx context.Context, c *cli.Command) error {
|
|
ctx, cancel := installSignals(ctx)
|
|
defer cancel()
|
|
|
|
if err := a.initDB(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
config := parseOAuth2Config(ctx, c)
|
|
if config.Provider == "openidConnect" {
|
|
discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL)
|
|
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
|
return fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", config.OpenIDConnectAutoDiscoveryURL)
|
|
}
|
|
}
|
|
|
|
return a.createAuthSource(ctx, &auth_model.Source{
|
|
Type: auth_model.OAuth2,
|
|
Name: c.String("name"),
|
|
IsActive: true,
|
|
Cfg: config,
|
|
})
|
|
}
|
|
|
|
func (a *authService) updateOauth(ctx context.Context, c *cli.Command) error {
|
|
if !c.IsSet("id") {
|
|
return errors.New("--id flag is missing")
|
|
}
|
|
|
|
ctx, cancel := installSignals(ctx)
|
|
defer cancel()
|
|
|
|
if err := a.initDB(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oAuth2Config := source.Cfg.(*oauth2.Source)
|
|
|
|
if c.IsSet("name") {
|
|
source.Name = c.String("name")
|
|
}
|
|
|
|
if c.IsSet("provider") {
|
|
oAuth2Config.Provider = c.String("provider")
|
|
}
|
|
|
|
if c.IsSet("key") {
|
|
oAuth2Config.ClientID = c.String("key")
|
|
}
|
|
|
|
if c.IsSet("secret") {
|
|
oAuth2Config.ClientSecret = c.String("secret")
|
|
}
|
|
|
|
if c.IsSet("auto-discover-url") {
|
|
oAuth2Config.OpenIDConnectAutoDiscoveryURL = c.String("auto-discover-url")
|
|
}
|
|
|
|
if c.IsSet("icon-url") {
|
|
oAuth2Config.IconURL = c.String("icon-url")
|
|
}
|
|
|
|
if c.IsSet("scopes") {
|
|
oAuth2Config.Scopes = c.StringSlice("scopes")
|
|
}
|
|
|
|
if c.IsSet("attribute-ssh-public-key") {
|
|
oAuth2Config.AttributeSSHPublicKey = c.String("attribute-ssh-public-key")
|
|
}
|
|
|
|
if c.IsSet("required-claim-name") {
|
|
oAuth2Config.RequiredClaimName = c.String("required-claim-name")
|
|
}
|
|
if c.IsSet("required-claim-value") {
|
|
oAuth2Config.RequiredClaimValue = c.String("required-claim-value")
|
|
}
|
|
|
|
if c.IsSet("group-claim-name") {
|
|
oAuth2Config.GroupClaimName = c.String("group-claim-name")
|
|
}
|
|
if c.IsSet("admin-group") {
|
|
oAuth2Config.AdminGroup = c.String("admin-group")
|
|
}
|
|
if c.IsSet("restricted-group") {
|
|
oAuth2Config.RestrictedGroup = c.String("restricted-group")
|
|
}
|
|
if c.IsSet("group-team-map") {
|
|
oAuth2Config.GroupTeamMap = c.String("group-team-map")
|
|
}
|
|
if c.IsSet("group-team-map-removal") {
|
|
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
|
|
}
|
|
if c.IsSet("dyn-group-maps") {
|
|
oAuth2Config.DynGroupMaps = c.String("dyn-group-maps")
|
|
}
|
|
if c.IsSet("dyn-group-maps-removal") {
|
|
oAuth2Config.DynGroupMapsRemoval = c.Bool("dyn-group-maps-removal")
|
|
}
|
|
if c.IsSet("quota-group-claim-name") {
|
|
oAuth2Config.QuotaGroupClaimName = c.String("quota-group-claim-name")
|
|
}
|
|
if c.IsSet("quota-group-map") {
|
|
oAuth2Config.QuotaGroupMap = c.String("quota-group-map")
|
|
}
|
|
if c.IsSet("quota-group-map-removal") {
|
|
oAuth2Config.QuotaGroupMapRemoval = c.Bool("quota-group-map-removal")
|
|
}
|
|
|
|
if c.IsSet("allow-username-change") {
|
|
oAuth2Config.AllowUsernameChange = c.Bool("allow-username-change")
|
|
}
|
|
|
|
// update custom URL mapping
|
|
customURLMapping := &oauth2.CustomURLMapping{}
|
|
|
|
if oAuth2Config.CustomURLMapping != nil {
|
|
customURLMapping.TokenURL = oAuth2Config.CustomURLMapping.TokenURL
|
|
customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
|
|
customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
|
|
customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
|
|
customURLMapping.Tenant = oAuth2Config.CustomURLMapping.Tenant
|
|
}
|
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
|
|
customURLMapping.TokenURL = c.String("custom-token-url")
|
|
}
|
|
|
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-auth-url") {
|
|
customURLMapping.AuthURL = c.String("custom-auth-url")
|
|
}
|
|
|
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-profile-url") {
|
|
customURLMapping.ProfileURL = c.String("custom-profile-url")
|
|
}
|
|
|
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-email-url") {
|
|
customURLMapping.EmailURL = c.String("custom-email-url")
|
|
}
|
|
|
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-tenant-id") {
|
|
customURLMapping.Tenant = c.String("custom-tenant-id")
|
|
}
|
|
|
|
oAuth2Config.CustomURLMapping = customURLMapping
|
|
source.Cfg = oAuth2Config
|
|
|
|
return a.updateAuthSource(ctx, source)
|
|
}
|