diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index c4d45ca27..a974d90b7 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -17,6 +17,9 @@ import { TAccessApprovalRequestsReviewersInsert, TAccessApprovalRequestsReviewersUpdate, TAccessApprovalRequestsUpdate, + TAdditionalPrivileges, + TAdditionalPrivilegesInsert, + TAdditionalPrivilegesUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -227,6 +230,15 @@ import { TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate, + TMembershipRoles, + TMembershipRolesInsert, + TMembershipRolesUpdate, + TMemberships, + TMembershipsInsert, + TMembershipsUpdate, + TNamespaces, + TNamespacesInsert, + TNamespacesUpdate, TOidcConfigs, TOidcConfigsInsert, TOidcConfigsUpdate, @@ -314,6 +326,9 @@ import { TResourceMetadata, TResourceMetadataInsert, TResourceMetadataUpdate, + TRoles, + TRolesInsert, + TRolesUpdate, TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, @@ -1316,5 +1331,19 @@ declare module "knex/types/tables" { [TableName.PamResource]: KnexOriginal.CompositeTableType; [TableName.PamAccount]: KnexOriginal.CompositeTableType; [TableName.PamSession]: KnexOriginal.CompositeTableType; + + [TableName.Namespace]: KnexOriginal.CompositeTableType; + [TableName.Membership]: KnexOriginal.CompositeTableType; + [TableName.MembershipRoles]: KnexOriginal.CompositeTableType< + TMembershipRoles, + TMembershipRolesInsert, + TMembershipRolesUpdate + >; + [TableName.Roles]: KnexOriginal.CompositeTableType; + [TableName.AdditionalPrivilege]: KnexOriginal.CompositeTableType< + TAdditionalPrivileges, + TAdditionalPrivilegesInsert, + TAdditionalPrivilegesUpdate + >; } } diff --git a/backend/src/db/migrations/20250925145952_simplify-membership.ts b/backend/src/db/migrations/20250925145952_simplify-membership.ts new file mode 100644 index 000000000..dfe2b5b2d --- /dev/null +++ b/backend/src/db/migrations/20250925145952_simplify-membership.ts @@ -0,0 +1,166 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.Namespace))) { + await knex.schema.createTable(TableName.Namespace, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.string("description"); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.Namespace); + } + + if (!(await knex.schema.hasTable(TableName.Membership))) { + await knex.schema.createTable(TableName.Membership, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("scope", 24).notNullable(); + + t.uuid("actorUserId"); + t.foreign("actorUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.uuid("actorIdentityId"); + t.foreign("actorIdentityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.uuid("actorGroupId"); + t.foreign("actorGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); + + t.uuid("scopeOrgId").notNullable(); + t.foreign("scopeOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("scopeProjectId", 36); + t.foreign("scopeProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("scopeNamespaceId"); + t.foreign("scopeNamespaceId").references("id").inTable(TableName.Namespace).onDelete("CASCADE"); + + t.boolean("isActive"); + t.string("status").defaultTo("invited"); + t.string("inviteEmail"); + t.datetime("lastInvitedAt"); + t.datetime("lastLoginAuthMethod"); + t.datetime("lastLoginTime"); + t.specificType("projectFavorites", "text[]"); + t.timestamps(true, true, true); + + t.check( + `(:actorUserIdColumn: IS NOT NULL AND :actorIdentityIdColumn: IS NULL AND :actorGroupIdColumn: IS NULL) OR + (:actorIdentityIdColumn: IS NOT NULL AND :actorUserIdColumn: IS NULL AND :actorGroupIdColumn: IS NULL) OR + (:actorGroupIdColumn: IS NOT NULL AND :actorUserIdColumn: IS NULL AND :actorIdentityIdColumn: IS NULL)`, + { + actorUserIdColumn: "actorUserId", + actorIdentityIdColumn: "actorIdentityId", + actorGroupIdColumn: "actorGroupId" + }, + "only_one_actor_type" + ); + + t.check( + `(:scopeColumn: = 'namespace' AND :scopeNamespaceIdColumn: IS NOT NULL) OR + (:scopeColumn: = 'project' AND :scopeProjectIdColumn: IS NOT NULL) OR + (:scopeColumn: = 'organization') + `, + { + scopeColumn: "scope", + scopeNamespaceIdColumn: "scopeNamespaceId", + scopeProjectIdColumn: "scopeProjectId" + }, + "scope_matches_id" + ); + }); + + await createOnUpdateTrigger(knex, TableName.Membership); + } + + if (!(await knex.schema.hasTable(TableName.Role))) { + await knex.schema.createTable(TableName.Role, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.string("description"); + t.string("slug").notNullable(); + t.jsonb("permissions").notNullable(); + + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("projectId", 36); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("namespaceId"); + t.foreign("namespaceId").references("id").inTable(TableName.Namespace).onDelete("CASCADE"); + + t.check( + `(:orgIdColumn: IS NOT NULL AND :namespaceIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:namespaceIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:projectIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :namespaceIdColumn: IS NULL)`, + { + orgIdColumn: "orgId", + namespaceIdColumn: "namespaceId", + projectIdColumn: "projectId" + }, + "only_one_scope_id" + ); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.Role); + } + + if (!(await knex.schema.hasTable(TableName.MembershipRole))) { + await knex.schema.createTable(TableName.MembershipRole, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("role").notNullable(); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + + t.uuid("customRoleId"); + t.foreign("customRoleId").references("id").inTable(TableName.Role); + t.uuid("membershipId").notNullable(); + t.foreign("membershipId").references("id").inTable(TableName.Membership).onDelete("CASCADE"); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.MembershipRole); + } + + if (!(await knex.schema.hasTable(TableName.AdditionalPrivilege))) { + await knex.schema.createTable(TableName.AdditionalPrivilege, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name", 60).notNullable(); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + t.jsonb("permissions").notNullable(); + + t.uuid("membershipId").notNullable(); + t.foreign("membershipId").references("id").inTable(TableName.Membership).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.AdditionalPrivilege); + } +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.AdditionalPrivilege); + await knex.schema.dropTableIfExists(TableName.AdditionalPrivilege); + + await dropOnUpdateTrigger(knex, TableName.MembershipRole); + await knex.schema.dropTableIfExists(TableName.MembershipRole); + + await dropOnUpdateTrigger(knex, TableName.Membership); + await knex.schema.dropTableIfExists(TableName.Membership); + + await dropOnUpdateTrigger(knex, TableName.Role); + await knex.schema.dropTableIfExists(TableName.Role); + + await dropOnUpdateTrigger(knex, TableName.Namespace); + await knex.schema.dropTableIfExists(TableName.Namespace); +} diff --git a/backend/src/db/schemas/additional-privileges.ts b/backend/src/db/schemas/additional-privileges.ts new file mode 100644 index 000000000..887148554 --- /dev/null +++ b/backend/src/db/schemas/additional-privileges.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AdditionalPrivilegesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + permissions: z.unknown(), + membershipId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAdditionalPrivileges = z.infer; +export type TAdditionalPrivilegesInsert = Omit, TImmutableDBKeys>; +export type TAdditionalPrivilegesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index f8ac885b4..2a10e0f1b 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -3,6 +3,7 @@ export * from "./access-approval-policies-approvers"; export * from "./access-approval-policies-bypassers"; export * from "./access-approval-requests"; export * from "./access-approval-requests-reviewers"; +export * from "./additional-privileges"; export * from "./api-keys"; export * from "./app-connections"; export * from "./audit-log-streams"; @@ -73,8 +74,11 @@ export * from "./kms-keys"; export * from "./kms-root-config"; export * from "./ldap-configs"; export * from "./ldap-group-maps"; +export * from "./membership-roles"; +export * from "./memberships"; export * from "./microsoft-teams-integrations"; export * from "./models"; +export * from "./namespaces"; export * from "./oidc-configs"; export * from "./org-bots"; export * from "./org-gateway-config"; @@ -108,6 +112,7 @@ export * from "./projects"; export * from "./rate-limit"; export * from "./relays"; export * from "./resource-metadata"; +export * from "./roles"; export * from "./saml-configs"; export * from "./scim-tokens"; export * from "./secret-approval-policies"; diff --git a/backend/src/db/schemas/membership-roles.ts b/backend/src/db/schemas/membership-roles.ts new file mode 100644 index 000000000..98927c9cb --- /dev/null +++ b/backend/src/db/schemas/membership-roles.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const MembershipRolesSchema = z.object({ + id: z.string().uuid(), + role: z.string(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + customRoleId: z.string().uuid().nullable().optional(), + membershipId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TMembershipRoles = z.infer; +export type TMembershipRolesInsert = Omit, TImmutableDBKeys>; +export type TMembershipRolesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/memberships.ts b/backend/src/db/schemas/memberships.ts new file mode 100644 index 000000000..2179bce6e --- /dev/null +++ b/backend/src/db/schemas/memberships.ts @@ -0,0 +1,32 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const MembershipsSchema = z.object({ + id: z.string().uuid(), + scope: z.string(), + actorUserId: z.string().uuid().nullable().optional(), + actorIdentityId: z.string().uuid().nullable().optional(), + actorGroupId: z.string().uuid().nullable().optional(), + scopeOrgId: z.string().uuid(), + scopeProjectId: z.string().nullable().optional(), + scopeNamespaceId: z.string().uuid().nullable().optional(), + isActive: z.boolean().nullable().optional(), + status: z.string().default("invited").nullable().optional(), + inviteEmail: z.string().nullable().optional(), + lastInvitedAt: z.date().nullable().optional(), + lastLoginAuthMethod: z.date().nullable().optional(), + lastLoginTime: z.date().nullable().optional(), + projectFavorites: z.string().array().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TMemberships = z.infer; +export type TMembershipsInsert = Omit, TImmutableDBKeys>; +export type TMembershipsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 09ecb367a..b4321b669 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -178,6 +178,14 @@ export enum TableName { SecretScanningScan = "secret_scanning_scans", SecretScanningFinding = "secret_scanning_findings", SecretScanningConfig = "secret_scanning_configs", + + Membership = "memberships", + MembershipRole = "membership_roles", + Role = "roles", + AdditionalPrivilege = "additional_privileges", + + Namespace = "namespaces", + // reminders Reminder = "reminders", ReminderRecipient = "reminders_recipients", diff --git a/backend/src/db/schemas/namespaces.ts b/backend/src/db/schemas/namespaces.ts new file mode 100644 index 000000000..b9f092fd3 --- /dev/null +++ b/backend/src/db/schemas/namespaces.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const NamespacesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TNamespaces = z.infer; +export type TNamespacesInsert = Omit, TImmutableDBKeys>; +export type TNamespacesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/roles.ts b/backend/src/db/schemas/roles.ts new file mode 100644 index 000000000..4801a57a6 --- /dev/null +++ b/backend/src/db/schemas/roles.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const RolesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + slug: z.string(), + permissions: z.unknown(), + orgId: z.string().uuid().nullable().optional(), + projectId: z.string().nullable().optional(), + namespaceId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TRoles = z.infer; +export type TRolesInsert = Omit, TImmutableDBKeys>; +export type TRolesUpdate = Partial, TImmutableDBKeys>>;