Introduce access-review source snapshot and normalize naming
Decouple each campaign from the live access-review sources it was started with by introducing a per-campaign source snapshot table (access_review_campaign_sources). The snapshot captures the source name, category, and connector at start time, so a review remains coherent even after the underlying source is edited or deleted. Fetch tracking becomes an append-only log (access_review_campaign_source_fetch_attempts) that preserves every attempt with its own status and error rather than overwriting a single row. Rename the shared access-review tables and enums to use a consistent access_review_ prefix throughout: access_entries → access_review_entries access_sources → access_review_sources access_source_category → access_review_source_category access_entry_* → access_review_entry_* The same rename propagates to every coredata type, service, GraphQL schema, MCP specification, CLI command, frontend component, and e2e test. The accessreview package gains dedicated actions.go and policies.go files for its own IAM policy set, mirroring the agentrun package pattern. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
224
pkg/coredata/access_review_campaign_source.go
Normal file
224
pkg/coredata/access_review_campaign_source.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
// AccessReviewCampaignSource is the per-campaign snapshot of an access
|
||||
// source. It captures the source identity (name, category, 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"`
|
||||
}
|
||||
|
||||
AccessReviewCampaignSources []*AccessReviewCampaignSource
|
||||
)
|
||||
|
||||
// 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
|
||||
// keep pointing at the same row.
|
||||
func (s *AccessReviewCampaignSource) Upsert(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO access_review_campaign_sources (
|
||||
id,
|
||||
tenant_id,
|
||||
access_review_campaign_id,
|
||||
access_review_source_id,
|
||||
name,
|
||||
category,
|
||||
connector_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@id,
|
||||
@tenant_id,
|
||||
@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
|
||||
`
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": s.ID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"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,
|
||||
}
|
||||
|
||||
if err := conn.QueryRow(ctx, q, args).Scan(&s.ID); err != nil {
|
||||
return fmt.Errorf("cannot upsert campaign source: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AccessReviewCampaignSource) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
id gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
tenant_id,
|
||||
access_review_campaign_id,
|
||||
access_review_source_id,
|
||||
name,
|
||||
category,
|
||||
connector_id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM access_review_campaign_sources
|
||||
WHERE
|
||||
%s
|
||||
AND id = @id
|
||||
LIMIT 1
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"id": id}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query campaign source: %w", err)
|
||||
}
|
||||
|
||||
result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[AccessReviewCampaignSource])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect campaign source: %w", err)
|
||||
}
|
||||
|
||||
*s = result
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AccessReviewCampaignSource) DeleteByCampaignIDAndAccessReviewSourceID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
scope Scoper,
|
||||
campaignID gid.GID,
|
||||
accessSourceID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
DELETE FROM access_review_campaign_sources
|
||||
WHERE
|
||||
%s
|
||||
AND access_review_campaign_id = @access_review_campaign_id
|
||||
AND access_review_source_id = @access_review_source_id
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"access_review_campaign_id": campaignID,
|
||||
"access_review_source_id": accessSourceID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
if _, err := conn.Exec(ctx, q, args); err != nil {
|
||||
return fmt.Errorf("cannot delete campaign source: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sources *AccessReviewCampaignSources) LoadByCampaignID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
campaignID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
tenant_id,
|
||||
access_review_campaign_id,
|
||||
access_review_source_id,
|
||||
name,
|
||||
category,
|
||||
connector_id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM access_review_campaign_sources
|
||||
WHERE
|
||||
%s
|
||||
AND access_review_campaign_id = @access_review_campaign_id
|
||||
ORDER BY name ASC
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"access_review_campaign_id": campaignID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query campaign sources: %w", err)
|
||||
}
|
||||
|
||||
result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[AccessReviewCampaignSource])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect campaign sources: %w", err)
|
||||
}
|
||||
|
||||
*sources = result
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user