Drop unused access review source category

Source category was never surfaced in the product and added noise to
snapshots and APIs. Remove the enum, columns, and service fields so
campaign sources track only the identity fields reviewers need.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-06-15 14:42:51 +02:00
parent 7138ae2273
commit 82c9800677
7 changed files with 94 additions and 200 deletions

View File

@@ -37,14 +37,12 @@ type (
OrganizationID gid.GID
ConnectorID *gid.GID
Name string
Category coredata.AccessReviewSourceCategory
CsvData *string
}
UpdateAccessReviewSourceRequest struct {
AccessReviewSourceID gid.GID
Name *string
Category *coredata.AccessReviewSourceCategory
Name **string
ConnectorID **gid.GID
CsvData **string
}
@@ -60,7 +58,6 @@ func (r *CreateAccessReviewSourceRequest) Validate() error {
v.Check(r.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(NameMaxLength))
v.Check(r.Category, "category", validator.OneOfSlice(coredata.AccessReviewSourceCategories()))
return v.Error()
}
@@ -79,7 +76,6 @@ func (r *UpdateAccessReviewSourceRequest) Validate() error {
v.Check(r.AccessReviewSourceID, "access_review_source_id", validator.Required(), validator.GID(coredata.AccessReviewSourceEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(NameMaxLength))
v.Check(r.Category, "category", validator.OneOfSlice(coredata.AccessReviewSourceCategories()))
return v.Error()
}
@@ -99,7 +95,6 @@ func (s *Service) CreateSource(
OrganizationID: req.OrganizationID,
ConnectorID: req.ConnectorID,
Name: req.Name,
Category: req.Category,
CsvData: req.CsvData,
CreatedAt: now,
UpdatedAt: now,
@@ -169,11 +164,9 @@ func (s *Service) UpdateSource(
}
if req.Name != nil {
source.Name = *req.Name
}
if req.Category != nil {
source.Category = *req.Category
if *req.Name != nil {
source.Name = **req.Name
}
}
if req.ConnectorID != nil {

View File

@@ -24,29 +24,78 @@ import (
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam/policy"
)
type (
// AccessReviewCampaignSource is the per-campaign snapshot of an access
// source. It captures the source identity (name, category, connector) at
// source. It captures the source identity (name, connector) at
// the time the source was scoped into the campaign so that the review's
// data survives even if the live access source is later deleted. Access
// entries and fetch attempts reference this snapshot, not the live source.
AccessReviewCampaignSource struct {
ID gid.GID `db:"id"`
TenantID gid.TenantID `db:"tenant_id"`
AccessReviewCampaignID gid.GID `db:"access_review_campaign_id"`
AccessReviewSourceID *gid.GID `db:"access_review_source_id"`
Name string `db:"name"`
Category AccessReviewSourceCategory `db:"category"`
ConnectorID *gid.GID `db:"connector_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
TenantID gid.TenantID `db:"tenant_id"`
AccessReviewCampaignID gid.GID `db:"access_review_campaign_id"`
AccessReviewSourceID *gid.GID `db:"access_review_source_id"`
Name string `db:"name"`
ConnectorID *gid.GID `db:"connector_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
AccessReviewCampaignSources []*AccessReviewCampaignSource
)
func (s *AccessReviewCampaignSource) AuthorizationAttributes(
ctx context.Context,
conn pg.Querier,
resourceIDs []gid.GID,
) (policy.AttributesByID, error) {
q := `
SELECT
cs.id,
c.organization_id
FROM
access_review_campaign_sources cs
JOIN
access_review_campaigns c ON c.id = cs.access_review_campaign_id
WHERE
cs.id = ANY(@resource_ids::text[])
`
args := pgx.StrictNamedArgs{
"resource_ids": resourceIDs,
}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return nil, fmt.Errorf("cannot query authorization attributes: %w", err)
}
defer rows.Close()
attrsByID := make(policy.AttributesByID)
for rows.Next() {
var id, organizationID gid.GID
if err := rows.Scan(&id, &organizationID); err != nil {
return nil, fmt.Errorf("cannot scan authorization attributes: %w", err)
}
attrsByID[id] = policy.Attributes{
"organization_id": organizationID.String(),
}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("cannot iterate authorization attributes: %w", err)
}
return attrsByID, nil
}
// Upsert inserts the snapshot or refreshes its denormalized identity from the
// live source. The generated ID is preserved across upserts because it is not
// part of the conflict target, so entries that already reference the snapshot
@@ -63,7 +112,6 @@ INSERT INTO access_review_campaign_sources (
access_review_campaign_id,
access_review_source_id,
name,
category,
connector_id,
created_at,
updated_at
@@ -73,14 +121,12 @@ INSERT INTO access_review_campaign_sources (
@access_review_campaign_id,
@access_review_source_id,
@name,
@category,
@connector_id,
@created_at,
@updated_at
)
ON CONFLICT (access_review_campaign_id, access_review_source_id) DO UPDATE SET
name = EXCLUDED.name,
category = EXCLUDED.category,
connector_id = EXCLUDED.connector_id,
updated_at = EXCLUDED.updated_at
RETURNING id
@@ -91,7 +137,6 @@ RETURNING id
"access_review_campaign_id": s.AccessReviewCampaignID,
"access_review_source_id": s.AccessReviewSourceID,
"name": s.Name,
"category": s.Category,
"connector_id": s.ConnectorID,
"created_at": s.CreatedAt,
"updated_at": s.UpdatedAt,
@@ -117,7 +162,6 @@ SELECT
access_review_campaign_id,
access_review_source_id,
name,
category,
connector_id,
created_at,
updated_at
@@ -193,7 +237,6 @@ SELECT
access_review_campaign_id,
access_review_source_id,
name,
category,
connector_id,
created_at,
updated_at

View File

@@ -66,7 +66,6 @@ func seedAccessReviewEntryFixture(t *testing.T, ctx context.Context, client *pg.
ID: sourceID,
OrganizationID: organizationID,
Name: "Upsert Freeze Test Source",
Category: coredata.AccessReviewSourceCategorySaaS,
CreatedAt: now,
UpdatedAt: now,
}
@@ -92,7 +91,6 @@ func seedAccessReviewEntryFixture(t *testing.T, ctx context.Context, client *pg.
AccessReviewCampaignID: campaignID,
AccessReviewSourceID: &sourceID,
Name: "Upsert Freeze Test Source",
Category: coredata.AccessReviewSourceCategorySaaS,
CreatedAt: now,
UpdatedAt: now,
}

View File

@@ -30,15 +30,14 @@ import (
type (
AccessReviewSource struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
ConnectorID *gid.GID `db:"connector_id"`
Name string `db:"name"`
Category AccessReviewSourceCategory `db:"category"`
CsvData *string `db:"csv_data"`
NameSyncedAt *time.Time `db:"name_synced_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
ConnectorID *gid.GID `db:"connector_id"`
Name string `db:"name"`
CsvData *string `db:"csv_data"`
NameSyncedAt *time.Time `db:"name_synced_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
AccessReviewSources []*AccessReviewSource
@@ -104,7 +103,6 @@ SELECT
organization_id,
connector_id,
name,
category,
csv_data,
name_synced_at,
created_at,
@@ -153,7 +151,6 @@ INSERT INTO
organization_id,
connector_id,
name,
category,
csv_data,
name_synced_at,
created_at,
@@ -165,7 +162,6 @@ VALUES (
@organization_id,
@connector_id,
@name,
@category,
@csv_data,
@name_synced_at,
@created_at,
@@ -179,7 +175,6 @@ VALUES (
"organization_id": as.OrganizationID,
"connector_id": as.ConnectorID,
"name": as.Name,
"category": as.Category,
"csv_data": as.CsvData,
"name_synced_at": as.NameSyncedAt,
"created_at": as.CreatedAt,
@@ -203,7 +198,6 @@ func (as *AccessReviewSource) Update(
UPDATE access_review_sources
SET
name = @name,
category = @category,
connector_id = @connector_id,
csv_data = @csv_data,
name_synced_at = @name_synced_at,
@@ -217,7 +211,6 @@ WHERE
args := pgx.StrictNamedArgs{
"id": as.ID,
"name": as.Name,
"category": as.Category,
"connector_id": as.ConnectorID,
"csv_data": as.CsvData,
"name_synced_at": as.NameSyncedAt,
@@ -276,7 +269,6 @@ SELECT
organization_id,
connector_id,
name,
category,
csv_data,
name_synced_at,
created_at,
@@ -378,7 +370,6 @@ SELECT
organization_id,
connector_id,
name,
category,
csv_data,
name_synced_at,
created_at,

View File

@@ -1,76 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 (
"encoding"
"fmt"
)
type AccessReviewSourceCategory string
const (
AccessReviewSourceCategorySaaS AccessReviewSourceCategory = "SAAS"
AccessReviewSourceCategoryCloudInfra AccessReviewSourceCategory = "CLOUD_INFRA"
AccessReviewSourceCategorySourceCode AccessReviewSourceCategory = "SOURCE_CODE"
AccessReviewSourceCategoryOther AccessReviewSourceCategory = "OTHER"
)
var (
_ fmt.Stringer = AccessReviewSourceCategory("")
_ encoding.TextMarshaler = AccessReviewSourceCategory("")
_ encoding.TextUnmarshaler = (*AccessReviewSourceCategory)(nil)
)
func AccessReviewSourceCategories() []AccessReviewSourceCategory {
return []AccessReviewSourceCategory{
AccessReviewSourceCategorySaaS,
AccessReviewSourceCategoryCloudInfra,
AccessReviewSourceCategorySourceCode,
AccessReviewSourceCategoryOther,
}
}
func (v AccessReviewSourceCategory) IsValid() bool {
switch v {
case
AccessReviewSourceCategorySaaS,
AccessReviewSourceCategoryCloudInfra,
AccessReviewSourceCategorySourceCode,
AccessReviewSourceCategoryOther:
return true
}
return false
}
func (v AccessReviewSourceCategory) String() string {
return string(v)
}
func (v AccessReviewSourceCategory) MarshalText() ([]byte, error) {
return []byte(v.String()), nil
}
func (v *AccessReviewSourceCategory) UnmarshalText(text []byte) error {
val := AccessReviewSourceCategory(text)
if !val.IsValid() {
return fmt.Errorf("invalid AccessReviewSourceCategory value: %q", string(text))
}
*v = val
return nil
}

View File

@@ -1,78 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 TestAccessReviewSourceCategoryIsValid(t *testing.T) {
t.Parallel()
for _, value := range AccessReviewSourceCategories() {
if !value.IsValid() {
t.Fatalf("IsValid() = false for %q", value)
}
}
if AccessReviewSourceCategory("BOGUS").IsValid() {
t.Fatal("IsValid() = true for invalid value")
}
}
func TestAccessReviewSourceCategoryUnmarshalText(t *testing.T) {
t.Parallel()
for _, value := range AccessReviewSourceCategories() {
t.Run(string(value), func(t *testing.T) {
t.Parallel()
var got AccessReviewSourceCategory
if err := got.UnmarshalText([]byte(value)); err != nil {
t.Fatalf("UnmarshalText(%q) returned error: %v", value, err)
}
if got != value {
t.Fatalf("UnmarshalText(%q) = %q, want %q", value, got, value)
}
})
}
t.Run("invalid", func(t *testing.T) {
t.Parallel()
var got AccessReviewSourceCategory
if err := got.UnmarshalText([]byte("BOGUS")); err == nil {
t.Fatal("UnmarshalText(BOGUS) expected error")
}
})
}
func TestAccessReviewSourceCategoryMarshalText(t *testing.T) {
t.Parallel()
for _, value := range AccessReviewSourceCategories() {
t.Run(string(value), func(t *testing.T) {
t.Parallel()
got, err := value.MarshalText()
if err != nil {
t.Fatalf("MarshalText() returned error: %v", err)
}
if string(got) != value.String() {
t.Fatalf("MarshalText() = %q, want %q", string(got), value.String())
}
})
}
}

View File

@@ -0,0 +1,23 @@
-- Copyright (c) 2026 Probo Inc <hello@probo.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.
-- Access review: drop unused source category.
ALTER TABLE access_review_sources
DROP COLUMN category;
ALTER TABLE access_review_campaign_sources
DROP COLUMN category;
DROP TYPE access_review_source_category;