fix: review changes

This commit is contained in:
Piyush Gupta
2025-12-01 18:29:49 +05:30
parent 19a43a3f9b
commit 2bfb1f37f4
19 changed files with 293 additions and 324 deletions
+11 -28
View File
@@ -43,15 +43,10 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
rateLimit: authRateLimit rateLimit: authRateLimit
}, },
schema: { schema: {
body: z body: z.object({
.object({ organizationId: z.string().trim(),
organizationId: z.string().trim().optional(), userAgent: z.enum(["cli"]).optional()
subOrganizationId: z.string().trim().optional(), }),
userAgent: z.enum(["cli"]).optional()
})
.refine((body) => Boolean(body.organizationId || body.subOrganizationId), {
message: "organizationId or subOrganizationId is required"
}),
response: { response: {
200: z.object({ 200: z.object({
token: z.string(), token: z.string(),
@@ -62,25 +57,13 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req, res) => { handler: async (req, res) => {
const cfg = getConfig(); const cfg = getConfig();
let tokens;
const targetOrgId = req.body.subOrganizationId ?? req.body.organizationId ?? ""; const tokens = await server.services.login.selectOrganization({
userAgent: req.body.userAgent ?? req.headers["user-agent"],
if (req.body.subOrganizationId) { authJwtToken: req.headers.authorization,
tokens = await server.services.login.selectSubOrganization({ organizationId: req.body.organizationId,
userAgent: req.body.userAgent ?? req.headers["user-agent"], ipAddress: req.realIp
authJwtToken: req.headers.authorization, });
subOrganizationId: req.body.subOrganizationId,
ipAddress: req.realIp
});
} else {
tokens = await server.services.login.selectOrganization({
userAgent: req.body.userAgent ?? req.headers["user-agent"],
authJwtToken: req.headers.authorization,
organizationId: req.body.organizationId as string,
ipAddress: req.realIp
});
}
if (tokens.isMfaEnabled) { if (tokens.isMfaEnabled) {
return { return {
@@ -93,7 +76,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN]; const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
if (githubOauthAccessToken) { if (githubOauthAccessToken) {
await server.services.githubOrgSync await server.services.githubOrgSync
.syncUserGroups(targetOrgId, tokens.user.userId, githubOauthAccessToken) .syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken)
.finally(() => { .finally(() => {
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", { void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
httpOnly: true, httpOnly: true,
+118 -210
View File
@@ -13,7 +13,13 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import {
BadRequestError,
DatabaseError,
ForbiddenRequestError,
NotFoundError,
UnauthorizedError
} from "@app/lib/errors";
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn"; import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
@@ -530,25 +536,77 @@ export const authLoginServiceFactory = ({
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId); const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" }); if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
// Check if the user actually has access to the specified organization.
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
const selectedOrg = await orgDAL.findById(organizationId); const selectedOrg = await orgDAL.findById(organizationId);
if (!selectedOrg) {
throw new NotFoundError({ message: `Organization with ID '${organizationId}' not found` });
}
if (!selectedOrgMembership) { const isSubOrganization = Boolean(selectedOrg.rootOrgId && selectedOrg.id !== selectedOrg.rootOrgId);
throw new ForbiddenRequestError({
message: `User does not have access to the organization named ${selectedOrg?.name}` let rootOrg = selectedOrg;
let membershipRole;
if (isSubOrganization) {
if (!selectedOrg.rootOrgId) {
throw new BadRequestError({
message: "Invalid sub-organization"
});
}
rootOrg = await orgDAL.findById(selectedOrg.rootOrgId);
if (!rootOrg) {
throw new BadRequestError({
message: "Invalid root organization"
});
}
// Check user membership in the sub-organization
const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: organizationId,
scope: AccessScope.Organization,
status: OrgMembershipStatus.Accepted
});
if (!orgMembership) {
throw new ForbiddenRequestError({
message: `User does not have access to the sub-organization named ${selectedOrg.name}`
});
}
// Check user membership in the root organization
const rootOrgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: rootOrg.id,
scope: AccessScope.Organization,
status: OrgMembershipStatus.Accepted
}); });
if (!rootOrgMembership) {
throw new ForbiddenRequestError({
message: "User does not have access to the root organization"
});
}
membershipRole = (await membershipRoleDAL.findOne({ membershipId: orgMembership.id })).role;
} else {
// For root organizations, check membership using the existing method
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
if (!selectedOrgMembership) {
throw new ForbiddenRequestError({
message: `User does not have access to the organization named ${selectedOrg.name}`
});
}
membershipRole = selectedOrgMembership.userRole;
} }
// Check if authEnforced is true and the current auth method is not an enforced method
if ( if (
selectedOrg.authEnforced && selectedOrg.authEnforced &&
!isAuthMethodSaml(decodedToken.authMethod) && !isAuthMethodSaml(decodedToken.authMethod) &&
decodedToken.authMethod !== AuthMethod.OIDC && decodedToken.authMethod !== AuthMethod.OIDC &&
!(selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin) !(selectedOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin)
) { ) {
throw new BadRequestError({ throw new BadRequestError({
message: "Login with the auth method required by your organization." message: "Login with the auth method required by your organization."
@@ -556,7 +614,7 @@ export const authLoginServiceFactory = ({
} }
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) { if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin; const canBypass = selectedOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin;
if (!canBypass) { if (!canBypass) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@@ -607,7 +665,8 @@ export const authLoginServiceFactory = ({
user, user,
userAgent, userAgent,
ip: ipAddress, ip: ipAddress,
organizationId, organizationId: isSubOrganization ? rootOrg.id : organizationId,
subOrganizationId: isSubOrganization ? organizationId : undefined,
isMfaVerified: decodedToken.isMfaVerified, isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod mfaMethod: decodedToken.mfaMethod
}); });
@@ -675,205 +734,55 @@ export const authLoginServiceFactory = ({
} }
} }
await auditLogService.createAuditLog({ // Create audit log for organization selection
orgId: organizationId, if (isSubOrganization) {
ipAddress, await auditLogService.createAuditLog({
userAgent, orgId: organizationId,
userAgentType: getUserAgentType(userAgent), ipAddress,
actor: { userAgent,
type: ActorType.USER, userAgentType: getUserAgentType(userAgent),
metadata: { actor: {
email: user.email, type: ActorType.USER,
userId: user.id, metadata: {
username: user.username, email: user.email,
authMethod: decodedToken.authMethod userId: user.id,
} username: user.username,
}, authMethod: decodedToken.authMethod
event: { }
type: EventType.SELECT_ORGANIZATION,
metadata: {
organizationId,
organizationName: selectedOrg.name
}
}
});
return {
...tokens,
user,
isMfaEnabled: false
};
};
const selectSubOrganization = async ({
userAgent,
authJwtToken,
ipAddress,
subOrganizationId
}: {
userAgent: string | undefined;
authJwtToken: string | undefined;
ipAddress: string;
subOrganizationId: string;
}) => {
const cfg = getConfig();
if (!authJwtToken) throw new UnauthorizedError({ name: "Authorization header is required" });
if (!userAgent) throw new UnauthorizedError({ name: "User-Agent header is required" });
// eslint-disable-next-line no-param-reassign
authJwtToken = authJwtToken.replace("Bearer ", "");
const decodedToken = crypto.jwt().verify(authJwtToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload;
if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" });
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
// Check user membership in the sub-organization
const userSubOrgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: subOrganizationId,
scope: AccessScope.Organization,
status: OrgMembershipStatus.Accepted
});
// Fetch the sub-organization
const subOrg = await orgDAL.findById(subOrganizationId);
if (!userSubOrgMembership) {
throw new ForbiddenRequestError({
message: `User does not have access to the sub-organization named ${subOrg.name}`
});
}
if (!subOrg.rootOrgId) {
throw new BadRequestError({
message: "Invalid sub-organization"
});
}
const rootOrg = await orgDAL.findById(subOrg.rootOrgId);
if (!rootOrg) {
throw new BadRequestError({
message: "Invalid root organization"
});
}
const rootOrgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: rootOrg.id,
scope: AccessScope.Organization,
status: OrgMembershipStatus.Accepted
});
if (!rootOrgMembership) {
throw new ForbiddenRequestError({
message: "User does not have access to the root organization"
});
}
const subOrgmembershipRole = await membershipRoleDAL.findOne({ membershipId: userSubOrgMembership.id });
// Check if authEnforced is true and the current auth method is not an enforced method
if (
subOrg.authEnforced &&
!isAuthMethodSaml(decodedToken.authMethod) &&
decodedToken.authMethod !== AuthMethod.OIDC &&
!(subOrg.bypassOrgAuthEnabled && subOrgmembershipRole.role === OrgMembershipRole.Admin)
) {
throw new BadRequestError({
message: "Login with the auth method required by your organization."
});
}
if (subOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
const canBypass = subOrg.bypassOrgAuthEnabled && subOrgmembershipRole.role === OrgMembershipRole.Admin;
if (!canBypass) {
throw new ForbiddenRequestError({
message: "Google SSO is enforced for this organization. Please use Google SSO to login.",
error: "GoogleSsoEnforced"
});
}
}
if (decodedToken.authMethod === AuthMethod.GOOGLE) {
await orgDAL.updateById(subOrg.id, {
googleSsoAuthLastUsed: new Date()
});
}
// Check MFA requirements for the sub-organization
const shouldCheckMfa = subOrg.enforceMfa || user.isMfaEnabled;
const orgMfaMethod = subOrg.enforceMfa ? (subOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
const mfaMethod = orgMfaMethod ?? userMfaMethod;
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
const mfaToken = crypto.jwt().sign(
{
authMethod: decodedToken.authMethod,
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user.id
}, },
cfg.AUTH_SECRET, event: {
{ type: EventType.SELECT_SUB_ORGANIZATION,
expiresIn: cfg.JWT_MFA_LIFETIME metadata: {
organizationId,
organizationName: selectedOrg.name,
rootOrganizationId: rootOrg.id
}
} }
); });
} else {
if (mfaMethod === MfaMethod.EMAIL && user.email) { await auditLogService.createAuditLog({
await sendUserMfaCode({ orgId: organizationId,
userId: user.id, ipAddress,
email: user.email userAgent,
}); userAgentType: getUserAgentType(userAgent),
} actor: {
type: ActorType.USER,
return { isMfaEnabled: true, mfa: mfaToken, mfaMethod } as const; metadata: {
email: user.email,
userId: user.id,
username: user.username,
authMethod: decodedToken.authMethod
}
},
event: {
type: EventType.SELECT_ORGANIZATION,
metadata: {
organizationId,
organizationName: selectedOrg.name
}
}
});
} }
// Generate tokens scoped to the sub-organization
const tokens = await generateUserTokens({
authMethod: decodedToken.authMethod,
user,
userAgent,
ip: ipAddress,
organizationId: rootOrg.id,
subOrganizationId,
isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod
});
// Create audit log for sub-organization selection
await auditLogService.createAuditLog({
orgId: subOrganizationId,
ipAddress,
userAgent,
userAgentType: getUserAgentType(userAgent),
actor: {
type: ActorType.USER,
metadata: {
email: user.email,
userId: user.id,
username: user.username,
authMethod: decodedToken.authMethod
}
},
event: {
type: EventType.SELECT_SUB_ORGANIZATION,
metadata: {
organizationId: subOrganizationId,
organizationName: subOrg.name,
rootOrganizationId: subOrg.rootOrgId ?? ""
}
}
});
return { return {
...tokens, ...tokens,
user, user,
@@ -1314,7 +1223,6 @@ export const authLoginServiceFactory = ({
resendMfaToken, resendMfaToken,
verifyMfaToken, verifyMfaToken,
selectOrganization, selectOrganization,
selectSubOrganization,
generateUserTokens, generateUserTokens,
login login
}; };
+4 -9
View File
@@ -58,15 +58,10 @@ export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
return data; return data;
}; };
export type SelectOrganizationParams = export type SelectOrganizationParams = {
| { organizationId: string;
organizationId: string; userAgent?: UserAgentType;
userAgent?: UserAgentType; };
}
| {
subOrganizationId: string;
userAgent?: UserAgentType;
};
export const selectOrganization = async (data: SelectOrganizationParams) => { export const selectOrganization = async (data: SelectOrganizationParams) => {
const { data: res } = await apiRequest.post<{ const { data: res } = await apiRequest.post<{
@@ -11,10 +11,7 @@ export const useCreateSubOrganization = () => {
mutationFn: async (dto: TCreateSubOrganizationDTO) => { mutationFn: async (dto: TCreateSubOrganizationDTO) => {
const { data } = await apiRequest.post<{ organization: TSubOrganization }>( const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
"/api/v1/sub-organizations", "/api/v1/sub-organizations",
dto, dto
{
headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header
}
); );
return data; return data;
}, },
@@ -62,11 +62,7 @@ import {
useGetOrgTrialUrl, useGetOrgTrialUrl,
useLogoutUser useLogoutUser
} from "@app/hooks/api"; } from "@app/hooks/api";
import { import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
authKeys,
selectOrganization,
type SelectOrganizationParams
} 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";
@@ -197,26 +193,18 @@ export const Navbar = () => {
const handleOrgSelection = async ({ const handleOrgSelection = async ({
organizationId, organizationId,
subOrganizationId,
navigateTo, navigateTo,
onSuccess onSuccess
}: { }: {
organizationId?: string; organizationId?: string;
subOrganizationId?: string;
navigateTo?: string; navigateTo?: string;
onSuccess?: () => void | Promise<void>; onSuccess?: () => void | Promise<void>;
}) => { }) => {
if (!organizationId && !subOrganizationId) return; if (!organizationId) return;
const targetId = subOrganizationId ?? organizationId; if (organizationId === currentOrg.id) return;
if (targetId === currentOrg.id) return; const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
const selectionPayload: SelectOrganizationParams = subOrganizationId
? { subOrganizationId }
: { organizationId: organizationId as string };
const { token, isMfaEnabled, mfaMethod } = await selectOrganization(selectionPayload);
if (isMfaEnabled) { if (isMfaEnabled) {
SecurityClient.setMfaToken(token); SecurityClient.setMfaToken(token);
@@ -225,7 +213,7 @@ export const Navbar = () => {
} }
toggleShowMfa.on(); toggleShowMfa.on();
setMfaSuccessCallback(() => async () => { setMfaSuccessCallback(() => async () => {
await handleOrgSelection({ organizationId, subOrganizationId, onSuccess }); await handleOrgSelection({ organizationId, onSuccess });
}); });
return; return;
} }
@@ -234,11 +222,12 @@ export const Navbar = () => {
SecurityClient.setProviderAuthToken(""); SecurityClient.setProviderAuthToken("");
queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
await router.invalidate();
await navigateUserToOrg({ navigate, organizationId: targetId, navigateTo });
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
await queryClient.refetchQueries({ queryKey: authKeys.getAuthToken });
await navigateUserToOrg({ navigate, organizationId, navigateTo });
if (onSuccess) { if (onSuccess) {
await onSuccess(); await onSuccess();
} }
@@ -387,14 +376,17 @@ export const Navbar = () => {
<button <button
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap" className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
type="button" type="button"
onClick={() => { onClick={async () => {
if (isSubOrganization) { if (isSubOrganization) {
handleOrgSelection({ organizationId: currentOrg.rootOrgId as string }); await handleOrgSelection({
organizationId: currentOrg.rootOrgId as string
});
} else {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
} }
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
}} }}
> >
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} /> <OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
@@ -471,7 +463,7 @@ export const Navbar = () => {
</div> </div>
{subOrganizations.map((subOrg) => ( {subOrganizations.map((subOrg) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })} onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
className="cursor-pointer font-normal" className="cursor-pointer font-normal"
key={subOrg.id} key={subOrg.id}
> >
@@ -486,18 +478,16 @@ export const Navbar = () => {
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
{Boolean(subOrganizations.length && !isSubOrganization) && ( {Boolean(subOrganizations.length) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />
)} )}
{!isSubOrganization && ( <DropdownMenuItem
<DropdownMenuItem className="cursor-pointer"
className="cursor-pointer" icon={<FontAwesomeIcon icon={faPlus} />}
icon={<FontAwesomeIcon icon={faPlus} />} onClick={() => setShowSubOrgForm(true)}
onClick={() => setShowSubOrgForm(true)} >
> New Sub-Organization
New Sub-Organization </DropdownMenuItem>
</DropdownMenuItem>
)}{" "}
</DropdownSubMenuContent> </DropdownSubMenuContent>
</DropdownSubMenu> </DropdownSubMenu>
); );
@@ -590,7 +580,7 @@ export const Navbar = () => {
</div> </div>
{subOrganizations.map((subOrg) => ( {subOrganizations.map((subOrg) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })} onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
className="cursor-pointer font-normal" className="cursor-pointer font-normal"
key={subOrg.id} key={subOrg.id}
> >
@@ -602,18 +592,16 @@ export const Navbar = () => {
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
{Boolean(subOrganizations.length && !isSubOrganization) && ( {Boolean(subOrganizations.length) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />
)} )}
{!isSubOrganization && ( <DropdownMenuItem
<DropdownMenuItem className="cursor-pointer"
className="cursor-pointer" icon={<FontAwesomeIcon icon={faPlus} />}
icon={<FontAwesomeIcon icon={faPlus} />} onClick={() => setShowSubOrgForm(true)}
onClick={() => setShowSubOrgForm(true)} >
> New Sub-Organization
New Sub-Organization </DropdownMenuItem>
</DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
@@ -7,6 +7,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { projectKeys, subOrganizationsQuery, useCreateSubOrganization } from "@app/hooks/api"; import { projectKeys, subOrganizationsQuery, useCreateSubOrganization } from "@app/hooks/api";
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
@@ -23,6 +24,7 @@ const AddOrgSchema = z.object({
type FormData = z.infer<typeof AddOrgSchema>; type FormData = z.infer<typeof AddOrgSchema>;
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
const { currentOrg, isSubOrganization } = useOrganization();
const createSubOrg = useCreateSubOrganization(); const createSubOrg = useCreateSubOrganization();
const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true }); const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -42,6 +44,15 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
const router = useRouter(); const router = useRouter();
const onSubmit = async ({ name }: FormData) => { const onSubmit = async ({ name }: FormData) => {
if (isSubOrganization && currentOrg.rootOrgId) {
const { token } = await selectOrganization({
organizationId: currentOrg.rootOrgId
});
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
}
const { organization } = await createSubOrg.mutateAsync({ const { organization } = await createSubOrg.mutateAsync({
name name
}); });
@@ -53,7 +64,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
onClose(); onClose();
const { token } = await selectOrganization({ const { token } = await selectOrganization({
subOrganizationId: organization.id organizationId: organization.id
}); });
SecurityClient.setToken(token); SecurityClient.setToken(token);
@@ -18,7 +18,7 @@ const tabs = [
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization settings? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
@@ -19,7 +19,7 @@ const tabs = [
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -39,7 +39,8 @@ export const SettingsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization settings? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
@@ -73,6 +73,11 @@ export const Route = createFileRoute("/_authenticate")({
}); });
}); });
return { organizationId: data.organizationId as string, isAuthenticated: true, user }; const isSubOrganization = !!data.subOrganizationId;
return {
organizationId: isSubOrganization ? data.subOrganizationId : (data.organizationId as string),
isAuthenticated: true,
user
};
} }
}); });
@@ -1,7 +1,11 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { authKeys, fetchAuthToken, selectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
import { projectKeys } from "@app/hooks/api/projects";
import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { subOrganizationsQuery } from "@app/hooks/api/subOrganizations";
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
// Route context to fill in organization's data like details, subscription etc // Route context to fill in organization's data like details, subscription etc
@@ -15,6 +19,33 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details")({
organizationId = context.organizationId!; organizationId = context.organizationId!;
} }
if ((params as { orgId?: string })?.orgId && context.organizationId) {
const urlOrgId = (params as { orgId: string }).orgId;
const currentTokenOrgId = context.organizationId;
if (urlOrgId !== currentTokenOrgId) {
try {
const { token, isMfaEnabled } = await selectOrganization({ organizationId: urlOrgId });
if (!isMfaEnabled && token) {
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
context.queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
context.queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
context.queryClient.removeQueries({ queryKey: subOrganizationsQuery.allKey() });
await context.queryClient.fetchQuery({
queryKey: authKeys.getAuthToken,
queryFn: fetchAuthToken
});
}
} catch (error) {
console.warn("Failed to automatically exchange token for organization:", error);
}
}
}
await context.queryClient.ensureQueryData({ await context.queryClient.ensureQueryData({
queryKey: organizationKeys.getOrgById(organizationId), queryKey: organizationKeys.getOrgById(organizationId),
queryFn: () => fetchOrganizationById(organizationId) queryFn: () => fetchOrganizationById(organizationId)
@@ -3,7 +3,8 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan"; import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
@@ -85,7 +86,19 @@ export const AccessManagementPage = () => {
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`} title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`}
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources." description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
/> >
{isSubOrganization && (
<Link
to="/organizations/$orgId/access-management"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization access control?
</Link>
)}
</PageHeader>
{!currentOrg.shouldUseNewPrivilegeSystem && ( {!currentOrg.shouldUseNewPrivilegeSystem && (
<div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5"> <div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-1 flex items-center text-sm"> <div className="mb-1 flex items-center text-sm">
@@ -1,4 +1,6 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
@@ -6,7 +8,7 @@ import { useOrganization } from "@app/context";
import { LogsSection } from "./components"; import { LogsSection } from "./components";
export const AuditLogsPage = () => { export const AuditLogsPage = () => {
const { isSubOrganization } = useOrganization(); const { isSubOrganization, currentOrg } = useOrganization();
return ( return (
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
@@ -21,7 +23,19 @@ export const AuditLogsPage = () => {
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`} title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`}
description="Audit logs for security and compliance teams to monitor information access." description="Audit logs for security and compliance teams to monitor information access."
/> >
{isSubOrganization && (
<Link
to="/organizations/$orgId/audit-logs"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization audit logs?
</Link>
)}
</PageHeader>
<LogsSection pageView /> <LogsSection pageView />
</div> </div>
</div> </div>
@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
@@ -8,7 +10,7 @@ import { OrgTabGroup } from "./components";
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isSubOrganization } = useOrganization(); const { isSubOrganization, currentOrg } = useOrganization();
return ( return (
<> <>
@@ -21,7 +23,19 @@ export const SettingsPage = () => {
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
description="Configure organization-wide settings" description="Configure organization-wide settings"
title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"} title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"}
/> >
{isSubOrganization && (
<Link
to="/organizations/$orgId/settings"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization settings?
</Link>
)}
</PageHeader>
<OrgTabGroup /> <OrgTabGroup />
</div> </div>
</div> </div>
@@ -11,7 +11,7 @@ import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/Pr
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -31,7 +31,8 @@ export const SettingsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization settings? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" defaultValue="tab-project-general"> <Tabs orientation="vertical" defaultValue="tab-project-general">
@@ -19,7 +19,7 @@ import {
const Page = () => { const Page = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const { currentProject } = useProject(); const { currentProject } = useProject();
const selectedTab = useSearch({ const selectedTab = useSearch({
strict: false, strict: false,
@@ -54,7 +54,8 @@ const Page = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization access control? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization access
control?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
@@ -3,12 +3,12 @@ import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react"; import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useProject } from "@app/context"; import { useOrganization, useProject } from "@app/context";
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components"; import { LogsSection } from "@app/pages/organization/AuditLogsPage/components";
export const AuditLogsPage = () => { export const AuditLogsPage = () => {
const { currentProject } = useProject(); const { currentProject } = useProject();
const { isSubOrganization } = useOrganization();
return ( return (
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet> <Helmet>
@@ -29,7 +29,8 @@ export const AuditLogsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization audit logs? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization audit
logs?
</Link> </Link>
</PageHeader> </PageHeader>
<LogsSection pageView project={currentProject} /> <LogsSection pageView project={currentProject} />
@@ -4,7 +4,7 @@ import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react"; import { InfoIcon } from "lucide-react";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useProject } from "@app/context"; import { useOrganization, useProject } from "@app/context";
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types"; import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
@@ -15,6 +15,8 @@ import { WorkflowIntegrationTab } from "./components/WorkflowIntegrationSection"
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
const { currentProject } = useProject(); const { currentProject } = useProject();
const tabs = [ const tabs = [
{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }, { name: "General", key: "tab-project-general", Component: ProjectGeneralTab },
@@ -55,7 +57,8 @@ export const SettingsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization settings? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
@@ -14,7 +14,7 @@ import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab"
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization settings? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" defaultValue="tab-project-general"> <Tabs orientation="vertical" defaultValue="tab-project-general">
@@ -14,7 +14,7 @@ import { ProjectSshTab } from "./components/ProjectSshTab";
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization settings? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" defaultValue="tab-project-general"> <Tabs orientation="vertical" defaultValue="tab-project-general">