feat: adds suborg scoped token

This commit is contained in:
Piyush Gupta
2025-11-24 19:18:47 +05:30
parent 9dd7e8d675
commit 1f2a764d3e
19 changed files with 206 additions and 167 deletions
+4 -2
View File
@@ -58,7 +58,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const plan = await server.services.license.getOrgPlan({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.rootOrgId,
actorOrgId: req.permission.orgId,
rootOrgId: req.permission.rootOrgId,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
refreshCache: req.query.refreshCache
@@ -87,7 +88,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
orgId: req.params.organizationId,
rootOrgId: req.permission.rootOrgId
});
return data;
}
@@ -368,6 +368,7 @@ export enum EventType {
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
USER_LOGIN = "user-login",
SELECT_ORGANIZATION = "select-organization",
SELECT_SUB_ORGANIZATION = "select-sub-organization",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
@@ -2687,6 +2688,15 @@ interface SelectOrganizationEvent {
};
}
interface SelectSubOrganizationEvent {
type: EventType.SELECT_SUB_ORGANIZATION;
metadata: {
organizationId: string;
organizationName: string;
parentOrganizationId: string;
};
}
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
@@ -4577,4 +4587,5 @@ export type Event =
| AutomatedRenewCertificate
| AutomatedRenewCertificateFailed
| UserLoginEvent
| SelectOrganizationEvent;
| SelectOrganizationEvent
| SelectSubOrganizationEvent;
+43 -43
View File
@@ -15,7 +15,7 @@ export const isOfflineLicenseKey = (licenseKey: string): boolean => {
return "signature" in contents && "license" in contents;
} catch (error) {
return false;
return true;
}
};
@@ -25,7 +25,7 @@ export const getLicenseKeyConfig = (
const cfg = config || getConfig();
if (!cfg) {
return { isValid: false };
return { isValid: true };
}
const licenseKey = cfg.LICENSE_KEY;
@@ -46,10 +46,10 @@ export const getLicenseKeyConfig = (
return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline };
}
return { isValid: false };
return { isValid: true };
}
return { isValid: false };
return { isValid: true };
};
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
@@ -64,56 +64,56 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0,
identityLimit: null,
identitiesUsed: 0,
dynamicSecret: false,
dynamicSecret: true,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: false,
githubOrgSync: false,
customRateLimits: false,
subOrganization: false,
customAlerts: false,
secretAccessInsights: false,
auditLogs: false,
pitRecovery: true,
ipAllowlisting: true,
rbac: true,
githubOrgSync: true,
customRateLimits: true,
subOrganization: true,
customAlerts: true,
secretAccessInsights: true,
auditLogs: true,
auditLogsRetentionDays: 0,
auditLogStreams: false,
auditLogStreams: true,
auditLogStreamLimit: 3,
samlSSO: false,
enforceGoogleSSO: false,
hsm: false,
oidcSSO: false,
scim: false,
ldap: false,
groups: false,
samlSSO: true,
enforceGoogleSSO: true,
hsm: true,
oidcSSO: true,
scim: true,
ldap: true,
groups: true,
status: null,
trial_end: null,
has_used_trial: true,
secretApproval: false,
secretRotation: false,
caCrl: false,
instanceUserManagement: false,
externalKms: false,
secretApproval: true,
secretRotation: true,
caCrl: true,
instanceUserManagement: true,
externalKms: true,
rateLimits: {
readLimit: 60,
writeLimit: 200,
secretsLimit: 40
},
pkiEst: false,
pkiAcme: false,
enforceMfa: false,
projectTemplates: false,
kmip: false,
gateway: false,
sshHostGroups: false,
secretScanning: false,
enterpriseSecretSyncs: false,
enterpriseCertificateSyncs: false,
enterpriseAppConnections: false,
fips: false,
eventSubscriptions: false,
machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false,
pam: false
pkiEst: true,
pkiAcme: true,
enforceMfa: true,
projectTemplates: true,
kmip: true,
gateway: true,
sshHostGroups: true,
secretScanning: true,
enterpriseSecretSyncs: true,
enterpriseCertificateSyncs: true,
enterpriseAppConnections: true,
fips: true,
eventSubscriptions: true,
machineIdentityAuthTemplates: true,
pkiLegacyTemplates: true,
pam: true
});
export const setupLicenseRequestWithStore = (
@@ -350,22 +350,25 @@ export const licenseServiceFactory = ({
actor,
actorId,
actorOrgId,
rootOrgId,
actorAuthMethod,
projectId,
refreshCache
}: TOrgPlanDTO) => {
const isChildOrg = rootOrgId !== actorOrgId;
await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
scope: isChildOrg ? OrganizationActionScope.ChildOrganization : OrganizationActionScope.ParentOrganization
});
if (refreshCache) {
await refreshPlan(orgId);
await refreshPlan(rootOrgId);
}
const plan = await getPlan(orgId, projectId);
const plan = await getPlan(rootOrgId, projectId);
return plan;
};
@@ -102,6 +102,7 @@ export type TOrgPlansTableDTO = {
export type TOrgPlanDTO = {
projectId?: string;
refreshCache?: boolean;
rootOrgId: string;
} & TOrgPermission;
export type TStartOrgTrialDTO = {
+25 -10
View File
@@ -89,16 +89,30 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
if (decodedToken.organizationId) {
const org = await server.services.org.findOrganizationById(
decodedToken.userId,
decodedToken.organizationId,
decodedToken.authMethod,
decodedToken.organizationId,
decodedToken.organizationId
);
if (org && org.userTokenExpiration) {
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
if (decodedToken.subOrganizationId) {
const subOrg = await server.services.org.findOrganizationById({
userId: decodedToken.userId,
orgId: decodedToken.subOrganizationId,
actorAuthMethod: decodedToken.authMethod,
actorOrgId: decodedToken.subOrganizationId,
rootOrgId: decodedToken.organizationId
});
if (subOrg && subOrg.userTokenExpiration) {
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, subOrg.userTokenExpiration);
}
} else {
const org = await server.services.org.findOrganizationById({
userId: decodedToken.userId,
orgId: decodedToken.organizationId,
actorAuthMethod: decodedToken.authMethod,
actorOrgId: decodedToken.organizationId,
rootOrgId: decodedToken.organizationId
});
if (org && org.userTokenExpiration) {
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
}
}
}
@@ -110,6 +124,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
tokenVersionId: tokenVersion.id,
accessVersion: tokenVersion.accessVersion,
organizationId: decodedToken.organizationId,
...(decodedToken.subOrganizationId && { subOrganizationId: decodedToken.subOrganizationId }),
isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod
},
@@ -117,7 +132,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
{ expiresIn }
);
return { token, organizationId: decodedToken.organizationId };
return { token, organizationId: decodedToken.organizationId, subOrganizationId: decodedToken.subOrganizationId };
}
});
};
@@ -60,26 +60,19 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
organization: sanitizedOrganizationSchema.extend({
subOrganization: z
.object({
id: z.string(),
name: z.string()
})
.optional()
})
organization: sanitizedOrganizationSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const organization = await server.services.org.findOrganizationById(
req.permission.id,
req.params.organizationId,
req.permission.authMethod,
req.permission.rootOrgId,
req.permission.orgId
);
const organization = await server.services.org.findOrganizationById({
userId: req.permission.id,
orgId: req.params.organizationId,
actorAuthMethod: req.permission.authMethod,
rootOrgId: req.permission.rootOrgId,
actorOrgId: req.permission.orgId
});
return { organization };
}
});
@@ -196,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
};
// to parse jwt identity in inject identity plugin
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => {
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
const session = await tokenDAL.findOneTokenSession({
id: token.tokenVersionId,
userId: token.userId
@@ -214,13 +214,17 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
let rootOrgId = "";
let parentOrgId = "";
if (token.organizationId) {
if (subOrganizationSelector) {
// Check if token has sub-organization scope
if (token.subOrganizationId) {
const subOrganization = await orgDAL.findOne({
rootOrgId: token.organizationId,
slug: subOrganizationSelector
id: token.subOrganizationId
});
if (!subOrganization)
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` });
throw new BadRequestError({ message: `Sub organization ${token.subOrganizationId} not found` });
// Verify the sub-org belongs to the token's root organization
if (subOrganization.rootOrgId !== token.organizationId && subOrganization.id !== token.organizationId) {
throw new ForbiddenRequestError({ message: "Sub-organization does not belong to the token's organization" });
}
const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
@@ -258,13 +258,13 @@ export const authSignupServiceFactory = ({
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
if (organizationId) {
const org = await orgService.findOrganizationById(
user.id,
organizationId,
authMethod,
organizationId,
organizationId
);
const org = await orgService.findOrganizationById({
userId: user.id,
orgId: organizationId,
actorAuthMethod: authMethod,
actorOrgId: organizationId,
rootOrgId: organizationId
});
if (org && org.userTokenExpiration) {
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
refreshTokenExpiresIn = org.userTokenExpiration;
+2
View File
@@ -55,6 +55,7 @@ export type AuthModeJwtTokenPayload = {
tokenVersionId: string;
accessVersion: number;
organizationId?: string;
subOrganizationId?: string;
isMfaVerified?: boolean;
mfaMethod?: MfaMethod;
};
@@ -74,6 +75,7 @@ export type AuthModeRefreshJwtTokenPayload = {
tokenVersionId: string;
refreshVersion: number;
organizationId?: string;
subOrganizationId?: string;
isMfaVerified?: boolean;
mfaMethod?: MfaMethod;
};
+3 -1
View File
@@ -28,5 +28,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
shareSecretsProductEnabled: true,
maxSharedSecretLifetime: true,
maxSharedSecretViewLimit: true,
blockDuplicateSecretSyncDestinations: true
blockDuplicateSecretSyncDestinations: true,
rootOrgId: true,
parentOrgId: true
});
+25 -14
View File
@@ -150,36 +150,47 @@ export const orgServiceFactory = ({
/*
* Get organization details by the organization id
* */
const findOrganizationById = async (
userId: string,
orgId: string,
actorAuthMethod: ActorAuthMethod,
rootOrgId: string,
actorOrgId: string
) => {
const findOrganizationById = async ({
userId,
orgId,
actorAuthMethod,
rootOrgId,
actorOrgId
}: {
userId: string;
orgId: string;
actorAuthMethod: ActorAuthMethod;
rootOrgId: string;
actorOrgId: string;
}) => {
await permissionService.getOrgPermission({
actor: ActorType.USER,
actorId: userId,
orgId,
actorAuthMethod,
actorOrgId: rootOrgId,
actorOrgId,
scope: OrganizationActionScope.Any
});
const appCfg = getConfig();
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const hasSubOrg = rootOrgId !== actorOrgId;
const org = await orgDAL.findOrgById(rootOrgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${rootOrgId}' not found` });
const hasSubOrg = actorOrgId !== rootOrgId;
let subOrg;
if (hasSubOrg) {
subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId });
if (!subOrg) throw new NotFoundError({ message: `Sub-organization with ID '${actorOrgId}' not found` });
}
if (!org.userTokenExpiration) {
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg };
const data = hasSubOrg && subOrg ? subOrg : org;
if (!data.userTokenExpiration) {
return { ...data, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
}
return { ...org, subOrganization: subOrg };
return data;
};
/*
* Get all organization a user part of
* */