package rbac_test import ( "context" "errors" "fmt" "testing" "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/incidents" "dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/rbac/policy" "dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/users" "github.com/el-mike/restrict/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRBAC(t *testing.T) { tests := []struct { name string subject entities.Subject resource entities.Resource action string expectErr error }{ { name: "admin update talkgroup", subject: &users.User{ ID: 2, IsAdmin: true, }, resource: &talkgroups.Talkgroup{}, action: entities.ActionUpdate, expectErr: nil, }, { name: "admin update incident", subject: &users.User{ ID: 2, IsAdmin: true, }, resource: &incidents.Incident{ Name: "test incident", Owner: 4, }, action: entities.ActionUpdate, expectErr: nil, }, { name: "user update incident not owner", subject: &users.User{ ID: 2, }, resource: &incidents.Incident{ Name: "test incident", Owner: 4, }, action: entities.ActionUpdate, expectErr: errors.New(`access denied for Action: "update" on Resource: "Incident"`), }, { name: "user update incident owner", subject: &users.User{ ID: 2, }, resource: &incidents.Incident{ Name: "test incident", Owner: 2, }, action: entities.ActionUpdate, expectErr: nil, }, { name: "user delete incident not owner", subject: &users.User{ ID: 2, }, resource: &incidents.Incident{ Name: "test incident", Owner: 6, }, action: entities.ActionDelete, expectErr: errors.New(`access denied for Action: "delete" on Resource: "Incident"`), }, { name: "admin update call", subject: &users.User{ ID: 2, IsAdmin: true, }, resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(4)), }, action: entities.ActionUpdate, expectErr: nil, }, { name: "user update call not owner", subject: &users.User{ ID: 2, }, resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(4)), }, action: entities.ActionUpdate, expectErr: errors.New(`access denied for Action: "update" on Resource: "Call"`), }, { name: "user update call owner", subject: &users.User{ ID: 2, }, resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(2)), }, action: entities.ActionUpdate, expectErr: nil, }, { name: "user update call nil submitter", subject: &users.User{ ID: 2, }, resource: &calls.Call{ Submitter: nil, }, action: entities.ActionUpdate, expectErr: errors.New(`access denied for Action: "update" on Resource: "Call"`), }, { name: "user delete call not owner", subject: &users.User{ ID: 2, }, resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(6)), }, action: entities.ActionDelete, expectErr: errors.New(`access denied for Action: "delete" on Resource: "Call"`), }, { name: "user share call not submitter", subject: &users.User{ ID: 2, }, resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(6)), }, action: entities.ActionShare, expectErr: nil, }, { name: "user share call admin", subject: &users.User{ ID: 2, IsAdmin: true, }, resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(6)), }, action: entities.ActionShare, expectErr: nil, }, { name: "user share call submitter", subject: &users.User{ ID: 6, }, resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(6)), }, action: entities.ActionShare, expectErr: nil, }, { name: "user share incident not owner", subject: &users.User{ ID: 2, }, resource: &incidents.Incident{ Owner: users.UserID(6), }, action: entities.ActionShare, expectErr: errors.New(`access denied for Action: "share" on Resource: "Incident"`), }, { name: "user share incident admin", subject: &users.User{ ID: 2, IsAdmin: true, }, resource: &incidents.Incident{ Owner: users.UserID(6), }, action: entities.ActionShare, expectErr: nil, }, { name: "user share incident owner", subject: &users.User{ ID: 6, }, resource: &incidents.Incident{ Owner: users.UserID(6), }, action: entities.ActionShare, expectErr: nil, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { ctx := entities.CtxWithSubject(context.Background(), tc.subject) rb, err := rbac.New(policy.Policy) require.NoError(t, err) sub, err := rb.Check(ctx, tc.resource, rbac.WithActions(tc.action)) if tc.expectErr != nil { assert.Equal(t, tc.expectErr.Error(), err.Error()) } else { if !assert.NoError(t, err) { accErr(err) } } assert.Equal(t, tc.subject, sub) }) } } func accErr(err error) { if accessError, ok := err.(*restrict.AccessDeniedError); ok { // Error() implementation. Returns a message in a form: "access denied for Action/s: ... on Resource: ..." fmt.Println(accessError) // Returns an AccessRequest that failed. fmt.Println(accessError.Request) // Returns first reason for the denied access. // Especially helpful in fail-early mode, where there will only be one Reason. fmt.Println(accessError.FirstReason()) // Reasons property will hold all errors that caused the access to be denied. for _, permissionErr := range accessError.Reasons { fmt.Println(permissionErr) fmt.Println(permissionErr.Action) fmt.Println(permissionErr.RoleName) fmt.Println(permissionErr.ResourceName) // Returns first ConditionNotSatisfied error for given PermissionError, if any was returned for given PermissionError. // Especially helpful in fail-early mode, where there will only be one failed Condition. fmt.Println(permissionErr.FirstConditionError()) // ConditionErrors property will hold all ConditionNotSatisfied errors. for _, conditionErr := range permissionErr.ConditionErrors { fmt.Println(conditionErr) fmt.Println(conditionErr.Reason) // Every ConditionNotSatisfied contains an instance of Condition that returned it, // so it can be tested using type assertion to get more details about failed Condition. if emptyCondition, ok := conditionErr.Condition.(*restrict.EmptyCondition); ok { fmt.Println(emptyCondition.ID) } } } } }