mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: adds suborg scoped token
This commit is contained in:
@@ -58,7 +58,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
|||||||
const plan = await server.services.license.getOrgPlan({
|
const plan = await server.services.license.getOrgPlan({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.rootOrgId,
|
actorOrgId: req.permission.orgId,
|
||||||
|
rootOrgId: req.permission.rootOrgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
orgId: req.params.organizationId,
|
orgId: req.params.organizationId,
|
||||||
refreshCache: req.query.refreshCache
|
refreshCache: req.query.refreshCache
|
||||||
@@ -87,7 +88,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
|||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
orgId: req.params.organizationId
|
orgId: req.params.organizationId,
|
||||||
|
rootOrgId: req.permission.rootOrgId
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ export enum EventType {
|
|||||||
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
||||||
USER_LOGIN = "user-login",
|
USER_LOGIN = "user-login",
|
||||||
SELECT_ORGANIZATION = "select-organization",
|
SELECT_ORGANIZATION = "select-organization",
|
||||||
|
SELECT_SUB_ORGANIZATION = "select-sub-organization",
|
||||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||||
DELETE_CERTIFICATE_TEMPLATE = "delete-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 {
|
interface CreateCertificateTemplateEstConfig {
|
||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -4577,4 +4587,5 @@ export type Event =
|
|||||||
| AutomatedRenewCertificate
|
| AutomatedRenewCertificate
|
||||||
| AutomatedRenewCertificateFailed
|
| AutomatedRenewCertificateFailed
|
||||||
| UserLoginEvent
|
| UserLoginEvent
|
||||||
| SelectOrganizationEvent;
|
| SelectOrganizationEvent
|
||||||
|
| SelectSubOrganizationEvent;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const isOfflineLicenseKey = (licenseKey: string): boolean => {
|
|||||||
|
|
||||||
return "signature" in contents && "license" in contents;
|
return "signature" in contents && "license" in contents;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export const getLicenseKeyConfig = (
|
|||||||
const cfg = config || getConfig();
|
const cfg = config || getConfig();
|
||||||
|
|
||||||
if (!cfg) {
|
if (!cfg) {
|
||||||
return { isValid: false };
|
return { isValid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const licenseKey = cfg.LICENSE_KEY;
|
const licenseKey = cfg.LICENSE_KEY;
|
||||||
@@ -46,10 +46,10 @@ export const getLicenseKeyConfig = (
|
|||||||
return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline };
|
return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isValid: false };
|
return { isValid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isValid: false };
|
return { isValid: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||||
@@ -64,56 +64,56 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
environmentsUsed: 0,
|
environmentsUsed: 0,
|
||||||
identityLimit: null,
|
identityLimit: null,
|
||||||
identitiesUsed: 0,
|
identitiesUsed: 0,
|
||||||
dynamicSecret: false,
|
dynamicSecret: true,
|
||||||
secretVersioning: true,
|
secretVersioning: true,
|
||||||
pitRecovery: false,
|
pitRecovery: true,
|
||||||
ipAllowlisting: false,
|
ipAllowlisting: true,
|
||||||
rbac: false,
|
rbac: true,
|
||||||
githubOrgSync: false,
|
githubOrgSync: true,
|
||||||
customRateLimits: false,
|
customRateLimits: true,
|
||||||
subOrganization: false,
|
subOrganization: true,
|
||||||
customAlerts: false,
|
customAlerts: true,
|
||||||
secretAccessInsights: false,
|
secretAccessInsights: true,
|
||||||
auditLogs: false,
|
auditLogs: true,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
auditLogStreams: false,
|
auditLogStreams: true,
|
||||||
auditLogStreamLimit: 3,
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: false,
|
samlSSO: true,
|
||||||
enforceGoogleSSO: false,
|
enforceGoogleSSO: true,
|
||||||
hsm: false,
|
hsm: true,
|
||||||
oidcSSO: false,
|
oidcSSO: true,
|
||||||
scim: false,
|
scim: true,
|
||||||
ldap: false,
|
ldap: true,
|
||||||
groups: false,
|
groups: true,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: true,
|
||||||
secretRotation: false,
|
secretRotation: true,
|
||||||
caCrl: false,
|
caCrl: true,
|
||||||
instanceUserManagement: false,
|
instanceUserManagement: true,
|
||||||
externalKms: false,
|
externalKms: true,
|
||||||
rateLimits: {
|
rateLimits: {
|
||||||
readLimit: 60,
|
readLimit: 60,
|
||||||
writeLimit: 200,
|
writeLimit: 200,
|
||||||
secretsLimit: 40
|
secretsLimit: 40
|
||||||
},
|
},
|
||||||
pkiEst: false,
|
pkiEst: true,
|
||||||
pkiAcme: false,
|
pkiAcme: true,
|
||||||
enforceMfa: false,
|
enforceMfa: true,
|
||||||
projectTemplates: false,
|
projectTemplates: true,
|
||||||
kmip: false,
|
kmip: true,
|
||||||
gateway: false,
|
gateway: true,
|
||||||
sshHostGroups: false,
|
sshHostGroups: true,
|
||||||
secretScanning: false,
|
secretScanning: true,
|
||||||
enterpriseSecretSyncs: false,
|
enterpriseSecretSyncs: true,
|
||||||
enterpriseCertificateSyncs: false,
|
enterpriseCertificateSyncs: true,
|
||||||
enterpriseAppConnections: false,
|
enterpriseAppConnections: true,
|
||||||
fips: false,
|
fips: true,
|
||||||
eventSubscriptions: false,
|
eventSubscriptions: true,
|
||||||
machineIdentityAuthTemplates: false,
|
machineIdentityAuthTemplates: true,
|
||||||
pkiLegacyTemplates: false,
|
pkiLegacyTemplates: true,
|
||||||
pam: false
|
pam: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (
|
export const setupLicenseRequestWithStore = (
|
||||||
|
|||||||
@@ -350,22 +350,25 @@ export const licenseServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
|
rootOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectId,
|
projectId,
|
||||||
refreshCache
|
refreshCache
|
||||||
}: TOrgPlanDTO) => {
|
}: TOrgPlanDTO) => {
|
||||||
|
const isChildOrg = rootOrgId !== actorOrgId;
|
||||||
|
|
||||||
await permissionService.getOrgPermission({
|
await permissionService.getOrgPermission({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
orgId,
|
orgId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
scope: OrganizationActionScope.ParentOrganization
|
scope: isChildOrg ? OrganizationActionScope.ChildOrganization : OrganizationActionScope.ParentOrganization
|
||||||
});
|
});
|
||||||
if (refreshCache) {
|
if (refreshCache) {
|
||||||
await refreshPlan(orgId);
|
await refreshPlan(rootOrgId);
|
||||||
}
|
}
|
||||||
const plan = await getPlan(orgId, projectId);
|
const plan = await getPlan(rootOrgId, projectId);
|
||||||
return plan;
|
return plan;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export type TOrgPlansTableDTO = {
|
|||||||
export type TOrgPlanDTO = {
|
export type TOrgPlanDTO = {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
refreshCache?: boolean;
|
refreshCache?: boolean;
|
||||||
|
rootOrgId: string;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
export type TStartOrgTrialDTO = {
|
export type TStartOrgTrialDTO = {
|
||||||
|
|||||||
@@ -89,16 +89,30 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||||
|
|
||||||
if (decodedToken.organizationId) {
|
if (decodedToken.organizationId) {
|
||||||
const org = await server.services.org.findOrganizationById(
|
if (decodedToken.subOrganizationId) {
|
||||||
decodedToken.userId,
|
const subOrg = await server.services.org.findOrganizationById({
|
||||||
decodedToken.organizationId,
|
userId: decodedToken.userId,
|
||||||
decodedToken.authMethod,
|
orgId: decodedToken.subOrganizationId,
|
||||||
decodedToken.organizationId,
|
actorAuthMethod: decodedToken.authMethod,
|
||||||
decodedToken.organizationId
|
actorOrgId: decodedToken.subOrganizationId,
|
||||||
);
|
rootOrgId: decodedToken.organizationId
|
||||||
if (org && org.userTokenExpiration) {
|
});
|
||||||
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
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,
|
tokenVersionId: tokenVersion.id,
|
||||||
accessVersion: tokenVersion.accessVersion,
|
accessVersion: tokenVersion.accessVersion,
|
||||||
organizationId: decodedToken.organizationId,
|
organizationId: decodedToken.organizationId,
|
||||||
|
...(decodedToken.subOrganizationId && { subOrganizationId: decodedToken.subOrganizationId }),
|
||||||
isMfaVerified: decodedToken.isMfaVerified,
|
isMfaVerified: decodedToken.isMfaVerified,
|
||||||
mfaMethod: decodedToken.mfaMethod
|
mfaMethod: decodedToken.mfaMethod
|
||||||
},
|
},
|
||||||
@@ -117,7 +132,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ expiresIn }
|
{ 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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organization: sanitizedOrganizationSchema.extend({
|
organization: sanitizedOrganizationSchema
|
||||||
subOrganization: z
|
|
||||||
.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const organization = await server.services.org.findOrganizationById(
|
const organization = await server.services.org.findOrganizationById({
|
||||||
req.permission.id,
|
userId: req.permission.id,
|
||||||
req.params.organizationId,
|
orgId: req.params.organizationId,
|
||||||
req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
req.permission.rootOrgId,
|
rootOrgId: req.permission.rootOrgId,
|
||||||
req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
);
|
});
|
||||||
return { organization };
|
return { organization };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
|
|||||||
};
|
};
|
||||||
|
|
||||||
// to parse jwt identity in inject identity plugin
|
// 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({
|
const session = await tokenDAL.findOneTokenSession({
|
||||||
id: token.tokenVersionId,
|
id: token.tokenVersionId,
|
||||||
userId: token.userId
|
userId: token.userId
|
||||||
@@ -214,13 +214,17 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
|
|||||||
let rootOrgId = "";
|
let rootOrgId = "";
|
||||||
let parentOrgId = "";
|
let parentOrgId = "";
|
||||||
if (token.organizationId) {
|
if (token.organizationId) {
|
||||||
if (subOrganizationSelector) {
|
// Check if token has sub-organization scope
|
||||||
|
if (token.subOrganizationId) {
|
||||||
const subOrganization = await orgDAL.findOne({
|
const subOrganization = await orgDAL.findOne({
|
||||||
rootOrgId: token.organizationId,
|
id: token.subOrganizationId
|
||||||
slug: subOrganizationSelector
|
|
||||||
});
|
});
|
||||||
if (!subOrganization)
|
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({
|
const orgMembership = await membershipUserDAL.findOne({
|
||||||
actorUserId: user.id,
|
actorUserId: user.id,
|
||||||
|
|||||||
@@ -258,13 +258,13 @@ export const authSignupServiceFactory = ({
|
|||||||
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
|
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
|
||||||
|
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const org = await orgService.findOrganizationById(
|
const org = await orgService.findOrganizationById({
|
||||||
user.id,
|
userId: user.id,
|
||||||
organizationId,
|
orgId: organizationId,
|
||||||
authMethod,
|
actorAuthMethod: authMethod,
|
||||||
organizationId,
|
actorOrgId: organizationId,
|
||||||
organizationId
|
rootOrgId: organizationId
|
||||||
);
|
});
|
||||||
if (org && org.userTokenExpiration) {
|
if (org && org.userTokenExpiration) {
|
||||||
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||||
refreshTokenExpiresIn = org.userTokenExpiration;
|
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export type AuthModeJwtTokenPayload = {
|
|||||||
tokenVersionId: string;
|
tokenVersionId: string;
|
||||||
accessVersion: number;
|
accessVersion: number;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
|
subOrganizationId?: string;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
mfaMethod?: MfaMethod;
|
mfaMethod?: MfaMethod;
|
||||||
};
|
};
|
||||||
@@ -74,6 +75,7 @@ export type AuthModeRefreshJwtTokenPayload = {
|
|||||||
tokenVersionId: string;
|
tokenVersionId: string;
|
||||||
refreshVersion: number;
|
refreshVersion: number;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
|
subOrganizationId?: string;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
mfaMethod?: MfaMethod;
|
mfaMethod?: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,5 +28,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
|||||||
shareSecretsProductEnabled: true,
|
shareSecretsProductEnabled: true,
|
||||||
maxSharedSecretLifetime: true,
|
maxSharedSecretLifetime: true,
|
||||||
maxSharedSecretViewLimit: true,
|
maxSharedSecretViewLimit: true,
|
||||||
blockDuplicateSecretSyncDestinations: true
|
blockDuplicateSecretSyncDestinations: true,
|
||||||
|
rootOrgId: true,
|
||||||
|
parentOrgId: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -150,36 +150,47 @@ export const orgServiceFactory = ({
|
|||||||
/*
|
/*
|
||||||
* Get organization details by the organization id
|
* Get organization details by the organization id
|
||||||
* */
|
* */
|
||||||
const findOrganizationById = async (
|
const findOrganizationById = async ({
|
||||||
userId: string,
|
userId,
|
||||||
orgId: string,
|
orgId,
|
||||||
actorAuthMethod: ActorAuthMethod,
|
actorAuthMethod,
|
||||||
rootOrgId: string,
|
rootOrgId,
|
||||||
actorOrgId: string
|
actorOrgId
|
||||||
) => {
|
}: {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
rootOrgId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
}) => {
|
||||||
await permissionService.getOrgPermission({
|
await permissionService.getOrgPermission({
|
||||||
actor: ActorType.USER,
|
actor: ActorType.USER,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
orgId,
|
orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId: rootOrgId,
|
actorOrgId,
|
||||||
scope: OrganizationActionScope.Any
|
scope: OrganizationActionScope.Any
|
||||||
});
|
});
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const hasSubOrg = rootOrgId !== actorOrgId;
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
|
||||||
|
const org = await orgDAL.findOrgById(rootOrgId);
|
||||||
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${rootOrgId}' not found` });
|
||||||
|
|
||||||
const hasSubOrg = actorOrgId !== rootOrgId;
|
|
||||||
let subOrg;
|
let subOrg;
|
||||||
if (hasSubOrg) {
|
if (hasSubOrg) {
|
||||||
subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId });
|
subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId });
|
||||||
|
|
||||||
|
if (!subOrg) throw new NotFoundError({ message: `Sub-organization with ID '${actorOrgId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.userTokenExpiration) {
|
const data = hasSubOrg && subOrg ? subOrg : org;
|
||||||
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg };
|
if (!data.userTokenExpiration) {
|
||||||
|
return { ...data, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
|
||||||
}
|
}
|
||||||
return { ...org, subOrganization: subOrg };
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get all organization a user part of
|
* Get all organization a user part of
|
||||||
* */
|
* */
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ apiRequest.interceptors.request.use((config) => {
|
|||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
const providerAuthToken = SecurityClient.getProviderAuthToken();
|
const providerAuthToken = SecurityClient.getProviderAuthToken();
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
if (config.headers) {
|
if (config.headers) {
|
||||||
if (signupTempToken) {
|
if (signupTempToken) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
@@ -40,17 +38,6 @@ apiRequest.interceptors.request.use((config) => {
|
|||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
config.headers.Authorization = `Bearer ${providerAuthToken}`;
|
config.headers.Authorization = `Bearer ${providerAuthToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootOrgHeader = config.headers.get("x-root-org");
|
|
||||||
|
|
||||||
if (rootOrgHeader) {
|
|
||||||
config.headers.delete("x-root-org");
|
|
||||||
} else {
|
|
||||||
const subOrganization = params.get("subOrganization");
|
|
||||||
if (subOrganization) {
|
|
||||||
config.headers.set("x-infisical-org", subOrganization);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useRouteContext, useSearch } from "@tanstack/react-router";
|
import { useRouteContext } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
|
||||||
|
|
||||||
@@ -10,28 +10,23 @@ export const useOrganization = () => {
|
|||||||
select: (el) => el.organizationId
|
select: (el) => el.organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
const subOrganization = useSearch({
|
|
||||||
strict: false,
|
|
||||||
select: (el) => el?.subOrganization
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: currentOrg } = useSuspenseQuery({
|
const { data: currentOrg } = useSuspenseQuery({
|
||||||
queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"),
|
queryKey: organizationKeys.getOrgById(organizationId),
|
||||||
queryFn: () => fetchOrganizationById(organizationId),
|
queryFn: () => fetchOrganizationById(organizationId),
|
||||||
staleTime: Infinity
|
staleTime: Infinity
|
||||||
});
|
});
|
||||||
|
const isSubOrganization = currentOrg.id !== currentOrg.rootOrgId && Boolean(currentOrg.rootOrgId);
|
||||||
|
|
||||||
const org = useMemo(
|
const org = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
currentOrg: {
|
currentOrg: {
|
||||||
...currentOrg,
|
...currentOrg,
|
||||||
id: currentOrg?.subOrganization?.id || currentOrg?.id,
|
parentOrgId: isSubOrganization ? currentOrg?.parentOrgId : null
|
||||||
parentOrgId: currentOrg.id
|
|
||||||
},
|
},
|
||||||
isSubOrganization: Boolean(currentOrg.subOrganization),
|
isSubOrganization,
|
||||||
isRootOrganization: !currentOrg.subOrganization
|
isRootOrganization: !isSubOrganization
|
||||||
}),
|
}),
|
||||||
[currentOrg, subOrganization]
|
[currentOrg]
|
||||||
);
|
);
|
||||||
|
|
||||||
return org;
|
return org;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const organizationKeys = {
|
|||||||
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
||||||
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
|
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
|
||||||
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
|
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
|
||||||
getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }],
|
getOrgById: (orgId: string) => ["organization", { orgId }],
|
||||||
getAvailableIdentities: () => ["available-identities"],
|
getAvailableIdentities: () => ["available-identities"],
|
||||||
getAvailableUsers: () => ["available-users"]
|
getAvailableUsers: () => ["available-users"]
|
||||||
};
|
};
|
||||||
@@ -67,7 +67,7 @@ export const fetchOrganizationById = async (id: string) => {
|
|||||||
const {
|
const {
|
||||||
data: { organization }
|
data: { organization }
|
||||||
} = await apiRequest.get<{
|
} = await apiRequest.get<{
|
||||||
organization: Organization & { subOrganization?: { id: string; name: string } };
|
organization: Organization;
|
||||||
}>(`/api/v1/organization/${id}`);
|
}>(`/api/v1/organization/${id}`);
|
||||||
return organization;
|
return organization;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export type Organization = {
|
|||||||
maxSharedSecretLifetime: number;
|
maxSharedSecretLifetime: number;
|
||||||
maxSharedSecretViewLimit: number | null;
|
maxSharedSecretViewLimit: number | null;
|
||||||
blockDuplicateSecretSyncDestinations: boolean;
|
blockDuplicateSecretSyncDestinations: boolean;
|
||||||
|
parentOrgId: string | null;
|
||||||
|
rootOrgId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateOrgDTO = {
|
export type UpdateOrgDTO = {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import {
|
|||||||
useGetOrgTrialUrl,
|
useGetOrgTrialUrl,
|
||||||
useLogoutUser
|
useLogoutUser
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
|
import { authKeys, selectOrganization, selectSubOrganization } from "@app/hooks/api/auth/queries";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
||||||
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
||||||
@@ -81,7 +81,11 @@ const getPlan = (subscription: SubscriptionPlan) => {
|
|||||||
return "Free";
|
return "Free";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => {
|
const getFormattedSupportEmailLink = (variables: {
|
||||||
|
org_id: string;
|
||||||
|
domain: string;
|
||||||
|
root_org_id?: string;
|
||||||
|
}) => {
|
||||||
const email = "support@infisical.com";
|
const email = "support@infisical.com";
|
||||||
|
|
||||||
const body = `Hello Infisical Support Team,
|
const body = `Hello Infisical Support Team,
|
||||||
@@ -95,6 +99,7 @@ Issue Details:
|
|||||||
|
|
||||||
Account Info:
|
Account Info:
|
||||||
- Organization ID: ${variables.org_id}
|
- Organization ID: ${variables.org_id}
|
||||||
|
${variables.root_org_id ? `- Root Organization ID: ${variables.root_org_id}` : ""}
|
||||||
- Domain: ${variables.domain}
|
- Domain: ${variables.domain}
|
||||||
|
|
||||||
Thank you,
|
Thank you,
|
||||||
@@ -170,6 +175,10 @@ export const Navbar = () => {
|
|||||||
|
|
||||||
const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days);
|
const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days);
|
||||||
|
|
||||||
|
const rootOrg = isSubOrganization
|
||||||
|
? orgs?.find((org) => org.id === currentOrg.rootOrgId) || currentOrg
|
||||||
|
: currentOrg;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isModalIntrusive) {
|
if (isModalIntrusive) {
|
||||||
setShowCardDeclinedModal(true);
|
setShowCardDeclinedModal(true);
|
||||||
@@ -208,6 +217,31 @@ export const Navbar = () => {
|
|||||||
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubOrgChange = async (subOrgId: string) => {
|
||||||
|
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||||
|
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||||
|
|
||||||
|
const { token, isMfaEnabled, mfaMethod } = await selectSubOrganization({
|
||||||
|
subOrganizationId: subOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMfaEnabled) {
|
||||||
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
|
toggleShowMfa.on();
|
||||||
|
setMfaSuccessCallback(() => () => handleSubOrgChange(subOrgId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.invalidate();
|
||||||
|
navigate({
|
||||||
|
to: "/organizations/$orgId/projects",
|
||||||
|
params: { orgId: subOrgId }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const { mutateAsync } = useGetOrgTrialUrl();
|
const { mutateAsync } = useGetOrgTrialUrl();
|
||||||
|
|
||||||
const logout = useLogoutUser();
|
const logout = useLogoutUser();
|
||||||
@@ -321,17 +355,14 @@ export const Navbar = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
navigate({
|
handleOrgChange(rootOrg.id);
|
||||||
to: "/organizations/$orgId/projects",
|
|
||||||
params: { orgId: currentOrg.id }
|
|
||||||
});
|
|
||||||
if (isSubOrganization) {
|
if (isSubOrganization) {
|
||||||
await router.invalidate({ sync: true }).catch(() => null);
|
await router.invalidate({ sync: true }).catch(() => null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OrgIcon className="size-[12px]" />
|
<OrgIcon className="size-[12px]" />
|
||||||
<span>{currentOrg?.name}</span>
|
<span>{rootOrg?.name}</span>
|
||||||
</button>
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block">
|
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block">
|
||||||
@@ -405,13 +436,7 @@ export const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
{subOrganizations.map((subOrg) => (
|
{subOrganizations.map((subOrg) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={() => handleSubOrgChange(subOrg.id)}
|
||||||
navigate({
|
|
||||||
to: "/organizations/$orgId/projects",
|
|
||||||
params: { orgId: subOrg.id }
|
|
||||||
});
|
|
||||||
await router.invalidate({ sync: true }).catch(() => null);
|
|
||||||
}}
|
|
||||||
className="cursor-pointer font-normal"
|
className="cursor-pointer font-normal"
|
||||||
key={subOrg.id}
|
key={subOrg.id}
|
||||||
>
|
>
|
||||||
@@ -466,7 +491,7 @@ export const Navbar = () => {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
{currentOrg.subOrganization && (
|
{isSubOrganization && (
|
||||||
<>
|
<>
|
||||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
@@ -483,7 +508,7 @@ export const Navbar = () => {
|
|||||||
>
|
>
|
||||||
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
|
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
|
||||||
<SubOrgIcon className="size-[12px]" />
|
<SubOrgIcon className="size-[12px]" />
|
||||||
<span>{currentOrg.subOrganization.name}</span>
|
<span>{currentOrg.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</Badge>
|
</Badge>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -509,13 +534,7 @@ export const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
{subOrganizations.map((subOrg) => (
|
{subOrganizations.map((subOrg) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={() => handleSubOrgChange(subOrg.id)}
|
||||||
navigate({
|
|
||||||
to: "/organizations/$orgId/projects",
|
|
||||||
params: { orgId: subOrg.id }
|
|
||||||
});
|
|
||||||
await router.invalidate({ sync: true }).catch(() => null);
|
|
||||||
}}
|
|
||||||
className="cursor-pointer font-normal"
|
className="cursor-pointer font-normal"
|
||||||
key={subOrg.id}
|
key={subOrg.id}
|
||||||
>
|
>
|
||||||
@@ -562,11 +581,11 @@ export const Navbar = () => {
|
|||||||
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||||
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
|
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!subscription || !currentOrg) return;
|
if (!subscription || !rootOrg) return;
|
||||||
|
|
||||||
// direct user to start pro trial
|
// direct user to start pro trial
|
||||||
const url = await mutateAsync({
|
const url = await mutateAsync({
|
||||||
orgId: currentOrg.id,
|
orgId: rootOrg.id,
|
||||||
success_url: window.location.href
|
success_url: window.location.href
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -611,6 +630,7 @@ export const Navbar = () => {
|
|||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600">
|
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600">
|
||||||
|
aple
|
||||||
<FontAwesomeIcon icon={faCircleQuestion} className="text-mineshaft-200" />
|
<FontAwesomeIcon icon={faCircleQuestion} className="text-mineshaft-200" />
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -620,7 +640,8 @@ export const Navbar = () => {
|
|||||||
text === "Email Support"
|
text === "Email Support"
|
||||||
? getUrl({
|
? getUrl({
|
||||||
org_id: currentOrg.id,
|
org_id: currentOrg.id,
|
||||||
domain: window.location.origin
|
domain: window.location.origin,
|
||||||
|
...(isSubOrganization && { root_org_id: rootOrg.id })
|
||||||
})
|
})
|
||||||
: getUrl();
|
: getUrl();
|
||||||
|
|
||||||
@@ -782,7 +803,7 @@ export const Navbar = () => {
|
|||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Link
|
<Link
|
||||||
to="/organizations/$orgId/billing"
|
to="/organizations/$orgId/billing"
|
||||||
params={{ orgId: currentOrg.id }}
|
params={{ orgId: rootOrg.id }}
|
||||||
className="inline-flex"
|
className="inline-flex"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const SubOrgNameChangeSection = (): JSX.Element => {
|
|||||||
const { handleSubmit, control } = useForm<FormData>({
|
const { handleSubmit, control } = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: currentOrg?.subOrganization?.name || ""
|
name: currentOrg?.name || ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const { mutateAsync, isPending } = useUpdateSubOrganization();
|
const { mutateAsync, isPending } = useUpdateSubOrganization();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { createRootRouteWithContext, Outlet, useSearch } from "@tanstack/react-router";
|
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { NotificationContainer } from "@app/components/notifications";
|
import { NotificationContainer } from "@app/components/notifications";
|
||||||
import { TooltipProvider } from "@app/components/v2";
|
import { TooltipProvider } from "@app/components/v2";
|
||||||
@@ -14,15 +13,6 @@ type TRouterContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RootPage = () => {
|
const RootPage = () => {
|
||||||
const subOrganization = useSearch({
|
|
||||||
strict: false,
|
|
||||||
select: (el) => el?.subOrganization
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
}, [subOrganization]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user