Add List*Organizations helpers in pkg/accessreview/drivers → Ignore .oauth-credentials.txt
- Add List*Organizations helpers in pkg/accessreview/drivers - Replace AccessSource picker switches with map dispatch - Drop dead per-provider connector settings wrappers - Ignore .oauth-credentials.txt Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ pkg/server/api/*/v1/schema.graphql
|
|||||||
cfg/dev_local.yaml
|
cfg/dev_local.yaml
|
||||||
cfg/dev.yaml
|
cfg/dev.yaml
|
||||||
.env
|
.env
|
||||||
|
.oauth-credentials.txt
|
||||||
|
|||||||
443
pkg/accessreview/drivers/organizations.go
Normal file
443
pkg/accessreview/drivers/organizations.go
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package drivers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Organization represents a tenant/workspace/team/group surfaced by a
|
||||||
|
// provider's "list orgs the authenticated user can access" endpoint.
|
||||||
|
// The OAuth picker UI consumes this to let the user choose which one
|
||||||
|
// scopes the access source.
|
||||||
|
type Organization struct {
|
||||||
|
Slug string
|
||||||
|
DisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListGitHubOrganizations fetches the organizations the authenticated
|
||||||
|
// GitHub user belongs to.
|
||||||
|
func ListGitHubOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/user/orgs", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create github organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch github organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch github organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgs []struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&orgs); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode github organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, len(orgs))
|
||||||
|
for i, org := range orgs {
|
||||||
|
displayName := org.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = org.Login
|
||||||
|
}
|
||||||
|
result[i] = Organization{Slug: org.Login, DisplayName: displayName}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSentryOrganizations fetches the organizations the authenticated
|
||||||
|
// Sentry user belongs to.
|
||||||
|
func ListSentryOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://sentry.io/api/0/organizations/?member=true",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create sentry organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch sentry organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch sentry organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgs []struct {
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&orgs); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode sentry organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, len(orgs))
|
||||||
|
for i, org := range orgs {
|
||||||
|
displayName := org.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = org.Slug
|
||||||
|
}
|
||||||
|
result[i] = Organization{Slug: org.Slug, DisplayName: displayName}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListGitLabOrganizations fetches the GitLab groups the authenticated
|
||||||
|
// user owns. Group IDs are int64; we surface them as strings so they fit
|
||||||
|
// the Organization.Slug shape.
|
||||||
|
func ListGitLabOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://gitlab.com/api/v4/groups?min_access_level=50&per_page=100",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create gitlab organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch gitlab organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch gitlab organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FullPath string `json:"full_path"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&groups); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode gitlab organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, len(groups))
|
||||||
|
for i, g := range groups {
|
||||||
|
displayName := g.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = g.FullPath
|
||||||
|
}
|
||||||
|
result[i] = Organization{
|
||||||
|
Slug: strconv.FormatInt(g.ID, 10),
|
||||||
|
DisplayName: displayName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBitbucketOrganizations fetches the workspaces the authenticated
|
||||||
|
// Bitbucket user belongs to. The legacy /2.0/workspaces endpoint was
|
||||||
|
// sunset by CHANGE-2770 (April 2026); /2.0/user/workspaces is the
|
||||||
|
// supported cross-workspace replacement (CHANGE-3022).
|
||||||
|
func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://api.bitbucket.org/2.0/user/workspaces?pagelen=100",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create bitbucket organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch bitbucket organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch bitbucket organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We tolerate both shapes (flat and nested under `workspace`) since
|
||||||
|
// Atlassian has shipped variants of similar endpoints with both.
|
||||||
|
var body struct {
|
||||||
|
Values []struct {
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Workspace struct {
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"workspace"`
|
||||||
|
} `json:"values"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode bitbucket organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, 0, len(body.Values))
|
||||||
|
for _, v := range body.Values {
|
||||||
|
slug, name := v.Slug, v.Name
|
||||||
|
if slug == "" {
|
||||||
|
slug = v.Workspace.Slug
|
||||||
|
name = v.Workspace.Name
|
||||||
|
}
|
||||||
|
displayName := name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = slug
|
||||||
|
}
|
||||||
|
result = append(result, Organization{Slug: slug, DisplayName: displayName})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListHerokuOrganizations fetches the teams the authenticated Heroku
|
||||||
|
// user belongs to.
|
||||||
|
func ListHerokuOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.heroku.com/teams", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create heroku organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch heroku organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch heroku organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var teams []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&teams); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode heroku organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, len(teams))
|
||||||
|
for i, t := range teams {
|
||||||
|
displayName := t.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = t.ID
|
||||||
|
}
|
||||||
|
result[i] = Organization{Slug: t.ID, DisplayName: displayName}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAsanaOrganizations fetches the workspaces the authenticated Asana
|
||||||
|
// user belongs to.
|
||||||
|
func ListAsanaOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://app.asana.com/api/1.0/workspaces?limit=100",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create asana organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch asana organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch asana organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Data []struct {
|
||||||
|
GID string `json:"gid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode asana organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, len(body.Data))
|
||||||
|
for i, w := range body.Data {
|
||||||
|
displayName := w.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = w.GID
|
||||||
|
}
|
||||||
|
result[i] = Organization{Slug: w.GID, DisplayName: displayName}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSnykOrganizations fetches the Snyk organizations the authenticated
|
||||||
|
// user belongs to. The Snyk REST API is JSON:API; the org id is the
|
||||||
|
// unique identifier surfaced as the slug.
|
||||||
|
func ListSnykOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://api.snyk.io/rest/orgs?version=2024-10-15&limit=100",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create snyk organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/vnd.api+json")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch snyk organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch snyk organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Attributes struct {
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode snyk organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, len(body.Data))
|
||||||
|
for i, org := range body.Data {
|
||||||
|
displayName := org.Attributes.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = org.Attributes.Slug
|
||||||
|
}
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = org.ID
|
||||||
|
}
|
||||||
|
result[i] = Organization{Slug: org.ID, DisplayName: displayName}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListNetlifyOrganizations fetches the Netlify accounts the authenticated
|
||||||
|
// user belongs to.
|
||||||
|
func ListNetlifyOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://api.netlify.com/api/v1/accounts",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create netlify organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch netlify organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch netlify organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var accounts []struct {
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&accounts); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode netlify organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, len(accounts))
|
||||||
|
for i, a := range accounts {
|
||||||
|
displayName := a.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = a.Slug
|
||||||
|
}
|
||||||
|
result[i] = Organization{Slug: a.Slug, DisplayName: displayName}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListClickUpOrganizations fetches the ClickUp teams (workspaces) the
|
||||||
|
// authenticated user belongs to.
|
||||||
|
func ListClickUpOrganizations(ctx context.Context, httpClient *http.Client) ([]Organization, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://api.clickup.com/api/v2/team",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create clickup organizations request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch clickup organizations: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("cannot fetch clickup organizations: unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Teams []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"teams"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode clickup organizations response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Organization, len(body.Teams))
|
||||||
|
for i, t := range body.Teams {
|
||||||
|
displayName := t.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = t.ID
|
||||||
|
}
|
||||||
|
result[i] = Organization{Slug: t.ID, DisplayName: displayName}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -97,160 +97,6 @@ func (c *Connector) SetSettings(v any) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SlackSettings unmarshals the connector's RawSettings into SlackConnectorSettings.
|
|
||||||
func (c *Connector) SlackSettings() (SlackConnectorSettings, error) {
|
|
||||||
var s SlackConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TallySettings unmarshals the connector's RawSettings into TallyConnectorSettings.
|
|
||||||
func (c *Connector) TallySettings() (TallyConnectorSettings, error) {
|
|
||||||
var s TallyConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnePasswordSettings unmarshals the connector's RawSettings into OnePasswordConnectorSettings.
|
|
||||||
func (c *Connector) OnePasswordSettings() (OnePasswordConnectorSettings, error) {
|
|
||||||
var s OnePasswordConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SentrySettings unmarshals the connector's RawSettings into SentryConnectorSettings.
|
|
||||||
func (c *Connector) SentrySettings() (SentryConnectorSettings, error) {
|
|
||||||
var s SentryConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupabaseSettings unmarshals the connector's RawSettings into SupabaseConnectorSettings.
|
|
||||||
func (c *Connector) SupabaseSettings() (SupabaseConnectorSettings, error) {
|
|
||||||
var s SupabaseConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GitHubSettings unmarshals the connector's RawSettings into GitHubConnectorSettings.
|
|
||||||
func (c *Connector) GitHubSettings() (GitHubConnectorSettings, error) {
|
|
||||||
var s GitHubConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnePasswordUsersAPISettings unmarshals the connector's RawSettings into OnePasswordUsersAPISettings.
|
|
||||||
func (c *Connector) OnePasswordUsersAPISettings() (OnePasswordUsersAPISettings, error) {
|
|
||||||
var s OnePasswordUsersAPISettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GitLabSettings unmarshals the connector's RawSettings into GitLabConnectorSettings.
|
|
||||||
func (c *Connector) GitLabSettings() (GitLabConnectorSettings, error) {
|
|
||||||
var s GitLabConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BitbucketSettings unmarshals the connector's RawSettings into BitbucketConnectorSettings.
|
|
||||||
func (c *Connector) BitbucketSettings() (BitbucketConnectorSettings, error) {
|
|
||||||
var s BitbucketConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HerokuSettings unmarshals the connector's RawSettings into HerokuConnectorSettings.
|
|
||||||
func (c *Connector) HerokuSettings() (HerokuConnectorSettings, error) {
|
|
||||||
var s HerokuConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PagerDutySettings unmarshals the connector's RawSettings into PagerDutyConnectorSettings.
|
|
||||||
func (c *Connector) PagerDutySettings() (PagerDutyConnectorSettings, error) {
|
|
||||||
var s PagerDutyConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsanaSettings unmarshals the connector's RawSettings into AsanaConnectorSettings.
|
|
||||||
func (c *Connector) AsanaSettings() (AsanaConnectorSettings, error) {
|
|
||||||
var s AsanaConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SnykSettings unmarshals the connector's RawSettings into SnykConnectorSettings.
|
|
||||||
func (c *Connector) SnykSettings() (SnykConnectorSettings, error) {
|
|
||||||
var s SnykConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetlifySettings unmarshals the connector's RawSettings into NetlifyConnectorSettings.
|
|
||||||
func (c *Connector) NetlifySettings() (NetlifyConnectorSettings, error) {
|
|
||||||
var s NetlifyConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClickUpSettings unmarshals the connector's RawSettings into ClickUpConnectorSettings.
|
|
||||||
func (c *Connector) ClickUpSettings() (ClickUpConnectorSettings, error) {
|
|
||||||
var s ClickUpConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VercelSettings unmarshals the connector's RawSettings into VercelConnectorSettings.
|
|
||||||
func (c *Connector) VercelSettings() (VercelConnectorSettings, error) {
|
|
||||||
var s VercelConnectorSettings
|
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connector) unmarshalSettings(v any) error {
|
|
||||||
if len(c.RawSettings) == 0 || string(c.RawSettings) == "null" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(c.RawSettings, v); err != nil {
|
|
||||||
return fmt.Errorf("cannot unmarshal connector settings: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectorSettings unmarshals the connector's RawSettings into the
|
// ConnectorSettings unmarshals the connector's RawSettings into the
|
||||||
// requested settings struct. Empty or null RawSettings yields the zero
|
// requested settings struct. Empty or null RawSettings yields the zero
|
||||||
// value with no error. Use as:
|
// value with no error. Use as:
|
||||||
@@ -258,8 +104,11 @@ func (c *Connector) unmarshalSettings(v any) error {
|
|||||||
// settings, err := coredata.ConnectorSettings[coredata.GitHubConnectorSettings](dbConnector)
|
// settings, err := coredata.ConnectorSettings[coredata.GitHubConnectorSettings](dbConnector)
|
||||||
func ConnectorSettings[T any](c *Connector) (T, error) {
|
func ConnectorSettings[T any](c *Connector) (T, error) {
|
||||||
var s T
|
var s T
|
||||||
if err := c.unmarshalSettings(&s); err != nil {
|
if len(c.RawSettings) == 0 || string(c.RawSettings) == "null" {
|
||||||
return s, err
|
return s, nil
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(c.RawSettings, &s); err != nil {
|
||||||
|
return s, fmt.Errorf("cannot unmarshal connector settings: %w", err)
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,76 +398,29 @@ func (r *accessSourceResolver) ProviderOrganizations(ctx context.Context, obj *t
|
|||||||
return nil, fmt.Errorf("cannot get connector HTTP client: %w", err)
|
return nil, fmt.Errorf("cannot get connector HTTP client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch dbConnector.Provider {
|
cfg, ok := providerOrgConfigs[dbConnector.Provider]
|
||||||
case coredata.ConnectorProviderGitHub:
|
if !ok || cfg.ListOrgs == nil {
|
||||||
orgs, err := fetchGitHubOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch github organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderSentry:
|
|
||||||
orgs, err := fetchSentryOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch sentry organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderGitLab:
|
|
||||||
orgs, err := fetchGitLabOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch gitlab organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderBitbucket:
|
|
||||||
orgs, err := fetchBitbucketOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch bitbucket organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderHeroku:
|
|
||||||
orgs, err := fetchHerokuOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch heroku organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderAsana:
|
|
||||||
orgs, err := fetchAsanaOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch asana organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderPagerDuty:
|
|
||||||
// PagerDuty uses Pattern 2-auto: the subdomain is captured during
|
|
||||||
// the OAuth callback, no picker UI is required.
|
|
||||||
return []*types.ProviderOrganization{}, nil
|
|
||||||
case coredata.ConnectorProviderSnyk:
|
|
||||||
orgs, err := fetchSnykOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch snyk organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderNetlify:
|
|
||||||
orgs, err := fetchNetlifyOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch netlify organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderClickUp:
|
|
||||||
orgs, err := fetchClickUpOrganizations(ctx, httpClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch clickup organizations: %w", err)
|
|
||||||
}
|
|
||||||
return orgs, nil
|
|
||||||
case coredata.ConnectorProviderVercel:
|
|
||||||
// Vercel uses Pattern 2-auto: the team_id is captured during the
|
|
||||||
// OAuth callback (or synthesized from /v2/user for personal
|
|
||||||
// accounts), no picker UI is required.
|
|
||||||
return []*types.ProviderOrganization{}, nil
|
|
||||||
default:
|
|
||||||
return []*types.ProviderOrganization{}, nil
|
return []*types.ProviderOrganization{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orgs, err := cfg.ListOrgs(ctx, httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]*types.ProviderOrganization, len(orgs))
|
||||||
|
for i, o := range orgs {
|
||||||
|
result[i] = &types.ProviderOrganization{Slug: o.Slug, DisplayName: o.DisplayName}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NeedsConfiguration is the resolver for the needsConfiguration field.
|
// NeedsConfiguration is the resolver for the needsConfiguration field.
|
||||||
|
//
|
||||||
|
// True when the provider has a picker UI (NeedsPicker) AND the user has
|
||||||
|
// not yet picked an org. 2-auto providers (PagerDuty, Vercel) always
|
||||||
|
// return false: the identifier is captured during the OAuth callback,
|
||||||
|
// not via a follow-up configure mutation.
|
||||||
func (r *accessSourceResolver) NeedsConfiguration(ctx context.Context, obj *types.AccessSource) (bool, error) {
|
func (r *accessSourceResolver) NeedsConfiguration(ctx context.Context, obj *types.AccessSource) (bool, error) {
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionAccessSourceGet); err != nil {
|
if err := r.authorize(ctx, obj.ID, probo.ActionAccessSourceGet); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -487,48 +440,11 @@ func (r *accessSourceResolver) NeedsConfiguration(ctx context.Context, obj *type
|
|||||||
panic(fmt.Errorf("cannot get connector: %w", err))
|
panic(fmt.Errorf("cannot get connector: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch dbConnector.Provider {
|
cfg, ok := providerOrgConfigs[dbConnector.Provider]
|
||||||
case coredata.ConnectorProviderGitHub:
|
if !ok || !cfg.NeedsPicker {
|
||||||
settings, _ := dbConnector.GitHubSettings()
|
|
||||||
return settings.Organization == "", nil
|
|
||||||
case coredata.ConnectorProviderSentry:
|
|
||||||
settings, _ := dbConnector.SentrySettings()
|
|
||||||
return settings.OrganizationSlug == "", nil
|
|
||||||
case coredata.ConnectorProviderGitLab:
|
|
||||||
settings, _ := dbConnector.GitLabSettings()
|
|
||||||
return settings.GroupID == "", nil
|
|
||||||
case coredata.ConnectorProviderBitbucket:
|
|
||||||
settings, _ := dbConnector.BitbucketSettings()
|
|
||||||
return settings.Workspace == "", nil
|
|
||||||
case coredata.ConnectorProviderHeroku:
|
|
||||||
settings, _ := dbConnector.HerokuSettings()
|
|
||||||
return settings.TeamID == "", nil
|
|
||||||
case coredata.ConnectorProviderAsana:
|
|
||||||
settings, _ := dbConnector.AsanaSettings()
|
|
||||||
return settings.WorkspaceGID == "", nil
|
|
||||||
case coredata.ConnectorProviderPagerDuty:
|
|
||||||
// 2-auto: subdomain is set during the OAuth callback. If for any
|
|
||||||
// reason it is missing, we still return false so the configure
|
|
||||||
// mutation does not surface — there is no manual picker.
|
|
||||||
return false, nil
|
|
||||||
case coredata.ConnectorProviderSnyk:
|
|
||||||
settings, _ := dbConnector.SnykSettings()
|
|
||||||
return settings.OrgID == "", nil
|
|
||||||
case coredata.ConnectorProviderNetlify:
|
|
||||||
settings, _ := dbConnector.NetlifySettings()
|
|
||||||
return settings.AccountSlug == "", nil
|
|
||||||
case coredata.ConnectorProviderClickUp:
|
|
||||||
settings, _ := dbConnector.ClickUpSettings()
|
|
||||||
return settings.TeamID == "", nil
|
|
||||||
case coredata.ConnectorProviderVercel:
|
|
||||||
// 2-auto: team_id is set during the OAuth callback (or synthesized
|
|
||||||
// from /v2/user for personal accounts). If for any reason it is
|
|
||||||
// missing, return false so the configure mutation does not surface
|
|
||||||
// — there is no manual picker.
|
|
||||||
return false, nil
|
|
||||||
default:
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
return cfg.SelectedSlug(dbConnector) == "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionStatus is the resolver for the connectionStatus field.
|
// ConnectionStatus is the resolver for the connectionStatus field.
|
||||||
@@ -582,65 +498,15 @@ func (r *accessSourceResolver) SelectedOrganization(ctx context.Context, obj *ty
|
|||||||
panic(fmt.Errorf("cannot get connector: %w", err))
|
panic(fmt.Errorf("cannot get connector: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch dbConnector.Provider {
|
cfg, ok := providerOrgConfigs[dbConnector.Provider]
|
||||||
case coredata.ConnectorProviderGitHub:
|
if !ok {
|
||||||
settings, _ := dbConnector.GitHubSettings()
|
return nil, nil
|
||||||
if settings.Organization != "" {
|
|
||||||
return &settings.Organization, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderSentry:
|
|
||||||
settings, _ := dbConnector.SentrySettings()
|
|
||||||
if settings.OrganizationSlug != "" {
|
|
||||||
return &settings.OrganizationSlug, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderGitLab:
|
|
||||||
settings, _ := dbConnector.GitLabSettings()
|
|
||||||
if settings.GroupID != "" {
|
|
||||||
return &settings.GroupID, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderBitbucket:
|
|
||||||
settings, _ := dbConnector.BitbucketSettings()
|
|
||||||
if settings.Workspace != "" {
|
|
||||||
return &settings.Workspace, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderHeroku:
|
|
||||||
settings, _ := dbConnector.HerokuSettings()
|
|
||||||
if settings.TeamID != "" {
|
|
||||||
return &settings.TeamID, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderPagerDuty:
|
|
||||||
settings, _ := dbConnector.PagerDutySettings()
|
|
||||||
if settings.Subdomain != "" {
|
|
||||||
return &settings.Subdomain, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderAsana:
|
|
||||||
settings, _ := dbConnector.AsanaSettings()
|
|
||||||
if settings.WorkspaceGID != "" {
|
|
||||||
return &settings.WorkspaceGID, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderSnyk:
|
|
||||||
settings, _ := dbConnector.SnykSettings()
|
|
||||||
if settings.OrgID != "" {
|
|
||||||
return &settings.OrgID, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderNetlify:
|
|
||||||
settings, _ := dbConnector.NetlifySettings()
|
|
||||||
if settings.AccountSlug != "" {
|
|
||||||
return &settings.AccountSlug, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderClickUp:
|
|
||||||
settings, _ := dbConnector.ClickUpSettings()
|
|
||||||
if settings.TeamID != "" {
|
|
||||||
return &settings.TeamID, nil
|
|
||||||
}
|
|
||||||
case coredata.ConnectorProviderVercel:
|
|
||||||
settings, _ := dbConnector.VercelSettings()
|
|
||||||
if settings.TeamID != "" {
|
|
||||||
return &settings.TeamID, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
slug := cfg.SelectedSlug(dbConnector)
|
||||||
return nil, nil
|
if slug == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &slug, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permission is the resolver for the permission field.
|
// Permission is the resolver for the permission field.
|
||||||
|
|||||||
134
pkg/server/api/console/v1/access_source_provider_config.go
Normal file
134
pkg/server/api/console/v1/access_source_provider_config.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package console_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/accessreview/drivers"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// providerOrgConfig binds a connector provider to its picker-UI behavior.
|
||||||
|
//
|
||||||
|
// ListOrgs returns the orgs/workspaces/teams the authenticated user can
|
||||||
|
// scope the connector to (nil for Pattern 2-auto providers like
|
||||||
|
// PagerDuty and Vercel where the value is captured during OAuth).
|
||||||
|
//
|
||||||
|
// SelectedSlug returns the currently-configured org identifier for the
|
||||||
|
// connector (empty string if none).
|
||||||
|
//
|
||||||
|
// NeedsPicker reports whether the picker mutation should surface in the
|
||||||
|
// UI; false for 2-auto providers.
|
||||||
|
type providerOrgConfig struct {
|
||||||
|
ListOrgs func(ctx context.Context, httpClient *http.Client) ([]drivers.Organization, error)
|
||||||
|
SelectedSlug func(c *coredata.Connector) string
|
||||||
|
NeedsPicker bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// providerOrgConfigs is the single source of truth that the three
|
||||||
|
// AccessSource picker resolvers (ProviderOrganizations,
|
||||||
|
// SelectedOrganization, NeedsConfiguration) dispatch through. Adding a
|
||||||
|
// provider takes one entry here, not three switch arms.
|
||||||
|
var providerOrgConfigs = map[coredata.ConnectorProvider]providerOrgConfig{
|
||||||
|
coredata.ConnectorProviderGitHub: {
|
||||||
|
ListOrgs: drivers.ListGitHubOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.GitHubConnectorSettings](c)
|
||||||
|
return s.Organization
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderSentry: {
|
||||||
|
ListOrgs: drivers.ListSentryOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.SentryConnectorSettings](c)
|
||||||
|
return s.OrganizationSlug
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderGitLab: {
|
||||||
|
ListOrgs: drivers.ListGitLabOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.GitLabConnectorSettings](c)
|
||||||
|
return s.GroupID
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderBitbucket: {
|
||||||
|
ListOrgs: drivers.ListBitbucketOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.BitbucketConnectorSettings](c)
|
||||||
|
return s.Workspace
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderHeroku: {
|
||||||
|
ListOrgs: drivers.ListHerokuOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.HerokuConnectorSettings](c)
|
||||||
|
return s.TeamID
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderAsana: {
|
||||||
|
ListOrgs: drivers.ListAsanaOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.AsanaConnectorSettings](c)
|
||||||
|
return s.WorkspaceGID
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderSnyk: {
|
||||||
|
ListOrgs: drivers.ListSnykOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.SnykConnectorSettings](c)
|
||||||
|
return s.OrgID
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderNetlify: {
|
||||||
|
ListOrgs: drivers.ListNetlifyOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.NetlifyConnectorSettings](c)
|
||||||
|
return s.AccountSlug
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderClickUp: {
|
||||||
|
ListOrgs: drivers.ListClickUpOrganizations,
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.ClickUpConnectorSettings](c)
|
||||||
|
return s.TeamID
|
||||||
|
},
|
||||||
|
NeedsPicker: true,
|
||||||
|
},
|
||||||
|
// Pattern 2-auto: identifier is captured during the OAuth callback
|
||||||
|
// (subdomain for PagerDuty, team_id or fallback /v2/user.id for
|
||||||
|
// Vercel). No picker UI; NeedsPicker = false.
|
||||||
|
coredata.ConnectorProviderPagerDuty: {
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.PagerDutyConnectorSettings](c)
|
||||||
|
return s.Subdomain
|
||||||
|
},
|
||||||
|
},
|
||||||
|
coredata.ConnectorProviderVercel: {
|
||||||
|
SelectedSlug: func(c *coredata.Connector) string {
|
||||||
|
s, _ := coredata.ConnectorSettings[coredata.VercelConnectorSettings](c)
|
||||||
|
return s.TeamID
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -16,444 +16,14 @@ package console_v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// fetchGitHubOrganizations fetches the list of organizations the
|
|
||||||
// authenticated GitHub user belongs to.
|
|
||||||
func fetchGitHubOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/user/orgs", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create github organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch github organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch github organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var orgs []struct {
|
|
||||||
Login string `json:"login"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&orgs); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode github organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(orgs))
|
|
||||||
for i, org := range orgs {
|
|
||||||
displayName := org.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = org.Login
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: org.Login,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchSentryOrganizations fetches the list of organizations the
|
|
||||||
// authenticated Sentry user belongs to.
|
|
||||||
func fetchSentryOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://sentry.io/api/0/organizations/?member=true", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create sentry organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch sentry organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch sentry organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var orgs []struct {
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&orgs); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode sentry organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(orgs))
|
|
||||||
for i, org := range orgs {
|
|
||||||
displayName := org.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = org.Slug
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: org.Slug,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchGitLabOrganizations fetches the list of groups the authenticated
|
|
||||||
// GitLab user owns. Group IDs are numeric int64 values; we surface them
|
|
||||||
// as strings so they fit the ProviderOrganization.Slug shape.
|
|
||||||
func fetchGitLabOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx,
|
|
||||||
http.MethodGet,
|
|
||||||
"https://gitlab.com/api/v4/groups?min_access_level=50&per_page=100",
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create gitlab organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch gitlab organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch gitlab organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var groups []struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
FullPath string `json:"full_path"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&groups); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode gitlab organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(groups))
|
|
||||||
for i, g := range groups {
|
|
||||||
displayName := g.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = g.FullPath
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: strconv.FormatInt(g.ID, 10),
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchBitbucketOrganizations fetches the list of workspaces the
|
|
||||||
// authenticated Bitbucket user belongs to.
|
|
||||||
func fetchBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx,
|
|
||||||
http.MethodGet,
|
|
||||||
"https://api.bitbucket.org/2.0/workspaces?role=member&pagelen=100",
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create bitbucket organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch bitbucket organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch bitbucket organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body struct {
|
|
||||||
Values []struct {
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"values"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode bitbucket organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(body.Values))
|
|
||||||
for i, w := range body.Values {
|
|
||||||
displayName := w.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = w.Slug
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: w.Slug,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchHerokuOrganizations fetches the list of teams the authenticated
|
|
||||||
// Heroku user belongs to.
|
|
||||||
func fetchHerokuOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.heroku.com/teams", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create heroku organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch heroku organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch heroku organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var teams []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&teams); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode heroku organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(teams))
|
|
||||||
for i, t := range teams {
|
|
||||||
displayName := t.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = t.ID
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: t.ID,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchAsanaOrganizations fetches the list of workspaces the
|
|
||||||
// authenticated Asana user belongs to.
|
|
||||||
func fetchAsanaOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx,
|
|
||||||
http.MethodGet,
|
|
||||||
"https://app.asana.com/api/1.0/workspaces?limit=100",
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create asana organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch asana organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch asana organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body struct {
|
|
||||||
Data []struct {
|
|
||||||
GID string `json:"gid"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode asana organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(body.Data))
|
|
||||||
for i, w := range body.Data {
|
|
||||||
displayName := w.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = w.GID
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: w.GID,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchSnykOrganizations fetches the list of Snyk organizations the
|
|
||||||
// authenticated user belongs to. The Snyk REST API is JSON:API style;
|
|
||||||
// the org id is the unique identifier surfaced as the slug.
|
|
||||||
func fetchSnykOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx,
|
|
||||||
http.MethodGet,
|
|
||||||
"https://api.snyk.io/rest/orgs?version=2024-10-15&limit=100",
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create snyk organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/vnd.api+json")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch snyk organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch snyk organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body struct {
|
|
||||||
Data []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Attributes struct {
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"attributes"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode snyk organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(body.Data))
|
|
||||||
for i, org := range body.Data {
|
|
||||||
displayName := org.Attributes.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = org.Attributes.Slug
|
|
||||||
}
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = org.ID
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: org.ID,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchNetlifyOrganizations fetches the list of Netlify accounts the
|
|
||||||
// authenticated user belongs to.
|
|
||||||
func fetchNetlifyOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx,
|
|
||||||
http.MethodGet,
|
|
||||||
"https://api.netlify.com/api/v1/accounts",
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create netlify organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch netlify organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch netlify organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var accounts []struct {
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&accounts); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode netlify organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(accounts))
|
|
||||||
for i, a := range accounts {
|
|
||||||
displayName := a.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = a.Slug
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: a.Slug,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchClickUpOrganizations fetches the list of ClickUp teams (workspaces)
|
|
||||||
// the authenticated user belongs to.
|
|
||||||
func fetchClickUpOrganizations(ctx context.Context, httpClient *http.Client) ([]*types.ProviderOrganization, error) {
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx,
|
|
||||||
http.MethodGet,
|
|
||||||
"https://api.clickup.com/api/v2/team",
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create clickup organizations request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot fetch clickup organizations: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot fetch clickup organizations: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body struct {
|
|
||||||
Teams []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"teams"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot decode clickup organizations response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.ProviderOrganization, len(body.Teams))
|
|
||||||
for i, t := range body.Teams {
|
|
||||||
displayName := t.Name
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = t.ID
|
|
||||||
}
|
|
||||||
result[i] = &types.ProviderOrganization{
|
|
||||||
Slug: t.ID,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// probeConnection makes a lightweight API call to the given URL to verify
|
// probeConnection makes a lightweight API call to the given URL to verify
|
||||||
// the OAuth token is still valid. The probe URL is configured per connector
|
// the OAuth token is still valid. The probe URL is configured per
|
||||||
// in the connector registry.
|
// connector in the connector registry.
|
||||||
func probeConnection(ctx context.Context, httpClient *http.Client, probeURL string) error {
|
func probeConnection(ctx context.Context, httpClient *http.Client, probeURL string) error {
|
||||||
if probeURL == "" {
|
if probeURL == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user