Skip to content

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:

{MODULE}.{EntityDef}.{Action}
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:

Backend:  ICOSYS_BPM_BUSINESS_PARTNER_DEF_VIEWER
Frontend: ICOSYS_BPM.BusinessPartnerDef.Viewer

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

const { canView, canCreate, canEdit, canDelete } = useRBAC(config.crud.moduleName)

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