mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-31 23:10:00 +02:00
add user checks in routes
This commit is contained in:
parent
f8e0219b49
commit
a9f0b9aa38
21 changed files with 302 additions and 133 deletions
|
@ -13,12 +13,19 @@ import moment from "moment";
|
|||
import { setHostMeta } from "@server/setup/setHostMeta";
|
||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||
|
||||
const keyTypes = ["HOST", "SITES"] as const;
|
||||
type KeyType = (typeof keyTypes)[number];
|
||||
|
||||
const keyTiers = ["PROFESSIONAL", "ENTERPRISE"] as const;
|
||||
type KeyTier = (typeof keyTiers)[number];
|
||||
|
||||
export type LicenseStatus = {
|
||||
isHostLicensed: boolean; // Are there any license keys?
|
||||
isLicenseValid: boolean; // Is the license key valid?
|
||||
hostId: string; // Host ID
|
||||
maxSites?: number;
|
||||
usedSites?: number;
|
||||
tier?: KeyTier;
|
||||
};
|
||||
|
||||
export type LicenseKeyCache = {
|
||||
|
@ -26,7 +33,8 @@ export type LicenseKeyCache = {
|
|||
licenseKeyEncrypted: string;
|
||||
valid: boolean;
|
||||
iat?: Date;
|
||||
type?: "LICENSE" | "SITES";
|
||||
type?: KeyType;
|
||||
tier?: KeyTier;
|
||||
numSites?: number;
|
||||
};
|
||||
|
||||
|
@ -54,7 +62,8 @@ type ValidateLicenseAPIResponse = {
|
|||
|
||||
type TokenPayload = {
|
||||
valid: boolean;
|
||||
type: "LICENSE" | "SITES";
|
||||
type: KeyType;
|
||||
tier: KeyTier;
|
||||
quantity: number;
|
||||
terminateAt: string; // ISO
|
||||
iat: number; // Issued at
|
||||
|
@ -182,11 +191,12 @@ LQIDAQAB
|
|||
licenseKeyEncrypted: key.licenseKeyId,
|
||||
valid: payload.valid,
|
||||
type: payload.type,
|
||||
tier: payload.tier,
|
||||
numSites: payload.quantity,
|
||||
iat: new Date(payload.iat * 1000)
|
||||
});
|
||||
|
||||
if (payload.type === "LICENSE") {
|
||||
if (payload.type === "HOST") {
|
||||
foundHostKey = true;
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -273,6 +283,7 @@ LQIDAQAB
|
|||
);
|
||||
cached.valid = payload.valid;
|
||||
cached.type = payload.type;
|
||||
cached.tier = payload.tier;
|
||||
cached.numSites = payload.quantity;
|
||||
cached.iat = new Date(payload.iat * 1000);
|
||||
|
||||
|
@ -311,8 +322,9 @@ LQIDAQAB
|
|||
|
||||
logger.debug("Checking key", cached);
|
||||
|
||||
if (cached.type === "LICENSE") {
|
||||
if (cached.type === "HOST") {
|
||||
status.isLicenseValid = cached.valid;
|
||||
status.tier = cached.tier;
|
||||
}
|
||||
|
||||
if (!cached.valid) {
|
||||
|
|
|
@ -172,9 +172,20 @@ export async function listAccessTokens(
|
|||
)
|
||||
);
|
||||
}
|
||||
const { orgId, resourceId } = parsedParams.data;
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
if (orgId && orgId !== req.userOrgId) {
|
||||
const orgId =
|
||||
parsedParams.data.orgId ||
|
||||
req.userOrg?.orgId ||
|
||||
req.apiKeyOrg?.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -183,21 +194,29 @@ export async function listAccessTokens(
|
|||
);
|
||||
}
|
||||
|
||||
const accessibleResources = await db
|
||||
.select({
|
||||
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
||||
})
|
||||
.from(userResources)
|
||||
.fullJoin(
|
||||
roleResources,
|
||||
eq(userResources.resourceId, roleResources.resourceId)
|
||||
)
|
||||
.where(
|
||||
or(
|
||||
eq(userResources.userId, req.user!.userId),
|
||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||
let accessibleResources;
|
||||
if (req.user) {
|
||||
accessibleResources = await db
|
||||
.select({
|
||||
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
||||
})
|
||||
.from(userResources)
|
||||
.fullJoin(
|
||||
roleResources,
|
||||
eq(userResources.resourceId, roleResources.resourceId)
|
||||
)
|
||||
);
|
||||
.where(
|
||||
or(
|
||||
eq(userResources.userId, req.user!.userId),
|
||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
accessibleResources = await db
|
||||
.select({ resourceId: resources.resourceId })
|
||||
.from(resources)
|
||||
.where(eq(resources.orgId, orgId));
|
||||
}
|
||||
|
||||
const accessibleResourceIds = accessibleResources.map(
|
||||
(resource) => resource.resourceId
|
||||
|
|
|
@ -49,7 +49,7 @@ export async function createNewt(
|
|||
|
||||
const { newtId, secret } = parsedBody.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
|
|
|
@ -3,13 +3,16 @@ import { z } from "zod";
|
|||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
apiKeyOrg,
|
||||
apiKeys,
|
||||
domains,
|
||||
Org,
|
||||
orgDomains,
|
||||
orgs,
|
||||
roleActions,
|
||||
roles,
|
||||
userOrgs
|
||||
userOrgs,
|
||||
users
|
||||
} from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
@ -55,7 +58,7 @@ export async function createOrg(
|
|||
try {
|
||||
// should this be in a middleware?
|
||||
if (config.getRawConfig().flags?.disable_user_create_org) {
|
||||
if (!req.user?.serverAdmin) {
|
||||
if (req.user && !req.user?.serverAdmin) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -143,12 +146,33 @@ export async function createOrg(
|
|||
}))
|
||||
);
|
||||
|
||||
await trx.insert(userOrgs).values({
|
||||
userId: req.user!.userId,
|
||||
orgId: newOrg[0].orgId,
|
||||
roleId: roleId,
|
||||
isOwner: true
|
||||
});
|
||||
if (req.user) {
|
||||
await trx.insert(userOrgs).values({
|
||||
userId: req.user!.userId,
|
||||
orgId: newOrg[0].orgId,
|
||||
roleId: roleId,
|
||||
isOwner: true
|
||||
});
|
||||
} else {
|
||||
// if org created by root api key, set the server admin as the owner
|
||||
const [serverAdmin] = await trx
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.serverAdmin, true));
|
||||
|
||||
if (!serverAdmin) {
|
||||
error = "Server admin not found";
|
||||
trx.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
await trx.insert(userOrgs).values({
|
||||
userId: serverAdmin.userId,
|
||||
orgId: newOrg[0].orgId,
|
||||
roleId: roleId,
|
||||
isOwner: true
|
||||
});
|
||||
}
|
||||
|
||||
const memberRole = await trx
|
||||
.insert(roles)
|
||||
|
@ -166,6 +190,18 @@ export async function createOrg(
|
|||
orgId
|
||||
}))
|
||||
);
|
||||
|
||||
const rootApiKeys = await trx
|
||||
.select()
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.isRoot, true));
|
||||
|
||||
for (const apiKey of rootApiKeys) {
|
||||
await trx.insert(apiKeyOrg).values({
|
||||
apiKeyId: apiKey.apiKeyId,
|
||||
orgId: newOrg[0].orgId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
|
|
|
@ -39,6 +39,7 @@ const createHttpResourceSchema = z
|
|||
isBaseDomain: z.boolean().optional(),
|
||||
siteId: z.number(),
|
||||
http: z.boolean(),
|
||||
protocol: z.string(),
|
||||
domainId: z.string()
|
||||
})
|
||||
.strict()
|
||||
|
@ -129,7 +130,7 @@ export async function createResource(
|
|||
|
||||
const { siteId, orgId } = parsedParams.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
|
@ -202,7 +203,7 @@ async function createHttpResource(
|
|||
);
|
||||
}
|
||||
|
||||
const { name, subdomain, isBaseDomain, http, domainId } =
|
||||
const { name, subdomain, isBaseDomain, http, protocol, domainId } =
|
||||
parsedBody.data;
|
||||
|
||||
const [orgDomain] = await db
|
||||
|
@ -261,7 +262,7 @@ async function createHttpResource(
|
|||
name,
|
||||
subdomain,
|
||||
http,
|
||||
protocol: "tcp",
|
||||
protocol,
|
||||
ssl: true,
|
||||
isBaseDomain
|
||||
})
|
||||
|
@ -284,7 +285,7 @@ async function createHttpResource(
|
|||
resourceId: newResource[0].resourceId
|
||||
});
|
||||
|
||||
if (req.userOrgRoleId != adminRole[0].roleId) {
|
||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
||||
// make sure the user can access the resource
|
||||
await trx.insert(userResources).values({
|
||||
userId: req.user?.userId!,
|
||||
|
|
|
@ -69,9 +69,7 @@ function queryResources(
|
|||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort,
|
||||
enabled: resources.enabled,
|
||||
tlsServerName: resources.tlsServerName,
|
||||
setHostHeader: resources.setHostHeader
|
||||
enabled: resources.enabled
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
|
@ -105,9 +103,7 @@ function queryResources(
|
|||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort,
|
||||
enabled: resources.enabled,
|
||||
tlsServerName: resources.tlsServerName,
|
||||
setHostHeader: resources.setHostHeader
|
||||
enabled: resources.enabled
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
|
@ -187,9 +183,17 @@ export async function listResources(
|
|||
)
|
||||
);
|
||||
}
|
||||
const { siteId, orgId } = parsedParams.data;
|
||||
const { siteId } = parsedParams.data;
|
||||
|
||||
if (orgId && orgId !== req.userOrgId) {
|
||||
const orgId = parsedParams.data.orgId || req.userOrg?.orgId || req.apiKeyOrg?.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -198,7 +202,9 @@ export async function listResources(
|
|||
);
|
||||
}
|
||||
|
||||
const accessibleResources = await db
|
||||
let accessibleResources;
|
||||
if (req.user) {
|
||||
accessibleResources = await db
|
||||
.select({
|
||||
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
||||
})
|
||||
|
@ -213,6 +219,11 @@ export async function listResources(
|
|||
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
accessibleResources = await db.select({
|
||||
resourceId: resources.resourceId
|
||||
}).from(resources).where(eq(resources.orgId, orgId));
|
||||
}
|
||||
|
||||
const accessibleResourceIds = accessibleResources.map(
|
||||
(resource) => resource.resourceId
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roleResources, roles } from "@server/db/schemas";
|
||||
import { apiKeys, roleResources, roles } from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
@ -74,6 +74,17 @@ export async function setResourceRoles(
|
|||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const orgId = req.userOrg?.orgId || req.apiKeyOrg?.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Organization not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// get this org's admin role
|
||||
const adminRole = await db
|
||||
.select()
|
||||
|
@ -81,7 +92,7 @@ export async function setResourceRoles(
|
|||
.where(
|
||||
and(
|
||||
eq(roles.name, "Admin"),
|
||||
eq(roles.orgId, req.userOrg!.orgId)
|
||||
eq(roles.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
@ -136,3 +147,4 @@ export async function setResourceRoles(
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,8 +45,8 @@ const updateHttpResourceBodySchema = z
|
|||
domainId: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
stickySession: z.boolean().optional(),
|
||||
tlsServerName: z.string().optional(),
|
||||
setHostHeader: z.string().optional()
|
||||
tlsServerName: z.string().nullable().optional(),
|
||||
setHostHeader: z.string().nullable().optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roles, userSites, sites, roleSites, Site } from "@server/db/schemas";
|
||||
import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
@ -10,7 +10,6 @@ import { eq, and } from "drizzle-orm";
|
|||
import { getUniqueSiteName } from "@server/db/names";
|
||||
import { addPeer } from "../gerbil/peers";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import { newts } from "@server/db/schemas";
|
||||
import moment from "moment";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
@ -78,8 +77,15 @@ export async function createSite(
|
|||
);
|
||||
}
|
||||
|
||||
const { name, type, exitNodeId, pubKey, subnet, newtId, secret } =
|
||||
parsedBody.data;
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
exitNodeId,
|
||||
pubKey,
|
||||
subnet,
|
||||
newtId,
|
||||
secret
|
||||
} = parsedBody.data;
|
||||
|
||||
const parsedParams = createSiteParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
|
@ -93,12 +99,23 @@ export async function createSite(
|
|||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
}
|
||||
|
||||
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
||||
|
||||
if (!org) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Organization with ID ${orgId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const niceId = await getUniqueSiteName(orgId);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
|
@ -159,7 +176,7 @@ export async function createSite(
|
|||
siteId: newSite.siteId
|
||||
});
|
||||
|
||||
if (req.userOrgRoleId != adminRole[0].roleId) {
|
||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
||||
// make sure the user can access the site
|
||||
trx.insert(userSites).values({
|
||||
userId: req.user?.userId!,
|
||||
|
|
|
@ -100,7 +100,7 @@ export async function listSites(
|
|||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (orgId && orgId !== req.userOrgId) {
|
||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -109,18 +109,26 @@ export async function listSites(
|
|||
);
|
||||
}
|
||||
|
||||
const accessibleSites = await db
|
||||
.select({
|
||||
siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`
|
||||
})
|
||||
.from(userSites)
|
||||
.fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId))
|
||||
.where(
|
||||
or(
|
||||
eq(userSites.userId, req.user!.userId),
|
||||
eq(roleSites.roleId, req.userOrgRoleId!)
|
||||
)
|
||||
);
|
||||
let accessibleSites;
|
||||
if (req.user) {
|
||||
accessibleSites = await db
|
||||
.select({
|
||||
siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`
|
||||
})
|
||||
.from(userSites)
|
||||
.fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId))
|
||||
.where(
|
||||
or(
|
||||
eq(userSites.userId, req.user!.userId),
|
||||
eq(roleSites.roleId, req.userOrgRoleId!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
accessibleSites = await db
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(eq(sites.orgId, orgId));
|
||||
}
|
||||
|
||||
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
|
||||
const baseQuery = querySites(orgId, accessibleSiteIds);
|
||||
|
|
|
@ -49,7 +49,7 @@ export async function addUserRole(
|
|||
|
||||
const { userId, roleId } = parsedParams.data;
|
||||
|
||||
if (!req.userOrg) {
|
||||
if (req.user && !req.userOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -58,7 +58,13 @@ export async function addUserRole(
|
|||
);
|
||||
}
|
||||
|
||||
const orgId = req.userOrg.orgId;
|
||||
const orgId = req.userOrg?.orgId || req.apiKeyOrg?.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const existingUser = await db
|
||||
.select()
|
||||
|
|
|
@ -106,7 +106,7 @@ export async function getOrgUser(
|
|||
);
|
||||
}
|
||||
|
||||
if (user.userId !== req.userOrg.userId) {
|
||||
if (req.user && user.userId !== req.userOrg.userId) {
|
||||
const hasPermission = await checkUserActionPermission(
|
||||
ActionsEnum.getOrgUser,
|
||||
req
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue