mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-04 09:46:40 +00:00
2fa policy check working
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
import { Org, User } from "@server/db";
|
||||
|
||||
type CheckOrgAccessPolicyProps = {
|
||||
export type CheckOrgAccessPolicyProps = {
|
||||
orgId?: string;
|
||||
org?: Org;
|
||||
userId?: string;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
export type CheckOrgAccessPolicyResult = {
|
||||
allowed: boolean;
|
||||
error?: string;
|
||||
policies?: {
|
||||
requiredTwoFactor?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export async function checkOrgAccessPolicy(
|
||||
props: CheckOrgAccessPolicyProps
|
||||
): Promise<{
|
||||
|
||||
@@ -5,6 +5,7 @@ import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function verifyOrgAccess(
|
||||
req: Request,
|
||||
@@ -51,7 +52,9 @@ export async function verifyOrgAccess(
|
||||
userId
|
||||
});
|
||||
|
||||
if (!policyCheck.success || policyCheck.error) {
|
||||
logger.debug("Org check policy result", { policyCheck });
|
||||
|
||||
if (!policyCheck.allowed || policyCheck.error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
||||
@@ -17,28 +17,26 @@ import { getOrgTierData } from "#private/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import license from "#private/license/license";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
type CheckOrgAccessPolicyProps = {
|
||||
orgId?: string;
|
||||
org?: Org;
|
||||
userId?: string;
|
||||
user?: User;
|
||||
};
|
||||
import {
|
||||
CheckOrgAccessPolicyProps,
|
||||
CheckOrgAccessPolicyResult
|
||||
} from "@server/lib/checkOrgAccessPolicy";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
export async function checkOrgAccessPolicy(
|
||||
props: CheckOrgAccessPolicyProps
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
): Promise<CheckOrgAccessPolicyResult> {
|
||||
const userId = props.userId || props.user?.userId;
|
||||
const orgId = props.orgId || props.org?.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
return { success: false, error: "Organization ID is required" };
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Organization ID is required"
|
||||
};
|
||||
}
|
||||
if (!userId) {
|
||||
return { success: false, error: "User ID is required" };
|
||||
return { allowed: false, error: "User ID is required" };
|
||||
}
|
||||
|
||||
if (build === "saas") {
|
||||
@@ -46,7 +44,7 @@ export async function checkOrgAccessPolicy(
|
||||
const subscribed = tier === TierId.STANDARD;
|
||||
// if not subscribed, don't check the policies
|
||||
if (!subscribed) {
|
||||
return { success: true };
|
||||
return { allowed: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +52,7 @@ export async function checkOrgAccessPolicy(
|
||||
const isUnlocked = await license.isUnlocked();
|
||||
// if not licensed, don't check the policies
|
||||
if (!isUnlocked) {
|
||||
return { success: true };
|
||||
return { allowed: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +65,7 @@ export async function checkOrgAccessPolicy(
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
props.org = orgQuery;
|
||||
if (!props.org) {
|
||||
return { success: false, error: "Organization not found" };
|
||||
return { allowed: false, error: "Organization not found" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,18 +76,22 @@ export async function checkOrgAccessPolicy(
|
||||
.where(eq(users.userId, userId));
|
||||
props.user = userQuery;
|
||||
if (!props.user) {
|
||||
return { success: false, error: "User not found" };
|
||||
return { allowed: false, error: "User not found" };
|
||||
}
|
||||
}
|
||||
|
||||
// now check the policies
|
||||
const policies: CheckOrgAccessPolicyResult["policies"] = {};
|
||||
|
||||
if (!props.org.requireTwoFactor && !props.user.twoFactorEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Two-factor authentication is required"
|
||||
};
|
||||
// only applies to internal users
|
||||
if (props.user.type === UserType.Internal && props.org.requireTwoFactor) {
|
||||
policies.requiredTwoFactor = props.user.twoFactorEnabled || false;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
const allowed = Object.values(policies).every((v) => v === true);
|
||||
|
||||
return {
|
||||
allowed,
|
||||
policies
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
136
server/routers/org/checkOrgUserAccess.ts
Normal file
136
server/routers/org/checkOrgUserAccess.ts
Normal 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,4 @@ export * from "./getOrgOverview";
|
||||
export * from "./listOrgs";
|
||||
export * from "./pickOrgDefaults";
|
||||
export * from "./applyBlueprint";
|
||||
export * from "./checkOrgUserAccess";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user