Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ CloudPAM is a modern IPAM solution designed for hybrid and multi-cloud environme
- **Schema Wizard** - Design IP schemas with conflict detection before deploying
- **AI Planning** - Optional OpenAI-compatible conversational planner with SSE streaming and plan apply
- **CIDR Search** - Unified search with containment queries across pools and accounts
- **Auth & RBAC** - Local users, session cookies, API keys, and optional OIDC/SSO
- **Auth & RBAC** - Local users, session cookies, API keys, custom roles, and optional OIDC/SSO
- **Release Notes & Updates** - Embedded changelog plus release-check and host-managed upgrade endpoints
- **Audit Logging** - Full activity tracking with filterable event log
- **Observability** - Structured logging (slog), Prometheus metrics, Sentry integration
Expand Down Expand Up @@ -60,7 +60,7 @@ See the [Deployment Guide](docs/DEPLOYMENT.md) for production setup.
| **Database** | PostgreSQL 15+ (production) / SQLite (development) / In-memory (demo) |
| **Frontend** | React 18 + Vite + TypeScript + Tailwind CSS |
| **API** | OpenAPI 3.1 |
| **Auth** | Local sessions + API keys + OIDC + RBAC |
| **Auth** | Local sessions + API keys + OIDC + custom-role RBAC |
| **Logging** | slog (Go std lib) |
| **Metrics** | Prometheus |
| **Error Tracking** | Sentry (backend + frontend) |
Expand Down
7 changes: 7 additions & 0 deletions cmd/cloudpam/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,15 @@ func main() {
auditLogger := selectAuditLogger(logger)
keyStore := selectKeyStore(logger)
userStore := selectUserStore(logger)
roleStore := selectRoleStore(logger, userStore)
auth.SetRoleStoreProvider(roleStore)
sessionStore := selectSessionStore(logger)
srv := api.NewServer(mux, store, logger, metrics, auditLogger)
srv.SetAppVersion(version)

// Check if this is a fresh install (no users exist) for first-boot setup.
srv.SetUserStore(userStore)
srv.SetRoleStore(roleStore)
existingUsers, _ := userStore.List(context.Background())
if len(existingUsers) == 0 {
srv.SetNeedsSetup(true)
Expand Down Expand Up @@ -239,12 +242,15 @@ func main() {
authSrv.SetSettingsStore(settingsStore)
authSrv.RegisterProtectedAuthRoutes(logger.Slog())
userSrv := api.NewUserServer(srv, keyStore, userStore, sessionStore, auditLogger)
userSrv.SetRoleStore(roleStore)
loginRL := api.LoginRateLimitMiddleware(api.LoginRateLimitConfig{
AttemptsPerMinute: 5,
ProxyConfig: proxyConfig,
})
userSrv.RegisterProtectedUserRoutes(logger.Slog(), api.WithLoginRateLimit(loginRL))
dualMW := api.DualAuthMiddleware(keyStore, sessionStore, userStore, true, logger.Slog())
roleSrv := api.NewRoleServer(srv, roleStore)
roleSrv.RegisterProtectedRoleRoutes(dualMW, logger.Slog())
discoverySrv.RegisterProtectedDiscoveryRoutes(dualMW, logger.Slog())
analysisSrv.RegisterProtectedAnalysisRoutes(dualMW, logger.Slog())
recSrv.RegisterProtectedRecommendationRoutes(dualMW, logger.Slog())
Expand Down Expand Up @@ -333,6 +339,7 @@ func main() {
} else {
logger.Info("database connection closed")
}
closeIfPossible(logger, roleStore, "role store")

// Flush Sentry events
if sentryEnabled {
Expand Down
22 changes: 22 additions & 0 deletions cmd/cloudpam/store_both.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,28 @@ func selectUserStore(logger observability.Logger) auth.UserStore {
return us
}

func selectRoleStore(logger observability.Logger, userStore auth.UserStore) auth.RoleStore {
if logger == nil {
logger = observability.NewLogger(observability.DefaultConfig())
}
if usePostgres() {
rs, err := auth.NewPostgresRoleStore(databaseURL(), userStore)
if err != nil {
logger.Error("postgres role store init failed; falling back to sqlite", "error", err)
} else {
logger.Info("using postgres role store")
return rs
}
}
rs, err := auth.NewSQLiteRoleStore(sqliteDSN(), userStore)
if err != nil {
logger.Error("sqlite role store init failed; falling back to memory", "error", err)
return auth.NewMemoryRoleStore(userStore)
}
logger.Info("using sqlite role store")
return rs
}

func selectSessionStore(logger observability.Logger) auth.SessionStore {
if logger == nil {
logger = observability.NewLogger(observability.DefaultConfig())
Expand Down
5 changes: 5 additions & 0 deletions cmd/cloudpam/store_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ func selectUserStore(_ observability.Logger) auth.UserStore {
return auth.NewMemoryUserStore()
}

// selectRoleStore returns an in-memory role store.
func selectRoleStore(_ observability.Logger, userStore auth.UserStore) auth.RoleStore {
return auth.NewMemoryRoleStore(userStore)
}

// selectSessionStore returns an in-memory session store.
func selectSessionStore(_ observability.Logger) auth.SessionStore {
return auth.NewMemorySessionStore()
Expand Down
15 changes: 15 additions & 0 deletions cmd/cloudpam/store_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ func selectUserStore(logger observability.Logger) auth.UserStore {
return us
}

// selectRoleStore returns a PostgreSQL-backed role store.
func selectRoleStore(logger observability.Logger, userStore auth.UserStore) auth.RoleStore {
if logger == nil {
logger = observability.NewLogger(observability.DefaultConfig())
}
url := databaseURL()
rs, err := auth.NewPostgresRoleStore(url, userStore)
if err != nil {
logger.Error("postgres role store init failed; falling back to memory", "error", err)
return auth.NewMemoryRoleStore(userStore)
}
logger.Info("using postgres role store")
return rs
}

// selectSessionStore returns a PostgreSQL-backed session store.
func selectSessionStore(logger observability.Logger) auth.SessionStore {
if logger == nil {
Expand Down
15 changes: 15 additions & 0 deletions cmd/cloudpam/store_sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ func selectUserStore(logger observability.Logger) auth.UserStore {
return us
}

// selectRoleStore returns a SQLite-backed role store.
func selectRoleStore(logger observability.Logger, userStore auth.UserStore) auth.RoleStore {
if logger == nil {
logger = observability.NewLogger(observability.DefaultConfig())
}
dsn := sqliteDSN()
rs, err := auth.NewSQLiteRoleStore(dsn, userStore)
if err != nil {
logger.Error("sqlite role store init failed; falling back to memory", "error", err)
return auth.NewMemoryRoleStore(userStore)
}
logger.Info("using sqlite role store")
return rs
}

// selectSessionStore returns a SQLite-backed session store.
func selectSessionStore(logger observability.Logger) auth.SessionStore {
if logger == nil {
Expand Down
86 changes: 49 additions & 37 deletions docs/AUTH_FLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,69 +290,81 @@ rate_limit:
|------|-------------|-----------------|
| **Admin** | Full access | All permissions |
| **Operator** | Manage IPAM and discovery resources | pools:*, accounts:*, discovery:* |
| **Viewer** | Read-only access | pools:read, accounts:read, discovery:read |
| **Auditor** | Audit-only access | audit:read |
| **Viewer** | Read-only access | pools:read/list, accounts:read/list, discovery:read/list |
| **Auditor** | Audit-only access | audit:read/list |

### Permission Structure

Permissions follow the pattern: `resource:action`
Permissions follow the pattern: `resource:action`. The catalog below mirrors the API permission checks and the **Identity > RBAC** permission matrix.

```
pools:read - View pools
pools:write - Create/update pools
pools:delete - Delete pools
pools:allocate - Allocate from pools
pools:create - Create address pools and planned allocations
pools:read - View pool details, blocks, utilization, and schema checks
pools:update - Edit pool metadata, hierarchy, and assignment
pools:delete - Delete pools and planned allocations
pools:list - Browse pool lists and tree views

accounts:read - View cloud accounts
accounts:write - Create/update accounts
accounts:delete - Remove accounts
accounts:sync - Trigger syncs
accounts:create - Create cloud account records
accounts:read - View account details and account-linked resources
accounts:update - Edit account metadata
accounts:delete - Delete account records
accounts:list - Browse account lists

schema:read - View plans/templates
schema:write - Create/update plans
schema:apply - Apply schema plans
apikeys:create - Create API tokens within the caller's permission envelope
apikeys:read - View API key metadata
apikeys:update - Reserved for future API key metadata updates
apikeys:delete - Revoke API keys
apikeys:list - Browse API key metadata

discovery:read - View discovered resources
discovery:link - Link resources to pools
discovery:sync - Trigger discovery
audit:read - View audit event details
audit:list - Browse audit events

audit:read - View audit logs
audit:export - Export audit data
users:create - Create local user accounts
users:read - View user account details
users:update - Edit users, roles, password state, and active status
users:delete - Deactivate user accounts
users:list - Browse user accounts

users:read - View users
users:invite - Invite new users
users:write - Update users
users:delete - Remove users
discovery:create - Start discovery syncs and register agents
discovery:read - View discovered resources, agents, drift, and recommendations
discovery:update - Apply discovery results and reconcile drift
discovery:delete - Reserved for future discovery cleanup operations
discovery:list - Browse discovery resources, jobs, agents, drift, and recommendations

roles:read - View roles
roles:write - Manage custom roles

org:read - View org settings
org:write - Update org settings

teams:read - View teams
teams:write - Manage teams
settings:read - View security, OIDC, update, and system configuration
settings:write - Change security, OIDC, update, and system configuration
```

### Custom Roles

Admins can create custom roles with specific permission sets:
Admins can create custom roles from **Identity > RBAC** or through the role API. Built-in roles are immutable. Custom roles can be assigned to users from **Identity > Users**, and a custom role cannot be deleted while it is assigned to any active user.

```json
{
"name": "Network Engineer",
"name": "network-engineer",
"description": "Can manage pools and view discovery",
"permissions": [
"pools:read",
"pools:write",
"pools:allocate",
"pools:list",
"pools:update",
"discovery:read",
"accounts:read",
"schema:read"
"discovery:list",
"accounts:read"
]
}
```

Role administration endpoints require `settings:read` for list/read operations and `settings:write` for create/update/delete operations:

```
GET /api/v1/auth/permissions
GET /api/v1/auth/roles
POST /api/v1/auth/roles
GET /api/v1/auth/roles/{name}
PATCH /api/v1/auth/roles/{name}
DELETE /api/v1/auth/roles/{name}
```

### Team-Based Access (Pool Scoping)

Teams can have access scoped to specific pools:
Expand Down
11 changes: 11 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.13.0] - 2026-05-08

### Added
- Custom RBAC roles with app-wide permission catalog endpoints, persisted role/permission storage, and immutable built-in role handling
- Identity RBAC UI for creating, editing, deleting, and reviewing roles against the same permission matrix enforced by the API
- User management role selectors now include custom roles, and role deletion is blocked while assigned to active users

### Changed
- Session metadata now includes effective permissions so the frontend can gate administration pages by permission instead of admin-only role checks
- API key scope creation now validates requested scopes against the caller's effective role permissions, including custom roles

## [0.12.0] - 2026-05-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/PROJECT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This summary reflects what is implemented in the current repository. The deeper

**Authentication & Authorization:**
- API key authentication with Argon2id hashing
- RBAC with 4 roles: admin, operator, viewer, auditor
- RBAC with immutable built-ins (admin, operator, viewer, auditor) plus admin-managed custom roles built from app-wide `resource:action` permissions
- Local user management with password authentication
- Session management (HttpOnly + Secure cookies)
- Dual auth: session cookies (browser) + Bearer tokens (API)
Expand Down
Loading
Loading