Skip to content

Authorization

SLOzy implements Role-Based Access Control (RBAC) with fine-grained permissions at organization, team, and resource levels.

RBAC Architecture

The RBAC system is built on three layers:

  1. Roles — Named collections of permissions (admin, manager, developer, viewer)
  2. User Roles — Assignments linking users to roles within organizations (optionally scoped to teams)
  3. Resource Permissions — Fine-grained access control for specific resources

All RBAC logic lives in internal/rbac/service.go and is enforced by internal/middleware/rbac.go.

Permission Check Flow

go
func (s *RBACService) CheckPermission(ctx context.Context, userID, organizationID int, permission string) (bool, error) {
    var hasPermission bool
    query := `SELECT check_permission($1, $2, $3)`
    err := s.db.QueryRow(ctx, query, userID, organizationID, permission).Scan(&hasPermission)
    return hasPermission, nil
}

The check_permission() PostgreSQL function (created in migration 000015_rbac_system) efficiently validates access by checking roles and direct resource permissions.

Roles and Permissions

Four default roles are seeded in migration 000015_rbac_system:

RolePermissions
adminread, write, delete, manage_users, manage_teams, manage_organizations, view_analytics, export_data
managerread, write, manage_teams, view_analytics, export_data
developerread, write, view_analytics
viewerread, view_analytics

Custom roles can be created via the API:

json
{
  "name": "custom_role",
  "description": "Custom role description",
  "permissions": ["read", "write", "view_analytics"]
}

Role Assignment

Assign a role to a user within an organization:

go
func (s *RBACService) AssignRole(ctx context.Context, req *models.AssignRoleRequest) (*models.UserRole, error) {
    query := `INSERT INTO user_roles (user_id, organization_id, role_id, team_id, assigned_at, expires_at)
    VALUES ($1, $2, $3, $4, NOW(), $5)
    ON CONFLICT (user_id, organization_id, role_id, team_id) DO UPDATE SET
    expires_at = EXCLUDED.expires_at
    RETURNING id, user_id, organization_id, role_id, team_id, assigned_at, expires_at`
    // ...
}

Role assignments can include:

  • Organization scope — Role applies across the entire organization
  • Team scope — Role is limited to a specific team (optional team_id)
  • Time-bound — Role can have an expiration date (optional expires_at)

Resource-Level Access

For fine-grained control, resource permissions allow access to specific resources:

go
type ResourcePermission struct {
    ResourceType  string   // 'slo', 'team', 'organization', 'user'
    ResourceID    int
    UserID        *int
    RoleID        *int
    Permissions   []string
    ExpiresAt     *time.Time
}

This enables scenarios like "User A can edit SLO #42 but not SLO #43" even within the same team.

API Keys

Programmatic access uses API keys stored in the api_keys table:

sql
CREATE TABLE api_keys (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL REFERENCES users(id),
    organization_id INT NOT NULL REFERENCES organizations(id),
    key_hash VARCHAR(255) UNIQUE NOT NULL,
    key_prefix VARCHAR(10) NOT NULL,
    name VARCHAR(255) NOT NULL,
    scopes TEXT[] NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE,
    is_active BOOLEAN DEFAULT TRUE
);

API Key Features

  • Hashed storage — Keys are hashed before storage; the plaintext key is shown only once at creation
  • Key prefix — Human-readable prefix for identification (e.g., slo_abc...)
  • Scoped permissions — Each API key can have a subset of available permissions
  • Expiration — Keys can be time-limited
  • Active/inactive — Keys can be deactivated without deletion
  • Last used tracking — Monitor API key usage via last_used_at

Creating an API Key

POST /api/v1/api-keys
json
{
  "name": "CI Pipeline Key",
  "scopes": ["read", "write"]
}

Middleware Enforcement

Routes are protected using middleware chains in internal/middleware/rbac.go:

go
// Require specific permission
mux.Handle("/api/v1/slos", rbacMiddleware.RequirePermission("write")(handler))

// Require resource-specific permission
mux.Handle("/api/v1/slos/:id", rbacMiddleware.RequireResourcePermission("slo", extractSLOID, "write")(handler))

Getting User Roles

Query roles for the current user:

GET /api/v1/roles