Permissions
The Permission system in Comby provides a flexible and extensible authorization framework for controlling access to commands, queries, runtime operations, and store operations. It implements a role-based access control (RBAC) system with support for custom authorization logic when needed.
Permission System Overview
Permissions are automatically registered by the framework for all commands and queries. The default RBAC system is sufficient for most use cases, where permissions are assigned to groups and identities inherit permissions through their group memberships. However, domains can override these default permissions with custom authorization logic when needed.
Key Concepts
- Automatic Registration: All commands and queries automatically get permission entries
- RBAC by Default: The standard role-based access control system handles most authorization needs
- Custom Overrides: Domains can provide custom permission functions for special cases
- Group-based Authorization: Permissions are assigned to groups, and identities inherit permissions through group membership
Permission Hierarchy
Comby implements three levels of permission requirements, forming a hierarchy from least to most restrictive:
1. Anonymous
No authentication required. Operations are completely public and accessible to anyone.
Required context: None Use cases: Public endpoints, health checks, public documentation
2. Authenticated
User must be logged in with a valid account and session, but does not need an active identity.
Required context: Account UUID + Session UUID Use cases: Account management, profile operations, listing available tenants
3. Authorized
User must be logged in AND have an active identity selected within a tenant context.
Required context: Tenant UUID + Identity UUID + Account UUID + Session UUID Use cases: Business operations within a tenant, resource management
Identity, Tenant, and Group Model
Understanding the relationship between these entities is crucial for implementing permissions correctly:
- An Identity can have multiple Groups assigned
- Each Group belongs to exactly one Tenant
- Each Group can have multiple Permissions assigned
- Authorized operations require: tenant + identity + account + session context
Permission Types
The permission system supports four types of operations:
Command Permissions
Control access to state-changing operations (write side).
// Permission factory functions
NewPermissionCmd(domain, commandData, description)
NewPermissionCmdFunc(domain, commandData, description, fn)Query Permissions
Control access to data retrieval operations (read side).
// Permission factory functions
NewPermissionQry(domain, queryData, description)
NewPermissionQryFunc(domain, queryData, description, fn)Runtime Permissions
Control access to runtime-level operations.
// Permission factory functions
NewPermissionRuntime(domain, runtimeType, description)
NewPermissionRuntimeFunc(domain, runtimeType, description, fn)Store Permissions
Control access to store-level operations.
// Permission factory functions
NewPermissionStore(domain, storeType, description)
NewPermissionStoreFunc(domain, storeType, description, fn)Default RBAC Behavior
For most domains, the default RBAC system is sufficient. Commands and queries are automatically assigned permissions that work with the group-based authorization system:
- System-admin group members can perform any operation
- Users with an identity belonging to a group that has the specific permission enabled can perform the operation
- The framework handles all permission checking automatically
Registering Permissions
Normal Commands and Queries
For standard business logic that uses the default RBAC system, you typically don't need to explicitly register permissions. The framework handles this automatically. However, you can register permissions to provide human-readable descriptions:
package order
import (
"context"
"github.com/gradientzero/comby/v2"
"github.com/gradientzero/comby/v2/domain/order/aggregate"
"github.com/gradientzero/comby/v2/domain/order/command"
"github.com/gradientzero/comby/v2/domain/order/query"
"github.com/gradientzero/comby/v2/domain/runtime"
)
func Register(ctx context.Context, fc *comby.Facade) error {
var domain = aggregate.NewAggregate().GetDomain()
// Register aggregate, handlers, etc.
comby.RegisterAggregate(fc, aggregate.NewAggregate)
// ... other registrations ...
// Register permissions with descriptions
runtime.RegisterPermission(
// Command permissions
runtime.NewPermissionCmd(
domain,
&command.CreateOrder{},
"Create a new order",
),
runtime.NewPermissionCmd(
domain,
&command.UpdateOrder{},
"Update an existing order",
),
runtime.NewPermissionCmd(
domain,
&command.CancelOrder{},
"Cancel an order",
),
// Query permissions
runtime.NewPermissionQry(
domain,
&query.GetOrderById{},
"Retrieve order by ID",
),
runtime.NewPermissionQry(
domain,
&query.ListOrders{},
"List all orders",
),
)
return nil
}These permissions will use the default RBAC system:
- System admins can perform these operations
- Users with identities belonging to groups that have these permissions enabled can perform them
- Authorization is handled automatically by the framework
Custom Permission Functions
When you need custom authorization logic beyond the standard RBAC system, use the Func variants. This is common for domains like account, group, and identity that require special permission checks.
Example: Identity Domain with Custom Permissions
package identity
import (
"context"
"fmt"
"github.com/gradientzero/comby/v2"
"github.com/gradientzero/comby/v2/domain/identity/aggregate"
"github.com/gradientzero/comby/v2/domain/identity/command"
"github.com/gradientzero/comby/v2/domain/runtime"
)
func Register(ctx context.Context, fc *comby.Facade) error {
var domain = aggregate.NewAggregate().GetDomain()
// Register aggregate, handlers, etc.
comby.RegisterAggregate(fc, aggregate.NewAggregate)
// ... other registrations ...
// Register custom permissions with validation functions
runtime.RegisterPermission(
runtime.NewPermissionCmdFunc(
domain,
&command.IdentityCommandAddToken{},
"Add token for any other identity",
customPermissionSameIdentity,
),
runtime.NewPermissionCmdFunc(
domain,
&command.IdentityCommandRemoveToken{},
"Remove token of any other identity",
customPermissionSameIdentity,
),
runtime.NewPermissionCmdFunc(
domain,
&command.IdentityCommandUpdateProfile{},
"Update profile of any other identity",
customPermissionSameIdentity,
),
)
return nil
}
// Custom permission function that checks if user is modifying their own identity
var customPermissionSameIdentity runtime.PermissionCommandFunc = func(
ctx context.Context,
cmd comby.Command,
) error {
// This custom function allows:
// - identities belonging to system-admin group
// - identities belonging to a group with this permission enabled
// - users modifying their own identity (checked below)
// Extract the target identity UUID from the command
var targetIdentityUuid string
switch c := cmd.GetDomainCmd().(type) {
case *command.IdentityCommandAddToken:
targetIdentityUuid = c.IdentityUuid
case *command.IdentityCommandRemoveToken:
targetIdentityUuid = c.IdentityUuid
case *command.IdentityCommandUpdateProfile:
targetIdentityUuid = c.IdentityUuid
default:
return fmt.Errorf("%s could not cast command to concrete type", cmd)
}
// Get request context
var reqCtx *comby.RequestContext = cmd.GetReqCtx()
// Allow users to modify their own identity
// Disallow modifying other identities (unless they have the permission via RBAC)
if reqCtx.SenderIdentityUuid != targetIdentityUuid {
return runtime.ErrPermissionDenied
}
// Allow operation
return nil
}Example: Account Domain with Mixed Permissions
package account
import (
"context"
"github.com/gradientzero/comby/v2"
"github.com/gradientzero/comby/v2/domain/account/aggregate"
"github.com/gradientzero/comby/v2/domain/account/command"
"github.com/gradientzero/comby/v2/domain/runtime"
)
func Register(ctx context.Context, fc *comby.Facade) error {
var domain = aggregate.NewAggregate().GetDomain()
comby.RegisterAggregate(fc, aggregate.NewAggregate)
// ... other registrations ...
runtime.RegisterPermission(
// Standard RBAC permission
runtime.NewPermissionCmd(
domain,
&command.AccountCommandActivate{},
"Activate an existing account",
),
// Custom permission with specific authorization logic
runtime.NewPermissionCmdFunc(
domain,
&command.AccountCommandPasswordReset{},
"Reset password of an existing account",
func(ctx context.Context, cmd comby.Command) error {
// Following can perform this operation:
// - identity belonging to system-admin group
// - identity belonging to a group with this permission enabled
// Add custom logic here if needed
// For example, check if user is resetting their own password
return runtime.AuthorizedCmdFn(ctx, cmd)
},
),
)
return nil
}Using Built-in Permission Functions
Comby provides pre-built permission functions for each hierarchy level:
// Command permission functions
runtime.AnonymousCmdFn // No authentication required
runtime.AuthenticatedCmdFn // Requires account + session
runtime.AuthorizedCmdFn // Requires tenant + identity + account + session
// Query permission functions
runtime.AnonymousQryFn
runtime.AuthenticatedQryFn
runtime.AuthorizedQryFn
// Runtime permission functions
runtime.AnonymousRuntimeFn
runtime.AuthenticatedRuntimeFn
runtime.AuthorizedRuntimeFnExample using built-in functions:
runtime.RegisterPermission(
// Public command, no authentication needed
runtime.NewPermissionCmdFunc(
domain,
&command.PublicCommand{},
"Public operation",
runtime.AnonymousCmdFn,
),
// Requires authentication only
runtime.NewPermissionCmdFunc(
domain,
&command.UserCommand{},
"User operation",
runtime.AuthenticatedCmdFn,
),
// Requires full authorization
runtime.NewPermissionCmdFunc(
domain,
&command.TenantCommand{},
"Tenant operation",
runtime.AuthorizedCmdFn,
),
)Permissions for Projections
Projections are read-only views built from aggregates. They use the standard query permission system since they serve data in response to queries.
Example: Order Projection with Permissions
package order
import (
"context"
"github.com/gradientzero/comby/v2"
"github.com/gradientzero/comby/v2/domain/order/aggregate"
"github.com/gradientzero/comby/v2/domain/order/query"
"github.com/gradientzero/comby/v2/domain/runtime"
)
func Register(ctx context.Context, fc *comby.Facade) error {
var domain = aggregate.NewAggregate().GetDomain()
// Register aggregate
comby.RegisterAggregate(fc, aggregate.NewAggregate)
// Create and register projection
projRm := comby.NewProjectionAggregate(
fc,
aggregate.NewAggregate,
)
comby.RegisterEventHandler(fc, projRm)
// Register query handler for the projection
projQh := comby.NewProjectionQueryHandler(projRm)
comby.RegisterQueryHandler(fc, projQh)
// Register permissions for projection queries
runtime.RegisterPermission(
// Standard projection queries use normal query permissions
runtime.NewPermissionQry(
domain,
&comby.ProjectionQueryGetRequest[*aggregate.Order]{},
"Retrieve order projection by ID",
),
runtime.NewPermissionQry(
domain,
&comby.ProjectionQueryListRequest[*aggregate.Order]{},
"List all order projections",
),
// You can also use custom functions if needed
runtime.NewPermissionQryFunc(
domain,
&query.ProjectionQueryGetSensitive{},
"Retrieve sensitive order data",
func(ctx context.Context, qry comby.Query) error {
// Custom logic for sensitive data access
// E.g., only allow certain roles or groups
return runtime.AuthorizedQryFn(ctx, qry)
},
),
)
return nil
}Projection queries work exactly like regular queries from a permission perspective:
- They can use default RBAC (most common)
- They can use custom permission functions when needed
- They follow the same three-level hierarchy (Anonymous, Authenticated, Authorized)
Permission Errors
The permission system uses two main error types:
// User is not authenticated (no valid session)
runtime.ErrUnauthorized
// User is authenticated but doesn't have permission
runtime.ErrPermissionDeniedUse these errors in custom permission functions to properly communicate authorization failures:
var customPermission runtime.PermissionCommandFunc = func(
ctx context.Context,
cmd comby.Command,
) error {
reqCtx := cmd.GetReqCtx()
// Check authentication first
if reqCtx.SenderAccountUuid == "" {
return runtime.ErrUnauthorized
}
// Check authorization
if !hasPermission(reqCtx) {
return runtime.ErrPermissionDenied
}
return nil
}Best Practices
Use Default RBAC When Possible: Most domains don't need custom permission logic. Let the framework handle authorization through groups.
Provide Clear Descriptions: Always include human-readable descriptions when registering permissions. These help administrators understand what permissions control.
Keep Permission Logic Simple: Custom permission functions should be straightforward and easy to understand. Complex authorization logic is a sign that your domain model may need adjustment.
Document Custom Permissions: When you do use custom permission functions, document why they're needed and what special logic they implement.
Test Permission Functions: Write unit tests for custom permission functions, especially those with complex logic.
Consider the Hierarchy: Choose the appropriate permission level (Anonymous, Authenticated, Authorized) based on the actual requirements of your operation.
Summary
The Comby permission system provides:
- Automatic registration of permissions for all commands and queries
- Default RBAC handling for most use cases
- Custom permission functions for special authorization requirements
- Three-level hierarchy (Anonymous, Authenticated, Authorized)
- Four operation types (Command, Query, Runtime, Store)
- Group-based authorization through identity-group-permission relationships
For most domains, simply registering your commands and queries is sufficient. The framework handles permissions automatically through the RBAC system. Use custom permission functions only when you need authorization logic beyond standard role-based access control.