From 35fbe6a014568a39640581985e7608a772094be4 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Thu, 10 Apr 2025 01:17:40 +0000 Subject: [PATCH] Implement custom role-based access control. Adds support for creating and managing custom roles, groups, and group memberships. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/c73adeb7-0636-4b42-81ad-b861016d2129.jpg --- client/src/pages/api-tokens.tsx | 4 +- client/src/pages/users-roles.tsx | 10 +-- shared/schema.ts | 145 ++++++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 9 deletions(-) diff --git a/client/src/pages/api-tokens.tsx b/client/src/pages/api-tokens.tsx index b841317..f6741a9 100644 --- a/client/src/pages/api-tokens.tsx +++ b/client/src/pages/api-tokens.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { MainLayout } from "@/components/layouts/main-layout"; -import { ApiToken, InsertApiToken, userRoles } from "@shared/schema"; +import { ApiToken, InsertApiToken, systemRoles } from "@shared/schema"; import { useAuth } from "@/hooks/use-auth"; import { useOrganization } from "@/context/organization-context"; import { apiRequest, queryClient } from "@/lib/queryClient"; @@ -337,7 +337,7 @@ export default function ApiTokensPage() { Select the permissions for this token - {userRoles.map((role) => ( + {systemRoles.map((role) => ( ({ resolver: zodResolver(z.object({ - role: z.enum(userRoles as [string, ...string[]]), + role: z.enum(systemRoles as [string, ...string[]]), })), defaultValues: { role: "user", @@ -551,7 +551,7 @@ export default function UsersRolesPage() { - {userRoles.map(role => ( + {systemRoles.map(role => ( {role.charAt(0).toUpperCase() + role.slice(1)} @@ -642,7 +642,7 @@ export default function UsersRolesPage() { - {userRoles.map(role => ( + {systemRoles.map(role => ( {role.charAt(0).toUpperCase() + role.slice(1)} diff --git a/shared/schema.ts b/shared/schema.ts index 36619db..588d3c7 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -89,6 +89,52 @@ export const apiTokens = pgTable("api_tokens", { expiresAt: timestamp("expires_at"), }); +// Custom roles table for user-defined roles beyond system defaults +export const customRoles = pgTable("custom_roles", { + id: uuid("id").defaultRandom().primaryKey(), + name: text("name").notNull().unique(), + description: text("description"), + permissions: text("permissions").array().notNull(), + isActive: boolean("is_active").default(true).notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + createdBy: uuid("created_by").notNull().references(() => users.id, { onDelete: "set null" }), +}); + +// Groups can contain users, organizations, or other groups +export const groups = pgTable("groups", { + id: uuid("id").defaultRandom().primaryKey(), + name: text("name").notNull(), + description: text("description"), + isActive: boolean("is_active").default(true).notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + createdBy: uuid("created_by").notNull().references(() => users.id, { onDelete: "set null" }), + parentGroupId: uuid("parent_group_id"), +}); + +// Group members - can be users, organizations, or other groups +export const groupMembers = pgTable("group_members", { + id: uuid("id").defaultRandom().primaryKey(), + groupId: uuid("group_id").notNull().references(() => groups.id, { onDelete: "cascade" }), + // Specify the type of member: "user", "organization", or "group" + memberType: text("member_type").notNull(), + // ID of the member (user, organization, or group) + memberId: uuid("member_id").notNull(), + addedAt: timestamp("added_at").defaultNow().notNull(), + addedBy: uuid("added_by").notNull().references(() => users.id, { onDelete: "set null" }), +}); + +// Group role assignments - associates groups with roles +export const groupRoles = pgTable("group_roles", { + id: uuid("id").defaultRandom().primaryKey(), + groupId: uuid("group_id").notNull().references(() => groups.id, { onDelete: "cascade" }), + // Can be either a system role (string) or a custom role ID (uuid) + roleId: text("role_id").notNull(), + // Indicates if this is a system role or a custom role + isSystemRole: boolean("is_system_role").notNull(), + assignedAt: timestamp("assigned_at").defaultNow().notNull(), + assignedBy: uuid("assigned_by").notNull().references(() => users.id, { onDelete: "set null" }), +}); + // Schema Validation export const insertUserSchema = createInsertSchema(users).pick({ username: true, @@ -140,6 +186,36 @@ export const insertApiTokenSchema = createInsertSchema(apiTokens).pick({ expiresAt: true, }); +export const insertCustomRoleSchema = createInsertSchema(customRoles).pick({ + name: true, + description: true, + permissions: true, + isActive: true, + createdBy: true, +}); + +export const insertGroupSchema = createInsertSchema(groups).pick({ + name: true, + description: true, + isActive: true, + createdBy: true, + parentGroupId: true, +}); + +export const insertGroupMemberSchema = createInsertSchema(groupMembers).pick({ + groupId: true, + memberType: true, + memberId: true, + addedBy: true, +}); + +export const insertGroupRoleSchema = createInsertSchema(groupRoles).pick({ + groupId: true, + roleId: true, + isSystemRole: true, + assignedBy: true, +}); + // Types export type InsertUser = z.infer; export type User = typeof users.$inferSelect; @@ -160,10 +236,27 @@ export type InsertApiToken = z.infer; export type ApiToken = typeof apiTokens.$inferSelect; export type DnsHistory = typeof dnsHistory.$inferSelect; +export type CustomRole = typeof customRoles.$inferSelect; +export type Group = typeof groups.$inferSelect; +export type GroupMember = typeof groupMembers.$inferSelect; +export type GroupRole = typeof groupRoles.$inferSelect; + +export type InsertCustomRole = z.infer; +export type InsertGroup = z.infer; +export type InsertGroupMember = z.infer; +export type InsertGroupRole = z.infer; // Role Types -export const userRoles = ['admin', 'manager', 'user', 'readonly'] as const; -export type UserRole = typeof userRoles[number]; +// System default roles - these will still be available alongside custom roles +export const systemRoles = ['admin', 'manager', 'user', 'readonly'] as const; +export type SystemRole = typeof systemRoles[number]; + +// User roles can be system roles or custom roles +export type UserRole = SystemRole | string; + +// Define member types for group members +export const memberTypes = ['user', 'organization', 'group'] as const; +export type MemberType = typeof memberTypes[number]; // Provider Types export const providerTypes = ['cloudflare', 'route53', 'godaddy', 'other'] as const; @@ -225,3 +318,51 @@ export const apiTokensRelations = relations(apiTokens, ({ one }) => ({ references: [users.id], }), })); + +// Custom roles relations +export const customRolesRelations = relations(customRoles, ({ one, many }) => ({ + creator: one(users, { + fields: [customRoles.createdBy], + references: [users.id], + }), + groupRoles: many(groupRoles), +})); + +// Group relations +export const groupsRelations = relations(groups, ({ one, many }) => ({ + creator: one(users, { + fields: [groups.createdBy], + references: [users.id], + }), + parentGroup: one(groups, { + fields: [groups.parentGroupId], + references: [groups.id], + relationName: "parentGroup", + }), + members: many(groupMembers), + roles: many(groupRoles), +})); + +// Group members relations +export const groupMembersRelations = relations(groupMembers, ({ one }) => ({ + group: one(groups, { + fields: [groupMembers.groupId], + references: [groups.id], + }), + addedByUser: one(users, { + fields: [groupMembers.addedBy], + references: [users.id], + }), +})); + +// Group roles relations +export const groupRolesRelations = relations(groupRoles, ({ one }) => ({ + group: one(groups, { + fields: [groupRoles.groupId], + references: [groups.id], + }), + assignedByUser: one(users, { + fields: [groupRoles.assignedBy], + references: [users.id], + }), +}));