For the complete documentation index, see llms.txt. This page is also available as Markdown.
Role Based Access Controller
Setelah memiliki autentikasi (siapa pengguna), langkah selanjutnya adalah otorisasi (apa yang boleh dilakukan pengguna). RBAC adalah pendekatan standar untuk mengelola hak akses berdasarkan peran (role) yang dimiliki pengguna.
📂 Kode Lengkap Bab Ini:
Seluruh kode yang dibahas di bab ini tersedia di GitHub:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Users │────▶│ Roles │────▶│ Accesses │
└──────────────┘ └──────────────┘ └──────────────┘
(siapa) (peran) (apa yang
boleh dilakukan)
Komponen
Deskripsi
Contoh
User
Pengguna sistem
admin@example.com
Role
Kumpulan akses
superadmin, manager, staff
Access
Ijin spesifik
users::create, users::delete
Prinsip: User memiliki Roles, Roles memiliki Accesses. Seorang user bisa memiliki multiple roles. Dam satu role bisa memiliki banyak akses.
18.2 Design
Database
Gambarkan ERD
Cardinality :
users : roles → Many-to-Many (via roles_users)
roles : access → Many-to-Many (via access_roles)
access : access → One-to-Many (self, untuk hierarki)
REST API
GET /access
GET /roles
GET /roles/{id}
POST /roles
PUT /roles/{id}
DELETE /roles/{id}
POST /roles/{id}/access/{access_id}
DELETE /roles/{id}/access/{access_id}
GET /users
POST /users
GET /users/{id}
PUT /users/{id}
DELETE /users/{id}
POST /login
CLI
Menambahkan routing cli scan-access, untuk menambahkan accesss setiap kali ada pembuatan routing baru.
Handler
Service
Repository
Middleware
Middleware Auth diberi kemampuan untuk melakukan otorisasi berdasarkan role-role yang dimiliki oleh user.
18.3 Implementasi Database
ERD
Migration Tabel
Buat file migration/1_0002_rbac.sql
Seed Data Awal
Buat file migration/3_0002_rbac.sql
18.4 Implementasi Model
18.5 Implementasi DTO (Reques/Response)
Login Response (diperluas)
Access Response (Tree Structure)
Role Request & Response
Users Request & Response (dengan Roles)
18.6 Implementasi Repository
Access Repository
Perhatikan untuk query create, tampak kompleks karena desain operasi insert hanya melalui scan file routing, sehingga jika ada duplikat data akan diabaikan, serta selalu mengembalikan id untuk keperluan mendapatkan id jika access tersebut adalah access parent.
Role Repository
Pada FindByID query termasuk mendapatkan relasi access
User Repository (dengan Roles)
18.7 Implementasi Service
Access Service
Catatan: Fungsi ScanAccess akan dipanggil dari routing cli
Auths Service (diperluas)
Role Service
User Service
18.8 Implementasi Handler
Accesss Handler
Auth Handler
Role Handler
User Handler
18.9 Route Definition untuk Scan Access
Karena kita perlu mendaftarkan semua route ke database untuk keperluan otorisasi, buat struct untuk mendefinisikan route:
18.10 Router dengan Route Definitions
Perhatikan ada fungsi baru: wrapWithRoutePattern, sebelum masuk ke middleware, kita sisipkan informasi terkait pattern dan group ke dalam context value.
18.11 CLI untuk Scan Access
Tambahkan command scan-access untuk mendaftarkan semua route ke database:
Penggunaan:
18.12 Middleware Auth dengan Otorisasi
Middleware sekarang memeriksa apakah user memiliki permission untuk mengakses route:
type AccessHandler interface {
List(w http.ResponseWriter, r *http.Request)
}
type RoleHandler interface {
List(w http.ResponseWriter, r *http.Request)
Create(w http.ResponseWriter, r *http.Request)
FindByID(w http.ResponseWriter, r *http.Request)
Update(w http.ResponseWriter, r *http.Request)
Delete(w http.ResponseWriter, r *http.Request)
Grant(w http.ResponseWriter, r *http.Request)
Revoke(w http.ResponseWriter, r *http.Request)
}
type UserHandler interface {
List(w http.ResponseWriter, r *http.Request)
Create(w http.ResponseWriter, r *http.Request)
FindByID(w http.ResponseWriter, r *http.Request)
Update(w http.ResponseWriter, r *http.Request)
Delete(w http.ResponseWriter, r *http.Request)
}
type Accesses interface {
List(ctx context.Context) (map[int]*model.AccessTree, *errors.BusinessError)
ScanAccess(ctx context.Context) error
}
type Auths interface {
Login(ctx context.Context, email, password string) (string, *model.User, []string, *errors.BusinessError)
}
type Roles interface {
List(ctx context.Context) ([]model.Role, *errors.BusinessError)
FindByID(ctx context.Context, id int) (*model.Role, *errors.BusinessError)
Create(ctx context.Context, role *model.Role) *errors.BusinessError
Update(ctx context.Context, role *model.Role) *errors.BusinessError
Delete(ctx context.Context, id int) *errors.BusinessError
Grant(ctx context.Context, roleID, accessID int) *errors.BusinessError
Revoke(ctx context.Context, roleID, accessID int) *errors.BusinessError
}
type Users interface {
List(ctx context.Context) ([]model.User, *errors.BusinessError)
Create(ctx context.Context, user *model.User) *errors.BusinessError
FindByID(ctx context.Context, id string) (*model.User, *errors.BusinessError)
Update(ctx context.Context, user *model.User) *errors.BusinessError
Delete(ctx context.Context, id string) *errors.BusinessError
}
-- =====================================================
-- TABEL access
-- =====================================================
CREATE TABLE IF NOT EXISTS access (
id SERIAL PRIMARY KEY,
parent_id INTEGER,
name VARCHAR(255) NOT NULL UNIQUE,
alias VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT timezone('utc', now())
);
-- Index untuk parent_id (hierarchical queries)
CREATE INDEX idx_access_parent_id ON access(parent_id);
-- =====================================================
-- TABEL roles
-- =====================================================
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT timezone('utc', now())
);
-- =====================================================
-- TABEL access_roles (many-to-many)
-- =====================================================
CREATE TABLE IF NOT EXISTS access_roles (
id SERIAL PRIMARY KEY,
access_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT timezone('utc', now()),
-- Constraint unique composite
CONSTRAINT access_roles_unique UNIQUE (access_id, role_id),
-- Foreign keys
CONSTRAINT fk_access_roles_to_access
FOREIGN KEY (access_id)
REFERENCES access(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_access_roles_to_roles
FOREIGN KEY (role_id)
REFERENCES roles(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
-- Index untuk performance
CREATE INDEX idx_access_roles_access_id ON access_roles(access_id);
CREATE INDEX idx_access_roles_role_id ON access_roles(role_id);
-- =====================================================
-- TABEL roles_users (many-to-many)
-- =====================================================
CREATE TABLE IF NOT EXISTS roles_users (
id BIGSERIAL PRIMARY KEY,
role_id INTEGER NOT NULL,
user_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT timezone('utc', now()),
-- Constraint unique composite
CONSTRAINT roles_users_unique UNIQUE (role_id, user_id),
-- Foreign keys
CONSTRAINT fk_roles_users_to_roles
FOREIGN KEY (role_id)
REFERENCES roles(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_roles_users_to_users
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
-- Index untuk performance
CREATE INDEX idx_roles_users_role_id ON roles_users(role_id);
CREATE INDEX idx_roles_users_user_id ON roles_users(user_id);
-- =====================================================
-- INDEX TAMBAHAN UNTUK PERFORMANCE (Opsional)
-- =====================================================
-- Index composite untuk query permission checking yang umum
CREATE INDEX idx_access_roles_composite_lookup
ON access_roles(role_id, access_id);
-- Index untuk roles_users jika sering join dengan users
CREATE INDEX idx_roles_users_user_role
ON roles_users(user_id, role_id);
INSERT INTO users (id, name, username, password, email, is_active) VALUES
('019eb960-a27d-73c8-9703-b23a9f50dc83', 'Admin', 'admin', '$2a$10$D7UJmo0/bnXUyvsvRNKmc.cLeiLPNGQ8TfBnQHc2hkQV.oSFBh.qO', 'admin@example.com', true);
INSERT INTO access (id, name, alias) VALUES (1, 'root', 'root');
INSERT INTO roles (id, name) VALUES (1, 'superadmin');
INSERT INTO access_roles (access_id, role_id) VALUES (1, 1);
INSERT INTO roles_users (role_id, user_id) VALUES (1, '019eb960-a27d-73c8-9703-b23a9f50dc83');
// internal/model/access.go
package model
type Access struct {
ID int
ParentID *int
Name string
Alias string
}
type AccessTree struct {
ID int
Name string
Alias string
Childrens []Access
}
// internal/model/role.go
package model
type Role struct {
ID int
Name string
Accesses []Access
}
// internal/model/user.go
package model
type User struct {
ID string
Name string
Username string
Password string
Email string
IsActive bool
Roles []Role
}