secure-role-guard 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +616 -0
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -21,6 +21,7 @@
21
21
  - [API Reference](#api-reference)
22
22
  - [Real-World Examples](#real-world-examples)
23
23
  - [Framework Compatibility](#framework-compatibility)
24
+ - [Fixed Roles vs Dynamic Roles](#fixed-roles-vs-dynamic-roles)
24
25
  - [Common Mistakes to Avoid](#common-mistakes-to-avoid)
25
26
  - [License](#license)
26
27
 
@@ -610,6 +611,621 @@ if (tenantId) {
610
611
 
611
612
  ---
612
613
 
614
+ ## Fixed Roles vs Dynamic Roles
615
+
616
+ This package supports both **Fixed (Hardcoded) Roles** and **Dynamic (Database-driven) Roles**. Choose the approach that fits your application.
617
+
618
+ ### Architecture Overview
619
+
620
+ ```
621
+ ┌─────────────────────────────────────────────────────────────────────────┐
622
+ │ YOUR APPLICATION │
623
+ ├─────────────────────────────────────────────────────────────────────────┤
624
+ │ │
625
+ │ OPTION A: Fixed Roles OPTION B: Dynamic Roles │
626
+ │ ┌───────────────────┐ ┌───────────────────┐ │
627
+ │ │ roles.ts file │ │ Database │ │
628
+ │ │ (hardcoded) │ │ (MongoDB/PG/SQL) │ │
629
+ │ └─────────┬─────────┘ └─────────┬─────────┘ │
630
+ │ │ │ │
631
+ │ ▼ ▼ │
632
+ │ ┌───────────────────┐ ┌───────────────────┐ │
633
+ │ │ defineRoles() │ │ loadRolesFromDB()│ │
634
+ │ │ (at build time) │ │ (at runtime) │ │
635
+ │ └─────────┬─────────┘ └─────────┬─────────┘ │
636
+ │ │ │ │
637
+ │ └──────────────┬───────────────────┘ │
638
+ │ ▼ │
639
+ │ ┌───────────────────────────────────┐ │
640
+ │ │ secure-role-guard │ │
641
+ │ │ (same API for both approaches) │ │
642
+ │ └───────────────────────────────────┘ │
643
+ │ │
644
+ └─────────────────────────────────────────────────────────────────────────┘
645
+ ```
646
+
647
+ ---
648
+
649
+ ### Approach 1: Fixed Roles (Hardcoded)
650
+
651
+ Best for applications with **predefined, unchanging roles**.
652
+
653
+ #### When to Use Fixed Roles
654
+
655
+ - ✅ Small to medium applications
656
+ - ✅ Roles rarely change
657
+ - ✅ Simple admin/user/viewer hierarchy
658
+ - ✅ You want faster startup (no DB query needed)
659
+
660
+ #### Frontend Example (React/Next.js)
661
+
662
+ ```typescript
663
+ // lib/roles.ts - Define roles at build time
664
+ import { defineRoles } from "secure-role-guard";
665
+
666
+ export const roleRegistry = defineRoles({
667
+ superadmin: ["*"], // Full access
668
+ admin: ["user.read", "user.create", "user.update", "user.delete", "report.*"],
669
+ manager: ["user.read", "user.update", "report.view"],
670
+ support: ["ticket.read", "ticket.reply", "user.read"],
671
+ viewer: ["user.read", "report.view"],
672
+ });
673
+
674
+ // -------------------------------------------------------
675
+ // app/providers.tsx - Setup Provider
676
+ ("use client");
677
+
678
+ import { PermissionProvider } from "secure-role-guard/react";
679
+ import { roleRegistry } from "@/lib/roles";
680
+
681
+ interface User {
682
+ id: string;
683
+ roles: string[];
684
+ permissions?: string[];
685
+ }
686
+
687
+ export function AuthProvider({
688
+ children,
689
+ user,
690
+ }: {
691
+ children: React.ReactNode;
692
+ user: User | null;
693
+ }) {
694
+ return (
695
+ <PermissionProvider user={user} registry={roleRegistry}>
696
+ {children}
697
+ </PermissionProvider>
698
+ );
699
+ }
700
+
701
+ // -------------------------------------------------------
702
+ // components/Dashboard.tsx - Use Permissions
703
+ import { Can, useCan } from "secure-role-guard/react";
704
+
705
+ export function Dashboard() {
706
+ const canManageUsers = useCan("user.update");
707
+
708
+ return (
709
+ <div>
710
+ <h1>Dashboard</h1>
711
+
712
+ {/* Declarative approach */}
713
+ <Can permission="user.create">
714
+ <button>Add New User</button>
715
+ </Can>
716
+
717
+ <Can permission="report.view">
718
+ <ReportsSection />
719
+ </Can>
720
+
721
+ <Can permissions={["user.delete", "user.update"]} anyOf>
722
+ <UserManagement />
723
+ </Can>
724
+
725
+ {/* Programmatic approach */}
726
+ {canManageUsers && <EditUserButton />}
727
+ </div>
728
+ );
729
+ }
730
+ ```
731
+
732
+ #### Backend Example (Express/Fastify)
733
+
734
+ ```typescript
735
+ // server.ts - Express with Fixed Roles
736
+ import express from "express";
737
+ import { defineRoles, canUser } from "secure-role-guard/core";
738
+ import { requirePermission } from "secure-role-guard/adapters/express";
739
+
740
+ const app = express();
741
+
742
+ // Same role definitions as frontend
743
+ const roleRegistry = defineRoles({
744
+ superadmin: ["*"],
745
+ admin: ["user.read", "user.create", "user.update", "user.delete"],
746
+ manager: ["user.read", "user.update"],
747
+ viewer: ["user.read"],
748
+ });
749
+
750
+ // YOUR auth middleware (this package doesn't do auth)
751
+ app.use(yourAuthMiddleware); // Sets req.user
752
+
753
+ // Protected routes with middleware
754
+ app.get(
755
+ "/api/users",
756
+ requirePermission("user.read", roleRegistry),
757
+ async (req, res) => {
758
+ const users = await db.users.findAll();
759
+ res.json(users);
760
+ }
761
+ );
762
+
763
+ app.post(
764
+ "/api/users",
765
+ requirePermission("user.create", roleRegistry),
766
+ async (req, res) => {
767
+ const user = await db.users.create(req.body);
768
+ res.json(user);
769
+ }
770
+ );
771
+
772
+ // Manual permission check (for complex logic)
773
+ app.put("/api/users/:id", async (req, res) => {
774
+ const user = req.user;
775
+
776
+ if (!canUser(user, "user.update", roleRegistry)) {
777
+ return res.status(403).json({ error: "Forbidden" });
778
+ }
779
+
780
+ // Additional business logic
781
+ const targetUser = await db.users.findById(req.params.id);
782
+
783
+ // Example: Managers can only edit non-admin users
784
+ if (
785
+ targetUser.roles.includes("admin") &&
786
+ !canUser(user, "admin.manage", roleRegistry)
787
+ ) {
788
+ return res.status(403).json({ error: "Cannot edit admin users" });
789
+ }
790
+
791
+ const updated = await db.users.update(req.params.id, req.body);
792
+ res.json(updated);
793
+ });
794
+
795
+ app.listen(3000);
796
+ ```
797
+
798
+ ---
799
+
800
+ ### Approach 2: Dynamic Roles (Database-Driven)
801
+
802
+ Best for applications where **admin can create/modify roles at runtime**.
803
+
804
+ #### When to Use Dynamic Roles
805
+
806
+ - ✅ Enterprise SaaS applications
807
+ - ✅ Admin should create custom roles (e.g., "HR Manager", "Finance Lead")
808
+ - ✅ Roles change frequently
809
+ - ✅ Multi-tenant with different roles per tenant
810
+
811
+ #### Database Schema Examples
812
+
813
+ **MongoDB:**
814
+
815
+ ```javascript
816
+ // roles collection
817
+ {
818
+ _id: ObjectId("..."),
819
+ name: "hr_manager",
820
+ display_name: "HR Manager",
821
+ permissions: ["employee.read", "employee.create", "employee.update", "leave.approve"],
822
+ is_active: true,
823
+ tenant_id: ObjectId("..."), // For multi-tenant
824
+ created_at: ISODate("...")
825
+ }
826
+
827
+ // users collection
828
+ {
829
+ _id: ObjectId("..."),
830
+ email: "john@example.com",
831
+ roles: [ObjectId("role1"), ObjectId("role2")],
832
+ direct_permissions: ["special.feature"], // User-specific permissions
833
+ tenant_id: ObjectId("...")
834
+ }
835
+ ```
836
+
837
+ **PostgreSQL:**
838
+
839
+ ```sql
840
+ -- roles table
841
+ CREATE TABLE roles (
842
+ id SERIAL PRIMARY KEY,
843
+ name VARCHAR(50) UNIQUE NOT NULL,
844
+ display_name VARCHAR(100),
845
+ is_active BOOLEAN DEFAULT true,
846
+ tenant_id INTEGER REFERENCES tenants(id),
847
+ created_at TIMESTAMP DEFAULT NOW()
848
+ );
849
+
850
+ -- permissions table
851
+ CREATE TABLE permissions (
852
+ id SERIAL PRIMARY KEY,
853
+ code VARCHAR(100) UNIQUE NOT NULL, -- e.g., 'user.read'
854
+ description TEXT
855
+ );
856
+
857
+ -- role_permissions (many-to-many)
858
+ CREATE TABLE role_permissions (
859
+ role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
860
+ permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
861
+ PRIMARY KEY (role_id, permission_id)
862
+ );
863
+
864
+ -- user_roles (many-to-many)
865
+ CREATE TABLE user_roles (
866
+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
867
+ role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
868
+ PRIMARY KEY (user_id, role_id)
869
+ );
870
+
871
+ -- user_permissions (direct permissions, bypass roles)
872
+ CREATE TABLE user_permissions (
873
+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
874
+ permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
875
+ PRIMARY KEY (user_id, permission_id)
876
+ );
877
+ ```
878
+
879
+ **MySQL:**
880
+
881
+ ```sql
882
+ -- Similar to PostgreSQL, with MySQL syntax
883
+ CREATE TABLE roles (
884
+ id INT AUTO_INCREMENT PRIMARY KEY,
885
+ name VARCHAR(50) UNIQUE NOT NULL,
886
+ display_name VARCHAR(100),
887
+ is_active TINYINT(1) DEFAULT 1,
888
+ tenant_id INT,
889
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
890
+ );
891
+
892
+ CREATE TABLE permissions (
893
+ id INT AUTO_INCREMENT PRIMARY KEY,
894
+ code VARCHAR(100) UNIQUE NOT NULL,
895
+ description TEXT
896
+ );
897
+
898
+ CREATE TABLE role_permissions (
899
+ role_id INT,
900
+ permission_id INT,
901
+ PRIMARY KEY (role_id, permission_id),
902
+ FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
903
+ FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
904
+ );
905
+ ```
906
+
907
+ #### Backend: Loading Dynamic Roles
908
+
909
+ ```typescript
910
+ // lib/dynamic-roles.ts
911
+ import { defineRoles, RoleRegistry } from "secure-role-guard/core";
912
+
913
+ // Interface for database abstraction
914
+ interface RoleFromDB {
915
+ name: string;
916
+ permissions: string[];
917
+ }
918
+
919
+ interface IRoleRepository {
920
+ getAllActiveRoles(): Promise<RoleFromDB[]>;
921
+ }
922
+
923
+ // ============================================================
924
+ // MongoDB Implementation
925
+ // ============================================================
926
+ class MongoRoleRepository implements IRoleRepository {
927
+ async getAllActiveRoles(): Promise<RoleFromDB[]> {
928
+ const roles = await RoleModel.find({ is_active: true })
929
+ .populate("permissions")
930
+ .lean();
931
+
932
+ return roles.map((role) => ({
933
+ name: role.name,
934
+ permissions: role.permissions.map((p: any) => p.code),
935
+ }));
936
+ }
937
+ }
938
+
939
+ // ============================================================
940
+ // PostgreSQL Implementation (using Prisma)
941
+ // ============================================================
942
+ class PostgresRoleRepository implements IRoleRepository {
943
+ async getAllActiveRoles(): Promise<RoleFromDB[]> {
944
+ const roles = await prisma.role.findMany({
945
+ where: { is_active: true },
946
+ include: {
947
+ role_permissions: {
948
+ include: { permission: true },
949
+ },
950
+ },
951
+ });
952
+
953
+ return roles.map((role) => ({
954
+ name: role.name,
955
+ permissions: role.role_permissions.map((rp) => rp.permission.code),
956
+ }));
957
+ }
958
+ }
959
+
960
+ // ============================================================
961
+ // MySQL Implementation (using mysql2)
962
+ // ============================================================
963
+ class MySQLRoleRepository implements IRoleRepository {
964
+ async getAllActiveRoles(): Promise<RoleFromDB[]> {
965
+ const [rows] = await pool.query(`
966
+ SELECT r.name, GROUP_CONCAT(p.code) as permissions
967
+ FROM roles r
968
+ LEFT JOIN role_permissions rp ON r.id = rp.role_id
969
+ LEFT JOIN permissions p ON rp.permission_id = p.id
970
+ WHERE r.is_active = 1
971
+ GROUP BY r.id, r.name
972
+ `);
973
+
974
+ return (rows as any[]).map((row) => ({
975
+ name: row.name,
976
+ permissions: row.permissions ? row.permissions.split(",") : [],
977
+ }));
978
+ }
979
+ }
980
+
981
+ // ============================================================
982
+ // Dynamic Role Registry Factory
983
+ // ============================================================
984
+ let cachedRegistry: RoleRegistry | null = null;
985
+ let cacheExpiry = 0;
986
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
987
+
988
+ export async function getDynamicRoleRegistry(
989
+ repository: IRoleRepository
990
+ ): Promise<RoleRegistry> {
991
+ const now = Date.now();
992
+
993
+ // Return cached if valid
994
+ if (cachedRegistry && now < cacheExpiry) {
995
+ return cachedRegistry;
996
+ }
997
+
998
+ // Fetch from database
999
+ const rolesFromDB = await repository.getAllActiveRoles();
1000
+
1001
+ // Transform to RoleDefinition format
1002
+ const roleDefinition: Record<string, readonly string[]> = {};
1003
+ for (const role of rolesFromDB) {
1004
+ roleDefinition[role.name] = role.permissions;
1005
+ }
1006
+
1007
+ // Create registry
1008
+ cachedRegistry = defineRoles(roleDefinition);
1009
+ cacheExpiry = now + CACHE_TTL;
1010
+
1011
+ return cachedRegistry;
1012
+ }
1013
+
1014
+ // Force refresh (call when admin updates roles)
1015
+ export function invalidateRoleCache(): void {
1016
+ cachedRegistry = null;
1017
+ cacheExpiry = 0;
1018
+ }
1019
+ ```
1020
+
1021
+ #### Backend: Using Dynamic Roles in Express
1022
+
1023
+ ```typescript
1024
+ // server.ts
1025
+ import express from "express";
1026
+ import { canUser } from "secure-role-guard/core";
1027
+ import {
1028
+ getDynamicRoleRegistry,
1029
+ invalidateRoleCache,
1030
+ } from "./lib/dynamic-roles";
1031
+
1032
+ const app = express();
1033
+ const roleRepository = new MongoRoleRepository(); // or PostgresRoleRepository
1034
+
1035
+ // Middleware to attach registry to request
1036
+ app.use(async (req, res, next) => {
1037
+ try {
1038
+ req.roleRegistry = await getDynamicRoleRegistry(roleRepository);
1039
+ next();
1040
+ } catch (error) {
1041
+ console.error("Failed to load roles:", error);
1042
+ res.status(500).json({ error: "Internal server error" });
1043
+ }
1044
+ });
1045
+
1046
+ // Protected routes using dynamic roles
1047
+ app.get("/api/employees", async (req, res) => {
1048
+ if (!canUser(req.user, "employee.read", req.roleRegistry)) {
1049
+ return res.status(403).json({ error: "Forbidden" });
1050
+ }
1051
+
1052
+ const employees = await db.employees.findAll();
1053
+ res.json(employees);
1054
+ });
1055
+
1056
+ // Admin creates a new role
1057
+ app.post("/api/admin/roles", async (req, res) => {
1058
+ if (!canUser(req.user, "role.create", req.roleRegistry)) {
1059
+ return res.status(403).json({ error: "Forbidden" });
1060
+ }
1061
+
1062
+ const { name, permissions } = req.body;
1063
+
1064
+ // Save to database
1065
+ await RoleModel.create({ name, permissions, is_active: true });
1066
+
1067
+ // Invalidate cache so new role is available
1068
+ invalidateRoleCache();
1069
+
1070
+ res.json({ success: true });
1071
+ });
1072
+
1073
+ app.listen(3000);
1074
+ ```
1075
+
1076
+ #### Frontend: Using Dynamic Roles
1077
+
1078
+ ```typescript
1079
+ // lib/auth-context.tsx
1080
+ "use client";
1081
+
1082
+ import { createContext, useContext, useEffect, useState } from "react";
1083
+ import {
1084
+ PermissionProvider,
1085
+ RoleRegistry,
1086
+ defineRoles,
1087
+ } from "secure-role-guard/react";
1088
+
1089
+ interface User {
1090
+ id: string;
1091
+ email: string;
1092
+ roles: string[];
1093
+ permissions: string[];
1094
+ }
1095
+
1096
+ interface AuthContextValue {
1097
+ user: User | null;
1098
+ isLoading: boolean;
1099
+ }
1100
+
1101
+ const AuthContext = createContext<AuthContextValue>({
1102
+ user: null,
1103
+ isLoading: true,
1104
+ });
1105
+
1106
+ export function DynamicAuthProvider({
1107
+ children,
1108
+ }: {
1109
+ children: React.ReactNode;
1110
+ }) {
1111
+ const [user, setUser] = useState<User | null>(null);
1112
+ const [roleRegistry, setRoleRegistry] = useState<RoleRegistry | null>(null);
1113
+ const [isLoading, setIsLoading] = useState(true);
1114
+
1115
+ useEffect(() => {
1116
+ async function loadUserAndRoles() {
1117
+ try {
1118
+ // Fetch current user
1119
+ const userRes = await fetch("/api/auth/me");
1120
+ const userData = await userRes.json();
1121
+
1122
+ if (!userData.user) {
1123
+ setIsLoading(false);
1124
+ return;
1125
+ }
1126
+
1127
+ // Fetch dynamic roles from backend
1128
+ const rolesRes = await fetch("/api/auth/roles");
1129
+ const rolesData = await rolesRes.json();
1130
+
1131
+ // Create registry from dynamic roles
1132
+ // rolesData format: { admin: ['user.read', ...], manager: [...] }
1133
+ const registry = defineRoles(rolesData.roles);
1134
+
1135
+ setUser(userData.user);
1136
+ setRoleRegistry(registry);
1137
+ } catch (error) {
1138
+ console.error("Failed to load auth:", error);
1139
+ } finally {
1140
+ setIsLoading(false);
1141
+ }
1142
+ }
1143
+
1144
+ loadUserAndRoles();
1145
+ }, []);
1146
+
1147
+ if (isLoading) {
1148
+ return <div>Loading...</div>;
1149
+ }
1150
+
1151
+ if (!roleRegistry) {
1152
+ return <div>Failed to load permissions</div>;
1153
+ }
1154
+
1155
+ return (
1156
+ <AuthContext.Provider value={{ user, isLoading }}>
1157
+ <PermissionProvider user={user} registry={roleRegistry}>
1158
+ {children}
1159
+ </PermissionProvider>
1160
+ </AuthContext.Provider>
1161
+ );
1162
+ }
1163
+
1164
+ // -------------------------------------------------------
1165
+ // API Route: Return roles for frontend
1166
+ // app/api/auth/roles/route.ts
1167
+
1168
+ import { NextResponse } from "next/server";
1169
+
1170
+ export async function GET() {
1171
+ // Fetch roles from database
1172
+ const roles = await RoleModel.find({ is_active: true }).lean();
1173
+
1174
+ // Transform to { roleName: permissions[] } format
1175
+ const roleMap: Record<string, string[]> = {};
1176
+ for (const role of roles) {
1177
+ roleMap[role.name] = role.permissions;
1178
+ }
1179
+
1180
+ return NextResponse.json({ roles: roleMap });
1181
+ }
1182
+
1183
+ // -------------------------------------------------------
1184
+ // Usage in Components (same as fixed roles!)
1185
+
1186
+ import { Can, useCan } from "secure-role-guard/react";
1187
+
1188
+ function EmployeeDashboard() {
1189
+ const canApproveLeave = useCan("leave.approve");
1190
+
1191
+ return (
1192
+ <div>
1193
+ <Can permission="employee.read">
1194
+ <EmployeeList />
1195
+ </Can>
1196
+
1197
+ <Can permission="employee.create">
1198
+ <AddEmployeeButton />
1199
+ </Can>
1200
+
1201
+ {canApproveLeave && <LeaveApprovalQueue />}
1202
+ </div>
1203
+ );
1204
+ }
1205
+ ```
1206
+
1207
+ ---
1208
+
1209
+ ### Comparison: Fixed vs Dynamic
1210
+
1211
+ | Feature | Fixed Roles | Dynamic Roles |
1212
+ | ----------------------- | --------------------- | ---------------------------- |
1213
+ | **Setup Complexity** | Simple | More complex |
1214
+ | **Runtime Performance** | Faster (no DB query) | Slight overhead (cached) |
1215
+ | **Flexibility** | Limited | Full flexibility |
1216
+ | **Admin Control** | Code changes required | UI-based role management |
1217
+ | **Use Case** | Simple apps, MVPs | Enterprise, SaaS |
1218
+ | **Role Changes** | Deploy required | Instant (cache invalidation) |
1219
+
1220
+ ### Key Points
1221
+
1222
+ 1. **Package is database-agnostic** - You fetch data, we check permissions
1223
+ 2. **Same API for both approaches** - `canUser()`, `<Can>`, `useCan()` work identically
1224
+ 3. **Frontend mirrors backend** - Keep role definitions in sync
1225
+ 4. **Always validate on backend** - Frontend is for UX, backend is for security
1226
+
1227
+ ---
1228
+
613
1229
  ## Common Mistakes to Avoid
614
1230
 
615
1231
  ### ❌ DON'T: Parse JWT in this package
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secure-role-guard",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Zero-vulnerability, framework-agnostic RBAC authorization library for React applications",
5
5
  "author": "Sohel Rahaman",
6
6
  "license": "MIT",