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 OrganizationID gid.GID
ConnectorID *gid.GID ConnectorID *gid.GID
Name string Name string
Category coredata.AccessReviewSourceCategory
CsvData *string CsvData *string
} }
UpdateAccessReviewSourceRequest struct { UpdateAccessReviewSourceRequest struct {
AccessReviewSourceID gid.GID AccessReviewSourceID gid.GID
Name *string Name **string
Category *coredata.AccessReviewSourceCategory
ConnectorID **gid.GID ConnectorID **gid.GID
CsvData **string 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.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(NameMaxLength)) v.Check(r.Name, "name", validator.SafeTextNoNewLine(NameMaxLength))
v.Check(r.Category, "category", validator.OneOfSlice(coredata.AccessReviewSourceCategories()))
return v.Error() 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.AccessReviewSourceID, "access_review_source_id", validator.Required(), validator.GID(coredata.AccessReviewSourceEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(NameMaxLength)) v.Check(r.Name, "name", validator.SafeTextNoNewLine(NameMaxLength))
v.Check(r.Category, "category", validator.OneOfSlice(coredata.AccessReviewSourceCategories()))
return v.Error() return v.Error()
} }
@@ -99,7 +95,6 @@ func (s *Service) CreateSource(
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
ConnectorID: req.ConnectorID, ConnectorID: req.ConnectorID,
Name: req.Name, Name: req.Name,
Category: req.Category,
CsvData: req.CsvData, CsvData: req.CsvData,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
@@ -169,11 +164,9 @@ func (s *Service) UpdateSource(
} }
if req.Name != nil { if req.Name != nil {
source.Name = *req.Name if *req.Name != nil {
} source.Name = **req.Name
}
if req.Category != nil {
source.Category = *req.Category
} }
if req.ConnectorID != nil { if req.ConnectorID != nil {

View File

@@ -24,29 +24,78 @@ import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam/policy"
) )
type ( type (
// AccessReviewCampaignSource is the per-campaign snapshot of an access // 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 // 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 // data survives even if the live access source is later deleted. Access
// entries and fetch attempts reference this snapshot, not the live source. // entries and fetch attempts reference this snapshot, not the live source.
AccessReviewCampaignSource struct { AccessReviewCampaignSource struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
TenantID gid.TenantID `db:"tenant_id"` TenantID gid.TenantID `db:"tenant_id"`
AccessReviewCampaignID gid.GID `db:"access_review_campaign_id"` AccessReviewCampaignID gid.GID `db:"access_review_campaign_id"`
AccessReviewSourceID *gid.GID `db:"access_review_source_id"` AccessReviewSourceID *gid.GID `db:"access_review_source_id"`
Name string `db:"name"` Name string `db:"name"`
Category AccessReviewSourceCategory `db:"category"` ConnectorID *gid.GID `db:"connector_id"`
ConnectorID *gid.GID `db:"connector_id"` CreatedAt time.Time `db:"created_at"`
CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"`
UpdatedAt time.Time `db:"updated_at"`
} }
AccessReviewCampaignSources []*AccessReviewCampaignSource 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 // Upsert inserts the snapshot or refreshes its denormalized identity from the
// live source. The generated ID is preserved across upserts because it is not // 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 // 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_campaign_id,
access_review_source_id, access_review_source_id,
name, name,
category,
connector_id, connector_id,
created_at, created_at,
updated_at updated_at
@@ -73,14 +121,12 @@ INSERT INTO access_review_campaign_sources (
@access_review_campaign_id, @access_review_campaign_id,
@access_review_source_id, @access_review_source_id,
@name, @name,
@category,
@connector_id, @connector_id,
@created_at, @created_at,
@updated_at @updated_at
) )
ON CONFLICT (access_review_campaign_id, access_review_source_id) DO UPDATE SET ON CONFLICT (access_review_campaign_id, access_review_source_id) DO UPDATE SET
name = EXCLUDED.name, name = EXCLUDED.name,
category = EXCLUDED.category,
connector_id = EXCLUDED.connector_id, connector_id = EXCLUDED.connector_id,
updated_at = EXCLUDED.updated_at updated_at = EXCLUDED.updated_at
RETURNING id RETURNING id
@@ -91,7 +137,6 @@ RETURNING id
"access_review_campaign_id": s.AccessReviewCampaignID, "access_review_campaign_id": s.AccessReviewCampaignID,
"access_review_source_id": s.AccessReviewSourceID, "access_review_source_id": s.AccessReviewSourceID,
"name": s.Name, "name": s.Name,
"category": s.Category,
"connector_id": s.ConnectorID, "connector_id": s.ConnectorID,
"created_at": s.CreatedAt, "created_at": s.CreatedAt,
"updated_at": s.UpdatedAt, "updated_at": s.UpdatedAt,
@@ -117,7 +162,6 @@ SELECT
access_review_campaign_id, access_review_campaign_id,
access_review_source_id, access_review_source_id,
name, name,
category,
connector_id, connector_id,
created_at, created_at,
updated_at updated_at
@@ -193,7 +237,6 @@ SELECT
access_review_campaign_id, access_review_campaign_id,
access_review_source_id, access_review_source_id,
name, name,
category,
connector_id, connector_id,
created_at, created_at,
updated_at updated_at

View File

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

View File

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