Skip to main content

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 data
  • alerts - Alert data
  • audit - Audit trail
  • dashboard - Dashboard metrics
  • analytics - Analytics data
  • tenants - Tenant management
  • users - User management
  • roles - Role management
  • camel - Apache Camel integration

Actions:

  • read - Read access
  • write - Write access
  • export - Export access
  • manage - Full CRUD access

Permission Examples

PermissionDescription
logs:readRead log data
alerts:readRead alerts
alerts:writeCreate/update alerts
audit:readRead audit logs
audit:exportExport audit logs
dashboard:readRead dashboard data
analytics:readRead analytics
tenants:manageManage tenants
users:manageManage users
roles:manageManage roles
camel:readRead Camel routes

Role Definitions

7 Predefined Roles

RoleDescriptionDefault Permissions
executiveExecutive managementdashboard:read, analytics:read
it-opsIT Operationslogs:read, dashboard:read, alerts:read
network-infraNetwork & Infrastructurelogs:read, dashboard:read
securitySecurity teamalerts:read, alerts:write, audit:read
complianceCompliance teamaudit:read, audit:export
helpdeskHelp desk supportlogs:read, alerts:read
adminSystem administratorFull 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

RoutePermissionDescription
GET /api/v1/logs/*logs:readLog viewing
POST /api/v1/logs/list/paginatelogs:readLog search
POST /api/v1/logs/histogramlogs:readLog histogram
GET /api/v1/alerts/*alerts:readAlert listing
POST /api/v1/alerts/*alerts:writeAlert management
GET /api/v1/alerting/monitors/*alerts:readMonitor listing
GET /api/v1/log-audit/*audit:readAudit log viewing
GET /api/v1/log-audit/export/*audit:exportAudit log export
GET /api/v1/dashboard/*dashboard:readDashboard data
GET /api/v1/analytics/*analytics:readAnalytics data
POST /api/tenantstenants:manageTenant CRUD
POST /api/usersusers:manageUser CRUD
POST /api/rolesroles:manageRole CRUD
GET /api/v1/camel/*camel:readCamel 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