add user checks in routes

This commit is contained in:
miloschwartz 2025-05-02 10:44:50 -04:00
parent f8e0219b49
commit a9f0b9aa38
No known key found for this signature in database
21 changed files with 302 additions and 133 deletions

View file

@ -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) {

View file

@ -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

View file

@ -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")
);

View file

@ -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) {

View file

@ -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!,

View file

@ -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

View file

@ -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(
);
}
}

View file

@ -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, {

View file

@ -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!,

View file

@ -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);

View file

@ -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()

View file

@ -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