RBAC (Role-Based Access Control)
Overview
Backend CLM menerapkan RBAC dengan permission-based access control yang di-check terhadap PostgreSQL database.
Permission Model
Structure
resource:action
Resources:
logs- Log dataalerts- Alert dataaudit- Audit traildashboard- Dashboard metricsanalytics- Analytics datatenants- Tenant managementusers- User managementroles- Role managementcamel- Apache Camel integration
Actions:
read- Read accesswrite- Write accessexport- Export accessmanage- Full CRUD access
Permission Examples
| Permission | Description |
|---|---|
logs:read | Read log data |
alerts:read | Read alerts |
alerts:write | Create/update alerts |
audit:read | Read audit logs |
audit:export | Export audit logs |
dashboard:read | Read dashboard data |
analytics:read | Read analytics |
tenants:manage | Manage tenants |
users:manage | Manage users |
roles:manage | Manage roles |
camel:read | Read Camel routes |
Role Definitions
7 Predefined Roles
| Role | Description | Default Permissions |
|---|---|---|
executive | Executive management | dashboard:read, analytics:read |
it-ops | IT Operations | logs:read, dashboard:read, alerts:read |
network-infra | Network & Infrastructure | logs:read, dashboard:read |
security | Security team | alerts:read, alerts:write, audit:read |
compliance | Compliance team | audit:read, audit:export |
helpdesk | Help desk support | logs:read, alerts:read |
admin | System administrator | Full access |
Database Schema
Users Table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
kc_id VARCHAR(255), -- Keycloak user ID
username VARCHAR(100) UNIQUE,
email VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Tenants Table
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
slug VARCHAR(100) UNIQUE,
created_at TIMESTAMP DEFAULT NOW()
);
Applications Table
CREATE TABLE applications (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
slug VARCHAR(100) UNIQUE,
description TEXT,
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW()
);
Roles Table
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
Permissions Table
CREATE TABLE permissions (
id SERIAL PRIMARY KEY,
resource VARCHAR(100),
action VARCHAR(100),
description TEXT,
UNIQUE(resource, action)
);
Role-Permissions Table
CREATE TABLE role_permissions (
role_id INT REFERENCES roles(id),
permission_id INT REFERENCES permissions(id),
PRIMARY KEY (role_id, permission_id)
);
User-Tenant-Roles Table
CREATE TABLE user_tenant_roles (
user_id INT REFERENCES users(id),
tenant_id INT REFERENCES tenants(id),
role_id INT REFERENCES roles(id),
PRIMARY KEY (user_id, tenant_id, role_id)
);
Permission Check
Database Query
SELECT COUNT(*) > 0
FROM user_tenant_roles utr
JOIN role_permissions rp ON utr.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
JOIN users u ON utr.user_id = u.id
WHERE u.username = $1
AND utr.tenant_id = (SELECT id FROM tenants WHERE slug = $2)
AND p.resource = $3
AND p.action = $4;
Go Implementation
func (db *Client) CheckPermission(username, tenantSlug, appSlug, resource, action string) (bool, error) {
query := `
SELECT COUNT(*) > 0
FROM user_tenant_roles utr
JOIN role_permissions rp ON utr.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
JOIN users u ON utr.user_id = u.id
JOIN tenants t ON utr.tenant_id = t.id
WHERE u.username = $1
AND t.slug = $2
AND p.resource = $3
AND p.action = $4;
`
var allowed bool
err := db.pool.QueryRow(query, username, tenantSlug, resource, action).Scan(&allowed)
if err != nil {
return false, err
}
return allowed, nil
}
Middleware
requirePermission
func (s *Server) requirePermission(appSlug, resource, action string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := tenantFromContext(r.Context())
username := usernameFromContext(r.Context())
if s.db == nil {
writeError(w, http.StatusInternalServerError, "database not available")
return
}
allowed, err := s.db.CheckPermission(username, tenantID, appSlug, resource, action)
if err != nil {
writeError(w, http.StatusInternalServerError, "permission check failed")
return
}
if !allowed {
writeError(w, http.StatusForbidden, "permission denied")
return
}
next.ServeHTTP(w, r)
})
}
}
requireRole
func (s *Server) requireRole(role string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := claimsFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
if !keycloak.HasRole(claims, role) {
writeError(w, http.StatusForbidden, fmt.Sprintf("role %s required", role))
return
}
next.ServeHTTP(w, r)
})
}
requireAnyRole
func (s *Server) requireAnyRole(roles []string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := claimsFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "missing claims")
return
}
for _, role := range roles {
if keycloak.HasRole(claims, role) {
next.ServeHTTP(w, r)
return
}
}
writeError(w, http.StatusForbidden, fmt.Sprintf("one of roles %v required", roles))
})
}
Route Permission Mapping
| Route | Permission | Description |
|---|---|---|
GET /api/v1/logs/* | logs:read | Log viewing |
POST /api/v1/logs/list/paginate | logs:read | Log search |
POST /api/v1/logs/histogram | logs:read | Log histogram |
GET /api/v1/alerts/* | alerts:read | Alert listing |
POST /api/v1/alerts/* | alerts:write | Alert management |
GET /api/v1/alerting/monitors/* | alerts:read | Monitor listing |
GET /api/v1/log-audit/* | audit:read | Audit log viewing |
GET /api/v1/log-audit/export/* | audit:export | Audit log export |
GET /api/v1/dashboard/* | dashboard:read | Dashboard data |
GET /api/v1/analytics/* | analytics:read | Analytics data |
POST /api/tenants | tenants:manage | Tenant CRUD |
POST /api/users | users:manage | User CRUD |
POST /api/roles | roles:manage | Role CRUD |
GET /api/v1/camel/* | camel:read | Camel routes |
Multi-Tenant Isolation
OpenSearch Aliases
// Create tenant-specific alias
filter := map[string]any{
"term": map[string]any{
"tenant_id.keyword": body.Slug,
},
}
_ = s.search.CreateAlias(r.Context(), "clm-"+body.Slug, "clm-v2-logs-*", filter)
Query Filtering
// Filter by tenant in queries
func (s *Server) handleLogPaginate(w http.ResponseWriter, r *http.Request) {
tenantSlug := tenantSlugFromContext(r.Context())
// Get tenant-specific indices
var indices []string
if s.db != nil && tenantSlug != "" {
aliases, err := s.db.GetTenantAliases(tenantSlug)
if err == nil && len(aliases) > 0 {
for _, alias := range aliases {
indices = append(indices, alias.IndexPattern)
}
}
}
// Always pass tenantID for filtering
tenantID := tenantSlug
result, err := s.search.SearchLogs(r.Context(), indices, req, "@timestamp", tenantID)
}
RBAC Management API
Tenants
// List tenants
GET /api/tenants
// Create tenant
POST /api/tenants
{
"name": "BJB Syariah",
"slug": "bjb-syariah"
}
Applications
// List applications
GET /api/applications
// Create application
POST /api/applications
{
"name": "CLM Backend",
"slug": "clm-backend",
"description": "Centralized Log Management Backend"
}
// Update application
PUT /api/applications
{
"slug": "clm-backend",
"name": "CLM Backend",
"description": "Updated description",
"status": "active"
}
// Delete application
DELETE /api/applications?slug=clm-backend
Users
// List users
GET /api/users
// Create user (also creates in Keycloak)
POST /api/users
{
"username": "newuser",
"email": "newuser@bjbsyariah.co.id",
"password": "Password123!",
"tenant": "bjb-syariah"
}
// Delete user
DELETE /api/users?username=newuser
Roles
// List roles
GET /api/roles
// Create role
POST /api/roles
{
"name": "custom-role",
"description": "Custom role"
}
Permissions
// List permissions
GET /api/permissions
User-Tenant-Roles
// List user-tenant-role mappings
GET /api/user-roles
// Assign role to user in tenant
POST /api/user-roles
{
"user_id": 1,
"tenant_id": 1,
"role_id": 1
}
Next Steps
- Configuration - Configuration guide
- API Endpoints - Full API documentation
- Security - Security overview