diff --git a/cmd/server/flags.go b/cmd/server/flags.go index 05d8b25cc..c6a8612f4 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -536,6 +536,11 @@ var flags = append([]cli.Flag{ TrimSpace: true, }, }, + &cli.BoolFlag{ // TODO: Remove this feature flag in next major version + Sources: cli.EnvVars("WOODPECKER_BITBUCKET_DC_ENABLE_OAUTH2_SCOPE_PROJECT_ADMIN"), + Name: "bitbucket-dc-oauth-enable-oauth2-scope-project-admin", + Usage: "Bitbucket DataCenter/Server oauth2 scope should be configured to include PROJECT_ADMIN configuration.", + }, // // development flags // diff --git a/docs/docs/30-administration/10-configuration/12-forges/60-bitbucket_datacenter.md b/docs/docs/30-administration/10-configuration/12-forges/60-bitbucket_datacenter.md index 0b6927a38..a144aa979 100644 --- a/docs/docs/30-administration/10-configuration/12-forges/60-bitbucket_datacenter.md +++ b/docs/docs/30-administration/10-configuration/12-forges/60-bitbucket_datacenter.md @@ -22,6 +22,7 @@ To enable Bitbucket Server you should configure the Woodpecker container using t + - WOODPECKER_BITBUCKET_DC_CLIENT_ID=xxx + - WOODPECKER_BITBUCKET_DC_CLIENT_SECRET=yyy + - WOODPECKER_BITBUCKET_DC_URL=http://stash.mycompany.com ++ - WOODPECKER_BITBUCKET_DC_ENABLE_OAUTH2_SCOPE_PROJECT_ADMIN=true woodpecker-agent: [...] @@ -124,3 +125,12 @@ Read the value for `WOODPECKER_BITBUCKET_DC_GIT_PASSWORD` from the specified fil - Default: `false` Configure if SSL verification should be skipped. + +--- + +### BITBUCKET_DC_ENABLE_OAUTH2_SCOPE_PROJECT_ADMIN + +- Name: `WOODPECKER_BITBUCKET_DC_ENABLE_OAUTH2_SCOPE_PROJECT_ADMIN` +- Default: `false` + +When enabled, the Bitbucket Application Link for Woodpecker should include the `PROJECT_ADMIN` scope. Enabling this feature flag will allow the users of Bitbucket Datacenter to use organization secrets and properly list repositories within the organization. diff --git a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go index f98c199a9..ef843836e 100644 --- a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go +++ b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go @@ -38,35 +38,38 @@ const listLimit = 250 // Opts defines configuration options. type Opts struct { - URL string // Bitbucket server url for API access. - Username string // Git machine account username. - Password string // Git machine account password. - OAuthClientID string // OAuth 2.0 client id - OAuthClientSecret string // OAuth 2.0 client secret - OAuthHost string // OAuth 2.0 host + URL string // Bitbucket server url for API access. + Username string // Git machine account username. + Password string // Git machine account password. + OAuthClientID string // OAuth 2.0 client id + OAuthClientSecret string // OAuth 2.0 client secret + OAuthHost string // OAuth 2.0 host + OAuthEnableProjectAdminScope bool // Whether to enable project admin scope. Should be set as default in the next major version. } type client struct { - url string - urlAPI string - clientID string - clientSecret string - oauthHost string - username string - password string + url string + urlAPI string + clientID string + clientSecret string + oauthHost string + username string + password string + oauthEnableProjectAdminScope bool } // New returns a Forge implementation that integrates with Bitbucket DataCenter/Server, // the on-premise edition of Bitbucket Cloud, formerly known as Stash. func New(opts Opts) (forge.Forge, error) { config := &client{ - url: opts.URL, - urlAPI: fmt.Sprintf("%s/rest", opts.URL), - clientID: opts.OAuthClientID, - clientSecret: opts.OAuthClientSecret, - oauthHost: opts.OAuthHost, - username: opts.Username, - password: opts.Password, + url: opts.URL, + urlAPI: fmt.Sprintf("%s/rest", opts.URL), + clientID: opts.OAuthClientID, + clientSecret: opts.OAuthClientSecret, + oauthHost: opts.OAuthHost, + username: opts.Username, + password: opts.Password, + oauthEnableProjectAdminScope: opts.OAuthEnableProjectAdminScope, } switch { @@ -638,9 +641,69 @@ func (*client) TeamPerm(_ *model.User, _ string) (*model.Perm, error) { // OrgMembership returns if user is member of organization and if user // is admin/owner in this organization. -func (c *client) OrgMembership(_ context.Context, _ *model.User, _ string) (*model.OrgPerm, error) { - // TODO: Not implemented currently - return nil, nil +func (c *client) OrgMembership(ctx context.Context, u *model.User, org string) (*model.OrgPerm, error) { + if !c.oauthEnableProjectAdminScope { + // This method cannot be implemented without the PROJECT_ADMIN scope included in the OAuth2 configuration + return nil, nil + } + bc, err := c.newClient(ctx, u) + if err != nil { + return nil, fmt.Errorf("unable to create bitbucket client: %w", err) + } + + // Check if the user is Bitbucket project admin + if c.hasProjectAdminAccess(ctx, bc, org) { + return &model.OrgPerm{Member: true, Admin: true}, nil + } + + // User is not Bitbucket project admin, check if they have write access to any repositories in the Bitbucket project. + // If they have, they are considered to be an organization member. + hasMembership, err := c.hasRepositoryWriteAccess(ctx, org, bc) + if err != nil { + return nil, fmt.Errorf("failed to check repository access: %w", err) + } + + if hasMembership { + return &model.OrgPerm{Member: true, Admin: false}, nil + } + + return &model.OrgPerm{Member: false, Admin: false}, nil +} + +func (c *client) hasProjectAdminAccess(ctx context.Context, client *bb.Client, org string) bool { + // If the user can access project permissions, the user has project admin access in the Bitbucket + perms, _, err := client.Projects.SearchProjectPermissions(ctx, org, &bb.ProjectPermissionSearchOptions{}) + if err == nil && len(perms) > 0 { + return true + } + return false +} + +func (c *client) hasRepositoryWriteAccess(ctx context.Context, org string, client *bb.Client) (bool, error) { + opts := &bb.RepositorySearchOptions{ + Archived: "ACTIVE", + ProjectKey: org, + Permission: bb.PermissionRepoWrite, + } + + for { + repos, resp, err := client.Projects.SearchRepositories(ctx, opts) + if err != nil { + return false, fmt.Errorf("failed to search repositories: %w", err) + } + + // If we find any repositories with write access, user has membership + if len(repos) > 0 { + return true, nil + } + + if resp.LastPage { + break + } + opts.Start = resp.NextPageStart + } + + return false, nil } // Org fetches the organization from the forge by name. If the name is a user an org with type user is returned. @@ -663,6 +726,17 @@ func (c *client) newOAuth2Config() *oauth2.Config { publicOAuthURL = c.urlAPI } + scopes := []string{ + string(bb.PermissionRepoRead), + string(bb.PermissionRepoWrite), + string(bb.PermissionRepoAdmin), + } + + // TODO: Remove this feature flag in the next major version and always include project admin scope + if c.oauthEnableProjectAdminScope { + scopes = append(scopes, string(bb.PermissionProjectAdmin)) + } + return &oauth2.Config{ ClientID: c.clientID, ClientSecret: c.clientSecret, @@ -670,7 +744,7 @@ func (c *client) newOAuth2Config() *oauth2.Config { AuthURL: fmt.Sprintf("%s/oauth2/latest/authorize", publicOAuthURL), TokenURL: fmt.Sprintf("%s/oauth2/latest/token", c.urlAPI), }, - Scopes: []string{string(bb.PermissionRepoRead), string(bb.PermissionRepoWrite), string(bb.PermissionRepoAdmin)}, + Scopes: scopes, RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost), } } diff --git a/server/forge/setup/setup.go b/server/forge/setup/setup.go index 9348ef523..81322945b 100644 --- a/server/forge/setup/setup.go +++ b/server/forge/setup/setup.go @@ -169,13 +169,19 @@ func setupBitbucketDatacenter(forge *model.Forge) (forge.Forge, error) { return nil, fmt.Errorf("missing git-password") } + enableProjectAdminScope, ok := forge.AdditionalOptions["oauth-enable-project-admin-scope"].(bool) + if !ok { + return nil, fmt.Errorf("incorrect type for oauth-enable-project-admin-scope value") + } + opts := bitbucketdatacenter.Opts{ - URL: forge.URL, - OAuthClientID: forge.OAuthClientID, - OAuthClientSecret: forge.OAuthClientSecret, - Username: gitUsername, - Password: gitPassword, - OAuthHost: forge.OAuthHost, + URL: forge.URL, + OAuthClientID: forge.OAuthClientID, + OAuthClientSecret: forge.OAuthClientSecret, + Username: gitUsername, + Password: gitPassword, + OAuthHost: forge.OAuthHost, + OAuthEnableProjectAdminScope: enableProjectAdminScope, } log.Debug(). Str("url", opts.URL). @@ -183,6 +189,7 @@ func setupBitbucketDatacenter(forge *model.Forge) (forge.Forge, error) { Bool("oauth-client-id-set", opts.OAuthClientID != ""). Bool("oauth-client-secret-set", opts.OAuthClientSecret != ""). Str("type", string(forge.Type)). + Bool("oauth-enable-project-admin-scope", opts.OAuthEnableProjectAdminScope). Msg("setting up forge") return bitbucketdatacenter.New(opts) } diff --git a/server/services/setup.go b/server/services/setup.go index a581965b3..94ec786cb 100644 --- a/server/services/setup.go +++ b/server/services/setup.go @@ -154,6 +154,7 @@ func setupForgeService(c *cli.Command, _store store.Store) error { _forge.Type = model.ForgeTypeBitbucketDatacenter _forge.AdditionalOptions["git-username"] = c.String("bitbucket-dc-git-username") _forge.AdditionalOptions["git-password"] = c.String("bitbucket-dc-git-password") + _forge.AdditionalOptions["oauth-enable-project-admin-scope"] = c.Bool("bitbucket-dc-oauth-enable-oauth2-scope-project-admin") default: return errors.New("forge not configured") }