Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Default organization slug for LDAP/SAML #2000

Merged
merged 19 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => {
DanielHougaard marked this conversation as resolved.
Show resolved Hide resolved
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
Loading