Add access review console GraphQL API
Add queries, mutations, and types for access review campaigns, access sources, access entries with decisions and flags, connector provider info, and provider org listing. Wire accessreview.Service into the Resolver. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
79
pkg/server/api/console/v1/connector_provider_info.go
Normal file
79
pkg/server/api/console/v1/connector_provider_info.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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 (
|
||||
"go.probo.inc/probo/pkg/accessreview/drivers"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
||||
)
|
||||
|
||||
var apiKeyProviders = map[coredata.ConnectorProvider]bool{
|
||||
coredata.ConnectorProviderHubSpot: true,
|
||||
coredata.ConnectorProviderDocuSign: true,
|
||||
coredata.ConnectorProviderNotion: true,
|
||||
coredata.ConnectorProviderGitHub: true,
|
||||
coredata.ConnectorProviderSentry: true,
|
||||
coredata.ConnectorProviderIntercom: true,
|
||||
coredata.ConnectorProviderBrex: true,
|
||||
coredata.ConnectorProviderTally: true,
|
||||
coredata.ConnectorProviderCloudflare: true,
|
||||
coredata.ConnectorProviderOpenAI: true,
|
||||
coredata.ConnectorProviderSupabase: true,
|
||||
coredata.ConnectorProviderResend: true,
|
||||
coredata.ConnectorProviderOnePassword: true,
|
||||
}
|
||||
|
||||
var clientCredentialsProviders = map[coredata.ConnectorProvider]bool{
|
||||
coredata.ConnectorProviderOnePassword: true,
|
||||
}
|
||||
|
||||
var providerExtraSettingsMap = map[coredata.ConnectorProvider][]*types.ConnectorProviderSettingInfo{
|
||||
coredata.ConnectorProviderGitHub: {
|
||||
{Key: "organization", Label: "Organization", Required: true},
|
||||
},
|
||||
coredata.ConnectorProviderSentry: {
|
||||
{Key: "organizationSlug", Label: "Organization Slug", Required: true},
|
||||
},
|
||||
coredata.ConnectorProviderTally: {
|
||||
{Key: "organizationId", Label: "Organization ID", Required: true},
|
||||
},
|
||||
coredata.ConnectorProviderSupabase: {
|
||||
{Key: "organizationSlug", Label: "Organization Slug", Required: true},
|
||||
},
|
||||
coredata.ConnectorProviderOnePassword: {
|
||||
{Key: "accountId", Label: "Account ID", Required: true},
|
||||
{Key: "region", Label: "Region", Required: true},
|
||||
},
|
||||
}
|
||||
|
||||
func providerDisplayName(provider coredata.ConnectorProvider) string {
|
||||
return drivers.ProviderDisplayName(provider)
|
||||
}
|
||||
|
||||
func providerSupportsAPIKey(provider coredata.ConnectorProvider) bool {
|
||||
return apiKeyProviders[provider]
|
||||
}
|
||||
|
||||
func providerSupportsClientCredentials(provider coredata.ConnectorProvider) bool {
|
||||
return clientCredentialsProviders[provider]
|
||||
}
|
||||
|
||||
func providerExtraSettings(provider coredata.ConnectorProvider) []*types.ConnectorProviderSettingInfo {
|
||||
if settings, ok := providerExtraSettingsMap[provider]; ok {
|
||||
return settings
|
||||
}
|
||||
return []*types.ConnectorProviderSettingInfo{}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview"
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
"go.probo.inc/probo/pkg/esign"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/mailman"
|
||||
@@ -27,14 +29,16 @@ import (
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, mailmanSvc *mailman.Service, customDomainCname string, logger *log.Logger) http.Handler {
|
||||
func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, accessReviewSvc *accessreview.Service, mailmanSvc *mailman.Service, connectorRegistry *connector.ConnectorRegistry, customDomainCname string, logger *log.Logger) http.Handler {
|
||||
config := schema.Config{
|
||||
Resolvers: &Resolver{
|
||||
authorize: authz.NewAuthorizeFunc(iamSvc, logger),
|
||||
probo: proboSvc,
|
||||
iam: iamSvc,
|
||||
esign: esignSvc,
|
||||
accessReview: accessReviewSvc,
|
||||
mailman: mailmanSvc,
|
||||
connectorRegistry: connectorRegistry,
|
||||
customDomainCname: customDomainCname,
|
||||
logger: logger,
|
||||
},
|
||||
|
||||
139
pkg/server/api/console/v1/provider_organizations.go
Normal file
139
pkg/server/api/console/v1/provider_organizations.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// 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
|
||||
// in the connector registry.
|
||||
func probeConnection(ctx context.Context, httpClient *http.Client, probeURL string) error {
|
||||
if probeURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, probeURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create probe request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("probe request failed: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
||||
return fmt.Errorf("token rejected: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
@@ -61,7 +62,9 @@ type (
|
||||
probo *probo.Service
|
||||
iam *iam.Service
|
||||
esign *esign.Service
|
||||
accessReview *accessreview.Service
|
||||
mailman *mailman.Service
|
||||
connectorRegistry *connector.ConnectorRegistry
|
||||
logger *log.Logger
|
||||
customDomainCname string
|
||||
}
|
||||
@@ -72,6 +75,7 @@ func NewMux(
|
||||
proboSvc *probo.Service,
|
||||
iamSvc *iam.Service,
|
||||
esignSvc *esign.Service,
|
||||
accessReviewSvc *accessreview.Service,
|
||||
mailmanSvc *mailman.Service,
|
||||
cookieConfig securecookie.Config,
|
||||
tokenSecret string,
|
||||
@@ -83,7 +87,7 @@ func NewMux(
|
||||
|
||||
safeRedirect := saferedirect.New(saferedirect.StaticHosts(baseURL.Host()))
|
||||
|
||||
graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, esignSvc, mailmanSvc, customDomainCname, logger)
|
||||
graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, esignSvc, accessReviewSvc, mailmanSvc, connectorRegistry, customDomainCname, logger)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
|
||||
@@ -95,8 +99,13 @@ func NewMux(
|
||||
|
||||
r.Get("/connectors/initiate", func(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.URL.Query().Get("provider")
|
||||
if provider != "SLACK" && provider != "GOOGLE_WORKSPACE" {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("unsupported provider"))
|
||||
if provider == "" {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("missing provider parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := connectorRegistry.Get(provider); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("unsupported provider: %q", provider))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,51 +158,69 @@ func NewMux(
|
||||
})
|
||||
|
||||
r.Get("/connectors/complete", func(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.URL.Query().Get("provider")
|
||||
if provider == "" {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("missing provider parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
var connectorProvider coredata.ConnectorProvider
|
||||
switch provider {
|
||||
case "SLACK":
|
||||
connectorProvider = coredata.ConnectorProviderSlack
|
||||
case "GOOGLE_WORKSPACE":
|
||||
connectorProvider = coredata.ConnectorProviderGoogleWorkspace
|
||||
default:
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("unsupported provider"))
|
||||
return
|
||||
}
|
||||
|
||||
stateToken := r.URL.Query().Get("state")
|
||||
if stateToken == "" {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("missing state parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
connection, organizationID, continueURL, err := connectorRegistry.Complete(r.Context(), provider, r)
|
||||
provider, err := connector.ExtractProviderFromState(stateToken)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot extract provider from state: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
var connectorProvider coredata.ConnectorProvider
|
||||
if err := connectorProvider.Scan(provider); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("unsupported provider: %q", provider))
|
||||
return
|
||||
}
|
||||
|
||||
connection, state, err := connectorRegistry.CompleteWithState(r.Context(), provider, r)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot complete connector: %w", err))
|
||||
}
|
||||
|
||||
organizationID, err := gid.ParseGID(state.OrganizationID)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot parse organization ID from state: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
svc := proboSvc.WithTenant(organizationID.TenantID())
|
||||
|
||||
connector, err := svc.Connectors.Create(
|
||||
r.Context(),
|
||||
probo.CreateConnectorRequest{
|
||||
OrganizationID: *organizationID,
|
||||
Provider: connectorProvider,
|
||||
Protocol: coredata.ConnectorProtocol(connection.Type()),
|
||||
Connection: connection,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot create or update connector: %w", err))
|
||||
var cnnctr *coredata.Connector
|
||||
|
||||
// If a connector_id was passed in the state, this is a
|
||||
// reconnection — update the existing connector's token.
|
||||
if state.ConnectorID != "" {
|
||||
connectorID, err := gid.ParseGID(state.ConnectorID)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot parse connector ID from state: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
cnnctr, err = svc.Connectors.Reconnect(r.Context(), connectorID, connection)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot reconnect connector: %w", err))
|
||||
}
|
||||
} else {
|
||||
cnnctr, err = svc.Connectors.Create(
|
||||
r.Context(),
|
||||
probo.CreateConnectorRequest{
|
||||
OrganizationID: organizationID,
|
||||
Provider: connectorProvider,
|
||||
Protocol: coredata.ConnectorProtocol(connection.Type()),
|
||||
Connection: connection,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot create connector: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Append connector_id to the redirect URL so frontend can create the bridge
|
||||
redirectURL := continueURL
|
||||
redirectURL := state.ContinueURL
|
||||
if redirectURL == "" {
|
||||
redirectURL = baseURL.WithPath("/organizations/" + organizationID.String()).MustString()
|
||||
}
|
||||
@@ -204,7 +231,8 @@ func NewMux(
|
||||
parsedURL, _ = url.Parse(baseURL.WithPath("/organizations/" + organizationID.String()).MustString())
|
||||
}
|
||||
q := parsedURL.Query()
|
||||
q.Set("connector_id", connector.ID.String())
|
||||
q.Set("connector_id", cnnctr.ID.String())
|
||||
q.Set("provider", string(connectorProvider))
|
||||
parsedURL.RawQuery = q.Encode()
|
||||
|
||||
safeRedirect.Redirect(w, r, parsedURL.String(), "/", http.StatusSeeOther)
|
||||
|
||||
@@ -1903,6 +1903,8 @@ type Organization implements Node {
|
||||
last: Int
|
||||
before: CursorKey
|
||||
): SlackConnectionConnection! @goField(forceResolver: true)
|
||||
connectors(filter: ConnectorFilter): [Connector!]! @goField(forceResolver: true)
|
||||
connectorProviderInfos: [ConnectorProviderInfo!]! @goField(forceResolver: true)
|
||||
|
||||
frameworks(
|
||||
first: Int
|
||||
@@ -2100,12 +2102,84 @@ type Organization implements Node {
|
||||
filter: AuditLogEntryFilter
|
||||
): AuditLogEntryConnection! @goField(forceResolver: true)
|
||||
|
||||
accessSources(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: AccessSourceOrder
|
||||
): AccessSourceConnection! @goField(forceResolver: true)
|
||||
|
||||
accessReviewCampaigns(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: AccessReviewCampaignOrder
|
||||
): AccessReviewCampaignConnection! @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
enum ConnectorProvider
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.ConnectorProvider") {
|
||||
SLACK @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSlack")
|
||||
GOOGLE_WORKSPACE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderGoogleWorkspace"
|
||||
)
|
||||
LINEAR @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderLinear")
|
||||
ONE_PASSWORD
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderOnePassword"
|
||||
)
|
||||
HUBSPOT
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderHubSpot")
|
||||
DOCUSIGN
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderDocuSign")
|
||||
NOTION @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderNotion")
|
||||
BREX @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderBrex")
|
||||
TALLY @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderTally")
|
||||
CLOUDFLARE
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderCloudflare")
|
||||
OPENAI @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderOpenAI")
|
||||
SENTRY @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSentry")
|
||||
SUPABASE
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSupabase")
|
||||
GITHUB @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderGitHub")
|
||||
INTERCOM
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderIntercom")
|
||||
RESEND @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderResend")
|
||||
}
|
||||
|
||||
type ConnectorProviderInfo {
|
||||
provider: ConnectorProvider!
|
||||
displayName: String!
|
||||
oauthConfigured: Boolean!
|
||||
apiKeySupported: Boolean!
|
||||
clientCredentialsSupported: Boolean!
|
||||
extraSettings: [ConnectorProviderSettingInfo!]!
|
||||
}
|
||||
|
||||
type ConnectorProviderSettingInfo {
|
||||
key: String!
|
||||
label: String!
|
||||
required: Boolean!
|
||||
}
|
||||
|
||||
input ConnectorFilter {
|
||||
providers: [ConnectorProvider!]
|
||||
}
|
||||
|
||||
type Connector {
|
||||
id: ID!
|
||||
provider: ConnectorProvider!
|
||||
createdAt: Datetime!
|
||||
}
|
||||
|
||||
type SlackConnection implements Node {
|
||||
id: ID!
|
||||
channel: String
|
||||
@@ -3983,6 +4057,62 @@ type Mutation {
|
||||
deleteCustomDomain(
|
||||
input: DeleteCustomDomainInput!
|
||||
): DeleteCustomDomainPayload!
|
||||
# Access Source mutations
|
||||
createAccessSource(
|
||||
input: CreateAccessSourceInput!
|
||||
): CreateAccessSourcePayload!
|
||||
updateAccessSource(
|
||||
input: UpdateAccessSourceInput!
|
||||
): UpdateAccessSourcePayload!
|
||||
deleteAccessSource(
|
||||
input: DeleteAccessSourceInput!
|
||||
): DeleteAccessSourcePayload!
|
||||
# Access Review Campaign mutations
|
||||
createAccessReviewCampaign(
|
||||
input: CreateAccessReviewCampaignInput!
|
||||
): CreateAccessReviewCampaignPayload!
|
||||
updateAccessReviewCampaign(
|
||||
input: UpdateAccessReviewCampaignInput!
|
||||
): UpdateAccessReviewCampaignPayload!
|
||||
deleteAccessReviewCampaign(
|
||||
input: DeleteAccessReviewCampaignInput!
|
||||
): DeleteAccessReviewCampaignPayload!
|
||||
startAccessReviewCampaign(
|
||||
input: StartAccessReviewCampaignInput!
|
||||
): StartAccessReviewCampaignPayload!
|
||||
closeAccessReviewCampaign(
|
||||
input: CloseAccessReviewCampaignInput!
|
||||
): CloseAccessReviewCampaignPayload!
|
||||
cancelAccessReviewCampaign(
|
||||
input: CancelAccessReviewCampaignInput!
|
||||
): CancelAccessReviewCampaignPayload!
|
||||
addAccessReviewCampaignScopeSource(
|
||||
input: AddAccessReviewCampaignScopeSourceInput!
|
||||
): AddAccessReviewCampaignScopeSourcePayload!
|
||||
removeAccessReviewCampaignScopeSource(
|
||||
input: RemoveAccessReviewCampaignScopeSourceInput!
|
||||
): RemoveAccessReviewCampaignScopeSourcePayload!
|
||||
# Access Entry mutations
|
||||
recordAccessEntryDecision(
|
||||
input: RecordAccessEntryDecisionInput!
|
||||
): RecordAccessEntryDecisionPayload!
|
||||
recordAccessEntryDecisions(
|
||||
input: RecordAccessEntryDecisionsInput!
|
||||
): RecordAccessEntryDecisionsPayload!
|
||||
flagAccessEntry(
|
||||
input: FlagAccessEntryInput!
|
||||
): FlagAccessEntryPayload!
|
||||
# Connector mutations
|
||||
createAPIKeyConnector(
|
||||
input: CreateAPIKeyConnectorInput!
|
||||
): CreateAPIKeyConnectorPayload!
|
||||
createClientCredentialsConnector(
|
||||
input: CreateClientCredentialsConnectorInput!
|
||||
): CreateClientCredentialsConnectorPayload!
|
||||
deleteConnector(input: DeleteConnectorInput!): DeleteConnectorPayload!
|
||||
configureAccessSource(
|
||||
input: ConfigureAccessSourceInput!
|
||||
): ConfigureAccessSourcePayload!
|
||||
# Slack Connection mutations
|
||||
deleteSlackConnection(
|
||||
input: DeleteSlackConnectionInput!
|
||||
@@ -6332,6 +6462,256 @@ type AuditLogEntry implements Node {
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
# ===== Access Review Types =====
|
||||
|
||||
enum AccessReviewCampaignStatus
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatus"
|
||||
) {
|
||||
DRAFT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatusDraft"
|
||||
)
|
||||
IN_PROGRESS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatusInProgress"
|
||||
)
|
||||
PENDING_ACTIONS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatusPendingActions"
|
||||
)
|
||||
FAILED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatusFailed"
|
||||
)
|
||||
COMPLETED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatusCompleted"
|
||||
)
|
||||
CANCELLED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatusCancelled"
|
||||
)
|
||||
}
|
||||
|
||||
enum AccessSourceCategory
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessSourceCategory"
|
||||
) {
|
||||
SAAS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessSourceCategorySaaS"
|
||||
)
|
||||
CLOUD_INFRA
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessSourceCategoryCloudInfra"
|
||||
)
|
||||
SOURCE_CODE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessSourceCategorySourceCode"
|
||||
)
|
||||
OTHER
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessSourceCategoryOther"
|
||||
)
|
||||
}
|
||||
|
||||
enum AccessReviewCampaignSourceFetchStatus
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignSourceFetchStatus"
|
||||
) {
|
||||
QUEUED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignSourceFetchStatusQueued"
|
||||
)
|
||||
FETCHING
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignSourceFetchStatusFetching"
|
||||
)
|
||||
SUCCESS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignSourceFetchStatusSuccess"
|
||||
)
|
||||
FAILED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignSourceFetchStatusFailed"
|
||||
)
|
||||
}
|
||||
|
||||
enum AccessEntryFlag
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.AccessEntryFlag") {
|
||||
NONE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagNone"
|
||||
)
|
||||
ORPHANED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagOrphaned"
|
||||
)
|
||||
INACTIVE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagInactive"
|
||||
)
|
||||
EXCESSIVE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagExcessive"
|
||||
)
|
||||
ROLE_MISMATCH
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagRoleMismatch"
|
||||
)
|
||||
NEW
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagNew"
|
||||
)
|
||||
DORMANT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagDormant"
|
||||
)
|
||||
TERMINATED_USER
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagTerminatedUser"
|
||||
)
|
||||
CONTRACTOR_EXPIRED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagContractorExpired"
|
||||
)
|
||||
SOD_CONFLICT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagSoDConflict"
|
||||
)
|
||||
PRIVILEGED_ACCESS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagPrivilegedAccess"
|
||||
)
|
||||
ROLE_CREEP
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagRoleCreep"
|
||||
)
|
||||
NO_BUSINESS_JUSTIFICATION
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagNoBusinessJustification"
|
||||
)
|
||||
OUT_OF_DEPARTMENT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagOutOfDepartment"
|
||||
)
|
||||
SHARED_ACCOUNT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagSharedAccount"
|
||||
)
|
||||
}
|
||||
|
||||
enum AccessEntryDecision
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessEntryDecision"
|
||||
) {
|
||||
PENDING
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionPending"
|
||||
)
|
||||
APPROVED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionApproved"
|
||||
)
|
||||
REVOKE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionRevoke"
|
||||
)
|
||||
DEFER
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionDefer"
|
||||
)
|
||||
ESCALATE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionEscalate"
|
||||
)
|
||||
}
|
||||
|
||||
enum AccessEntryIncrementalTag
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTag") {
|
||||
NEW
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTagNew"
|
||||
)
|
||||
REMOVED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTagRemoved"
|
||||
)
|
||||
UNCHANGED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTagUnchanged"
|
||||
)
|
||||
}
|
||||
|
||||
enum MfaStatus
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.MFAStatus") {
|
||||
ENABLED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.MFAStatusEnabled"
|
||||
)
|
||||
DISABLED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.MFAStatusDisabled"
|
||||
)
|
||||
UNKNOWN
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.MFAStatusUnknown"
|
||||
)
|
||||
}
|
||||
|
||||
enum AccessEntryAuthMethod
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethod"
|
||||
) {
|
||||
SSO
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodSSO"
|
||||
)
|
||||
PASSWORD
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodPassword"
|
||||
)
|
||||
API_KEY
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodAPIKey"
|
||||
)
|
||||
SERVICE_ACCOUNT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodServiceAccount"
|
||||
)
|
||||
UNKNOWN
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodUnknown"
|
||||
)
|
||||
}
|
||||
|
||||
type AccessReview implements Node {
|
||||
id: ID!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
identitySource: AccessSource @goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
accessSources(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: AccessSourceOrder
|
||||
): AccessSourceConnection! @goField(forceResolver: true)
|
||||
|
||||
campaigns(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: AccessReviewCampaignOrder
|
||||
): AccessReviewCampaignConnection! @goField(forceResolver: true)
|
||||
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type AuditLogEntryConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AuditLogEntryConnection"
|
||||
@@ -6345,3 +6725,439 @@ type AuditLogEntryEdge {
|
||||
cursor: CursorKey!
|
||||
node: AuditLogEntry!
|
||||
}
|
||||
|
||||
enum AccessEntryAccountType
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessEntryAccountType"
|
||||
) {
|
||||
USER
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAccountTypeUser"
|
||||
)
|
||||
SERVICE_ACCOUNT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAccountTypeServiceAccount"
|
||||
)
|
||||
}
|
||||
|
||||
type ProviderOrganization {
|
||||
slug: String!
|
||||
displayName: String!
|
||||
}
|
||||
|
||||
enum AccessSourceConnectionStatus {
|
||||
CONNECTED
|
||||
DISCONNECTED
|
||||
NOT_APPLICABLE
|
||||
}
|
||||
|
||||
type AccessSource implements Node {
|
||||
id: ID!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
connectorId: ID
|
||||
connector: Connector @goField(forceResolver: true)
|
||||
name: String!
|
||||
csvData: String
|
||||
providerOrganizations: [ProviderOrganization!]! @goField(forceResolver: true)
|
||||
needsConfiguration: Boolean! @goField(forceResolver: true)
|
||||
connectionStatus: AccessSourceConnectionStatus! @goField(forceResolver: true)
|
||||
selectedOrganization: String @goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type AccessReviewCampaignScopeSource
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessReviewCampaignScopeSource"
|
||||
) {
|
||||
id: ID!
|
||||
source: AccessSource!
|
||||
name: String!
|
||||
fetchStatus: AccessReviewCampaignSourceFetchStatus!
|
||||
fetchedAccountsCount: Int!
|
||||
attemptCount: Int!
|
||||
lastError: String
|
||||
fetchStartedAt: Datetime
|
||||
fetchCompletedAt: Datetime
|
||||
|
||||
entries(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: AccessEntryOrder
|
||||
filter: AccessEntryFilter
|
||||
): AccessEntryConnection! @goField(forceResolver: true)
|
||||
|
||||
statistics: AccessReviewCampaignStatistics! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type AccessSourceConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessSourceConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [AccessSourceEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type AccessSourceEdge {
|
||||
cursor: CursorKey!
|
||||
node: AccessSource!
|
||||
}
|
||||
|
||||
input AccessSourceOrder {
|
||||
direction: OrderDirection!
|
||||
field: AccessSourceOrderField!
|
||||
}
|
||||
|
||||
enum AccessSourceOrderField
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessSourceOrderField"
|
||||
) {
|
||||
CREATED_AT
|
||||
}
|
||||
|
||||
type AccessReviewCampaign implements Node {
|
||||
id: ID!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
name: String!
|
||||
description: String!
|
||||
status: AccessReviewCampaignStatus!
|
||||
startedAt: Datetime
|
||||
completedAt: Datetime
|
||||
frameworkControls: [String!]
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
scopeSources: [AccessReviewCampaignScopeSource!]! @goField(forceResolver: true)
|
||||
|
||||
entries(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: AccessEntryOrder
|
||||
accessSourceId: ID
|
||||
filter: AccessEntryFilter
|
||||
): AccessEntryConnection! @goField(forceResolver: true)
|
||||
|
||||
pendingEntryCount: Int! @goField(forceResolver: true)
|
||||
|
||||
statistics: AccessReviewCampaignStatistics! @goField(forceResolver: true)
|
||||
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type AccessReviewCampaignConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessReviewCampaignConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [AccessReviewCampaignEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type AccessReviewCampaignEdge {
|
||||
cursor: CursorKey!
|
||||
node: AccessReviewCampaign!
|
||||
}
|
||||
|
||||
input AccessReviewCampaignOrder {
|
||||
direction: OrderDirection!
|
||||
field: AccessReviewCampaignOrderField!
|
||||
}
|
||||
|
||||
enum AccessReviewCampaignOrderField
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignOrderField"
|
||||
) {
|
||||
CREATED_AT
|
||||
}
|
||||
|
||||
type AccessEntry implements Node {
|
||||
id: ID!
|
||||
campaign: AccessReviewCampaign! @goField(forceResolver: true)
|
||||
accessSource: AccessSource! @goField(forceResolver: true)
|
||||
email: String!
|
||||
fullName: String!
|
||||
role: String!
|
||||
jobTitle: String!
|
||||
isAdmin: Boolean!
|
||||
mfaStatus: MfaStatus!
|
||||
authMethod: AccessEntryAuthMethod!
|
||||
accountType: AccessEntryAccountType!
|
||||
lastLogin: Datetime
|
||||
accountCreatedAt: Datetime
|
||||
externalId: String!
|
||||
incrementalTag: AccessEntryIncrementalTag!
|
||||
flags: [AccessEntryFlag!]!
|
||||
flagReasons: [String!]!
|
||||
decision: AccessEntryDecision!
|
||||
decisionNote: String
|
||||
decidedBy: ID
|
||||
decidedAt: Datetime
|
||||
decisionHistory: [AccessEntryDecisionHistoryEntry!]!
|
||||
@goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type AccessEntryDecisionHistoryEntry {
|
||||
id: ID!
|
||||
decision: AccessEntryDecision!
|
||||
decisionNote: String
|
||||
decidedBy: ID
|
||||
decidedAt: Datetime!
|
||||
createdAt: Datetime!
|
||||
}
|
||||
|
||||
type AccessEntryConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessEntryConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [AccessEntryEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type AccessEntryEdge {
|
||||
cursor: CursorKey!
|
||||
node: AccessEntry!
|
||||
}
|
||||
|
||||
input AccessEntryOrder {
|
||||
direction: OrderDirection!
|
||||
field: AccessEntryOrderField!
|
||||
}
|
||||
|
||||
enum AccessEntryOrderField
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessEntryOrderField"
|
||||
) {
|
||||
CREATED_AT
|
||||
}
|
||||
|
||||
input AccessEntryFilter
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.AccessEntryFilter"
|
||||
) {
|
||||
decision: AccessEntryDecision
|
||||
flag: AccessEntryFlag
|
||||
incrementalTag: AccessEntryIncrementalTag
|
||||
isAdmin: Boolean
|
||||
authMethod: AccessEntryAuthMethod
|
||||
accountType: AccessEntryAccountType
|
||||
}
|
||||
|
||||
type AccessReviewCampaignStatistics {
|
||||
totalCount: Int!
|
||||
decisionCounts: [AccessEntryDecisionCount!]!
|
||||
flagCounts: [AccessEntryFlagCount!]!
|
||||
incrementalTagCounts: [AccessEntryIncrementalTagCount!]!
|
||||
}
|
||||
|
||||
type AccessEntryDecisionCount {
|
||||
decision: AccessEntryDecision!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
type AccessEntryFlagCount {
|
||||
flag: AccessEntryFlag!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
type AccessEntryIncrementalTagCount {
|
||||
incrementalTag: AccessEntryIncrementalTag!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
# Access Review Inputs & Payloads
|
||||
|
||||
input CreateAccessSourceInput {
|
||||
organizationId: ID!
|
||||
connectorId: ID
|
||||
name: String!
|
||||
csvData: String
|
||||
}
|
||||
|
||||
type CreateAccessSourcePayload {
|
||||
accessSourceEdge: AccessSourceEdge!
|
||||
}
|
||||
|
||||
input UpdateAccessSourceInput {
|
||||
accessSourceId: ID!
|
||||
name: String @goField(omittable: true)
|
||||
connectorId: ID @goField(omittable: true)
|
||||
csvData: String @goField(omittable: true)
|
||||
}
|
||||
|
||||
type UpdateAccessSourcePayload {
|
||||
accessSource: AccessSource!
|
||||
}
|
||||
|
||||
input DeleteAccessSourceInput {
|
||||
accessSourceId: ID!
|
||||
}
|
||||
|
||||
type DeleteAccessSourcePayload {
|
||||
deletedAccessSourceId: ID!
|
||||
}
|
||||
|
||||
input ConfigureAccessSourceInput {
|
||||
accessSourceId: ID!
|
||||
organizationSlug: String!
|
||||
}
|
||||
|
||||
type ConfigureAccessSourcePayload {
|
||||
accessSource: AccessSource!
|
||||
}
|
||||
|
||||
input CreateAccessReviewCampaignInput {
|
||||
organizationId: ID!
|
||||
name: String!
|
||||
description: String
|
||||
frameworkControls: [String!]
|
||||
accessSourceIds: [ID!]
|
||||
}
|
||||
|
||||
type CreateAccessReviewCampaignPayload {
|
||||
accessReviewCampaignEdge: AccessReviewCampaignEdge!
|
||||
}
|
||||
|
||||
input UpdateAccessReviewCampaignInput {
|
||||
accessReviewCampaignId: ID!
|
||||
name: String @goField(omittable: true)
|
||||
description: String @goField(omittable: true)
|
||||
frameworkControls: [String!] @goField(omittable: true)
|
||||
}
|
||||
|
||||
type UpdateAccessReviewCampaignPayload {
|
||||
accessReviewCampaign: AccessReviewCampaign!
|
||||
}
|
||||
|
||||
input DeleteAccessReviewCampaignInput {
|
||||
accessReviewCampaignId: ID!
|
||||
}
|
||||
|
||||
type DeleteAccessReviewCampaignPayload {
|
||||
deletedAccessReviewCampaignId: ID!
|
||||
}
|
||||
|
||||
input StartAccessReviewCampaignInput {
|
||||
accessReviewCampaignId: ID!
|
||||
}
|
||||
|
||||
input AddAccessReviewCampaignScopeSourceInput {
|
||||
accessReviewCampaignId: ID!
|
||||
accessSourceId: ID!
|
||||
}
|
||||
|
||||
type AddAccessReviewCampaignScopeSourcePayload {
|
||||
accessReviewCampaign: AccessReviewCampaign!
|
||||
}
|
||||
|
||||
input RemoveAccessReviewCampaignScopeSourceInput {
|
||||
accessReviewCampaignId: ID!
|
||||
accessSourceId: ID!
|
||||
}
|
||||
|
||||
type RemoveAccessReviewCampaignScopeSourcePayload {
|
||||
accessReviewCampaign: AccessReviewCampaign!
|
||||
}
|
||||
|
||||
type StartAccessReviewCampaignPayload {
|
||||
accessReviewCampaign: AccessReviewCampaign!
|
||||
}
|
||||
|
||||
input CloseAccessReviewCampaignInput {
|
||||
accessReviewCampaignId: ID!
|
||||
}
|
||||
|
||||
type CloseAccessReviewCampaignPayload {
|
||||
accessReviewCampaign: AccessReviewCampaign!
|
||||
}
|
||||
|
||||
input CancelAccessReviewCampaignInput {
|
||||
accessReviewCampaignId: ID!
|
||||
}
|
||||
|
||||
type CancelAccessReviewCampaignPayload {
|
||||
accessReviewCampaign: AccessReviewCampaign!
|
||||
}
|
||||
|
||||
input RecordAccessEntryDecisionInput {
|
||||
accessEntryId: ID!
|
||||
decision: AccessEntryDecision!
|
||||
decisionNote: String
|
||||
}
|
||||
|
||||
type RecordAccessEntryDecisionPayload {
|
||||
accessEntry: AccessEntry!
|
||||
}
|
||||
|
||||
input RecordAccessEntryDecisionsInput {
|
||||
decisions: [AccessEntryDecisionInput!]!
|
||||
}
|
||||
|
||||
input AccessEntryDecisionInput {
|
||||
accessEntryId: ID!
|
||||
decision: AccessEntryDecision!
|
||||
decisionNote: String
|
||||
}
|
||||
|
||||
type RecordAccessEntryDecisionsPayload {
|
||||
accessEntries: [AccessEntry!]!
|
||||
}
|
||||
|
||||
input FlagAccessEntryInput {
|
||||
accessEntryId: ID!
|
||||
flags: [AccessEntryFlag!]!
|
||||
flagReasons: [String!]
|
||||
}
|
||||
|
||||
type FlagAccessEntryPayload {
|
||||
accessEntry: AccessEntry!
|
||||
}
|
||||
|
||||
input CreateAPIKeyConnectorInput {
|
||||
organizationId: ID!
|
||||
provider: ConnectorProvider!
|
||||
apiKey: String!
|
||||
tallyOrganizationId: String
|
||||
sentryOrganizationSlug: String
|
||||
supabaseOrganizationSlug: String
|
||||
githubOrganization: String
|
||||
onePasswordScimBridgeUrl: String
|
||||
}
|
||||
|
||||
type CreateAPIKeyConnectorPayload {
|
||||
connector: Connector!
|
||||
}
|
||||
|
||||
input CreateClientCredentialsConnectorInput {
|
||||
organizationId: ID!
|
||||
provider: ConnectorProvider!
|
||||
clientId: String!
|
||||
clientSecret: String!
|
||||
tokenUrl: String!
|
||||
scope: String
|
||||
onePasswordAccountId: String
|
||||
onePasswordRegion: String
|
||||
}
|
||||
|
||||
type CreateClientCredentialsConnectorPayload {
|
||||
connector: Connector
|
||||
}
|
||||
|
||||
input DeleteConnectorInput {
|
||||
connectorId: ID!
|
||||
}
|
||||
|
||||
type DeleteConnectorPayload {
|
||||
deletedConnectorId: ID!
|
||||
}
|
||||
|
||||
302
pkg/server/api/console/v1/types/access_review.go
Normal file
302
pkg/server/api/console/v1/types/access_review.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
AccessSourceOrderBy OrderBy[coredata.AccessSourceOrderField]
|
||||
AccessReviewCampaignOrderBy OrderBy[coredata.AccessReviewCampaignOrderField]
|
||||
AccessEntryOrderBy OrderBy[coredata.AccessEntryOrderField]
|
||||
|
||||
AccessSourceConnection struct {
|
||||
TotalCount int
|
||||
Edges []*AccessSourceEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
}
|
||||
|
||||
AccessReviewCampaignConnection struct {
|
||||
TotalCount int
|
||||
Edges []*AccessReviewCampaignEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
}
|
||||
|
||||
AccessEntryConnection struct {
|
||||
TotalCount int
|
||||
Edges []*AccessEntryEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
SourceID *gid.GID
|
||||
Filter *coredata.AccessEntryFilter
|
||||
}
|
||||
)
|
||||
|
||||
// AccessSource helpers
|
||||
|
||||
func NewAccessSourceConnection(
|
||||
p *page.Page[*coredata.AccessSource, coredata.AccessSourceOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
) *AccessSourceConnection {
|
||||
edges := make([]*AccessSourceEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewAccessSourceEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &AccessSourceConnection{
|
||||
Edges: edges,
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: parentType,
|
||||
ParentID: parentID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccessSourceEdge(s *coredata.AccessSource, orderBy coredata.AccessSourceOrderField) *AccessSourceEdge {
|
||||
return &AccessSourceEdge{
|
||||
Cursor: s.CursorKey(orderBy),
|
||||
Node: NewAccessSource(s),
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccessSource(s *coredata.AccessSource) *AccessSource {
|
||||
return &AccessSource{
|
||||
ID: s.ID,
|
||||
Organization: &Organization{
|
||||
ID: s.OrganizationID,
|
||||
},
|
||||
ConnectorID: s.ConnectorID,
|
||||
Name: s.Name,
|
||||
CSVData: s.CsvData,
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccessReviewCampaignScopeSource(
|
||||
campaignID gid.GID,
|
||||
source *coredata.AccessSource,
|
||||
fetch *coredata.AccessReviewCampaignSourceFetch,
|
||||
) *AccessReviewCampaignScopeSource {
|
||||
status := coredata.AccessReviewCampaignSourceFetchStatusQueued
|
||||
fetchedAccountsCount := 0
|
||||
attemptCount := 0
|
||||
var lastError *string
|
||||
var fetchStartedAt *time.Time
|
||||
var fetchCompletedAt *time.Time
|
||||
if fetch != nil {
|
||||
status = fetch.Status
|
||||
fetchedAccountsCount = fetch.FetchedAccountsCount
|
||||
attemptCount = fetch.AttemptCount
|
||||
lastError = fetch.LastError
|
||||
fetchStartedAt = fetch.StartedAt
|
||||
fetchCompletedAt = fetch.CompletedAt
|
||||
}
|
||||
|
||||
return &AccessReviewCampaignScopeSource{
|
||||
ID: source.ID,
|
||||
CampaignID: campaignID,
|
||||
Source: NewAccessSource(source),
|
||||
Name: source.Name,
|
||||
FetchStatus: status,
|
||||
FetchedAccountsCount: fetchedAccountsCount,
|
||||
AttemptCount: attemptCount,
|
||||
LastError: lastError,
|
||||
FetchStartedAt: fetchStartedAt,
|
||||
FetchCompletedAt: fetchCompletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// AccessReviewCampaign helpers
|
||||
|
||||
func NewAccessReviewCampaignConnection(
|
||||
p *page.Page[*coredata.AccessReviewCampaign, coredata.AccessReviewCampaignOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
) *AccessReviewCampaignConnection {
|
||||
edges := make([]*AccessReviewCampaignEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewAccessReviewCampaignEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &AccessReviewCampaignConnection{
|
||||
Edges: edges,
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: parentType,
|
||||
ParentID: parentID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccessReviewCampaignEdge(c *coredata.AccessReviewCampaign, orderBy coredata.AccessReviewCampaignOrderField) *AccessReviewCampaignEdge {
|
||||
return &AccessReviewCampaignEdge{
|
||||
Cursor: c.CursorKey(orderBy),
|
||||
Node: NewAccessReviewCampaign(c),
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccessReviewCampaign(c *coredata.AccessReviewCampaign) *AccessReviewCampaign {
|
||||
campaign := &AccessReviewCampaign{
|
||||
ID: c.ID,
|
||||
Organization: &Organization{
|
||||
ID: c.OrganizationID,
|
||||
},
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
Status: c.Status,
|
||||
StartedAt: c.StartedAt,
|
||||
CompletedAt: c.CompletedAt,
|
||||
FrameworkControls: c.FrameworkControls,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
|
||||
return campaign
|
||||
}
|
||||
|
||||
func NewAccessEntryDecisionHistoryEntry(h *coredata.AccessEntryDecisionHistory) *AccessEntryDecisionHistoryEntry {
|
||||
entry := &AccessEntryDecisionHistoryEntry{
|
||||
ID: h.ID,
|
||||
Decision: h.Decision,
|
||||
DecisionNote: h.DecisionNote,
|
||||
DecidedAt: h.DecidedAt,
|
||||
CreatedAt: h.CreatedAt,
|
||||
}
|
||||
|
||||
if h.DecidedBy != nil {
|
||||
entry.DecidedBy = h.DecidedBy
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
// AccessEntry helpers
|
||||
|
||||
func NewAccessEntryConnection(
|
||||
p *page.Page[*coredata.AccessEntry, coredata.AccessEntryOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
sourceID *gid.GID,
|
||||
filter *coredata.AccessEntryFilter,
|
||||
) *AccessEntryConnection {
|
||||
edges := make([]*AccessEntryEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewAccessEntryEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &AccessEntryConnection{
|
||||
Edges: edges,
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: parentType,
|
||||
ParentID: parentID,
|
||||
SourceID: sourceID,
|
||||
Filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccessEntryEdge(e *coredata.AccessEntry, orderBy coredata.AccessEntryOrderField) *AccessEntryEdge {
|
||||
return &AccessEntryEdge{
|
||||
Cursor: e.CursorKey(orderBy),
|
||||
Node: NewAccessEntry(e),
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccessEntry(e *coredata.AccessEntry) *AccessEntry {
|
||||
entry := &AccessEntry{
|
||||
ID: e.ID,
|
||||
Campaign: &AccessReviewCampaign{
|
||||
ID: e.AccessReviewCampaignID,
|
||||
},
|
||||
AccessSource: &AccessSource{
|
||||
ID: e.AccessSourceID,
|
||||
},
|
||||
Email: e.Email,
|
||||
FullName: e.FullName,
|
||||
Role: e.Role,
|
||||
JobTitle: e.JobTitle,
|
||||
IsAdmin: e.IsAdmin,
|
||||
MfaStatus: e.MFAStatus,
|
||||
AuthMethod: e.AuthMethod,
|
||||
AccountType: e.AccountType,
|
||||
LastLogin: e.LastLogin,
|
||||
AccountCreatedAt: e.AccountCreatedAt,
|
||||
ExternalID: e.ExternalID,
|
||||
IncrementalTag: e.IncrementalTag,
|
||||
Flags: e.Flags,
|
||||
FlagReasons: e.FlagReasons,
|
||||
Decision: e.Decision,
|
||||
DecisionNote: e.DecisionNote,
|
||||
DecidedAt: e.DecidedAt,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
|
||||
if e.DecidedBy != nil {
|
||||
entry.DecidedBy = e.DecidedBy
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
func NewAccessReviewCampaignStatistics(stats *coredata.AccessEntryStatistics) *AccessReviewCampaignStatistics {
|
||||
decisionCounts := make([]*AccessEntryDecisionCount, 0, len(stats.DecisionCounts))
|
||||
for decision, count := range stats.DecisionCounts {
|
||||
decisionCounts = append(
|
||||
decisionCounts,
|
||||
&AccessEntryDecisionCount{Decision: decision, Count: count},
|
||||
)
|
||||
}
|
||||
|
||||
flagCounts := make([]*AccessEntryFlagCount, 0, len(stats.FlagCounts))
|
||||
for flag, count := range stats.FlagCounts {
|
||||
flagCounts = append(
|
||||
flagCounts,
|
||||
&AccessEntryFlagCount{Flag: flag, Count: count},
|
||||
)
|
||||
}
|
||||
|
||||
incrementalTagCounts := make([]*AccessEntryIncrementalTagCount, 0, len(stats.IncrementalTagCounts))
|
||||
for tag, count := range stats.IncrementalTagCounts {
|
||||
incrementalTagCounts = append(
|
||||
incrementalTagCounts,
|
||||
&AccessEntryIncrementalTagCount{IncrementalTag: tag, Count: count},
|
||||
)
|
||||
}
|
||||
|
||||
return &AccessReviewCampaignStatistics{
|
||||
TotalCount: stats.TotalCount,
|
||||
DecisionCounts: decisionCounts,
|
||||
FlagCounts: flagCounts,
|
||||
IncrementalTagCounts: incrementalTagCounts,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type AccessReviewCampaignScopeSource struct {
|
||||
ID gid.GID `json:"id"`
|
||||
CampaignID gid.GID `json:"-"`
|
||||
Source *AccessSource `json:"source"`
|
||||
Name string `json:"name"`
|
||||
FetchStatus coredata.AccessReviewCampaignSourceFetchStatus `json:"fetchStatus"`
|
||||
FetchedAccountsCount int `json:"fetchedAccountsCount"`
|
||||
AttemptCount int `json:"attemptCount"`
|
||||
LastError *string `json:"lastError,omitempty"`
|
||||
FetchStartedAt *time.Time `json:"fetchStartedAt,omitempty"`
|
||||
FetchCompletedAt *time.Time `json:"fetchCompletedAt,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
func TestNewAccessReviewCampaignScopeSource_DefaultFetchState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tenantID := gid.NewTenantID()
|
||||
source := &coredata.AccessSource{
|
||||
ID: gid.New(tenantID, coredata.AccessSourceEntityType),
|
||||
OrganizationID: gid.New(tenantID, coredata.OrganizationEntityType),
|
||||
Name: "Google Workspace",
|
||||
}
|
||||
|
||||
campaignID := gid.New(tenantID, coredata.AccessReviewCampaignEntityType)
|
||||
got := NewAccessReviewCampaignScopeSource(campaignID, source, nil)
|
||||
if got.FetchStatus != coredata.AccessReviewCampaignSourceFetchStatusQueued {
|
||||
t.Fatalf("fetch status = %q, want QUEUED", got.FetchStatus)
|
||||
}
|
||||
if got.FetchedAccountsCount != 0 {
|
||||
t.Fatalf("fetched accounts count = %d, want 0", got.FetchedAccountsCount)
|
||||
}
|
||||
if got.AttemptCount != 0 {
|
||||
t.Fatalf("attempt count = %d, want 0", got.AttemptCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAccessReviewCampaignScopeSource_UsesFetchState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
errMsg := "connector timeout"
|
||||
tenantID := gid.NewTenantID()
|
||||
source := &coredata.AccessSource{
|
||||
ID: gid.New(tenantID, coredata.AccessSourceEntityType),
|
||||
OrganizationID: gid.New(tenantID, coredata.OrganizationEntityType),
|
||||
Name: "Linear",
|
||||
}
|
||||
fetch := &coredata.AccessReviewCampaignSourceFetch{
|
||||
Status: coredata.AccessReviewCampaignSourceFetchStatusFailed,
|
||||
FetchedAccountsCount: 42,
|
||||
AttemptCount: 3,
|
||||
LastError: &errMsg,
|
||||
StartedAt: &now,
|
||||
CompletedAt: &now,
|
||||
}
|
||||
|
||||
campaignID := gid.New(tenantID, coredata.AccessReviewCampaignEntityType)
|
||||
got := NewAccessReviewCampaignScopeSource(campaignID, source, fetch)
|
||||
if got.FetchStatus != coredata.AccessReviewCampaignSourceFetchStatusFailed {
|
||||
t.Fatalf("fetch status = %q, want FAILED", got.FetchStatus)
|
||||
}
|
||||
if got.FetchedAccountsCount != 42 {
|
||||
t.Fatalf("fetched accounts count = %d, want 42", got.FetchedAccountsCount)
|
||||
}
|
||||
if got.AttemptCount != 3 {
|
||||
t.Fatalf("attempt count = %d, want 3", got.AttemptCount)
|
||||
}
|
||||
if got.LastError == nil || *got.LastError != errMsg {
|
||||
t.Fatalf("last error = %v, want %q", got.LastError, errMsg)
|
||||
}
|
||||
}
|
||||
34
pkg/server/api/console/v1/types/connector.go
Normal file
34
pkg/server/api/console/v1/types/connector.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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 types
|
||||
|
||||
import "go.probo.inc/probo/pkg/coredata"
|
||||
|
||||
func NewConnectors(connectors coredata.Connectors) []*Connector {
|
||||
items := make([]*Connector, 0, len(connectors))
|
||||
for _, cnnctr := range connectors {
|
||||
items = append(items, NewConnector(cnnctr))
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func NewConnector(c *coredata.Connector) *Connector {
|
||||
return &Connector{
|
||||
ID: c.ID,
|
||||
Provider: c.Provider,
|
||||
CreatedAt: c.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -46,14 +46,13 @@ func NewSlackConnection(c *coredata.Connector) *SlackConnection {
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
|
||||
// Extract channel information from settings
|
||||
if len(c.Settings) > 0 {
|
||||
if channel, ok := c.Settings["channel"].(string); ok && channel != "" {
|
||||
conn.Channel = &channel
|
||||
}
|
||||
if channelID, ok := c.Settings["channel_id"].(string); ok && channelID != "" {
|
||||
conn.ChannelID = &channelID
|
||||
}
|
||||
// Extract channel information from typed settings
|
||||
settings, _ := c.SlackSettings()
|
||||
if settings.Channel != "" {
|
||||
conn.Channel = &settings.Channel
|
||||
}
|
||||
if settings.ChannelID != "" {
|
||||
conn.ChannelID = &settings.ChannelID
|
||||
}
|
||||
|
||||
return conn
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user