Skip to content
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
6d69c7f
feat(sub-org): add join sub-organization API endpoint
IgorHorta Mar 4, 2026
f00e2f3
feat(sub-org): add useJoinSubOrganization frontend hook
IgorHorta Mar 4, 2026
5a3d3c2
feat(sub-org): add Sub Orgs tab to organization overview page
IgorHorta Mar 4, 2026
d3b58a8
docs(sub-org): add API reference docs for sub-organization endpoints
IgorHorta Mar 4, 2026
fe74a13
feat(sub-org): add searchable filter to sub-org dropdown menus
IgorHorta Mar 5, 2026
5a906c1
feat(sub-org): improve SubOrgsView UX and permission handling
IgorHorta Mar 5, 2026
c71775a
style(sub-org): increase ellipsis menu button surface area
IgorHorta Mar 5, 2026
5801f90
style(sub-org): apply lint formatting fixes
IgorHorta Mar 5, 2026
b5d3a40
feat(sub-org): add Edit/Delete permissions to OrgPermissionSubOrgActions
IgorHorta Mar 5, 2026
31567e7
feat(sub-org): expose Edit/Delete actions in sub-org role permissions UI
IgorHorta Mar 5, 2026
de92a8b
feat(projects-page): persist selected tab in URL search params
IgorHorta Mar 5, 2026
3ee0cfc
docs(sub-org): rename join.mdx to memberships.mdx to match API route
IgorHorta Mar 5, 2026
7e29e69
docs(sub-org): update memberships.mdx title to match membership patterns
IgorHorta Mar 5, 2026
968941f
refactor(navbar): extract SubOrgFilterList to deduplicate sub-org dro…
IgorHorta Mar 5, 2026
366e7ec
fix(sub-org): throw error when joining a sub-org the user is already …
IgorHorta Mar 5, 2026
737f663
fix: issue where user without permission to add new suborg would see …
IgorHorta Mar 5, 2026
bdc2d13
fix(sub-orgs): handle MFA flow when logging into a sub-organization
IgorHorta Mar 5, 2026
63b9f3a
fix(sub-orgs): hide Add Sub Org button for users without Create permi…
IgorHorta Mar 5, 2026
b5c3080
feat(sub-org): add server-side search, ordering, and totalCount to li…
IgorHorta Mar 5, 2026
3fe8e77
feat(sub-org): migrate SubOrgsView to server-side search, ordering, a…
IgorHorta Mar 5, 2026
ddf5c11
refactor(projects-page): remove sub-org tab and restore to projects-o…
IgorHorta Mar 5, 2026
82ee7ec
feat(sub-org): add Sub Organizations tab to organization settings
IgorHorta Mar 5, 2026
1edcc60
fix(sub-org): default join role to Member, fix isAccessible transform…
IgorHorta Mar 5, 2026
16a83d8
revert(sub-org): align isAccessible transform with codebase pattern
IgorHorta Mar 5, 2026
d5561ab
fix(sub-org): prevent gated tab from being active without a rendered …
IgorHorta Mar 5, 2026
41ac154
fix(lint): sort imports in Navbar and fix prettier in OrgTabGroup
IgorHorta Mar 5, 2026
4546ac9
fix(pagination): skip page reset when totalCount is 0 on initial load
IgorHorta Mar 5, 2026
e8d11b0
fix(sub-org): handle unhandled promise rejection on sub-org login click
IgorHorta Mar 5, 2026
b47b299
fix(lint): fix import sort order in Navbar
IgorHorta Mar 5, 2026
bc29752
Merge branch 'main' into igor/platfrm-216-address-sub-org-menu-select…
IgorHorta Mar 6, 2026
327c126
feat(sub-org): add operationId to all sub-organization routes
IgorHorta Mar 6, 2026
09a2eb1
refactor(sub-org): rename permissionActor to permission
IgorHorta Mar 6, 2026
ed4dafb
refactor(sub-org): use typed enums for orderBy and orderDirection
IgorHorta Mar 6, 2026
7935269
refactor(sub-org): use typed enums for orderBy/orderDirection in fron…
IgorHorta Mar 6, 2026
640f5c3
fix(useResetPageHelper): revert totalCount > 0 guard
IgorHorta Mar 6, 2026
5675f69
fix(sub-org): check parent org permissions for update and delete oper…
IgorHorta Mar 6, 2026
79d03b7
docs: add edit and delete actions to sub-organization permissions
IgorHorta Mar 6, 2026
18d0e06
refactor(sub-org): embed permission-gated action in SubOrgFilterList
IgorHorta Mar 6, 2026
4e52694
refactor(sub-org): migrate OrgSubOrgsTab to v3 components with permis…
IgorHorta Mar 6, 2026
081f030
fix(sub-org): use CASL_ACTION_SCHEMA_NATIVE_ENUM for sub-org permissions
IgorHorta Mar 6, 2026
4adedce
feat(sub-org): display sub-organization ID in settings page
IgorHorta Mar 6, 2026
606fd3c
fix(sub-org): show Edit/Delete actions regardless of membership status
IgorHorta Mar 6, 2026
2e861e3
fix(sub-org): add license check to joinSubOrg
IgorHorta Mar 6, 2026
c020527
style(sub-org): fix prettier formatting on orderDirection schema
IgorHorta Mar 6, 2026
a4fae32
fix(sub-org): remove hardcoded limit from navbar sub-org query
IgorHorta Mar 6, 2026
18eed97
fix: bad lint
IgorHorta Mar 6, 2026
11e5ada
revert(license): restore default on-prem feature flags to false
IgorHorta Mar 6, 2026
91fc49c
fix: lint
IgorHorta Mar 6, 2026
ca4ed0e
refactor(sub-org): move Join button into Status column
IgorHorta Mar 6, 2026
57cb462
fix(sub-org): remove duplicate error notifications
IgorHorta Mar 6, 2026
7b3fbef
chore: revert tmp directory gitignore entry
IgorHorta Mar 6, 2026
08e6202
fix: prettier formatting in OrgSubOrgsTab
IgorHorta Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 117 additions & 6 deletions backend/src/ee/routes/v1/sub-org-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { z } from "zod";

import { OrganizationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SubOrgOrderBy } from "@app/ee/services/sub-org/sub-org-types";
import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs";
import { pick } from "@app/lib/fn";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { GenericResourceNameSchema, slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
Expand All @@ -27,6 +29,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
},
schema: {
hide: false,
operationId: "createSubOrganization",
tags: [ApiDocsTags.SubOrganizations],
description: "Create a sub organization",
security: [
Expand All @@ -49,7 +52,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
const { organization } = await server.services.subOrganization.createSubOrg({
name: req.body.name,
slug: req.body.slug,
permissionActor: req.permission
permission: req.permission
});

await server.services.auditLog.createAuditLog({
Expand All @@ -76,6 +79,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
},
schema: {
hide: false,
operationId: "listSubOrganizations",
tags: [ApiDocsTags.SubOrganizations],
description: "List of sub organizations",
security: [
Expand All @@ -86,6 +90,12 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
limit: z.coerce.number().min(1).max(1000).default(25).describe(SUB_ORGANIZATIONS.LIST.limit),
offset: z.coerce.number().min(0).default(0).describe(SUB_ORGANIZATIONS.LIST.offset),
search: z.string().trim().optional().describe(SUB_ORGANIZATIONS.LIST.search),
orderBy: z.nativeEnum(SubOrgOrderBy).default(SubOrgOrderBy.Name).describe(SUB_ORGANIZATIONS.LIST.orderBy),
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(SUB_ORGANIZATIONS.LIST.orderDirection),
isAccessible: z
.enum(["true", "false"])
.optional()
Expand All @@ -94,22 +104,26 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
organizations: sanitizedSubOrganizationSchema.array()
organizations: sanitizedSubOrganizationSchema.array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { organizations } = await server.services.subOrganization.listSubOrgs({
permissionActor: req.permission,
const { organizations, totalCount } = await server.services.subOrganization.listSubOrgs({
permission: req.permission,
data: {
limit: req.query.limit,
offset: req.query.offset,
search: req.query.search,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
isAccessible: req.query.isAccessible
}
});

return { organizations };
return { organizations, totalCount };
}
});

Expand All @@ -121,6 +135,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
},
schema: {
hide: false,
operationId: "updateSubOrganization",
tags: [ApiDocsTags.SubOrganizations],
description: "Update a sub organization",
security: [
Expand Down Expand Up @@ -151,7 +166,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
subOrgId: req.params.subOrgId,
name: req.body.name,
slug: req.body.slug,
permissionActor: req.permission
permission: req.permission
});

await server.services.auditLog.createAuditLog({
Expand All @@ -169,4 +184,100 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
return { organization };
}
});

server.route({
method: "POST",
url: "/:subOrgId/memberships",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
operationId: "createSubOrganizationMembership",
tags: [ApiDocsTags.SubOrganizations],
description: "Join a sub organization",
security: [
{
bearerAuth: []
}
],
params: z.object({
subOrgId: z.string().trim().describe(SUB_ORGANIZATIONS.JOIN.subOrgId)
}),
response: {
200: z.object({
organization: sanitizedSubOrganizationSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { organization } = await server.services.subOrganization.joinSubOrg({
subOrgId: req.params.subOrgId,
permission: req.permission
});

await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.JOIN_SUB_ORGANIZATION,
metadata: {
...pick(organization, ["name", "slug"]),
organizationId: organization.id
}
}
});

return { organization };
}
});

server.route({
method: "DELETE",
url: "/:subOrgId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
operationId: "deleteSubOrganization",
tags: [ApiDocsTags.SubOrganizations],
description: "Delete a sub organization",
security: [
{
bearerAuth: []
}
],
params: z.object({
subOrgId: z.string().trim().describe(SUB_ORGANIZATIONS.DELETE.subOrgId)
}),
response: {
200: z.object({
organization: sanitizedSubOrganizationSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { organization } = await server.services.subOrganization.deleteSubOrg({
subOrgId: req.params.subOrgId,
permission: req.permission
});

await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_SUB_ORGANIZATION,
metadata: {
...pick(organization, ["name", "slug"]),
organizationId: organization.id
}
}
});

return { organization };
}
});
};
22 changes: 22 additions & 0 deletions backend/src/ee/services/audit-log/audit-log-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ export enum EventType {

CREATE_SUB_ORGANIZATION = "create-sub-organization",
UPDATE_SUB_ORGANIZATION = "update-sub-organization",
DELETE_SUB_ORGANIZATION = "delete-sub-organization",
JOIN_SUB_ORGANIZATION = "join-sub-organization",

CREATE_IDENTITY = "create-identity",
UPDATE_IDENTITY = "update-identity",
Expand Down Expand Up @@ -805,6 +807,24 @@ interface UpdateSubOrganizationEvent {
};
}

interface DeleteSubOrganizationEvent {
type: EventType.DELETE_SUB_ORGANIZATION;
metadata: {
name: string;
slug: string;
organizationId: string;
};
}

interface JoinSubOrganizationEvent {
type: EventType.JOIN_SUB_ORGANIZATION;
metadata: {
name: string;
slug: string;
organizationId: string;
};
}

type TSecretMetadata = { key: string; value: string }[];

interface GetSecretEvent {
Expand Down Expand Up @@ -5186,6 +5206,8 @@ interface ListDynamicSecretLeasesEvent {
export type Event =
| CreateSubOrganizationEvent
| UpdateSubOrganizationEvent
| DeleteSubOrganizationEvent
| JoinSubOrganizationEvent
| GetSecretsEvent
| GetSecretEvent
| CreateSecretEvent
Expand Down
13 changes: 7 additions & 6 deletions backend/src/ee/services/permission/org-permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export enum OrgPermissionActions {

export enum OrgPermissionSubOrgActions {
Create = "create",
Edit = "edit",
Delete = "delete",
DirectAccess = "direct-access",
LinkGroup = "link-group"
}
Expand Down Expand Up @@ -196,12 +198,9 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
}),
z.object({
subject: z.literal(OrgPermissionSubjects.SubOrganization).describe("The entity this permission pertains to."),
// Use CASL_ACTION_SCHEMA_ENUM so OpenAPI anyOf structure matches reference (string enum + array), avoiding oasdiff breaking change
action: CASL_ACTION_SCHEMA_ENUM([
OrgPermissionSubOrgActions.Create,
OrgPermissionSubOrgActions.DirectAccess,
OrgPermissionSubOrgActions.LinkGroup
]).describe("Describe what action an entity can take.")
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionSubOrgActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."),
Expand Down Expand Up @@ -328,6 +327,8 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Create, OrgPermissionSubjects.Project);

can(OrgPermissionSubOrgActions.Create, OrgPermissionSubjects.SubOrganization);
can(OrgPermissionSubOrgActions.Edit, OrgPermissionSubjects.SubOrganization);
can(OrgPermissionSubOrgActions.Delete, OrgPermissionSubjects.SubOrganization);
can(OrgPermissionSubOrgActions.DirectAccess, OrgPermissionSubjects.SubOrganization);
can(OrgPermissionSubOrgActions.LinkGroup, OrgPermissionSubjects.SubOrganization);

Expand Down
Loading
Loading