Consolidating Fragmented Permission Logic

Consolidating Fragmented Permission Logic

During the process of refactoring the frontend code of the last project, we also modified the permission management structure. Permission management is a cross-cutting concern used throughout the service, including in router settings, menu rendering logic, and individual screen components. Through refactoring, we developed a structure that allows us to manage this logic in a more declarative and consistent way. The improvements can be categorized into three main areas.

  1. Integration of page access logic:I changed the code managing permissions for the router and menus to be managed in a single file.

  2. Uniformity of the internal code on the screen:The method of checking permissions varies for each screen, so I unified and managed it as a custom hook.

  3. Performance optimization: Improved the part where permission calculations were repeated on each rendering using useMemo, optimizing to recalculate only when the permission information changes.

Integrated the criteria for router and menu permissions: PAGE_PERMISSIONS

In the existing structure, the definitions of permissions to access specific pages were fragmented within the router security guard and the sidebar menu rendering logic. As a result, every time a change in permissions was planned, it was necessary to locate and modify all related files, leading to frequent inconsistency issues where menus would be visible but access was blocked, or vice versa, where access was possible but menus were not displayed if even one location was missed.

To address this, we created a single permission map called PAGE_PERMISSIONS that manages the roles allowed for each path in an array format, utilizing a constant called PATHS that defines all the routes within the project as keys.

export const PAGE_PERMISSIONS: Record <string, string[]>= {
  [PATHS.NOTICE_LIST]: ['USER', 'MANAGER', 'ADMIN'],
  [PATHS.NOTICE_DETAIL]: ['MANAGER', 'ADMIN'],
  [PATHS.ORG_MANAGEMENT]: ['ADMIN'],
;

By creating a permission map like this, the router guard or menu component no longer needs to write separate logic; it only needs to pass the current path to check the allowed roles in that object.

<RoleCheckLayout allowedRoles={PAGE_PERMISSIONS[PATHS.NOTICE_LIST]}>
  <NoticeListPage />
</RoleCheckLayout>

This method is for setting permissions Completely separate from business logicThis allows for declarative management. Even if new roles are added or the security policies of specific pages are strengthened in the future, by modifying only the PAGE_PERMISSIONS object, the scalability to be immediately reflected across the entire service is achieved.

Consolidating scattered internal logic: useAuthCheck

In addition to page-level access control, the detailed UI control logic, such as exposing specific buttons or disabling forms within the screen, was also an important task. Previously, each component directly fetched the user state to perform includes operations or redundantly wrote complex conditionals.

This fragmented logic into a single custom called useAuthCheck hookAbstracted into. In particular, this hook not only checks roles, but also incorporates logic that merges roleNames managed in global state with tenantRoleNames dependent on specific tenants or organizations, designed to be intuitive for developers even in projects with complex permission structures.


export const useAuthCheck = () => {
  const { roleNames = [], tenantRoleNames = [] } = useAuth();
  
  const hasRole = useCallback(
    (role:                        RoleName) 
[...roleNames, ...tenantRoleNames].includes(role),
   [roleNames, tenantRoleNames]
  );
  
  const { isUser, isManager, isAdmin } = useMemo(() => ({
    isUser: hasRole(RoleName.USER),
    isManager: hasRole(RoleName.MANAGER) || hasRole(RoleName.ADMIN),
    isAdmin: hasRole(RoleName.ADMIN),
  }), [hasRole]);
    
  return { hasRole, isUser, isManager, isAdmin };
};

In the actual screen implementation phase, it can be addressed with very concise code as follows.


const { isAdmin, isManager } = useAuthCheck();
    
return (
 <>
    {isManager && <Button>Edit</Button>}
    {isAdmin && <Button>Delete</Button>}
 </>
);

The biggest advantage gained from the introduction of this hook is A flexible structure for changesIf the definition of 'manager' is changed to include 'senior user', instead of modifying the code on numerous screens, only the calculation logic of isManager inside useAuthCheck needs to be adjusted. This significantly reduces human error and enhances the readability of the code.

Performance Optimization and Stability: Utilizing useMemo and useCallback

The permission verification logic has the characteristic of being executed repeatedly every time the component re-renders. Particularly, if there are many user roles, or if operations such as merging and iterating over arrays like [...roleNames, ...tenantRoleNames] are included, unnecessary computation costs arise. Furthermore, if the function is created anew each time, it can cause unnecessary re-renders of child components that receive it as props.

In the refactoring process, we actively utilized React's optimization API to minimize unnecessary computational overhead.

  • Preventing function recreationWrapped the hasRole function with useCallback to maintain the same reference as long as the dependency array roleNames, etc. does not change. This allows for optimization of the child components using that function (e.g., React.memo).

  • Caching of derived stateThe result values for role determination, such as isAdmin and isManager, have a purely functional nature, so they have been cached using useMemo. This prevents complex permission combination calculations from being repeated on every render and allows the previous value to be reused if the computation result is the same.

Such optimization is valid not only for increasing execution speed but also for ensuring data consistency. Even if permission information becomes temporarily unstable during rendering, stable UI can be maintained through memoized values.

The key to this refactoring is the unification of permission management. By integrating fragmented logic into a clear, single interface called PAGE_PERMISSIONS and useAuthCheck, we have achieved practical benefits such as these.

  • Maximizing maintenance efficiencyWhen changing permission policies, the scope of modification has been limited to a single file, resulting in faster response times.

  • Improvement in the declarative nature of the codeThe code has become one that focuses on "who can access this feature" rather than "how to check permissions."

  • Ensuring system stabilityWe have fundamentally blocked security vulnerabilities caused by permission omissions or discrepancies through centralized management.

It seemed like a simple task to consolidate fragmented logic into one, but through this, we were able to achieve both readability and maintainability, establishing a solid foundation that can be reliably expanded even as future services become more complex.

Lynn

Site footer