mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-06 18:56:39 +00:00
Merge branch 'dev' into feat-blueprint-ui-on-dashboard
This commit is contained in:
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 {
|
||||
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,
|
||||
session: req.session
|
||||
});
|
||||
|
||||
// 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const getOrgSchema = z
|
||||
.strict();
|
||||
|
||||
export type GetOrgResponse = {
|
||||
org: Org & { settings: { } | null };
|
||||
org: Org;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
@@ -49,13 +49,13 @@ export async function getOrg(
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const org = await db
|
||||
const [org] = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (org.length === 0) {
|
||||
if (!org) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
@@ -64,23 +64,9 @@ export async function getOrg(
|
||||
);
|
||||
}
|
||||
|
||||
// Parse settings from JSON string back to object
|
||||
let parsedSettings = null;
|
||||
if (org[0].settings) {
|
||||
try {
|
||||
parsedSettings = JSON.parse(org[0].settings);
|
||||
} catch (error) {
|
||||
// If parsing fails, keep as string for backward compatibility
|
||||
parsedSettings = org[0].settings;
|
||||
}
|
||||
}
|
||||
|
||||
return response<GetOrgResponse>(res, {
|
||||
data: {
|
||||
org: {
|
||||
...org[0],
|
||||
settings: parsedSettings
|
||||
}
|
||||
org
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -7,3 +7,5 @@ export * from "./checkId";
|
||||
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,18 +9,36 @@ 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({
|
||||
orgId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
const updateOrgBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
settings: z.object({
|
||||
}).optional(),
|
||||
requireTwoFactor: z.boolean().optional(),
|
||||
maxSessionLengthHours: z.number().nullable().optional(),
|
||||
passwordExpiryDays: z.number().nullable().optional(),
|
||||
settingsLogRetentionDaysRequest: z
|
||||
.number()
|
||||
.min(build === "saas" ? 0 : -1)
|
||||
.optional(),
|
||||
settingsLogRetentionDaysAccess: z
|
||||
.number()
|
||||
.min(build === "saas" ? 0 : -1)
|
||||
.optional(),
|
||||
settingsLogRetentionDaysAction: z
|
||||
.number()
|
||||
.min(build === "saas" ? 0 : -1)
|
||||
.optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
@@ -73,13 +91,40 @@ export async function updateOrg(
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const settings = parsedBody.data.settings ? JSON.stringify(parsedBody.data.settings) : undefined;
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (!isLicensed) {
|
||||
parsedBody.data.requireTwoFactor = undefined;
|
||||
parsedBody.data.maxSessionLengthHours = undefined;
|
||||
parsedBody.data.passwordExpiryDays = undefined;
|
||||
}
|
||||
|
||||
const { tier } = await getOrgTierData(orgId);
|
||||
if (
|
||||
tier != TierId.STANDARD &&
|
||||
parsedBody.data.settingsLogRetentionDaysRequest &&
|
||||
parsedBody.data.settingsLogRetentionDaysRequest > 30
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You are not allowed to set log retention days greater than 30 with your current subscription"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const updatedOrg = await db
|
||||
.update(orgs)
|
||||
.set({
|
||||
name: parsedBody.data.name,
|
||||
settings: settings
|
||||
requireTwoFactor: parsedBody.data.requireTwoFactor,
|
||||
maxSessionLengthHours: parsedBody.data.maxSessionLengthHours,
|
||||
passwordExpiryDays: parsedBody.data.passwordExpiryDays,
|
||||
settingsLogRetentionDaysRequest:
|
||||
parsedBody.data.settingsLogRetentionDaysRequest,
|
||||
settingsLogRetentionDaysAccess:
|
||||
parsedBody.data.settingsLogRetentionDaysAccess,
|
||||
settingsLogRetentionDaysAction:
|
||||
parsedBody.data.settingsLogRetentionDaysAction
|
||||
})
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.returning();
|
||||
@@ -107,3 +152,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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user