diff --git a/pkg/iam/errors.go b/pkg/iam/errors.go index a797e9eee..571e18c76 100644 --- a/pkg/iam/errors.go +++ b/pkg/iam/errors.go @@ -193,6 +193,64 @@ func (e ErrInsufficientPermissions) Error() string { return fmt.Sprintf("identity %q does not have sufficient permissions to perform action %s on entity %q", e.IdentityID, e.Action, e.EntityID) } +type ErrMixedOrganizationBatch struct { + Action Action + OrganizationIDs []string +} + +func NewMixedOrganizationBatchError(action Action, organizationIDs []string) error { + return &ErrMixedOrganizationBatch{Action: action, OrganizationIDs: organizationIDs} +} + +func (e ErrMixedOrganizationBatch) Error() string { + return fmt.Sprintf( + "cannot authorize batch action %s across organization ids %q", + e.Action, + e.OrganizationIDs, + ) +} + +type ErrMixedEntityTypeBatch struct { + Action Action + EntityTypes []uint16 +} + +func NewMixedEntityTypeBatchError(action Action, entityTypes []uint16) error { + return &ErrMixedEntityTypeBatch{Action: action, EntityTypes: entityTypes} +} + +func (e ErrMixedEntityTypeBatch) Error() string { + return fmt.Sprintf( + "cannot authorize batch action %s across entity types %v", + e.Action, + e.EntityTypes, + ) +} + +type ErrEmptyResourceBatch struct { + Action Action +} + +func NewEmptyResourceBatchError(action Action) error { + return &ErrEmptyResourceBatch{Action: action} +} + +func (e ErrEmptyResourceBatch) Error() string { + return fmt.Sprintf("cannot authorize batch action %s with an empty resource set", e.Action) +} + +type ErrBatchAuthorizationUnsupportedResourceType struct { + EntityType uint16 +} + +func NewBatchAuthorizationUnsupportedResourceTypeError(entityType uint16) error { + return &ErrBatchAuthorizationUnsupportedResourceType{EntityType: entityType} +} + +func (e ErrBatchAuthorizationUnsupportedResourceType) Error() string { + return fmt.Sprintf("resource type %d does not support batch authorization attributes", e.EntityType) +} + type ErrAssumptionRequired struct { IdentityID gid.GID MembershipID gid.GID diff --git a/pkg/iam/errors_test.go b/pkg/iam/errors_test.go new file mode 100644 index 000000000..76f1c2f2d --- /dev/null +++ b/pkg/iam/errors_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 iam_test + +import ( + "fmt" + "testing" + + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/iam" +) + +func TestAuthorizeRelatedErrors_Error(t *testing.T) { + t.Parallel() + + tenantID := gid.NewTenantID() + identityID := gid.New(gid.NilTenant, coredata.IdentityEntityType) + resourceID := gid.New(tenantID, coredata.FrameworkEntityType) + membershipID := gid.New(tenantID, coredata.MembershipEntityType) + sessionID := gid.New(gid.NilTenant, coredata.SessionEntityType) + action := iam.Action("core:framework:get") + mixedOrgIDs := []string{ + gid.New(tenantID, coredata.OrganizationEntityType).String(), + gid.New(tenantID, coredata.OrganizationEntityType).String(), + } + mixedEntityTypes := []uint16{coredata.OrganizationEntityType, coredata.FrameworkEntityType} + + tests := []struct { + name string + err error + want string + }{ + { + name: "insufficient permissions", + err: iam.NewInsufficientPermissionsError(identityID, resourceID, action), + want: fmt.Sprintf( + "identity %q does not have sufficient permissions to perform action %s on entity %q", + identityID, + action, + resourceID, + ), + }, + { + name: "unsupported principal type", + err: iam.NewUnsupportedPrincipalTypeError(coredata.OrganizationEntityType), + want: fmt.Sprintf("unsupported principal type: %d", coredata.OrganizationEntityType), + }, + { + name: "empty resource batch", + err: iam.NewEmptyResourceBatchError(action), + want: fmt.Sprintf("cannot authorize batch action %s with an empty resource set", action), + }, + { + name: "mixed entity type batch", + err: iam.NewMixedEntityTypeBatchError(action, mixedEntityTypes), + want: fmt.Sprintf("cannot authorize batch action %s across entity types %v", action, mixedEntityTypes), + }, + { + name: "mixed organization batch", + err: iam.NewMixedOrganizationBatchError(action, mixedOrgIDs), + want: fmt.Sprintf("cannot authorize batch action %s across organization ids %q", action, mixedOrgIDs), + }, + { + name: "batch unsupported resource type", + err: iam.NewBatchAuthorizationUnsupportedResourceTypeError(coredata.OAuth2AccessTokenEntityType), + want: fmt.Sprintf( + "resource type %d does not support batch authorization attributes", + coredata.OAuth2AccessTokenEntityType, + ), + }, + { + name: "assumption required", + err: iam.NewAssumptionRequiredError(identityID, membershipID), + want: fmt.Sprintf("assumption for identity %q required for membership %q", identityID, membershipID), + }, + { + name: "session not found with nil id", + err: iam.NewSessionNotFoundError(gid.Nil), + want: "session not found", + }, + { + name: "session not found with specific id", + err: iam.NewSessionNotFoundError(sessionID), + want: fmt.Sprintf("session %q not found", sessionID), + }, + { + name: "session expired", + err: iam.NewSessionExpiredError(sessionID), + want: fmt.Sprintf("session %q expired", sessionID), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if tt.err.Error() != tt.want { + t.Errorf("Error() = %q, want %q", tt.err.Error(), tt.want) + } + }) + } +}