allow wildcard emails in email whitelist

This commit is contained in:
Milo Schwartz 2025-01-26 18:12:30 -05:00
parent 9f1f2910e4
commit 61b34c8b16
No known key found for this signature in database
4 changed files with 104 additions and 16 deletions

View file

@ -13,9 +13,7 @@ import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import { createResourceSession } from "@server/auth/sessions/resource";
createResourceSession,
} from "@server/auth/sessions/resource";
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp"; import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
import logger from "@server/logger"; import logger from "@server/logger";
@ -90,20 +88,48 @@ export async function authWithWhitelist(
.leftJoin(orgs, eq(orgs.orgId, resources.orgId)) .leftJoin(orgs, eq(orgs.orgId, resources.orgId))
.limit(1); .limit(1);
const resource = result?.resources; let resource = result?.resources;
const org = result?.orgs; let org = result?.orgs;
const whitelistedEmail = result?.resourceWhitelist; let whitelistedEmail = result?.resourceWhitelist;
if (!whitelistedEmail) { if (!whitelistedEmail) {
return next( // if email is not found, check for wildcard email
createHttpError( const wildcard = "*@" + email.split("@")[1];
HttpCode.UNAUTHORIZED,
createHttpError( logger.debug("Checking for wildcard email: " + wildcard)
HttpCode.BAD_REQUEST,
"Email is not whitelisted" const [result] = await db
.select()
.from(resourceWhitelist)
.where(
and(
eq(resourceWhitelist.resourceId, resourceId),
eq(resourceWhitelist.email, wildcard)
) )
) )
); .leftJoin(
resources,
eq(resources.resourceId, resourceWhitelist.resourceId)
)
.leftJoin(orgs, eq(orgs.orgId, resources.orgId))
.limit(1);
resource = result?.resources;
org = result?.orgs;
whitelistedEmail = result?.resourceWhitelist;
// if wildcard is still not found, return unauthorized
if (!whitelistedEmail) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
createHttpError(
HttpCode.BAD_REQUEST,
"Email is not whitelisted"
)
)
);
}
} }
if (!org) { if (!org) {

View file

@ -12,7 +12,17 @@ import { and, eq } from "drizzle-orm";
const setResourceWhitelistBodySchema = z const setResourceWhitelistBodySchema = z
.object({ .object({
emails: z emails: z
.array(z.string().email()) .array(
z
.string()
.email()
.or(
z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
message:
"Invalid email address. Wildcard (*) must be the entire local part."
})
)
)
.max(50) .max(50)
.transform((v) => v.map((e) => e.toLowerCase())) .transform((v) => v.map((e) => e.toLowerCase()))
}) })

View file

@ -48,6 +48,7 @@ import {
SettingsSectionFooter SettingsSectionFooter
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { InfoPopup } from "@app/components/ui/info-popup";
const UsersRolesFormSchema = z.object({ const UsersRolesFormSchema = z.object({
roles: z.array( roles: z.array(
@ -665,10 +666,12 @@ export default function ResourceAuthenticationPage() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
Whitelisted Emails <InfoPopup
text="Whitelisted Emails"
info="Only users with these email addresses will be able to access this resource. They will be prompted to enter a one-time password sent to their email. Wildcards (*@example.com) can be used to allow any email address from a domain."
/>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
{/* @ts-ignore */}
{/* @ts-ignore */} {/* @ts-ignore */}
<TagInput <TagInput
{...field} {...field}
@ -681,6 +684,17 @@ export default function ResourceAuthenticationPage() {
return z return z
.string() .string()
.email() .email()
.or(
z
.string()
.regex(
/^\*@[\w.-]+\.[a-zA-Z]{2,}$/,
{
message:
"Invalid email address. Wildcard (*) must be the entire local part."
}
)
)
.safeParse( .safeParse(
tag tag
).success; ).success;

View file

@ -0,0 +1,38 @@
"use client";
import React from "react";
import { Info } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
interface InfoPopupProps {
text: string;
info: string;
}
export function InfoPopup({ text, info }: InfoPopupProps) {
return (
<div className="flex items-center space-x-2">
<span>{text}</span>
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 rounded-full p-0"
>
<Info className="h-4 w-4" />
<span className="sr-only">Show info</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<p className="text-sm text-muted-foreground">{info}</p>
</PopoverContent>
</Popover>
</div>
);
}