2fa policy check working

This commit is contained in:
miloschwartz
2025-10-24 14:31:50 -07:00
parent ddcf77a62d
commit 629f17294a
16 changed files with 724 additions and 88 deletions

View File

@@ -606,6 +606,7 @@ authenticated.post(
);
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
authenticated.get("/org/:orgId/user/:userId/check", org.checkOrgUserAccess);
authenticated.post(
"/user/:userId/2fa",
@@ -675,8 +676,6 @@ authenticated.post(
idp.updateOidcIdp
);
authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
@@ -705,7 +704,6 @@ authenticated.get(
idp.listIdpOrgPolicies
);
authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);

View File

@@ -0,0 +1,136 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, idp, idpOidcConfig } from "@server/db";
import { roles, userOrgs, users } from "@server/db";
import { and, eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import { OpenAPITags, registry } from "@server/openApi";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { CheckOrgAccessPolicyResult } from "@server/lib/checkOrgAccessPolicy";
async function queryUser(orgId: string, userId: string) {
const [user] = await db
.select({
orgId: userOrgs.orgId,
userId: users.userId,
email: users.email,
username: users.username,
name: users.name,
type: users.type,
roleId: userOrgs.roleId,
roleName: roles.name,
isOwner: userOrgs.isOwner,
isAdmin: roles.isAdmin,
twoFactorEnabled: users.twoFactorEnabled,
autoProvisioned: userOrgs.autoProvisioned,
idpId: users.idpId,
idpName: idp.name,
idpType: idp.type,
idpVariant: idpOidcConfig.variant,
idpAutoProvision: idp.autoProvision
})
.from(userOrgs)
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
.leftJoin(users, eq(userOrgs.userId, users.userId))
.leftJoin(idp, eq(users.idpId, idp.idpId))
.leftJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId))
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.limit(1);
return user;
}
export type CheckOrgUserAccessResponse = CheckOrgAccessPolicyResult;
const paramsSchema = z.object({
userId: z.string(),
orgId: z.string()
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/user/{userId}/check",
description: "Check a user's access in an organization.",
tags: [OpenAPITags.Org, OpenAPITags.User],
request: {
params: paramsSchema
},
responses: {}
});
export async function checkOrgUserAccess(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
logger.debug("here0 ")
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, userId } = parsedParams.data;
if (userId !== req.user?.userId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You do not have permission to check this user's access"
)
);
}
let user;
user = await queryUser(orgId, userId);
if (!user) {
const [fullUser] = await db
.select()
.from(users)
.where(eq(users.email, userId))
.limit(1);
if (fullUser) {
user = await queryUser(orgId, fullUser.userId);
}
}
if (!user) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`User with ID ${userId} not found in org`
)
);
}
const policyCheck = await checkOrgAccessPolicy({
orgId,
userId
});
// if we get here, the user has an org join, we just don't know if they pass the policies
return response<CheckOrgUserAccessResponse>(res, {
data: policyCheck,
success: true,
error: false,
message: "User access checked successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -8,3 +8,4 @@ export * from "./getOrgOverview";
export * from "./listOrgs";
export * from "./pickOrgDefaults";
export * from "./applyBlueprint";
export * from "./checkOrgUserAccess";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { orgs } from "@server/db";
import { orgs, users } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -9,6 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { UserType } from "@server/types/UserTypes";
import license from "#dynamic/license/license";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
const updateOrgParamsSchema = z
.object({
@@ -18,7 +23,8 @@ const updateOrgParamsSchema = z
const updateOrgBodySchema = z
.object({
name: z.string().min(1).max(255).optional()
name: z.string().min(1).max(255).optional(),
requireTwoFactor: z.boolean().optional()
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
@@ -71,10 +77,30 @@ export async function updateOrg(
const { orgId } = parsedParams.data;
const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) {
parsedBody.data.requireTwoFactor = undefined;
}
if (
req.user &&
req.user.type === UserType.Internal &&
parsedBody.data.requireTwoFactor === true &&
!req.user.twoFactorEnabled
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You must enable two-factor authentication for your account before enforcing it for all users"
)
);
}
const updatedOrg = await db
.update(orgs)
.set({
name: parsedBody.data.name
name: parsedBody.data.name,
requireTwoFactor: parsedBody.data.requireTwoFactor
})
.where(eq(orgs.orgId, orgId))
.returning();
@@ -102,3 +128,22 @@ export async function updateOrg(
);
}
}
async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
if (build === "enterprise") {
const isUnlocked = await license.isUnlocked();
if (!isUnlocked) {
return false;
}
}
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return false;
}
}
return true;
}

View File

@@ -10,11 +10,10 @@ import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { generateSessionToken } from "@server/auth/sessions/app";
import config from "@server/lib/config";
import {
encodeHexLowerCase
} from "@oslojs/encoding";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { response } from "@server/lib/response";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
const getExchangeTokenParams = z
.object({
@@ -74,6 +73,22 @@ export async function getExchangeToken(
);
}
// check org policy here
const hasAccess = await checkOrgAccessPolicy({
orgId: resource[0].orgId,
userId: req.user!.userId
});
if (!hasAccess.allowed || hasAccess.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(hasAccess.error || "Unknown error")
)
);
}
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(ssoSession))
);