Skip to content

Commit

Permalink
Merge pull request #2000 from Infisical/daniel/default-org
Browse files Browse the repository at this point in the history
Feat: Default organization slug for LDAP/SAML
  • Loading branch information
maidul98 committed Jun 24, 2024
2 parents 1b8a77f + 5a95751 commit 60cb420
Show file tree
Hide file tree
Showing 16 changed files with 367 additions and 128 deletions.
27 changes: 27 additions & 0 deletions backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Knex } from "knex";

import { TableName } from "../schemas";

const DEFAULT_AUTH_ORG_ID_FIELD = "defaultAuthOrgId";

export async function up(knex: Knex): Promise<void> {
const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD);

await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (!hasDefaultOrgColumn) {
t.uuid(DEFAULT_AUTH_ORG_ID_FIELD).nullable();
t.foreign(DEFAULT_AUTH_ORG_ID_FIELD).references("id").inTable(TableName.Organization).onDelete("SET NULL");
}
});
}

export async function down(knex: Knex): Promise<void> {
const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD);

await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (hasDefaultOrgColumn) {
t.dropForeign([DEFAULT_AUTH_ORG_ID_FIELD]);
t.dropColumn(DEFAULT_AUTH_ORG_ID_FIELD);
}
});
}
3 changes: 2 additions & 1 deletion backend/src/db/schemas/super-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export const SuperAdminSchema = z.object({
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"),
trustSamlEmails: z.boolean().default(false).nullable().optional(),
trustLdapEmails: z.boolean().default(false).nullable().optional(),
trustOidcEmails: z.boolean().default(false).nullable().optional()
trustOidcEmails: z.boolean().default(false).nullable().optional(),
defaultAuthOrgId: z.string().uuid().nullable().optional()
});

export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
Expand Down
8 changes: 6 additions & 2 deletions backend/src/server/routes/v1/admin-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({
isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(),
isSecretScanningDisabled: z.boolean()
})
})
Expand Down Expand Up @@ -52,11 +53,14 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean().optional(),
trustLdapEmails: z.boolean().optional(),
trustOidcEmails: z.boolean().optional()
trustOidcEmails: z.boolean().optional(),
defaultAuthOrgId: z.string().optional().nullable()
}),
response: {
200: z.object({
config: SuperAdminSchema
config: SuperAdminSchema.extend({
defaultAuthOrgSlug: z.string().nullable()
})
})
}
},
Expand Down
54 changes: 52 additions & 2 deletions backend/src/services/super-admin/super-admin-dal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,57 @@
import { Knex } from "knex";

import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TableName, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";

export type TSuperAdminDALFactory = ReturnType<typeof superAdminDALFactory>;

export const superAdminDALFactory = (db: TDbClient) => ormify(db, TableName.SuperAdmin, {});
export const superAdminDALFactory = (db: TDbClient) => {
const superAdminOrm = ormify(db, TableName.SuperAdmin);

const findById = async (id: string, tx?: Knex) => {
const config = await (tx || db)(TableName.SuperAdmin)
.where(`${TableName.SuperAdmin}.id`, id)
.leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`)
.select(
db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin,
db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug")
)
.first();

if (!config) {
return null;
}

return {
...config,
defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null
} as TSuperAdmin & { defaultAuthOrgSlug: string | null };
};

const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => {
const updatedConfig = await (superAdminOrm || tx).transaction(async (trx: Knex) => {
await superAdminOrm.updateById(id, data, trx);
const config = await findById(id, trx);

if (!config) {
throw new DatabaseError({
error: "Failed to find updated super admin config",
message: "Failed to update super admin config",
name: "UpdateById"
});
}

return config;
});

return updatedConfig;
};

return {
...superAdminOrm,
findById,
updateById
};
};
23 changes: 17 additions & 6 deletions backend/src/services/super-admin/super-admin-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type TSuperAdminServiceFactoryDep = {
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;

// eslint-disable-next-line
export let getServerCfg: () => Promise<TSuperAdmin>;
export let getServerCfg: () => Promise<TSuperAdmin & { defaultAuthOrgSlug: string | null }>;

const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
Expand All @@ -42,16 +42,20 @@ export const superAdminServiceFactory = ({
// TODO(akhilmhdh): bad pattern time less change this later to me itself
getServerCfg = async () => {
const config = await keyStore.getItem(ADMIN_CONFIG_KEY);

// missing in keystore means fetch from db
if (!config) {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg) {
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore

if (!serverCfg) {
throw new BadRequestError({ name: "Admin config", message: "Admin config not found" });
}

await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore
return serverCfg;
}

const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin;
const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultAuthOrgSlug: string | null };
return {
...keyStoreServerCfg,
// this is to allow admin router to work
Expand All @@ -65,14 +69,21 @@ export const superAdminServiceFactory = ({
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg) return;

// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true, id: ADMIN_CONFIG_DB_UUID });
const newCfg = await serverCfgDAL.create({
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
id: ADMIN_CONFIG_DB_UUID,
initialized: false,
allowSignUp: true,
defaultAuthOrgId: null
});
return newCfg;
};

const updateServerCfg = async (data: TSuperAdminUpdate) => {
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data);

await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));

return updatedServerCfg;
};

Expand Down
154 changes: 104 additions & 50 deletions frontend/src/components/v2/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,61 +36,73 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
ref
): JSX.Element => {
return (
<SelectPrimitive.Root {...props} disabled={isDisabled}>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
`inline-flex items-center justify-between rounded-md
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`,
className,
isDisabled && "cursor-not-allowed opacity-50"
)}
>
<SelectPrimitive.Value placeholder={placeholder}>
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
</SelectPrimitive.Value>
<div className="flex items-center space-x-2">
<SelectPrimitive.Root
{...props}
onValueChange={(value) => {
if (!props.onValueChange) return;

<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon
icon={faCaretDown}
size="sm"
className={twMerge(isDisabled && "opacity-30")}
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
const newValue = value === "EMPTY-VALUE" ? "" : value;
props.onValueChange(newValue);
}}
disabled={isDisabled}
>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
position === "popper" && "max-h-72",
dropdownContainerClassName
`inline-flex items-center justify-between rounded-md
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`,
className,
isDisabled && "cursor-not-allowed opacity-50"
)}
position={position}
style={{ width: "var(--radix-select-trigger-width)" }}
>
<SelectPrimitive.ScrollUpButton>
<div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretUp} size="sm" />
</div>
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1">
{isLoading ? (
<div className="flex items-center space-x-2">
{props.icon && <FontAwesomeIcon icon={props.icon} />}
<SelectPrimitive.Value placeholder={placeholder} />
</div>

<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon
icon={faCaretDown}
size="sm"
className={twMerge(isDisabled && "opacity-30")}
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
className={twMerge(
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
position === "popper" && "max-h-72",
dropdownContainerClassName
)}
position={position}
style={{ width: "var(--radix-select-trigger-width)" }}
>
<SelectPrimitive.ScrollUpButton>
<div className="flex items-center justify-center">
<Spinner size="xs" />
<span className="ml-2 text-xs text-gray-500">Loading...</span>
<FontAwesomeIcon icon={faCaretUp} size="sm" />
</div>
) : (
children
)}
</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton>
<div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretDown} size="sm" />
</div>
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1">
{isLoading ? (
<div className="flex items-center justify-center">
<Spinner size="xs" />
<span className="ml-2 text-xs text-gray-500">Loading...</span>
</div>
) : (
children
)}
</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton>
<div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretDown} size="sm" />
</div>
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
</div>
);
}
);
Expand All @@ -114,7 +126,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary",
isDisabled &&
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
className
)}
ref={forwardedRef}
Expand All @@ -129,3 +141,45 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
);

SelectItem.displayName = "SelectItem";

export type SelectClearProps = Omit<SelectItemProps, "disabled" | "value"> & {
onClear: () => void;
selectValue: string;
};

export const SelectClear = forwardRef<HTMLDivElement, SelectClearProps>(
(
{ children, className, isSelected, isDisabled, onClear, selectValue, ...props },
forwardedRef
) => {
return (
<SelectPrimitive.Item
{...props}
value="EMPTY-VALUE"
onSelect={() => onClear()}
onClick={() => onClear()}
className={twMerge(
`relative mb-0.5 flex
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary",
isDisabled &&
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
className
)}
ref={forwardedRef}
>
<div
className={twMerge(
"absolute left-3.5 text-primary",
selectValue === "" ? "visible" : "hidden"
)}
>
<FontAwesomeIcon icon={faCheck} />
</div>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
);
SelectClear.displayName = "SelectClear";
2 changes: 1 addition & 1 deletion frontend/src/components/v2/Select/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { SelectItemProps, SelectProps } from "./Select";
export { Select, SelectItem } from "./Select";
export { Select, SelectClear, SelectItem } from "./Select";
2 changes: 2 additions & 0 deletions frontend/src/hooks/api/admin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type TServerConfig = {
trustLdapEmails: boolean;
trustOidcEmails: boolean;
isSecretScanningDisabled: boolean;
defaultAuthOrgSlug: string | null;
defaultAuthOrgId: string | null;
};

export type TCreateAdminUserDTO = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/api/serverDetails/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export type ServerStatus = {
emailConfigured: boolean;
secretScanningConfigured: boolean;
redisConfigured: boolean;
samlDefaultOrgSlug: boolean
samlDefaultOrgSlug: string;
};
Loading

0 comments on commit 60cb420

Please sign in to comment.