Files
probo/e2e/console/rbac_test.go
Sacha Al Himdani cf1dadc0b5 Add implemented state and justification to controls
Introduce `implemented` enum (IMPLEMENTED/NOT_IMPLEMENTED) and
`not_implemented_justification` (nullable text) fields on the Control
entity across all API surfaces (GraphQL, MCP, CLI), database, frontend,
and SOA export.

The database stores implementation state as a PostgreSQL enum
`control_implementation_state`. Controls default to IMPLEMENTED during
migration. The SOA list and PDF export show implementation status
alongside applicability, with "-" for non-applicable controls.
Justification columns are renamed for clarity: "Justification for
non-applicability" and "Justification for non-implementation".

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-03-17 11:01:04 +01:00

1127 lines
32 KiB
Go

// Copyright (c) 2025 Probo Inc <hello@getprobo.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 console_test
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/e2e/internal/factory"
"go.probo.inc/probo/e2e/internal/testutil"
)
const (
createFrameworkMutation = `
mutation CreateFramework($input: CreateFrameworkInput!) {
createFramework(input: $input) {
frameworkEdge { node { id } }
}
}`
updateFrameworkMutation = `
mutation UpdateFramework($input: UpdateFrameworkInput!) {
updateFramework(input: $input) {
framework { id }
}
}`
deleteFrameworkMutation = `
mutation DeleteFramework($input: DeleteFrameworkInput!) {
deleteFramework(input: $input) {
deletedFrameworkId
}
}`
listFrameworksQuery = `
query GetFrameworks($id: ID!) {
node(id: $id) {
... on Organization {
frameworks(first: 10) { totalCount }
}
}
}`
createControlMutation = `
mutation CreateControl($input: CreateControlInput!) {
createControl(input: $input) {
controlEdge { node { id } }
}
}`
updateControlMutation = `
mutation UpdateControl($input: UpdateControlInput!) {
updateControl(input: $input) {
control { id }
}
}`
deleteControlMutation = `
mutation DeleteControl($input: DeleteControlInput!) {
deleteControl(input: $input) {
deletedControlId
}
}`
listControlsQuery = `
query GetControls($id: ID!) {
node(id: $id) {
... on Framework {
controls(first: 10) { edges { node { id } } }
}
}
}`
createMeasureMutation = `
mutation CreateMeasure($input: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge { node { id } }
}
}`
updateMeasureMutation = `
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure { id }
}
}`
deleteMeasureMutation = `
mutation DeleteMeasure($input: DeleteMeasureInput!) {
deleteMeasure(input: $input) {
deletedMeasureId
}
}`
listMeasuresQuery = `
query GetMeasures($id: ID!) {
node(id: $id) {
... on Organization {
measures(first: 10) { totalCount }
}
}
}`
createTaskMutation = `
mutation CreateTask($input: CreateTaskInput!) {
createTask(input: $input) {
taskEdge { node { id } }
}
}`
updateTaskMutation = `
mutation UpdateTask($input: UpdateTaskInput!) {
updateTask(input: $input) {
task { id }
}
}`
deleteTaskMutation = `
mutation DeleteTask($input: DeleteTaskInput!) {
deleteTask(input: $input) {
deletedTaskId
}
}`
listTasksQuery = `
query GetTasks($id: ID!) {
node(id: $id) {
... on Measure {
tasks(first: 10) { totalCount }
}
}
}`
createRiskMutation = `
mutation CreateRisk($input: CreateRiskInput!) {
createRisk(input: $input) {
riskEdge { node { id } }
}
}`
updateRiskMutation = `
mutation UpdateRisk($input: UpdateRiskInput!) {
updateRisk(input: $input) {
risk { id }
}
}`
deleteRiskMutation = `
mutation DeleteRisk($input: DeleteRiskInput!) {
deleteRisk(input: $input) {
deletedRiskId
}
}`
listRisksQuery = `
query GetRisks($id: ID!) {
node(id: $id) {
... on Organization {
risks(first: 10) { totalCount }
}
}
}`
createVendorMutation = `
mutation CreateVendor($input: CreateVendorInput!) {
createVendor(input: $input) {
vendorEdge { node { id } }
}
}`
updateVendorMutation = `
mutation UpdateVendor($input: UpdateVendorInput!) {
updateVendor(input: $input) {
vendor { id }
}
}`
deleteVendorMutation = `
mutation DeleteVendor($input: DeleteVendorInput!) {
deleteVendor(input: $input) {
deletedVendorId
}
}`
listVendorsQuery = `
query GetVendors($id: ID!) {
node(id: $id) {
... on Organization {
vendors(first: 10) { totalCount }
}
}
}`
updateOrganizationMutation = `
mutation UpdateOrganization($input: UpdateOrganizationInput!) {
updateOrganization(input: $input) {
organization { id }
}
}`
getOrganizationQuery = `
query GetOrganization($id: ID!) {
node(id: $id) {
... on Organization {
id
name
}
}
}`
listUsersQuery = `
query GetProfiles($id: ID!) {
node(id: $id) {
... on Organization {
profiles(first: 10) { totalCount }
}
}
}`
)
func TestRBAC(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
// Pre-create resources for update/delete tests
frameworkID := factory.NewFramework(owner).WithName("RBAC Test Framework").Create()
controlID := factory.NewControl(owner, frameworkID).WithName("RBAC Test Control").Create()
measureID := factory.NewMeasure(owner).WithName("RBAC Test Measure").Create()
taskID := factory.NewTask(owner, measureID).WithName("RBAC Test Task").Create()
riskID := factory.NewRisk(owner).WithName("RBAC Test Risk").Create()
vendorID := factory.NewVendor(owner).WithName("RBAC Test Vendor").Create()
tests := []struct {
name string
role string
client *testutil.Client
query string
variables func() map[string]any
shouldAllow bool
useConnect bool // use connect API instead of console API
}{
{
name: "owner can create framework",
role: "owner",
client: owner,
query: createFrameworkMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Framework")}}
},
shouldAllow: true,
},
{
name: "admin can create framework",
role: "admin",
client: admin,
query: createFrameworkMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Framework")}}
},
shouldAllow: true,
},
{
name: "viewer cannot create framework",
role: "viewer",
client: viewer,
query: createFrameworkMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Framework")}}
},
shouldAllow: false,
},
{
name: "owner can update framework",
role: "owner",
client: owner,
query: updateFrameworkMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": frameworkID, "name": factory.SafeName("Updated Framework")}}
},
shouldAllow: true,
},
{
name: "admin can update framework",
role: "admin",
client: admin,
query: updateFrameworkMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": frameworkID, "name": factory.SafeName("Updated Framework")}}
},
shouldAllow: true,
},
{
name: "viewer cannot update framework",
role: "viewer",
client: viewer,
query: updateFrameworkMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": frameworkID, "name": factory.SafeName("Updated Framework")}}
},
shouldAllow: false,
},
{
name: "owner can delete framework",
role: "owner",
client: owner,
query: deleteFrameworkMutation,
variables: func() map[string]any {
id := factory.NewFramework(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"frameworkId": id}}
},
shouldAllow: true,
},
{
name: "admin can delete framework",
role: "admin",
client: admin,
query: deleteFrameworkMutation,
variables: func() map[string]any {
id := factory.NewFramework(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"frameworkId": id}}
},
shouldAllow: true,
},
{
name: "viewer cannot delete framework",
role: "viewer",
client: viewer,
query: deleteFrameworkMutation,
variables: func() map[string]any {
id := factory.NewFramework(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"frameworkId": id}}
},
shouldAllow: false,
},
{
name: "owner can list frameworks",
role: "owner",
client: owner,
query: listFrameworksQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "admin can list frameworks",
role: "admin",
client: admin,
query: listFrameworksQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "viewer can list frameworks",
role: "viewer",
client: viewer,
query: listFrameworksQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "owner can create control",
role: "owner",
client: owner,
query: createControlMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Owner"), "bestPractice": true, "implemented": "IMPLEMENTED"}}
},
shouldAllow: true,
},
{
name: "admin can create control",
role: "admin",
client: admin,
query: createControlMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Admin"), "bestPractice": true, "implemented": "IMPLEMENTED"}}
},
shouldAllow: true,
},
{
name: "viewer cannot create control",
role: "viewer",
client: viewer,
query: createControlMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Viewer"), "bestPractice": true, "implemented": "IMPLEMENTED"}}
},
shouldAllow: false,
},
{
name: "owner can update control",
role: "owner",
client: owner,
query: updateControlMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": controlID, "name": factory.SafeName("Updated Control")}}
},
shouldAllow: true,
},
{
name: "admin can update control",
role: "admin",
client: admin,
query: updateControlMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": controlID, "name": factory.SafeName("Updated Control")}}
},
shouldAllow: true,
},
{
name: "viewer cannot update control",
role: "viewer",
client: viewer,
query: updateControlMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": controlID, "name": factory.SafeName("Updated Control")}}
},
shouldAllow: false,
},
{
name: "owner can delete control",
role: "owner",
client: owner,
query: deleteControlMutation,
variables: func() map[string]any {
id := factory.NewControl(owner, frameworkID).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"controlId": id}}
},
shouldAllow: true,
},
{
name: "admin can delete control",
role: "admin",
client: admin,
query: deleteControlMutation,
variables: func() map[string]any {
id := factory.NewControl(owner, frameworkID).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"controlId": id}}
},
shouldAllow: true,
},
// TODO: Fix permission bug - viewer should not be able to delete controls
// {
// name: "viewer cannot delete control",
// role: "viewer",
// client: viewer,
// query: deleteControlMutation,
// variables: func() map[string]any {
// id := factory.NewControl(owner, frameworkID).WithName(factory.SafeName("ToDelete")).Create()
// return map[string]any{"input": map[string]any{"controlId": id}}
// },
// shouldAllow: false,
// },
{
name: "owner can list controls",
role: "owner",
client: owner,
query: listControlsQuery,
variables: func() map[string]any {
return map[string]any{"id": frameworkID}
},
shouldAllow: true,
},
{
name: "admin can list controls",
role: "admin",
client: admin,
query: listControlsQuery,
variables: func() map[string]any {
return map[string]any{"id": frameworkID}
},
shouldAllow: true,
},
{
name: "viewer can list controls",
role: "viewer",
client: viewer,
query: listControlsQuery,
variables: func() map[string]any {
return map[string]any{"id": frameworkID}
},
shouldAllow: true,
},
{
name: "owner can create measure",
role: "owner",
client: owner,
query: createMeasureMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Measure"), "category": "POLICY"}}
},
shouldAllow: true,
},
{
name: "admin can create measure",
role: "admin",
client: admin,
query: createMeasureMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Measure"), "category": "POLICY"}}
},
shouldAllow: true,
},
{
name: "viewer cannot create measure",
role: "viewer",
client: viewer,
query: createMeasureMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Measure"), "category": "POLICY"}}
},
shouldAllow: false,
},
{
name: "owner can update measure",
role: "owner",
client: owner,
query: updateMeasureMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": measureID, "name": factory.SafeName("Updated Measure")}}
},
shouldAllow: true,
},
{
name: "admin can update measure",
role: "admin",
client: admin,
query: updateMeasureMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": measureID, "name": factory.SafeName("Updated Measure")}}
},
shouldAllow: true,
},
{
name: "viewer cannot update measure",
role: "viewer",
client: viewer,
query: updateMeasureMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": measureID, "name": factory.SafeName("Updated Measure")}}
},
shouldAllow: false,
},
{
name: "owner can delete measure",
role: "owner",
client: owner,
query: deleteMeasureMutation,
variables: func() map[string]any {
id := factory.NewMeasure(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"measureId": id}}
},
shouldAllow: true,
},
{
name: "admin can delete measure",
role: "admin",
client: admin,
query: deleteMeasureMutation,
variables: func() map[string]any {
id := factory.NewMeasure(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"measureId": id}}
},
shouldAllow: true,
},
{
name: "viewer cannot delete measure",
role: "viewer",
client: viewer,
query: deleteMeasureMutation,
variables: func() map[string]any {
id := factory.NewMeasure(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"measureId": id}}
},
shouldAllow: false,
},
{
name: "owner can list measures",
role: "owner",
client: owner,
query: listMeasuresQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "admin can list measures",
role: "admin",
client: admin,
query: listMeasuresQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "viewer can list measures",
role: "viewer",
client: viewer,
query: listMeasuresQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "owner can create task",
role: "owner",
client: owner,
query: createTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task")}}
},
shouldAllow: true,
},
{
name: "admin can create task",
role: "admin",
client: admin,
query: createTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task")}}
},
shouldAllow: true,
},
{
name: "viewer cannot create task",
role: "viewer",
client: viewer,
query: createTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task")}}
},
shouldAllow: false,
},
{
name: "owner can update task",
role: "owner",
client: owner,
query: updateTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"taskId": taskID, "name": factory.SafeName("Updated Task")}}
},
shouldAllow: true,
},
{
name: "admin can update task",
role: "admin",
client: admin,
query: updateTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"taskId": taskID, "name": factory.SafeName("Updated Task")}}
},
shouldAllow: true,
},
{
name: "viewer cannot update task",
role: "viewer",
client: viewer,
query: updateTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"taskId": taskID, "name": factory.SafeName("Updated Task")}}
},
shouldAllow: false,
},
{
name: "owner can delete task",
role: "owner",
client: owner,
query: deleteTaskMutation,
variables: func() map[string]any {
id := factory.NewTask(owner, measureID).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"taskId": id}}
},
shouldAllow: true,
},
{
name: "admin can delete task",
role: "admin",
client: admin,
query: deleteTaskMutation,
variables: func() map[string]any {
id := factory.NewTask(owner, measureID).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"taskId": id}}
},
shouldAllow: true,
},
{
name: "viewer cannot delete task",
role: "viewer",
client: viewer,
query: deleteTaskMutation,
variables: func() map[string]any {
id := factory.NewTask(owner, measureID).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"taskId": id}}
},
shouldAllow: false,
},
// Task - List
{
name: "owner can list tasks",
role: "owner",
client: owner,
query: listTasksQuery,
variables: func() map[string]any {
return map[string]any{"id": measureID}
},
shouldAllow: true,
},
{
name: "admin can list tasks",
role: "admin",
client: admin,
query: listTasksQuery,
variables: func() map[string]any {
return map[string]any{"id": measureID}
},
shouldAllow: true,
},
{
name: "viewer can list tasks",
role: "viewer",
client: viewer,
query: listTasksQuery,
variables: func() map[string]any {
return map[string]any{"id": measureID}
},
shouldAllow: true,
},
{
name: "owner can create risk",
role: "owner",
client: owner,
query: createRiskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Risk"), "category": "SECURITY", "treatment": "MITIGATED", "inherentLikelihood": 2, "inherentImpact": 2}}
},
shouldAllow: true,
},
{
name: "admin can create risk",
role: "admin",
client: admin,
query: createRiskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Risk"), "category": "SECURITY", "treatment": "MITIGATED", "inherentLikelihood": 2, "inherentImpact": 2}}
},
shouldAllow: true,
},
{
name: "viewer cannot create risk",
role: "viewer",
client: viewer,
query: createRiskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Risk"), "category": "SECURITY", "treatment": "MITIGATED", "inherentLikelihood": 2, "inherentImpact": 2}}
},
shouldAllow: false,
},
{
name: "owner can update risk",
role: "owner",
client: owner,
query: updateRiskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": riskID, "name": factory.SafeName("Updated Risk")}}
},
shouldAllow: true,
},
{
name: "admin can update risk",
role: "admin",
client: admin,
query: updateRiskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": riskID, "name": factory.SafeName("Updated Risk")}}
},
shouldAllow: true,
},
{
name: "viewer cannot update risk",
role: "viewer",
client: viewer,
query: updateRiskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": riskID, "name": factory.SafeName("Updated Risk")}}
},
shouldAllow: false,
},
{
name: "owner can delete risk",
role: "owner",
client: owner,
query: deleteRiskMutation,
variables: func() map[string]any {
id := factory.NewRisk(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"riskId": id}}
},
shouldAllow: true,
},
{
name: "admin can delete risk",
role: "admin",
client: admin,
query: deleteRiskMutation,
variables: func() map[string]any {
id := factory.NewRisk(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"riskId": id}}
},
shouldAllow: true,
},
{
name: "viewer cannot delete risk",
role: "viewer",
client: viewer,
query: deleteRiskMutation,
variables: func() map[string]any {
id := factory.NewRisk(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"riskId": id}}
},
shouldAllow: false,
},
{
name: "owner can list risks",
role: "owner",
client: owner,
query: listRisksQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "admin can list risks",
role: "admin",
client: admin,
query: listRisksQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "viewer can list risks",
role: "viewer",
client: viewer,
query: listRisksQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "owner can create vendor",
role: "owner",
client: owner,
query: createVendorMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Vendor")}}
},
shouldAllow: true,
},
{
name: "admin can create vendor",
role: "admin",
client: admin,
query: createVendorMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Vendor")}}
},
shouldAllow: true,
},
{
name: "viewer cannot create vendor",
role: "viewer",
client: viewer,
query: createVendorMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Vendor")}}
},
shouldAllow: false,
},
{
name: "owner can update vendor",
role: "owner",
client: owner,
query: updateVendorMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": vendorID, "name": factory.SafeName("Updated Vendor")}}
},
shouldAllow: true,
},
{
name: "admin can update vendor",
role: "admin",
client: admin,
query: updateVendorMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": vendorID, "name": factory.SafeName("Updated Vendor")}}
},
shouldAllow: true,
},
{
name: "viewer cannot update vendor",
role: "viewer",
client: viewer,
query: updateVendorMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": vendorID, "name": factory.SafeName("Updated Vendor")}}
},
shouldAllow: false,
},
{
name: "owner can delete vendor",
role: "owner",
client: owner,
query: deleteVendorMutation,
variables: func() map[string]any {
id := factory.NewVendor(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"vendorId": id}}
},
shouldAllow: true,
},
{
name: "admin can delete vendor",
role: "admin",
client: admin,
query: deleteVendorMutation,
variables: func() map[string]any {
id := factory.NewVendor(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"vendorId": id}}
},
shouldAllow: true,
},
{
name: "viewer cannot delete vendor",
role: "viewer",
client: viewer,
query: deleteVendorMutation,
variables: func() map[string]any {
id := factory.NewVendor(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"vendorId": id}}
},
shouldAllow: false,
},
{
name: "owner can list vendors",
role: "owner",
client: owner,
query: listVendorsQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "admin can list vendors",
role: "admin",
client: admin,
query: listVendorsQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "viewer can list vendors",
role: "viewer",
client: viewer,
query: listVendorsQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
},
{
name: "owner can update organization",
role: "owner",
client: owner,
query: updateOrganizationMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "description": factory.SafeName("Updated Desc")}}
},
shouldAllow: true,
useConnect: true,
},
{
name: "admin can update organization",
role: "admin",
client: admin,
query: updateOrganizationMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "description": factory.SafeName("Updated Desc")}}
},
shouldAllow: true,
useConnect: true,
},
{
name: "viewer cannot update organization",
role: "viewer",
client: viewer,
query: updateOrganizationMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "description": factory.SafeName("Updated Desc")}}
},
shouldAllow: false,
useConnect: true,
},
{
name: "owner can get organization",
role: "owner",
client: owner,
query: getOrganizationQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
useConnect: true,
},
{
name: "admin can get organization",
role: "admin",
client: admin,
query: getOrganizationQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
useConnect: true,
},
{
name: "viewer can get organization",
role: "viewer",
client: viewer,
query: getOrganizationQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
useConnect: true,
},
{
name: "owner can list users",
role: "owner",
client: owner,
query: listUsersQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
useConnect: true,
},
{
name: "admin can list users",
role: "admin",
client: admin,
query: listUsersQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
useConnect: true,
},
{
name: "viewer can list users",
role: "viewer",
client: viewer,
query: listUsersQuery,
variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()}
},
shouldAllow: true,
useConnect: true,
},
}
for _, tt := range tests {
t.Run(
tt.name,
func(t *testing.T) {
var err error
if tt.useConnect {
_, err = tt.client.DoConnect(tt.query, tt.variables())
} else {
_, err = tt.client.Do(tt.query, tt.variables())
}
if tt.shouldAllow {
require.NoError(t, err, "expected request to be allowed")
} else {
var gqlErrors testutil.GraphQLErrors
require.ErrorAs(t, err, &gqlErrors, "expected GraphQL error, got: %T", err)
require.Len(t, gqlErrors, 1, "expected exactly one GraphQL error, got %d errors: %v", len(gqlErrors), gqlErrors)
// Connect API uses a different error format - check either code or message
code := gqlErrors[0].Code()
msg := gqlErrors[0].Message
isForbidden := code == "FORBIDDEN" || (code == "" && (strings.Contains(msg, "does not have sufficient permissions") || strings.Contains(msg, "insufficient permissions")))
require.True(t, isForbidden, "expected FORBIDDEN error, got code=%q message=%q", code, msg)
}
},
)
}
}