Files
probo/pkg/server/api/console/v1/graphql/access_review_campaign.graphql
Bryan Frimin 4b64e59da4 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>
2026-06-15 15:33:22 +02:00

686 lines
19 KiB
GraphQL

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"
)
COMPLETED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatusCompleted"
)
CANCELLED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatusCancelled"
)
}
enum AccessReviewSourceCategory
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewSourceCategory"
) {
SAAS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewSourceCategorySaaS"
)
CLOUD_INFRA
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewSourceCategoryCloudInfra"
)
SOURCE_CODE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewSourceCategorySourceCode"
)
OTHER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewSourceCategoryOther"
)
}
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 AccessReviewEntryFlag
@goModel(model: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlag") {
NONE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagNone"
)
ORPHANED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagOrphaned"
)
INACTIVE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagInactive"
)
EXCESSIVE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagExcessive"
)
ROLE_MISMATCH
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagRoleMismatch"
)
NEW
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagNew"
)
DORMANT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagDormant"
)
TERMINATED_USER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagTerminatedUser"
)
CONTRACTOR_EXPIRED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagContractorExpired"
)
SOD_CONFLICT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagSoDConflict"
)
PRIVILEGED_ACCESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagPrivilegedAccess"
)
ROLE_CREEP
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagRoleCreep"
)
NO_BUSINESS_JUSTIFICATION
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagNoBusinessJustification"
)
OUT_OF_DEPARTMENT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagOutOfDepartment"
)
SHARED_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFlagSharedAccount"
)
}
enum AccessReviewEntryDecision
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryDecision"
) {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryDecisionPending"
)
APPROVED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryDecisionApproved"
)
REVOKE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryDecisionRevoke"
)
DEFER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryDecisionDefer"
)
ESCALATE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryDecisionEscalate"
)
}
enum AccessReviewEntryIncrementalTag
@goModel(model: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryIncrementalTag") {
NEW
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryIncrementalTagNew"
)
REMOVED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryIncrementalTagRemoved"
)
UNCHANGED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryIncrementalTagUnchanged"
)
}
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 AccessReviewEntryAuthMethod
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAuthMethod"
) {
SSO
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAuthMethodSSO"
)
PASSWORD
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAuthMethodPassword"
)
API_KEY
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAuthMethodAPIKey"
)
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAuthMethodServiceAccount"
)
UNKNOWN
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAuthMethodUnknown"
)
}
enum AccessReviewEntryAccountType
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAccountType"
) {
USER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAccountTypeUser"
)
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryAccountTypeServiceAccount"
)
}
enum AccessReviewSourceOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewSourceOrderField"
) {
CREATED_AT
}
enum AccessReviewCampaignOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignOrderField"
) {
CREATED_AT
}
enum AccessReviewEntryOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryOrderField"
) {
CREATED_AT
}
enum AccessReviewSourceConnectionStatus {
CONNECTED
DISCONNECTED
NOT_APPLICABLE
}
input AccessReviewSourceOrder {
direction: OrderDirection!
field: AccessReviewSourceOrderField!
}
input AccessReviewCampaignOrder {
direction: OrderDirection!
field: AccessReviewCampaignOrderField!
}
input AccessReviewEntryOrder {
direction: OrderDirection!
field: AccessReviewEntryOrderField!
}
input AccessReviewEntryFilter
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewEntryFilter"
) {
decision: AccessReviewEntryDecision
flag: AccessReviewEntryFlag
incrementalTag: AccessReviewEntryIncrementalTag
isAdmin: Boolean
active: Boolean
authMethod: AccessReviewEntryAuthMethod
accountType: AccessReviewEntryAccountType
}
type ProviderOrganization {
slug: String!
displayName: String!
}
type AccessReviewSource 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: AccessReviewSourceConnectionStatus! @goField(forceResolver: true)
selectedOrganization: String @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type AccessReviewCampaignSourceFetchAttempt
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessReviewCampaignSourceFetchAttempt"
) {
id: ID!
attemptNumber: Int!
status: AccessReviewCampaignSourceFetchStatus!
fetchedAccountsCount: Int!
error: String
startedAt: Datetime
completedAt: Datetime
createdAt: Datetime!
updatedAt: Datetime!
}
type AccessReviewCampaignSource
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessReviewCampaignSource"
) {
id: ID!
"The live access source this snapshot was taken from. Null once the source is deleted."
source: AccessReviewSource @goField(forceResolver: true)
name: String!
"Current fetch status, derived from the latest fetch attempt."
fetchStatus: AccessReviewCampaignSourceFetchStatus!
fetchedAccountsCount: Int!
attemptCount: Int!
"Generic, user-facing error from the latest attempt. Raw errors stay in logs."
lastError: String
fetchStartedAt: Datetime
fetchCompletedAt: Datetime
"Full append-only history of fetch attempts, most recent first."
fetchAttempts: [AccessReviewCampaignSourceFetchAttempt!]! @goField(forceResolver: true)
entries(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: AccessReviewEntryOrder
filter: AccessReviewEntryFilter
): AccessReviewEntryConnection! @goField(forceResolver: true)
statistics: AccessReviewStatistics! @goField(forceResolver: true)
}
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!
sources: [AccessReviewCampaignSource!]! @goField(forceResolver: true)
entries(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: AccessReviewEntryOrder
campaignSourceId: ID
filter: AccessReviewEntryFilter
): AccessReviewEntryConnection! @goField(forceResolver: true)
pendingEntryCount: Int! @goField(forceResolver: true)
statistics: AccessReviewStatistics! @goField(forceResolver: true)
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type AccessReviewEntry implements Node {
id: ID!
campaign: AccessReviewCampaign! @goField(forceResolver: true)
"The live access source this entry came from. Null once the source is deleted."
accessReviewSource: AccessReviewSource @goField(forceResolver: true)
email: String!
fullName: String!
role: String!
jobTitle: String!
isAdmin: Boolean!
active: Boolean
mfaStatus: MfaStatus!
authMethod: AccessReviewEntryAuthMethod!
accountType: AccessReviewEntryAccountType!
lastLogin: Datetime
accountCreatedAt: Datetime
externalId: String!
incrementalTag: AccessReviewEntryIncrementalTag!
flags: [AccessReviewEntryFlag!]!
flagReasons: [String!]!
decision: AccessReviewEntryDecision!
decisionNote: String
decidedBy: ID
decidedAt: Datetime
decisionHistory: [AccessReviewEntryDecisionHistoryEntry!]!
@goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type AccessReviewEntryDecisionHistoryEntry {
id: ID!
decision: AccessReviewEntryDecision!
decisionNote: String
decidedBy: ID
decidedAt: Datetime!
createdAt: Datetime!
}
type AccessReviewSourceConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessReviewSourceConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [AccessReviewSourceEdge!]!
pageInfo: PageInfo!
}
type AccessReviewSourceEdge {
cursor: CursorKey!
node: AccessReviewSource!
}
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!
}
type AccessReviewEntryConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessReviewEntryConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [AccessReviewEntryEdge!]!
pageInfo: PageInfo!
}
type AccessReviewEntryEdge {
cursor: CursorKey!
node: AccessReviewEntry!
}
type AccessReviewStatistics {
totalCount: Int!
decisionCounts: [AccessReviewEntryDecisionCount!]!
flagCounts: [AccessReviewEntryFlagCount!]!
incrementalTagCounts: [AccessReviewEntryIncrementalTagCount!]!
}
type AccessReviewEntryDecisionCount {
decision: AccessReviewEntryDecision!
count: Int!
}
type AccessReviewEntryFlagCount {
flag: AccessReviewEntryFlag!
count: Int!
}
type AccessReviewEntryIncrementalTagCount {
incrementalTag: AccessReviewEntryIncrementalTag!
count: Int!
}
extend type Mutation {
createAccessReviewSource(
input: CreateAccessReviewSourceInput!
): CreateAccessReviewSourcePayload!
updateAccessReviewSource(
input: UpdateAccessReviewSourceInput!
): UpdateAccessReviewSourcePayload!
deleteAccessReviewSource(
input: DeleteAccessReviewSourceInput!
): DeleteAccessReviewSourcePayload!
configureAccessReviewSource(
input: ConfigureAccessReviewSourceInput!
): ConfigureAccessReviewSourcePayload!
createAccessReviewCampaign(
input: CreateAccessReviewCampaignInput!
): CreateAccessReviewCampaignPayload!
updateAccessReviewCampaign(
input: UpdateAccessReviewCampaignInput!
): UpdateAccessReviewCampaignPayload!
deleteAccessReviewCampaign(
input: DeleteAccessReviewCampaignInput!
): DeleteAccessReviewCampaignPayload!
startAccessReviewCampaign(
input: StartAccessReviewCampaignInput!
): StartAccessReviewCampaignPayload!
closeAccessReviewCampaign(
input: CloseAccessReviewCampaignInput!
): CloseAccessReviewCampaignPayload!
cancelAccessReviewCampaign(
input: CancelAccessReviewCampaignInput!
): CancelAccessReviewCampaignPayload!
addAccessReviewCampaignSource(
input: AddAccessReviewCampaignSourceInput!
): AddAccessReviewCampaignSourcePayload!
removeAccessReviewCampaignSource(
input: RemoveAccessReviewCampaignSourceInput!
): RemoveAccessReviewCampaignSourcePayload!
recordAccessReviewEntryDecision(
input: RecordAccessReviewEntryDecisionInput!
): RecordAccessReviewEntryDecisionPayload!
recordAccessReviewEntryDecisions(
input: RecordAccessReviewEntryDecisionsInput!
): RecordAccessReviewEntryDecisionsPayload!
flagAccessReviewEntry(
input: FlagAccessReviewEntryInput!
): FlagAccessReviewEntryPayload!
}
input CreateAccessReviewSourceInput {
organizationId: ID!
connectorId: ID
name: String!
csvData: String
}
type CreateAccessReviewSourcePayload {
accessReviewSourceEdge: AccessReviewSourceEdge!
}
input UpdateAccessReviewSourceInput {
accessReviewSourceId: ID!
name: String @goField(omittable: true)
connectorId: ID @goField(omittable: true)
csvData: String @goField(omittable: true)
}
type UpdateAccessReviewSourcePayload {
accessReviewSource: AccessReviewSource!
}
input DeleteAccessReviewSourceInput {
accessReviewSourceId: ID!
}
type DeleteAccessReviewSourcePayload {
deletedAccessReviewSourceId: ID!
}
input ConfigureAccessReviewSourceInput {
accessReviewSourceId: ID!
organizationSlug: String!
}
type ConfigureAccessReviewSourcePayload {
accessReviewSource: AccessReviewSource!
}
input CreateAccessReviewCampaignInput {
organizationId: ID!
name: String!
description: String
frameworkControls: [String!]
accessReviewSourceIds: [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!
}
type StartAccessReviewCampaignPayload {
accessReviewCampaign: AccessReviewCampaign!
}
input CloseAccessReviewCampaignInput {
accessReviewCampaignId: ID!
}
type CloseAccessReviewCampaignPayload {
accessReviewCampaign: AccessReviewCampaign!
}
input CancelAccessReviewCampaignInput {
accessReviewCampaignId: ID!
}
type CancelAccessReviewCampaignPayload {
accessReviewCampaign: AccessReviewCampaign!
}
input AddAccessReviewCampaignSourceInput {
accessReviewCampaignId: ID!
accessReviewSourceId: ID!
}
type AddAccessReviewCampaignSourcePayload {
accessReviewCampaign: AccessReviewCampaign!
}
input RemoveAccessReviewCampaignSourceInput {
accessReviewCampaignId: ID!
accessReviewSourceId: ID!
}
type RemoveAccessReviewCampaignSourcePayload {
accessReviewCampaign: AccessReviewCampaign!
}
input RecordAccessReviewEntryDecisionInput {
accessReviewEntryId: ID!
decision: AccessReviewEntryDecision!
decisionNote: String
}
type RecordAccessReviewEntryDecisionPayload {
accessReviewEntry: AccessReviewEntry!
}
input RecordAccessReviewEntryDecisionsInput {
decisions: [AccessReviewEntryDecisionInput!]!
}
input AccessReviewEntryDecisionInput {
accessReviewEntryId: ID!
decision: AccessReviewEntryDecision!
decisionNote: String
}
type RecordAccessReviewEntryDecisionsPayload {
accessReviewEntries: [AccessReviewEntry!]!
}
input FlagAccessReviewEntryInput {
accessReviewEntryId: ID!
flags: [AccessReviewEntryFlag!]!
flagReasons: [String!]
}
type FlagAccessReviewEntryPayload {
accessReviewEntry: AccessReviewEntry!
}