mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 08:28:59 +00:00
feat: added root org identity link functionality
This commit is contained in:
@@ -21,6 +21,7 @@ export const useOrganization = () => {
|
||||
id: currentOrg?.subOrganization?.id || currentOrg?.id,
|
||||
parentOrgId: currentOrg.id
|
||||
},
|
||||
isSubOrganization: Boolean(currentOrg.subOrganization)
|
||||
isSubOrganization: Boolean(currentOrg.subOrganization),
|
||||
isRootOrganization: !currentOrg.subOrganization
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateOrgIdentityMembership, useDeleteOrgIdentityMembership } from "./mutation";
|
||||
export type { TCreateOrgIdentityMembershipDTO, TDeleteOrgIdentityMembershipDTO, TOrgIdentityMembership } from "./types";
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
TCreateOrgIdentityMembershipDTO,
|
||||
TDeleteOrgIdentityMembershipDTO,
|
||||
TOrgIdentityMembership
|
||||
} from "./types";
|
||||
|
||||
export const useCreateOrgIdentityMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ identityId, roles }: TCreateOrgIdentityMembershipDTO) => {
|
||||
const { data } = await apiRequest.post<{ identityMembership: TOrgIdentityMembership }>(
|
||||
`/api/v1/organization/identity-memberships/${identityId}`,
|
||||
{ roles }
|
||||
);
|
||||
return data.identityMembership;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate relevant queries if needed
|
||||
queryClient.invalidateQueries({ queryKey: ["organization"] });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteOrgIdentityMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ identityId }: TDeleteOrgIdentityMembershipDTO) => {
|
||||
const { data } = await apiRequest.delete<{ identityMembership: TOrgIdentityMembership }>(
|
||||
`/api/v1/organization/identity-memberships/${identityId}`
|
||||
);
|
||||
return data.identityMembership;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate relevant queries if needed
|
||||
queryClient.invalidateQueries({ queryKey: ["organization"] });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TemporaryPermissionMode } from "@app/db/schemas";
|
||||
|
||||
export type TOrgIdentityMembership = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
identityId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateOrgIdentityMembershipDTO = {
|
||||
identityId: string;
|
||||
roles: Array<
|
||||
| {
|
||||
role: string;
|
||||
isTemporary?: false;
|
||||
}
|
||||
| {
|
||||
role: string;
|
||||
isTemporary: true;
|
||||
temporaryMode: TemporaryPermissionMode;
|
||||
temporaryRange: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export type TDeleteOrgIdentityMembershipDTO = {
|
||||
identityId: string;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
useAddOrgPmtMethod,
|
||||
useGetAvailableOrgIdentities,
|
||||
useAddOrgTaxId,
|
||||
useCreateCustomerPortalSession,
|
||||
useCreateOrg,
|
||||
|
||||
@@ -42,7 +42,9 @@ export const organizationKeys = {
|
||||
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
||||
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
|
||||
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
|
||||
getOrgById: (orgId: string) => ["organization", { orgId }]
|
||||
getOrgById: (orgId: string) => ["organization", { orgId }],
|
||||
getAvailableIdentities: () => ["available-identities"],
|
||||
getAvailableUsers: () => ["available-users"]
|
||||
};
|
||||
|
||||
export const fetchOrganizations = async () => {
|
||||
@@ -574,3 +576,29 @@ export const useGetOrgIntegrationAuths = <TData = IntegrationAuth[],>(
|
||||
select
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAvailableOrgIdentities = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: organizationKeys.getAvailableIdentities(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ identities: { name: string; id: string }[] }>(
|
||||
`/api/v1/organization/identities/available`
|
||||
);
|
||||
|
||||
return data.identities;
|
||||
},
|
||||
enabled
|
||||
});
|
||||
|
||||
export const useGetAvailableOrgUsers = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: organizationKeys.getAvailableUsers(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
users: { username: string; id: string; firstName: string; lastName: string }[];
|
||||
}>(`/api/v1/organization/users/available`);
|
||||
|
||||
return data.users;
|
||||
},
|
||||
enabled
|
||||
});
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetAvailableOrgIdentities, useGetOrgRoles } from "@app/hooks/api";
|
||||
import { useCreateOrgIdentityMembership } from "@app/hooks/api/orgIdentityMembership";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
identity: z.object({ name: z.string(), id: z.string() }),
|
||||
role: z.object({ name: z.string(), slug: z.string() })
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const IdentityLinkForm = ({ onClose }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { data: roles } = useGetOrgRoles(orgId);
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateOrgIdentityMembership();
|
||||
const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useGetAvailableOrgIdentities();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {}
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ identity, role }: FormData) => {
|
||||
try {
|
||||
await createMutateAsync({
|
||||
identityId: identity.id,
|
||||
roles: [{ role: role.slug, isTemporary: false }]
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully linked identity",
|
||||
type: "success"
|
||||
});
|
||||
navigate({
|
||||
to: "/organization/identities/$identityId",
|
||||
params: {
|
||||
identityId: identity.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to link identity";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="identity"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select identity..."
|
||||
options={rootOrgIdentities}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
isLoading={isRootOrgLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={roles}
|
||||
placeholder="Select role..."
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+44
-16
@@ -1,10 +1,15 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faLink,
|
||||
faPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -23,11 +28,12 @@ import { IdentityModal } from "./IdentityModal";
|
||||
import { IdentityTable } from "./IdentityTable";
|
||||
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
|
||||
import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal";
|
||||
import { IdentityLinkForm } from "./IdentityLinkForm";
|
||||
|
||||
export const IdentitySection = withPermission(
|
||||
() => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
|
||||
@@ -43,7 +49,8 @@ export const IdentitySection = withPermission(
|
||||
"createTemplate",
|
||||
"editTemplate",
|
||||
"deleteTemplate",
|
||||
"viewUsages"
|
||||
"viewUsages",
|
||||
"linkIdentity"
|
||||
] as const);
|
||||
|
||||
const isMoreIdentitiesAllowed = subscription?.identityLimit
|
||||
@@ -105,8 +112,8 @@ export const IdentitySection = withPermission(
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="mb-4 flex w-full items-center gap-4">
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
@@ -123,6 +130,26 @@ export const IdentitySection = withPermission(
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{isSubOrganization && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Create}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faLink} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("linkIdentity");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Link Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Create}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
@@ -210,16 +237,17 @@ export const IdentitySection = withPermission(
|
||||
?.name || ""
|
||||
}
|
||||
/>
|
||||
{/* <IdentityAuthMethodModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/> */}
|
||||
{/* <IdentityUniversalAuthClientSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/> */}
|
||||
<Modal
|
||||
isOpen={popUp.linkIdentity.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("linkIdentity", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Assign Existing Identity"
|
||||
subTitle="Assign an existing identity from your organization or namespace to this project. The identity will continue to be managed at its original scope."
|
||||
>
|
||||
<IdentityLinkForm onClose={() => handlePopUpClose("linkIdentity")} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteIdentity.isOpen}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
import { BillingPage } from "./BillingPage";
|
||||
|
||||
@@ -6,7 +6,14 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/billing"
|
||||
)({
|
||||
component: BillingPage,
|
||||
beforeLoad: () => {
|
||||
beforeLoad: ({ search }) => {
|
||||
if (search.subOrganization) {
|
||||
throw redirect({
|
||||
to: "/organization/projects",
|
||||
search
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
breadcrumbs: [
|
||||
{
|
||||
|
||||
+32
-14
@@ -14,25 +14,39 @@ import { OrgSecurityTab } from "../OrgSecurityTab";
|
||||
import { OrgSsoTab } from "../OrgSsoTab";
|
||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab";
|
||||
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
export const OrgTabGroup = () => {
|
||||
const search = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SettingsPage.id
|
||||
});
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general", component: OrgGeneralTab },
|
||||
{
|
||||
name: "SSO",
|
||||
key: "sso-settings",
|
||||
component: OrgSsoTab
|
||||
component: OrgSsoTab,
|
||||
isHidden: isSubOrganization
|
||||
},
|
||||
{
|
||||
name: "Provisioning",
|
||||
key: "provisioning-settings",
|
||||
component: OrgProvisioningTab
|
||||
component: OrgProvisioningTab,
|
||||
isHidden: isSubOrganization
|
||||
},
|
||||
{
|
||||
name: "Security",
|
||||
key: "tab-org-security",
|
||||
component: OrgSecurityTab,
|
||||
isHidden: isSubOrganization
|
||||
},
|
||||
{
|
||||
name: "Encryption",
|
||||
key: "tab-org-encryption",
|
||||
component: OrgEncryptionTab
|
||||
},
|
||||
{ name: "Security", key: "tab-org-security", component: OrgSecurityTab },
|
||||
{ name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab },
|
||||
{
|
||||
name: "Workflow Integrations",
|
||||
key: "workflow-integrations",
|
||||
@@ -57,17 +71,21 @@ export const OrgTabGroup = () => {
|
||||
return (
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
{tabs
|
||||
.filter((el) => !el.isHidden)
|
||||
.map((tab) => (
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabs.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
{tabs
|
||||
.filter((el) => !el.isHidden)
|
||||
.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user