Add access review dependencies and enum tests

Add go-vcr dependency, dev config for new providers,
connector service changes for access review, connect
schema updates, and unit tests for enum Scan/Value.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-04-02 11:52:27 +02:00
parent b396359162
commit ff20b383d9
14 changed files with 735 additions and 42 deletions

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ compose/keycloak/probo-realm.json
# Generated files (codegen)
__generated__/
pkg/server/api/*/v1/types/types.go
cfg/dev_local.yaml

View File

@@ -109,29 +109,124 @@ probod:
- provider: "SLACK"
protocol: "oauth2"
config:
client-id: "slack-client-id"
client-secret: "thisisnotasecret"
client-id: "your-slack-client-id"
client-secret: "your-slack-client-secret"
redirect-uri: "https://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://slack.com/oauth/v2/authorize"
token-url: "https://slack.com/api/oauth.v2.access"
scopes:
- "chat:write"
- "channels:join"
- "incoming-webhook"
- "users:read"
- "users:read.email"
settings:
signing-secret: "this-is-not-a-secret-for-slack-signing"
signing-secret: "your-slack-signing-secret"
- provider: "GOOGLE_WORKSPACE"
protocol: "oauth2"
config:
client-id: "google-workspace-client-id"
client-secret: "thisisnotasecret"
client-id: "your-google-client-id.apps.googleusercontent.com"
client-secret: "your-google-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://accounts.google.com/o/oauth2/v2/auth"
token-url: "https://oauth2.googleapis.com/token"
scopes:
- "https://www.googleapis.com/auth/admin.directory.user.readonly"
- "https://www.googleapis.com/auth/admin.directory.userschema.readonly"
- "https://www.googleapis.com/auth/admin.directory.group.member.readonly"
- "https://www.googleapis.com/auth/admin.directory.customer.readonly"
extra-auth-params:
access_type: "offline"
prompt: "consent"
- provider: "LINEAR"
protocol: "oauth2"
config:
client-id: "your-linear-client-id"
client-secret: "your-linear-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://linear.app/oauth/authorize"
token-url: "https://api.linear.app/oauth/token"
scopes:
- "read"
- "write"
- provider: "BREX"
protocol: "oauth2"
config:
client-id: "your-brex-client-id"
client-secret: "your-brex-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://accounts-api.brex.com/oauth2/default/v1/authorize"
token-url: "https://accounts-api.brex.com/oauth2/default/v1/token"
scopes:
- "openid"
- "offline_access"
- provider: "HUBSPOT"
protocol: "oauth2"
config:
client-id: "your-hubspot-client-id"
client-secret: "your-hubspot-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://app.hubspot.com/oauth/authorize"
token-url: "https://api.hubapi.com/oauth/v1/token"
scopes:
- "settings.users.read"
- provider: "DOCUSIGN"
protocol: "oauth2"
config:
client-id: "your-docusign-client-id"
client-secret: "your-docusign-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://account-d.docusign.com/oauth/auth"
token-url: "https://account-d.docusign.com/oauth/token"
scopes:
- "signature"
token-endpoint-auth: "basic-form"
- provider: "NOTION"
protocol: "oauth2"
config:
client-id: "your-notion-client-id"
client-secret: "your-notion-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://api.notion.com/v1/oauth/authorize"
token-url: "https://api.notion.com/v1/oauth/token"
extra-auth-params:
owner: "user"
token-endpoint-auth: "basic-json"
- provider: "GITHUB"
protocol: "oauth2"
config:
client-id: "your-github-client-id"
client-secret: "your-github-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://github.com/login/oauth/authorize"
token-url: "https://github.com/login/oauth/access_token"
scopes:
- "read:org"
- provider: "SENTRY"
protocol: "oauth2"
config:
client-id: "your-sentry-client-id"
client-secret: "your-sentry-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://sentry.io/oauth/authorize/"
token-url: "https://sentry.io/oauth/token/"
scopes:
- "org:read"
- "member:read"
- provider: "INTERCOM"
protocol: "oauth2"
config:
client-id: "your-intercom-client-id"
client-secret: "your-intercom-client-secret"
redirect-uri: "http://localhost:8080/api/console/v1/connectors/complete"
auth-url: "https://app.intercom.com/oauth"
token-url: "https://api.intercom.io/auth/eagle/token"

2
go.mod
View File

@@ -45,6 +45,7 @@ require (
golang.org/x/oauth2 v0.35.0
golang.org/x/sync v0.20.0
google.golang.org/api v0.269.0
gopkg.in/dnaeon/go-vcr.v4 v4.0.6
gopkg.in/yaml.v3 v3.0.1
)
@@ -70,6 +71,7 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
)

4
go.sum
View File

@@ -366,6 +366,8 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
@@ -409,6 +411,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/dnaeon/go-vcr.v4 v4.0.6 h1:PiJkrakkmzc5s7EfBnZOnyiLwi7o7A9fwPzN0X2uwe0=
gopkg.in/dnaeon/go-vcr.v4 v4.0.6/go.mod h1:sbq5oMEcM4PXngbcNbHhzfCP9OdZodLhrbRYoyg09HY=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,67 @@
// 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 coredata
import "testing"
func TestAccessEntryAccountTypeScan(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input any
want AccessEntryAccountType
wantErr bool
}{
{name: "user string", input: "USER", want: AccessEntryAccountTypeUser},
{name: "service_account bytes", input: []byte("SERVICE_ACCOUNT"), want: AccessEntryAccountTypeServiceAccount},
{name: "invalid value", input: "BOGUS", wantErr: true},
{name: "unsupported type", input: 42, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got AccessEntryAccountType
err := got.Scan(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("Scan(%v) expected error", tt.input)
}
return
}
if err != nil {
t.Fatalf("Scan(%v) returned error: %v", tt.input, err)
}
if got != tt.want {
t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestAccessEntryAccountTypeValue(t *testing.T) {
t.Parallel()
got, err := AccessEntryAccountTypeUser.Value()
if err != nil {
t.Fatalf("Value() returned error: %v", err)
}
if got != "USER" {
t.Fatalf("Value() = %q, want %q", got, "USER")
}
}

View File

@@ -0,0 +1,86 @@
// 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 coredata
import "testing"
func TestAccessEntryDecisionScan(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input any
want AccessEntryDecision
wantErr bool
}{
{name: "pending string", input: "PENDING", want: AccessEntryDecisionPending},
{name: "approved string", input: "APPROVED", want: AccessEntryDecisionApproved},
{name: "revoke string", input: "REVOKE", want: AccessEntryDecisionRevoke},
{name: "defer bytes", input: []byte("DEFER"), want: AccessEntryDecisionDefer},
{name: "escalate string", input: "ESCALATE", want: AccessEntryDecisionEscalate},
{name: "invalid value", input: "BOGUS", wantErr: true},
{name: "unsupported type", input: 42, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got AccessEntryDecision
err := got.Scan(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("Scan(%v) expected error", tt.input)
}
return
}
if err != nil {
t.Fatalf("Scan(%v) returned error: %v", tt.input, err)
}
if got != tt.want {
t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestAccessEntryDecisionValue(t *testing.T) {
t.Parallel()
tests := []struct {
name string
decision AccessEntryDecision
want string
}{
{name: "pending", decision: AccessEntryDecisionPending, want: "PENDING"},
{name: "approved", decision: AccessEntryDecisionApproved, want: "APPROVED"},
{name: "revoke", decision: AccessEntryDecisionRevoke, want: "REVOKE"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := tt.decision.Value()
if err != nil {
t.Fatalf("Value() returned error: %v", err)
}
if got != tt.want {
t.Fatalf("Value() = %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,71 @@
// 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 coredata
import "testing"
func TestAccessEntryFlagScan(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input any
want AccessEntryFlag
wantErr bool
}{
{name: "none string", input: "NONE", want: AccessEntryFlagNone},
{name: "orphaned string", input: "ORPHANED", want: AccessEntryFlagOrphaned},
{name: "inactive string", input: "INACTIVE", want: AccessEntryFlagInactive},
{name: "excessive string", input: "EXCESSIVE", want: AccessEntryFlagExcessive},
{name: "role_mismatch bytes", input: []byte("ROLE_MISMATCH"), want: AccessEntryFlagRoleMismatch},
{name: "new string", input: "NEW", want: AccessEntryFlagNew},
{name: "invalid value", input: "BOGUS", wantErr: true},
{name: "unsupported type", input: 42, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got AccessEntryFlag
err := got.Scan(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("Scan(%v) expected error", tt.input)
}
return
}
if err != nil {
t.Fatalf("Scan(%v) returned error: %v", tt.input, err)
}
if got != tt.want {
t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestAccessEntryFlagValue(t *testing.T) {
t.Parallel()
got, err := AccessEntryFlagNone.Value()
if err != nil {
t.Fatalf("Value() returned error: %v", err)
}
if got != "NONE" {
t.Fatalf("Value() = %q, want %q", got, "NONE")
}
}

View File

@@ -0,0 +1,68 @@
// 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 coredata
import "testing"
func TestAccessEntryIncrementalTagScan(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input any
want AccessEntryIncrementalTag
wantErr bool
}{
{name: "new string", input: "NEW", want: AccessEntryIncrementalTagNew},
{name: "removed bytes", input: []byte("REMOVED"), want: AccessEntryIncrementalTagRemoved},
{name: "unchanged string", input: "UNCHANGED", want: AccessEntryIncrementalTagUnchanged},
{name: "invalid value", input: "BOGUS", wantErr: true},
{name: "unsupported type", input: 42, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got AccessEntryIncrementalTag
err := got.Scan(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("Scan(%v) expected error", tt.input)
}
return
}
if err != nil {
t.Fatalf("Scan(%v) returned error: %v", tt.input, err)
}
if got != tt.want {
t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestAccessEntryIncrementalTagValue(t *testing.T) {
t.Parallel()
got, err := AccessEntryIncrementalTagNew.Value()
if err != nil {
t.Fatalf("Value() returned error: %v", err)
}
if got != "NEW" {
t.Fatalf("Value() = %q, want %q", got, "NEW")
}
}

View File

@@ -0,0 +1,87 @@
// 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 coredata
import "testing"
func TestAccessReviewCampaignStatusScan(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input any
want AccessReviewCampaignStatus
wantErr bool
}{
{name: "draft string", input: "DRAFT", want: AccessReviewCampaignStatusDraft},
{name: "in_progress string", input: "IN_PROGRESS", want: AccessReviewCampaignStatusInProgress},
{name: "pending_actions string", input: "PENDING_ACTIONS", want: AccessReviewCampaignStatusPendingActions},
{name: "failed string", input: "FAILED", want: AccessReviewCampaignStatusFailed},
{name: "completed string", input: "COMPLETED", want: AccessReviewCampaignStatusCompleted},
{name: "cancelled bytes", input: []byte("CANCELLED"), want: AccessReviewCampaignStatusCancelled},
{name: "invalid value", input: "BOGUS", wantErr: true},
{name: "unsupported type", input: 42, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got AccessReviewCampaignStatus
err := got.Scan(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("Scan(%v) expected error", tt.input)
}
return
}
if err != nil {
t.Fatalf("Scan(%v) returned error: %v", tt.input, err)
}
if got != tt.want {
t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestAccessReviewCampaignStatusValue(t *testing.T) {
t.Parallel()
tests := []struct {
name string
status AccessReviewCampaignStatus
want string
}{
{name: "draft", status: AccessReviewCampaignStatusDraft, want: "DRAFT"},
{name: "in_progress", status: AccessReviewCampaignStatusInProgress, want: "IN_PROGRESS"},
{name: "completed", status: AccessReviewCampaignStatusCompleted, want: "COMPLETED"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := tt.status.Value()
if err != nil {
t.Fatalf("Value() returned error: %v", err)
}
if got != tt.want {
t.Fatalf("Value() = %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,69 @@
// 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 coredata
import "testing"
func TestAccessSourceCategoryScan(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input any
want AccessSourceCategory
wantErr bool
}{
{name: "saas string", input: "SAAS", want: AccessSourceCategorySaaS},
{name: "cloud_infra string", input: "CLOUD_INFRA", want: AccessSourceCategoryCloudInfra},
{name: "source_code bytes", input: []byte("SOURCE_CODE"), want: AccessSourceCategorySourceCode},
{name: "other string", input: "OTHER", want: AccessSourceCategoryOther},
{name: "invalid value", input: "BOGUS", wantErr: true},
{name: "unsupported type", input: 42, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got AccessSourceCategory
err := got.Scan(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("Scan(%v) expected error", tt.input)
}
return
}
if err != nil {
t.Fatalf("Scan(%v) returned error: %v", tt.input, err)
}
if got != tt.want {
t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestAccessSourceCategoryValue(t *testing.T) {
t.Parallel()
got, err := AccessSourceCategorySaaS.Value()
if err != nil {
t.Fatalf("Value() returned error: %v", err)
}
if got != "SAAS" {
t.Fatalf("Value() = %q, want %q", got, "SAAS")
}
}

View File

@@ -1,40 +1,36 @@
-- 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.
-- Rename priority to rank
ALTER TABLE tasks RENAME COLUMN priority TO rank;
-- Add organization_id to access_entries and access_entry_decision_history
-- to avoid JOINs in AuthorizationAttributes lookups.
ALTER TABLE tasks DROP CONSTRAINT tasks_organization_id_state_priority_key;
-- 1. access_entries
ALTER TABLE access_entries
ADD COLUMN organization_id TEXT REFERENCES organizations(id);
-- Add task priority enum
CREATE TYPE task_priority AS ENUM ('URGENT', 'HIGH', 'MEDIUM', 'LOW');
UPDATE access_entries ae
SET organization_id = arc.organization_id
FROM access_review_campaigns arc
WHERE ae.access_review_campaign_id = arc.id;
ALTER TABLE tasks ADD COLUMN priority task_priority NOT NULL DEFAULT 'MEDIUM'::task_priority;
ALTER TABLE access_entries
ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE tasks ALTER COLUMN priority DROP DEFAULT;
-- 2. access_entry_decision_history
ALTER TABLE access_entry_decision_history
ADD COLUMN organization_id TEXT REFERENCES organizations(id);
-- Rank is now scoped to (state, priority) — backfill ranks per group
WITH ranked AS (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY organization_id, state, priority
ORDER BY rank
) AS new_rank
FROM tasks
)
UPDATE tasks SET rank = ranked.new_rank FROM ranked WHERE tasks.id = ranked.id;
UPDATE access_entry_decision_history h
SET organization_id = ae.organization_id
FROM access_entries ae
WHERE h.access_entry_id = ae.id;
ALTER TABLE tasks
ADD CONSTRAINT tasks_organization_id_state_priority_rank_key
UNIQUE (organization_id, state, priority, rank)
DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE access_entry_decision_history
ALTER COLUMN organization_id SET NOT NULL;
-- Computed column for composite ordering (priority level then rank)
ALTER TABLE tasks ADD COLUMN priority_rank int GENERATED ALWAYS AS (
(CASE priority
WHEN 'URGENT' THEN 1
WHEN 'HIGH' THEN 2
WHEN 'MEDIUM' THEN 3
WHEN 'LOW' THEN 4
END) * 1000000 + rank
) STORED;

View File

@@ -0,0 +1,40 @@
-- 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.
-- Add organization_id to access_entries and access_entry_decision_history
-- to avoid JOINs in AuthorizationAttributes lookups.
-- 1. access_entries
ALTER TABLE access_entries
ADD COLUMN organization_id TEXT REFERENCES organizations(id);
UPDATE access_entries ae
SET organization_id = arc.organization_id
FROM access_review_campaigns arc
WHERE ae.access_review_campaign_id = arc.id;
ALTER TABLE access_entries
ALTER COLUMN organization_id SET NOT NULL;
-- 2. access_entry_decision_history
ALTER TABLE access_entry_decision_history
ADD COLUMN organization_id TEXT REFERENCES organizations(id);
UPDATE access_entry_decision_history h
SET organization_id = ae.organization_id
FROM access_entries ae
WHERE h.access_entry_id = ae.id;
ALTER TABLE access_entry_decision_history
ALTER COLUMN organization_id SET NOT NULL;

View File

@@ -49,10 +49,16 @@ type (
}
CreateConnectorRequest struct {
OrganizationID gid.GID
Provider coredata.ConnectorProvider
Protocol coredata.ConnectorProtocol
Connection connector.Connection
OrganizationID gid.GID
Provider coredata.ConnectorProvider
Protocol coredata.ConnectorProtocol
Connection connector.Connection
TallySettings *coredata.TallyConnectorSettings
OnePasswordSettings *coredata.OnePasswordConnectorSettings
SentrySettings *coredata.SentryConnectorSettings
SupabaseSettings *coredata.SupabaseConnectorSettings
GitHubSettings *coredata.GitHubConnectorSettings
OnePasswordUsersAPISettings *coredata.OnePasswordUsersAPISettings
}
)
@@ -94,6 +100,30 @@ func (s *ConnectorService) ListForOrganizationID(
return page.NewPage(connectors, cursor), nil
}
func (s *ConnectorService) ListAllForOrganizationID(
ctx context.Context,
organizationID gid.GID,
) (coredata.Connectors, error) {
var connectors coredata.Connectors
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return connectors.LoadAllByOrganizationIDWithoutDecryptedConnection(
ctx,
conn,
s.svc.scope,
organizationID,
)
},
)
if err != nil {
return nil, fmt.Errorf("cannot list all connectors: %w", err)
}
return connectors, nil
}
func (s *ConnectorService) GetByOrganizationIDAndProvider(
ctx context.Context,
organizationID gid.GID,
@@ -127,6 +157,25 @@ func (s *ConnectorService) GetByOrganizationIDAndProvider(
return connectors[0], nil
}
func (s *ConnectorService) Get(
ctx context.Context,
connectorID gid.GID,
) (*coredata.Connector, error) {
connector := &coredata.Connector{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return connector.LoadMetadataByID(ctx, conn, s.svc.scope, connectorID)
},
)
if err != nil {
return nil, fmt.Errorf("cannot get connector: %w", err)
}
return connector, nil
}
func (s *ConnectorService) Delete(
ctx context.Context,
connectorID gid.GID,
@@ -161,6 +210,33 @@ func (s *ConnectorService) Create(
UpdatedAt: now,
}
switch {
case req.TallySettings != nil:
if err := newConnector.SetSettings(req.TallySettings); err != nil {
return nil, fmt.Errorf("cannot set tally settings: %w", err)
}
case req.OnePasswordSettings != nil:
if err := newConnector.SetSettings(req.OnePasswordSettings); err != nil {
return nil, fmt.Errorf("cannot set one password settings: %w", err)
}
case req.SentrySettings != nil:
if err := newConnector.SetSettings(req.SentrySettings); err != nil {
return nil, fmt.Errorf("cannot set sentry settings: %w", err)
}
case req.SupabaseSettings != nil:
if err := newConnector.SetSettings(req.SupabaseSettings); err != nil {
return nil, fmt.Errorf("cannot set supabase settings: %w", err)
}
case req.GitHubSettings != nil:
if err := newConnector.SetSettings(req.GitHubSettings); err != nil {
return nil, fmt.Errorf("cannot set github settings: %w", err)
}
case req.OnePasswordUsersAPISettings != nil:
if err := newConnector.SetSettings(req.OnePasswordUsersAPISettings); err != nil {
return nil, fmt.Errorf("cannot set one password users api settings: %w", err)
}
}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
@@ -211,3 +287,30 @@ func (s *ConnectorService) Create(
return newConnector, nil
}
// Reconnect updates an existing connector's connection (token) without
// changing its settings or identity. Used when an OAuth token expires
// and the user re-authenticates.
func (s *ConnectorService) Reconnect(
ctx context.Context,
connectorID gid.GID,
connection connector.Connection,
) (*coredata.Connector, error) {
cnnctr := &coredata.Connector{}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
if err := cnnctr.LoadMetadataByID(ctx, conn, s.svc.scope, connectorID); err != nil {
return fmt.Errorf("cannot load connector: %w", err)
}
cnnctr.Connection = connection
cnnctr.UpdatedAt = time.Now()
return cnnctr.Update(ctx, conn, s.svc.scope, s.svc.encryptionKey)
})
if err != nil {
return nil, fmt.Errorf("cannot reconnect connector: %w", err)
}
return cnnctr, nil
}

View File

@@ -410,6 +410,10 @@ enum ConnectorProvider
SLACK @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSlack")
GOOGLE_WORKSPACE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderGoogleWorkspace")
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")
}
enum SCIMBridgeType