Permissions¶
Türkçe
ICOSYS'ta yetkilendirme 3 katmanlı bir koruma ile çalışır: 1. Rol tabanlı erişim kontrolü (RBAC) 2. Hesap sahibi / admin bypass 3. Entity sahiplik kontrolü (linkedAccounts) Bu 3 katman CrudPage bileşeninde sırayla uygulanır.
Overview¶
The ICOSYS permission system provides role-based access control (RBAC) with
owner/admin bypass capabilities. It spans both backend (@Secured) and frontend (useRBAC),
ensuring consistent authorization across the stack.
User Request
│
▼
┌─────────────────────────────┐
│ Layer 1: RBAC Role Check │ ← Does user have the required role?
│ useRBAC(moduleName) │
├─────────────────────────────┤
│ Layer 2: Owner/Admin │ ← Is user account owner or admin?
│ allowOwner / allowAdmin │ (config-driven bypass)
├─────────────────────────────┤
│ Layer 3: Entity Ownership │ ← Does entity belong to user's accounts?
│ linkedAccounts check │ (VIEW/EDIT mode only)
└─────────────────────────────┘
│
├── Authorized → Render page
└── Denied → Redirect to /403
RBAC (Role-Based Access Control)¶
Role Format¶
Roles follow a hierarchical naming pattern:
| Part | Example | Description |
|---|---|---|
| Module | ICOSYS_BPM |
Backend module name |
| EntityDef | BusinessPartnerDef |
Entity definition name |
| Action | Viewer |
Permission level |
Full examples:
| Role | Grants |
|---|---|
ICOSYS_BPM.BusinessPartnerDef.Viewer |
View business partners |
ICOSYS_BPM.BusinessPartnerDef.Creator |
Create business partners |
ICOSYS_BPM.BusinessPartnerDef.Updater |
Edit business partners |
ICOSYS_BPM.BusinessPartnerDef.Deleter |
Delete business partners |
ICOSYS_BPM.BranchDef.Viewer |
View branches |
useRBAC Hook¶
The primary authorization hook:
// src/app/auth/useRBAC.ts
export function useRBAC(module: string) {
const { user, hasRole } = useAuth()
return useMemo(() => {
// SuperAdmin bypasses all checks
if (user?.isSuperAdmin) {
return { canView: true, canCreate: true, canEdit: true, canDelete: true }
}
return {
canView: hasRole(`${module}.Viewer`),
canCreate: hasRole(`${module}.Creator`),
canEdit: hasRole(`${module}.Updater`),
canDelete: hasRole(`${module}.Deleter`),
}
}, [user, module, hasRole])
}
Usage:
const { canView, canCreate, canEdit, canDelete } = useRBAC("ICOSYS_BPM.BusinessPartnerDef")
// canView = true if user has role "ICOSYS_BPM.BusinessPartnerDef.Viewer"
// OR if user.isSuperAdmin === true
hasRole Implementation¶
hasRole: (role: string) =>
user?.isSuperAdmin || // SuperAdmin bypasses everything
(user?.roles.includes(role) ?? false)
Role Extraction from Backend¶
Türkçe
Rol çıkarımı MUTLAKA aktif hesaba (activeAccountNo) göre filtrelenmeli.
Aksi halde kullanıcı başka hesaplardaki rolleri miras alır — bu güvenlik açığıdır!
Backend → Frontend Role Transformation¶
Backend roles use UPPER_SNAKE_CASE. The frontend transforms them:
Transformation logic:
function extractRoles(
groups: BackendGroupDto[],
activeAccountNo: string // CRITICAL: filter by active account
): string[] {
const roles = new Set<string>()
for (const group of groups) {
// Only include roles from the active account!
if (group.accountNo !== activeAccountNo) continue
for (const role of group.roles) {
const prefix = role.roleType?.roleType || ""
const mapped = transformRole(prefix, role.roleName)
if (mapped) roles.add(mapped)
}
}
return Array.from(roles)
}
Action Suffix Map¶
| Backend Suffix | Frontend Action |
|---|---|
VIEWER |
Viewer |
CREATOR |
Creator |
UPDATER |
Updater |
DELETER |
Deleter |
APPROVER |
Approver |
MANAGER |
Manager |
Three-Layer Authorization Guard¶
Layer 1: Role Check¶
If user has the required role → authorized.
Layer 2: Owner/Admin Bypass¶
If the user doesn't have the role, check for bypass:
const ownerBypass = config.crud.allowOwner === true && user?.isAccountOwner === true
const adminBypass = config.crud.allowAdmin === true && user?.isAccountAdmin === true
Config properties control this per handler:
crud: {
allowOwner: true, // Account owners can access without role
allowAdmin: true, // Account admins can access without role
}
If bypass applies AND mode is LIST → authorized (backend filters data).
Layer 3: Entity Ownership Check¶
For VIEW/EDIT modes with owner/admin bypass, the specific entity must belong to the user's linked accounts:
const linkedAccountNos = user?.linkedAccounts?.map(la => la.accountNo) ?? []
const isAuthorized = linkedAccountNos.includes(String(entityId))
This prevents a user from accessing entities outside their account scope, even with owner/admin bypass.
Türkçe
Owner bypass + LIST modu → backend crProcessId filtresiyle verileri zaten kısıtlar.
VIEW/EDIT modunda ise frontend ek olarak linkedAccounts kontrolü yapar.
Complete Guard Code¶
const isAuthorized = (() => {
// Layer 1: Explicit role or superAdmin
if (canView) return true
// Layer 2: Owner/Admin bypass
const ownerBypass = config.crud.allowOwner && user?.isAccountOwner
const adminBypass = config.crud.allowAdmin && user?.isAccountAdmin
if (!ownerBypass && !adminBypass) return false
// LIST mode — allow (backend filters data)
if (!entityId) return true
// Layer 3: Entity ownership
const linkedAccountNos = user?.linkedAccounts?.map(la => la.accountNo) ?? []
return linkedAccountNos.includes(String(entityId))
})()
if (!isAuthorized) {
return <Navigate to="/403" replace />
}
User Info Structure¶
interface UserInfo {
id: number
username: string
displayName: string
email: string
accountNo: string // Active account
accountName: string
crProcessId: number // Tenant ID
isSuperAdmin: boolean // Global admin
isAccountOwner: boolean // Owner of active account
isAccountAdmin: boolean // Admin of active account
roles: string[] // Roles (filtered by active account!)
linkedAccounts: LinkedAccount[] // All accessible accounts
}
interface LinkedAccount {
accountNo: string
accountName: string
ownerStatus: boolean // Is owner of this account?
adminStatus: boolean // Is admin of this account?
}
Türkçe
roles dizisi sadece aktif hesaptaki (accountNo) rolleri içerir.
Hesap değiştirildiğinde (switchAccount) roller yeniden çıkarılır.
SubHandler Permissions¶
SubHandler tabs have their own RBAC, with override options:
// SubHandlerConfig
{
allowOwnerCrud: true, // Owner gets full CRUD on child
allowAdminCrud: true, // Admin gets full CRUD on child
useParentRbac: false, // Use parent's RBAC module instead of child's
}
Permission resolution:
const rbacModule = item.useParentRbac
? parentConfig.crud.moduleName // Inherit parent's module
: childConfig.crud.moduleName // Use own module
const rbac = useRBAC(rbacModule)
const ownerOverride = item.allowOwnerCrud && user?.isAccountOwner
const adminOverride = item.allowAdminCrud && user?.isAccountAdmin
const canCreate = rbac.canCreate || ownerOverride || adminOverride
const canEdit = rbac.canEdit || ownerOverride || adminOverride
const canDelete = rbac.canDelete || ownerOverride || adminOverride
Backend Integration¶
@Secured Annotation¶
Backend controllers use @Secured to enforce authorization:
@Secured(role = "ICOSYS_BPM.BusinessPartnerDef.Viewer")
@GetMapping("/{id}")
public ResponseEntity<BranchEditDto> findById(@PathVariable Long id) { ... }
@Secured(role = "ICOSYS_BPM.BusinessPartnerDef.Creator")
@PostMapping
public ResponseEntity<BranchEditDto> create(@RequestBody BranchEditDto dto) { ... }
Frontend-Backend Role Mapping¶
| Frontend Check | Backend Annotation |
|---|---|
canView → {module}.Viewer |
@Secured(role = "{MODULE}_{ENTITY}_DEF_VIEWER") |
canCreate → {module}.Creator |
@Secured(role = "{MODULE}_{ENTITY}_DEF_CREATOR") |
canEdit → {module}.Updater |
@Secured(role = "{MODULE}_{ENTITY}_DEF_UPDATER") |
canDelete → {module}.Deleter |
@Secured(role = "{MODULE}_{ENTITY}_DEF_DELETER") |
CrProcess Tenant Isolation¶
In addition to RBAC, every data query is filtered by crProcessId:
// Backend: Specification always filters by tenant
predicates.add(cb.equal(root.get("crProcess").get("id"), filter.getCrProcessId()));
// Frontend: crProcessId is injected from auth store
filter: { crProcessId: user.crProcessId, ...otherFilters }
This ensures users only see data belonging to their active account/tenant, regardless of their role permissions.
Authorization Decision Matrix¶
| Scenario | Layer 1 (Role) | Layer 2 (Bypass) | Layer 3 (Ownership) | Result |
|---|---|---|---|---|
| Has role | PASS | - | - | Allowed |
| SuperAdmin | PASS | - | - | Allowed |
| Owner + allowOwner + LIST | FAIL | PASS | N/A | Allowed |
| Owner + allowOwner + VIEW own entity | FAIL | PASS | PASS | Allowed |
| Owner + allowOwner + VIEW other entity | FAIL | PASS | FAIL | 403 |
| Admin + allowAdmin disabled | FAIL | FAIL | - | 403 |
| No role, no bypass | FAIL | FAIL | - | 403 |