Files
probo/pkg/server/api/console/v1/graphql/access_review_campaign.graphql
Bryan Frimin 392f81bd74 Enforce IAM authorization on every console resolver
Audited pkg/server/api/console/v1 for resolvers that touched tenant
data without calling r.authorize, batchAuthorize, or Permission. Closed
every gap so every data-bearing field goes through IAM (and produces an
audit log entry when an organization_id is present).

* High-severity reads now authorize: accessSourceResolver.Connector and
  ConnectionStatus, controlResolver.Regulatory/Contractual/RiskAssessment,
  electronicSignatureResolver.CertificateFileURL/Events,
  commonThirdPartyResolver.LogoURL, and the proper
  accessSourceResolver/accessReviewCampaignResolver/auditLogEntryResolver
  Organization resolvers (authorize + dataloader load, fixing the latent
  empty-name bug from the previous force-resolver no-op implementations).
* TotalCount/DetectedCount aggregates now authorize the matching list
  action across access review, audit log, statement of applicability,
  detected tracker, tracker pattern, and tracker resource connections.
* queryResolver.CommonThirdParties authorizes against the principal's
  identity via the new identity-scoped CommonThirdPartyCatalogPolicy.
* Add ActionCommonThirdPartyGet/List, ActionElectronicSignatureGet probo
  action constants; wire ActionElectronicSignatureGet into ViewerPolicy
  and AuditorPolicy.
* Implement AuthorizationAttributes on CommonThirdParty (no org) and
  ElectronicSignature (organization_id) so the authorizer can resolve
  attributes for the new actions.
* Delete the dead "type AccessReview" GraphQL type (no Go constructor,
  no frontend reference) and drop its orphan resolver bundle.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 13:31:38 -07:00

662 lines
17 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 AccessSourceCategory
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessSourceCategory"
) {
SAAS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessSourceCategorySaaS"
)
CLOUD_INFRA
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessSourceCategoryCloudInfra"
)
SOURCE_CODE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessSourceCategorySourceCode"
)
OTHER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessSourceCategoryOther"
)
}
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 AccessEntryFlag
@goModel(model: "go.probo.inc/probo/pkg/coredata.AccessEntryFlag") {
NONE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagNone"
)
ORPHANED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagOrphaned"
)
INACTIVE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagInactive"
)
EXCESSIVE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagExcessive"
)
ROLE_MISMATCH
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagRoleMismatch"
)
NEW
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagNew"
)
DORMANT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagDormant"
)
TERMINATED_USER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagTerminatedUser"
)
CONTRACTOR_EXPIRED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagContractorExpired"
)
SOD_CONFLICT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagSoDConflict"
)
PRIVILEGED_ACCESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagPrivilegedAccess"
)
ROLE_CREEP
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagRoleCreep"
)
NO_BUSINESS_JUSTIFICATION
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagNoBusinessJustification"
)
OUT_OF_DEPARTMENT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagOutOfDepartment"
)
SHARED_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryFlagSharedAccount"
)
}
enum AccessEntryDecision
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessEntryDecision"
) {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionPending"
)
APPROVED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionApproved"
)
REVOKE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionRevoke"
)
DEFER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionDefer"
)
ESCALATE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryDecisionEscalate"
)
}
enum AccessEntryIncrementalTag
@goModel(model: "go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTag") {
NEW
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTagNew"
)
REMOVED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTagRemoved"
)
UNCHANGED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTagUnchanged"
)
}
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 AccessEntryAuthMethod
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethod"
) {
SSO
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodSSO"
)
PASSWORD
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodPassword"
)
API_KEY
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodAPIKey"
)
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodServiceAccount"
)
UNKNOWN
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethodUnknown"
)
}
enum AccessEntryAccountType
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessEntryAccountType"
) {
USER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAccountTypeUser"
)
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AccessEntryAccountTypeServiceAccount"
)
}
enum AccessSourceOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessSourceOrderField"
) {
CREATED_AT
}
enum AccessReviewCampaignOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessReviewCampaignOrderField"
) {
CREATED_AT
}
enum AccessEntryOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessEntryOrderField"
) {
CREATED_AT
}
enum AccessSourceConnectionStatus {
CONNECTED
DISCONNECTED
NOT_APPLICABLE
}
input AccessSourceOrder {
direction: OrderDirection!
field: AccessSourceOrderField!
}
input AccessReviewCampaignOrder {
direction: OrderDirection!
field: AccessReviewCampaignOrderField!
}
input AccessEntryOrder {
direction: OrderDirection!
field: AccessEntryOrderField!
}
input AccessEntryFilter
@goModel(
model: "go.probo.inc/probo/pkg/coredata.AccessEntryFilter"
) {
decision: AccessEntryDecision
flag: AccessEntryFlag
incrementalTag: AccessEntryIncrementalTag
isAdmin: Boolean
authMethod: AccessEntryAuthMethod
accountType: AccessEntryAccountType
}
type ProviderOrganization {
slug: String!
displayName: String!
}
type AccessSource 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: AccessSourceConnectionStatus! @goField(forceResolver: true)
selectedOrganization: String @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type AccessReviewCampaignScopeSource
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessReviewCampaignScopeSource"
) {
id: ID!
source: AccessSource!
name: String!
fetchStatus: AccessReviewCampaignSourceFetchStatus!
fetchedAccountsCount: Int!
attemptCount: Int!
lastError: String
fetchStartedAt: Datetime
fetchCompletedAt: Datetime
entries(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: AccessEntryOrder
filter: AccessEntryFilter
): AccessEntryConnection! @goField(forceResolver: true)
statistics: AccessReviewCampaignStatistics! @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!
scopeSources: [AccessReviewCampaignScopeSource!]! @goField(forceResolver: true)
entries(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: AccessEntryOrder
accessSourceId: ID
filter: AccessEntryFilter
): AccessEntryConnection! @goField(forceResolver: true)
pendingEntryCount: Int! @goField(forceResolver: true)
statistics: AccessReviewCampaignStatistics! @goField(forceResolver: true)
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type AccessEntry implements Node {
id: ID!
campaign: AccessReviewCampaign! @goField(forceResolver: true)
accessSource: AccessSource! @goField(forceResolver: true)
email: String!
fullName: String!
role: String!
jobTitle: String!
isAdmin: Boolean!
mfaStatus: MfaStatus!
authMethod: AccessEntryAuthMethod!
accountType: AccessEntryAccountType!
lastLogin: Datetime
accountCreatedAt: Datetime
externalId: String!
incrementalTag: AccessEntryIncrementalTag!
flags: [AccessEntryFlag!]!
flagReasons: [String!]!
decision: AccessEntryDecision!
decisionNote: String
decidedBy: ID
decidedAt: Datetime
decisionHistory: [AccessEntryDecisionHistoryEntry!]!
@goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type AccessEntryDecisionHistoryEntry {
id: ID!
decision: AccessEntryDecision!
decisionNote: String
decidedBy: ID
decidedAt: Datetime!
createdAt: Datetime!
}
type AccessSourceConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessSourceConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [AccessSourceEdge!]!
pageInfo: PageInfo!
}
type AccessSourceEdge {
cursor: CursorKey!
node: AccessSource!
}
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 AccessEntryConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AccessEntryConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [AccessEntryEdge!]!
pageInfo: PageInfo!
}
type AccessEntryEdge {
cursor: CursorKey!
node: AccessEntry!
}
type AccessReviewCampaignStatistics {
totalCount: Int!
decisionCounts: [AccessEntryDecisionCount!]!
flagCounts: [AccessEntryFlagCount!]!
incrementalTagCounts: [AccessEntryIncrementalTagCount!]!
}
type AccessEntryDecisionCount {
decision: AccessEntryDecision!
count: Int!
}
type AccessEntryFlagCount {
flag: AccessEntryFlag!
count: Int!
}
type AccessEntryIncrementalTagCount {
incrementalTag: AccessEntryIncrementalTag!
count: Int!
}
extend type Mutation {
createAccessSource(
input: CreateAccessSourceInput!
): CreateAccessSourcePayload!
updateAccessSource(
input: UpdateAccessSourceInput!
): UpdateAccessSourcePayload!
deleteAccessSource(
input: DeleteAccessSourceInput!
): DeleteAccessSourcePayload!
configureAccessSource(
input: ConfigureAccessSourceInput!
): ConfigureAccessSourcePayload!
createAccessReviewCampaign(
input: CreateAccessReviewCampaignInput!
): CreateAccessReviewCampaignPayload!
updateAccessReviewCampaign(
input: UpdateAccessReviewCampaignInput!
): UpdateAccessReviewCampaignPayload!
deleteAccessReviewCampaign(
input: DeleteAccessReviewCampaignInput!
): DeleteAccessReviewCampaignPayload!
startAccessReviewCampaign(
input: StartAccessReviewCampaignInput!
): StartAccessReviewCampaignPayload!
closeAccessReviewCampaign(
input: CloseAccessReviewCampaignInput!
): CloseAccessReviewCampaignPayload!
cancelAccessReviewCampaign(
input: CancelAccessReviewCampaignInput!
): CancelAccessReviewCampaignPayload!
addAccessReviewCampaignScopeSource(
input: AddAccessReviewCampaignScopeSourceInput!
): AddAccessReviewCampaignScopeSourcePayload!
removeAccessReviewCampaignScopeSource(
input: RemoveAccessReviewCampaignScopeSourceInput!
): RemoveAccessReviewCampaignScopeSourcePayload!
recordAccessEntryDecision(
input: RecordAccessEntryDecisionInput!
): RecordAccessEntryDecisionPayload!
recordAccessEntryDecisions(
input: RecordAccessEntryDecisionsInput!
): RecordAccessEntryDecisionsPayload!
flagAccessEntry(
input: FlagAccessEntryInput!
): FlagAccessEntryPayload!
}
input CreateAccessSourceInput {
organizationId: ID!
connectorId: ID
name: String!
csvData: String
}
type CreateAccessSourcePayload {
accessSourceEdge: AccessSourceEdge!
}
input UpdateAccessSourceInput {
accessSourceId: ID!
name: String @goField(omittable: true)
connectorId: ID @goField(omittable: true)
csvData: String @goField(omittable: true)
}
type UpdateAccessSourcePayload {
accessSource: AccessSource!
}
input DeleteAccessSourceInput {
accessSourceId: ID!
}
type DeleteAccessSourcePayload {
deletedAccessSourceId: ID!
}
input ConfigureAccessSourceInput {
accessSourceId: ID!
organizationSlug: String!
}
type ConfigureAccessSourcePayload {
accessSource: AccessSource!
}
input CreateAccessReviewCampaignInput {
organizationId: ID!
name: String!
description: String
frameworkControls: [String!]
accessSourceIds: [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 AddAccessReviewCampaignScopeSourceInput {
accessReviewCampaignId: ID!
accessSourceId: ID!
}
type AddAccessReviewCampaignScopeSourcePayload {
accessReviewCampaign: AccessReviewCampaign!
}
input RemoveAccessReviewCampaignScopeSourceInput {
accessReviewCampaignId: ID!
accessSourceId: ID!
}
type RemoveAccessReviewCampaignScopeSourcePayload {
accessReviewCampaign: AccessReviewCampaign!
}
input RecordAccessEntryDecisionInput {
accessEntryId: ID!
decision: AccessEntryDecision!
decisionNote: String
}
type RecordAccessEntryDecisionPayload {
accessEntry: AccessEntry!
}
input RecordAccessEntryDecisionsInput {
decisions: [AccessEntryDecisionInput!]!
}
input AccessEntryDecisionInput {
accessEntryId: ID!
decision: AccessEntryDecision!
decisionNote: String
}
type RecordAccessEntryDecisionsPayload {
accessEntries: [AccessEntry!]!
}
input FlagAccessEntryInput {
accessEntryId: ID!
flags: [AccessEntryFlag!]!
flagReasons: [String!]
}
type FlagAccessEntryPayload {
accessEntry: AccessEntry!
}