mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-24 22:34:38 +02:00
Merge branch 'dev' into refactor/batch-status-requests
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import {
|
||||
generateId,
|
||||
generateIdFromEntropySize,
|
||||
@@ -8,18 +7,18 @@ import { db } from "@server/db";
|
||||
import {
|
||||
ResourceAccessToken,
|
||||
resourceAccessToken,
|
||||
resources
|
||||
resources,
|
||||
userOrgs
|
||||
} from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { createDate, TimeSpan } from "oslo";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
@@ -28,7 +27,9 @@ export const generateAccessTokenBodySchema = z.strictObject({
|
||||
validForSeconds: z.int().positive().optional(), // seconds
|
||||
title: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
description: z.string().optional()
|
||||
description: z.string().optional(),
|
||||
persistSession: z.boolean().optional().default(false),
|
||||
userId: z.string().optional()
|
||||
});
|
||||
|
||||
export const generateAccssTokenParamsSchema = z.strictObject({
|
||||
@@ -44,6 +45,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/access-token",
|
||||
description: "Generate a new access token for a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: generateAccssTokenParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: generateAccessTokenBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/access-token",
|
||||
description: "Generate a new access token for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: generateAccssTokenParamsSchema,
|
||||
@@ -101,7 +135,14 @@ export async function generateAccessToken(
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
const { validForSeconds, title, path, description } = parsedBody.data;
|
||||
const {
|
||||
validForSeconds,
|
||||
title,
|
||||
path,
|
||||
description,
|
||||
persistSession,
|
||||
userId
|
||||
} = parsedBody.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
@@ -112,6 +153,28 @@ export async function generateAccessToken(
|
||||
return next(createHttpError(HttpCode.NOT_FOUND, "Resource not found"));
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const [membership] = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, resource.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!membership) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"User is not a member of this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionLength = validForSeconds
|
||||
? validForSeconds * 1000
|
||||
@@ -133,23 +196,27 @@ export async function generateAccessToken(
|
||||
accessTokenId: id,
|
||||
orgId: resource.orgId,
|
||||
resourceId,
|
||||
userId: userId || null,
|
||||
tokenHash,
|
||||
expiresAt: expiresAt || null,
|
||||
sessionLength: sessionLength,
|
||||
title: title || null,
|
||||
path: path || null,
|
||||
description: description || null,
|
||||
persistSession,
|
||||
createdAt: new Date().getTime()
|
||||
})
|
||||
.returning({
|
||||
accessTokenId: resourceAccessToken.accessTokenId,
|
||||
orgId: resourceAccessToken.orgId,
|
||||
resourceId: resourceAccessToken.resourceId,
|
||||
userId: resourceAccessToken.userId,
|
||||
expiresAt: resourceAccessToken.expiresAt,
|
||||
sessionLength: resourceAccessToken.sessionLength,
|
||||
title: resourceAccessToken.title,
|
||||
path: resourceAccessToken.path,
|
||||
description: resourceAccessToken.description,
|
||||
persistSession: resourceAccessToken.persistSession,
|
||||
createdAt: resourceAccessToken.createdAt
|
||||
})
|
||||
.execute();
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
userResources,
|
||||
roleResources,
|
||||
resourceAccessToken,
|
||||
sites
|
||||
sites,
|
||||
users
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { sql, eq, or, inArray, and, count, isNull, lt, gt } from "drizzle-orm";
|
||||
import { sql, eq, or, inArray, and, count, isNull, gt } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
@@ -55,11 +56,16 @@ function queryAccessTokens(
|
||||
accessTokenId: resourceAccessToken.accessTokenId,
|
||||
orgId: resourceAccessToken.orgId,
|
||||
resourceId: resourceAccessToken.resourceId,
|
||||
userId: resourceAccessToken.userId,
|
||||
userName: users.name,
|
||||
username: users.username,
|
||||
userEmail: users.email,
|
||||
sessionLength: resourceAccessToken.sessionLength,
|
||||
expiresAt: resourceAccessToken.expiresAt,
|
||||
tokenHash: resourceAccessToken.tokenHash,
|
||||
title: resourceAccessToken.title,
|
||||
description: resourceAccessToken.description,
|
||||
persistSession: resourceAccessToken.persistSession,
|
||||
createdAt: resourceAccessToken.createdAt,
|
||||
resourceName: resources.name,
|
||||
resourceNiceId: resources.niceId,
|
||||
@@ -75,6 +81,7 @@ function queryAccessTokens(
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
||||
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
@@ -97,6 +104,7 @@ function queryAccessTokens(
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
||||
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
@@ -151,6 +159,35 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/access-tokens",
|
||||
description: "List all access tokens for a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: z.object({
|
||||
resourceId: z.number()
|
||||
}),
|
||||
query: listAccessTokensSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}/access-tokens",
|
||||
description: "List all access tokens for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: z.object({
|
||||
|
||||
@@ -16,18 +16,26 @@ export async function logout(
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const { user, session } = await verifySession(req);
|
||||
const isSecure = req.protocol === "https";
|
||||
|
||||
// Always clear the session cookie so logout is idempotent, even when
|
||||
// the session is already missing or invalid
|
||||
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
||||
|
||||
if (!user || !session) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Log out failed because missing or invalid session. IP: ${req.ip}.`
|
||||
`Log out with missing or invalid session. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"You must be logged in to sign out"
|
||||
)
|
||||
);
|
||||
|
||||
return response<null>(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Logged out successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -37,9 +45,6 @@ export async function logout(
|
||||
logger.error("Failed to invalidate session", error);
|
||||
}
|
||||
|
||||
const isSecure = req.protocol === "https";
|
||||
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
||||
|
||||
return response<null>(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function requestPasswordReset(
|
||||
});
|
||||
});
|
||||
|
||||
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`;
|
||||
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(email)}&token=${token}`;
|
||||
|
||||
if (!config.getRawConfig().email) {
|
||||
logger.info(
|
||||
|
||||
@@ -236,6 +236,38 @@ function runTests() {
|
||||
"Root path should not match non-root path"
|
||||
);
|
||||
|
||||
// Path traversal / encoded-slash bypass regression tests
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public/../admin"),
|
||||
false,
|
||||
"Literal .. traversal out of an allowed prefix must not match"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public%2F..%2Fadmin"),
|
||||
false,
|
||||
"Encoded-slash traversal out of an allowed prefix must not match"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public%2f..%2fadmin%2ffile"),
|
||||
false,
|
||||
"Encoded-slash traversal is case-insensitively decoded before matching"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("admin/*", "public/../admin/secret"),
|
||||
true,
|
||||
".. traversal INTO a restricted path should still be caught by its own rule"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public%2Ffoo"),
|
||||
true,
|
||||
"Encoded slash without traversal should still resolve and match normally"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public/./foo"),
|
||||
true,
|
||||
"Single-dot segments are a no-op and should not affect matching"
|
||||
);
|
||||
|
||||
console.log("All path matching tests passed!");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
||||
import {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie,
|
||||
validateResourceSessionToken
|
||||
} from "@server/auth/sessions/resource";
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import {
|
||||
getResourceByDomain,
|
||||
@@ -13,6 +18,7 @@ import {
|
||||
LoginPage,
|
||||
Org,
|
||||
Resource,
|
||||
ResourceAccessToken,
|
||||
ResourceHeaderAuth,
|
||||
ResourceHeaderAuthExtendedCompatibility,
|
||||
ResourcePassword,
|
||||
@@ -21,7 +27,10 @@ import {
|
||||
ResourcePolicyPassword,
|
||||
ResourcePolicyHeaderAuth,
|
||||
ResourceRule,
|
||||
ResourceSession
|
||||
ResourceSession,
|
||||
db,
|
||||
resourceAccessToken,
|
||||
users
|
||||
} from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
||||
@@ -41,11 +50,13 @@ import {
|
||||
enforceResourceSessionLength
|
||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { logRequestAudit } from "./logRequestAudit";
|
||||
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
|
||||
import { REGIONS } from "@server/db/regions";
|
||||
import { localCache } from "#dynamic/lib/cache";
|
||||
import { APP_VERSION } from "@server/lib/consts";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.record(z.string(), z.string()).optional(),
|
||||
@@ -350,22 +361,15 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
if (valid && tokenItem) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
}
|
||||
},
|
||||
parsedBody.data
|
||||
return await allowAccessToken(
|
||||
res,
|
||||
resource,
|
||||
tokenItem,
|
||||
sessions,
|
||||
dontStripSession,
|
||||
parsedBody.data,
|
||||
ipCC
|
||||
);
|
||||
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,27 +405,20 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
if (valid && tokenItem) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
}
|
||||
},
|
||||
parsedBody.data
|
||||
return await allowAccessToken(
|
||||
res,
|
||||
resource,
|
||||
tokenItem,
|
||||
sessions,
|
||||
dontStripSession,
|
||||
parsedBody.data,
|
||||
ipCC
|
||||
);
|
||||
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
}
|
||||
|
||||
// check for HTTP Basic Auth header
|
||||
const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`;
|
||||
const clientHeaderAuthKey = `headerAuth:${resource.resourceId}:${clientHeaderAuth}`;
|
||||
if (headerAuth && clientHeaderAuth) {
|
||||
if (localCache.get(clientHeaderAuthKey)) {
|
||||
logger.debug(
|
||||
@@ -666,22 +663,37 @@ export async function verifyResourceSession(
|
||||
"Resource allowed because access token session is valid"
|
||||
);
|
||||
|
||||
logRequestAudit(
|
||||
const [tokenItem] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(
|
||||
eq(
|
||||
resourceAccessToken.accessTokenId,
|
||||
resourceSession.accessTokenId
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const userData = tokenItem
|
||||
? await getAccessTokenUserData(
|
||||
tokenItem,
|
||||
resource.orgId
|
||||
)
|
||||
: undefined;
|
||||
|
||||
logAccessTokenRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: null,
|
||||
apiKeyId: resourceSession.accessTokenId
|
||||
}
|
||||
accessTokenId: resourceSession.accessTokenId,
|
||||
tokenTitle: tokenItem?.title ?? null,
|
||||
userData
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
return allowed(res, userData, dontStripSession);
|
||||
}
|
||||
|
||||
if (resourceSession.userSessionId && sso) {
|
||||
@@ -892,6 +904,227 @@ function allowed(
|
||||
return response<VerifyUserResponse>(res, data);
|
||||
}
|
||||
|
||||
async function allowAccessToken(
|
||||
res: Response,
|
||||
resource: Resource,
|
||||
tokenItem: ResourceAccessToken,
|
||||
sessions: Record<string, string> | undefined,
|
||||
dontStripSession: boolean | undefined,
|
||||
auditBody: VerifyResourceSessionSchema,
|
||||
location?: string
|
||||
) {
|
||||
const userData = await getAccessTokenUserData(tokenItem, resource.orgId);
|
||||
|
||||
logAccessTokenRequestAudit(
|
||||
{
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location,
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
tokenTitle: tokenItem.title,
|
||||
userData
|
||||
},
|
||||
auditBody
|
||||
);
|
||||
|
||||
if (!tokenItem.persistSession) {
|
||||
logAccessTokenAccessAudit(tokenItem, resource, userData, auditBody);
|
||||
return allowed(res, userData, dontStripSession);
|
||||
}
|
||||
|
||||
const resourceSessionToken = extractResourceSessionToken(
|
||||
sessions ?? {},
|
||||
resource.ssl
|
||||
);
|
||||
|
||||
if (resourceSessionToken) {
|
||||
const sessionCacheKey = `session:${resourceSessionToken}`;
|
||||
let resourceSession: ResourceSession | null | undefined =
|
||||
localCache.get(sessionCacheKey);
|
||||
|
||||
if (!resourceSession) {
|
||||
const result = await validateResourceSessionToken(
|
||||
resourceSessionToken,
|
||||
resource.resourceId
|
||||
);
|
||||
resourceSession = result?.resourceSession;
|
||||
localCache.set(sessionCacheKey, resourceSession, 5);
|
||||
}
|
||||
|
||||
if (
|
||||
resourceSession &&
|
||||
!resourceSession.isRequestToken &&
|
||||
resourceSession.accessTokenId === tokenItem.accessTokenId
|
||||
) {
|
||||
logger.debug(
|
||||
"Resource allowed because existing access token session is valid"
|
||||
);
|
||||
return allowed(res, userData, dontStripSession);
|
||||
}
|
||||
}
|
||||
|
||||
logAccessTokenAccessAudit(tokenItem, resource, userData, auditBody);
|
||||
return await createAccessTokenSession(res, resource, tokenItem, userData);
|
||||
}
|
||||
|
||||
async function createAccessTokenSession(
|
||||
res: Response,
|
||||
resource: Resource,
|
||||
tokenItem: ResourceAccessToken,
|
||||
userData?: BasicUserData
|
||||
) {
|
||||
const token = generateSessionToken();
|
||||
const sess = await createResourceSession({
|
||||
resourceId: resource.resourceId,
|
||||
token,
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
sessionLength: tokenItem.sessionLength,
|
||||
expiresAt: tokenItem.expiresAt,
|
||||
doNotExtend: tokenItem.expiresAt ? true : false
|
||||
});
|
||||
const cookieName = config.getRawConfig().server.session_cookie_name;
|
||||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
resource.fullDomain!,
|
||||
token,
|
||||
!resource.ssl,
|
||||
new Date(sess.expiresAt)
|
||||
);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
logger.debug("Access token is valid, creating new session");
|
||||
return allowed(res, userData);
|
||||
}
|
||||
|
||||
async function getAccessTokenUserData(
|
||||
tokenItem: ResourceAccessToken,
|
||||
orgId: string
|
||||
): Promise<BasicUserData | undefined> {
|
||||
if (!tokenItem.userId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cacheKey = `accessTokenUser:${tokenItem.userId}:${orgId}`;
|
||||
const cached = localCache.get(cacheKey) as BasicUserData | null | undefined;
|
||||
if (cached !== undefined) {
|
||||
return cached ?? undefined;
|
||||
}
|
||||
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.userId, tokenItem.userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
localCache.set(cacheKey, null, 5);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
|
||||
const userData: BasicUserData = {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
|
||||
};
|
||||
|
||||
localCache.set(cacheKey, userData, 12);
|
||||
return userData;
|
||||
}
|
||||
|
||||
function logAccessTokenRequestAudit(
|
||||
data: {
|
||||
resourceId: number;
|
||||
orgId: string;
|
||||
location?: string;
|
||||
accessTokenId: string;
|
||||
tokenTitle: string | null;
|
||||
userData?: BasicUserData;
|
||||
},
|
||||
body: VerifyResourceSessionSchema
|
||||
) {
|
||||
if (data.userData) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: data.resourceId,
|
||||
orgId: data.orgId,
|
||||
location: data.location,
|
||||
user: {
|
||||
username: data.userData.username,
|
||||
userId: data.userData.userId
|
||||
},
|
||||
metadata: {
|
||||
accessTokenId: data.accessTokenId,
|
||||
accessTokenTitle: data.tokenTitle
|
||||
}
|
||||
},
|
||||
body
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: data.resourceId,
|
||||
orgId: data.orgId,
|
||||
location: data.location,
|
||||
apiKey: {
|
||||
name: data.tokenTitle,
|
||||
apiKeyId: data.accessTokenId
|
||||
}
|
||||
},
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
function logAccessTokenAccessAudit(
|
||||
tokenItem: ResourceAccessToken,
|
||||
resource: Resource,
|
||||
userData: BasicUserData | undefined,
|
||||
body: VerifyResourceSessionSchema
|
||||
) {
|
||||
const userAgent =
|
||||
body.headers?.["user-agent"] || body.headers?.["User-Agent"];
|
||||
|
||||
if (userData) {
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "accessToken",
|
||||
user: {
|
||||
username: userData.username,
|
||||
userId: userData.userId
|
||||
},
|
||||
metadata: {
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
accessTokenTitle: tokenItem.title
|
||||
},
|
||||
userAgent,
|
||||
requestIp: body.requestIp
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "accessToken",
|
||||
apiKey: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
},
|
||||
userAgent,
|
||||
requestIp: body.requestIp
|
||||
});
|
||||
}
|
||||
|
||||
async function headerAuthChallenged(
|
||||
res: Response,
|
||||
redirectPath?: string,
|
||||
|
||||
@@ -33,7 +33,7 @@ const UpdateDomainResponseDataSchema = z.object({
|
||||
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
method: "post",
|
||||
path: "/org/{orgId}/domain/{domainId}",
|
||||
description: "Update a domain by domainId.",
|
||||
tags: [OpenAPITags.Domain],
|
||||
@@ -55,6 +55,31 @@ registry.registerPath({
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/org/{orgId}/domain/{domainId}",
|
||||
description:
|
||||
"Update a domain by domainId. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.Domain],
|
||||
request: {
|
||||
params: z.object({
|
||||
domainId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createApiResponseSchema(UpdateDomainResponseDataSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function updateOrgDomain(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
@@ -248,6 +248,22 @@ authenticated.post(
|
||||
site.updateSite
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site/:siteId/approve",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateSiteApprovals),
|
||||
logActionAudit(ActionsEnum.updateSiteApprovals),
|
||||
site.approveSite
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site/:siteId/reject",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateSiteApprovals),
|
||||
logActionAudit(ActionsEnum.updateSiteApprovals),
|
||||
site.rejectSite
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/site/:siteId",
|
||||
verifySiteAccess,
|
||||
|
||||
+267
-70
@@ -138,6 +138,7 @@ authenticated.post(
|
||||
logActionAudit(ActionsEnum.updateSite),
|
||||
site.updateSite
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/reset-bandwidth",
|
||||
verifyApiKeyOrgAccess,
|
||||
@@ -162,7 +163,7 @@ authenticated.get(
|
||||
|
||||
// Site Resource endpoints
|
||||
authenticated.put(
|
||||
"/org/:orgId/site-resource",
|
||||
["/org/:orgId/site-resource", "/org/:orgId/private-resource"],
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
|
||||
@@ -171,7 +172,10 @@ authenticated.put(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/site/:siteId/resources",
|
||||
[
|
||||
"/org/:orgId/site/:siteId/resources",
|
||||
"/org/:orgId/site/:siteId/private-resources"
|
||||
],
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listSiteResources),
|
||||
@@ -179,21 +183,21 @@ authenticated.get(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/site-resources",
|
||||
["/org/:orgId/site-resources", "/org/:orgId/private-resources"],
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listSiteResources),
|
||||
siteResource.listAllSiteResourcesByOrg
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site-resource/:siteResourceId",
|
||||
["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getSiteResource),
|
||||
siteResource.getSiteResource
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId",
|
||||
["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
|
||||
@@ -202,7 +206,7 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/site-resource/:siteResourceId",
|
||||
["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
|
||||
logActionAudit(ActionsEnum.deleteSiteResource),
|
||||
@@ -210,28 +214,40 @@ authenticated.delete(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site-resource/:siteResourceId/roles",
|
||||
[
|
||||
"/site-resource/:siteResourceId/roles",
|
||||
"/private-resource/:siteResourceId/roles"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
|
||||
siteResource.listSiteResourceRoles
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site-resource/:siteResourceId/users",
|
||||
[
|
||||
"/site-resource/:siteResourceId/users",
|
||||
"/private-resource/:siteResourceId/users"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
|
||||
siteResource.listSiteResourceUsers
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site-resource/:siteResourceId/clients",
|
||||
[
|
||||
"/site-resource/:siteResourceId/clients",
|
||||
"/private-resource/:siteResourceId/clients"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
|
||||
siteResource.listSiteResourceClients
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/roles",
|
||||
[
|
||||
"/site-resource/:siteResourceId/roles",
|
||||
"/private-resource/:siteResourceId/roles"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
@@ -241,7 +257,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/users",
|
||||
[
|
||||
"/site-resource/:siteResourceId/users",
|
||||
"/private-resource/:siteResourceId/users"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
@@ -251,7 +270,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/roles/add",
|
||||
[
|
||||
"/site-resource/:siteResourceId/roles/add",
|
||||
"/private-resource/:siteResourceId/roles/add"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
@@ -261,7 +283,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/roles/remove",
|
||||
[
|
||||
"/site-resource/:siteResourceId/roles/remove",
|
||||
"/private-resource/:siteResourceId/roles/remove"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
@@ -271,7 +296,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/users/add",
|
||||
[
|
||||
"/site-resource/:siteResourceId/users/add",
|
||||
"/private-resource/:siteResourceId/users/add"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
@@ -281,7 +309,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/users/remove",
|
||||
[
|
||||
"/site-resource/:siteResourceId/users/remove",
|
||||
"/private-resource/:siteResourceId/users/remove"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
@@ -291,7 +322,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients",
|
||||
[
|
||||
"/site-resource/:siteResourceId/clients",
|
||||
"/private-resource/:siteResourceId/clients"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits,
|
||||
@@ -301,7 +335,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients/add",
|
||||
[
|
||||
"/site-resource/:siteResourceId/clients/add",
|
||||
"/private-resource/:siteResourceId/clients/add"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits,
|
||||
@@ -311,7 +348,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients/remove",
|
||||
[
|
||||
"/site-resource/:siteResourceId/clients/remove",
|
||||
"/private-resource/:siteResourceId/clients/remove"
|
||||
],
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits,
|
||||
@@ -321,7 +361,7 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/client/:clientId/site-resources",
|
||||
["/client/:clientId/site-resources", "/client/:clientId/private-resources"],
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
@@ -329,7 +369,7 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/resource",
|
||||
["/org/:orgId/resource", "/org/:orgId/public-resource"],
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
@@ -338,7 +378,10 @@ authenticated.put(
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/site/:siteId/resource",
|
||||
[
|
||||
"/org/:orgId/site/:siteId/resource",
|
||||
"/org/:orgId/site/:siteId/public-resource"
|
||||
],
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
@@ -347,14 +390,14 @@ authenticated.put(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site/:siteId/resources",
|
||||
["/site/:siteId/resources", "/site/:siteId/public-resources"],
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
||||
resource.listResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/resources",
|
||||
["/org/:orgId/resources", "/org/:orgId/public-resources"],
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
||||
resource.listResources
|
||||
@@ -383,6 +426,15 @@ authenticated.put(
|
||||
domain.createOrgDomain
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyDomainAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrgDomain),
|
||||
domain.updateOrgDomain
|
||||
);
|
||||
|
||||
// Deprecated: use POST instead. Kept for backward compatibility.
|
||||
authenticated.patch(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyApiKeyOrgAccess,
|
||||
@@ -442,42 +494,45 @@ authenticated.delete(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/roles",
|
||||
["/resource/:resourceId/roles", "/public-resource/:resourceId/roles"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
|
||||
resource.listResourceRoles
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/users",
|
||||
["/resource/:resourceId/users", "/public-resource/:resourceId/users"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
|
||||
resource.listResourceUsers
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId",
|
||||
["/resource/:resourceId", "/public-resource/:resourceId"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResource),
|
||||
resource.getResource
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
"/public-resource-policy/:resourcePolicyId"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
|
||||
policy.getResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/policies",
|
||||
["/resource/:resourceId/policies", "/public-resource/:resourceId/policies"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
|
||||
resource.getResourcePolicies
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
["/resource/:resourceId", "/public-resource/:resourceId"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
||||
@@ -485,15 +540,29 @@ authenticated.post(
|
||||
resource.updateResource
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
"/public-resource-policy/:resourcePolicyId"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
|
||||
policy.updateResourcePolicy
|
||||
);
|
||||
|
||||
// Deprecated: use POST instead. Kept for backward compatibility.
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
"/public-resource-policy/:resourcePolicyId"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
|
||||
policy.updateResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId",
|
||||
["/resource/:resourceId", "/public-resource/:resourceId"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResource),
|
||||
logActionAudit(ActionsEnum.deleteResource),
|
||||
@@ -501,7 +570,7 @@ authenticated.delete(
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/target",
|
||||
["/resource/:resourceId/target", "/public-resource/:resourceId/target"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createTarget),
|
||||
@@ -510,14 +579,14 @@ authenticated.put(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/targets",
|
||||
["/resource/:resourceId/targets", "/public-resource/:resourceId/targets"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listTargets),
|
||||
target.listTargets
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/rule",
|
||||
["/resource/:resourceId/rule", "/public-resource/:resourceId/rule"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
|
||||
@@ -526,14 +595,17 @@ authenticated.put(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/rules",
|
||||
["/resource/:resourceId/rules", "/public-resource/:resourceId/rules"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceRules),
|
||||
resource.listResourceRules
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
[
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
"/public-resource/:resourceId/rule/:ruleId"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
|
||||
@@ -542,7 +614,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
[
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
"/public-resource/:resourceId/rule/:ruleId"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResourceRule),
|
||||
logActionAudit(ActionsEnum.deleteResourceRule),
|
||||
@@ -624,7 +699,7 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/roles",
|
||||
["/resource/:resourceId/roles", "/public-resource/:resourceId/roles"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
@@ -634,7 +709,7 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/users",
|
||||
["/resource/:resourceId/users", "/public-resource/:resourceId/users"],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
@@ -643,8 +718,11 @@ authenticated.post(
|
||||
resource.setResourceUsers
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/access-control",
|
||||
authenticated.post(
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/access-control",
|
||||
"/public-resource-policy/:resourcePolicyId/access-control"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
@@ -655,8 +733,27 @@ authenticated.put(
|
||||
policy.setResourcePolicyAccessControl
|
||||
);
|
||||
|
||||
// Deprecated: use POST instead. Kept for backward compatibility.
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/password",
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/access-control",
|
||||
"/public-resource-policy/:resourcePolicyId/access-control"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyUsers),
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyRoles),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyUsers),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyRoles),
|
||||
policy.setResourcePolicyAccessControl
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/password",
|
||||
"/public-resource-policy/:resourcePolicyId/password"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword),
|
||||
@@ -664,8 +761,24 @@ authenticated.put(
|
||||
policy.setResourcePolicyPassword
|
||||
);
|
||||
|
||||
// Deprecated: use POST instead. Kept for backward compatibility.
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/pincode",
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/password",
|
||||
"/public-resource-policy/:resourcePolicyId/password"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyPassword),
|
||||
policy.setResourcePolicyPassword
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/pincode",
|
||||
"/public-resource-policy/:resourcePolicyId/pincode"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode),
|
||||
@@ -673,8 +786,24 @@ authenticated.put(
|
||||
policy.setResourcePolicyPincode
|
||||
);
|
||||
|
||||
// Deprecated: use POST instead. Kept for backward compatibility.
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/header-auth",
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/pincode",
|
||||
"/public-resource-policy/:resourcePolicyId/pincode"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyPincode),
|
||||
policy.setResourcePolicyPincode
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/header-auth",
|
||||
"/public-resource-policy/:resourcePolicyId/header-auth"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||
@@ -682,8 +811,24 @@ authenticated.put(
|
||||
policy.setResourcePolicyHeaderAuth
|
||||
);
|
||||
|
||||
// Deprecated: use POST instead. Kept for backward compatibility.
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/whitelist",
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/header-auth",
|
||||
"/public-resource-policy/:resourcePolicyId/header-auth"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||
policy.setResourcePolicyHeaderAuth
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/whitelist",
|
||||
"/public-resource-policy/:resourcePolicyId/whitelist"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist),
|
||||
@@ -691,8 +836,37 @@ authenticated.put(
|
||||
policy.setResourcePolicyWhitelist
|
||||
);
|
||||
|
||||
// Deprecated: use POST instead. Kept for backward compatibility.
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/rules",
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/whitelist",
|
||||
"/public-resource-policy/:resourcePolicyId/whitelist"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyWhitelist),
|
||||
policy.setResourcePolicyWhitelist
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/rules",
|
||||
"/public-resource-policy/:resourcePolicyId/rules"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyRules),
|
||||
policy.setResourcePolicyRules
|
||||
);
|
||||
|
||||
// Deprecated: use POST instead. Kept for backward compatibility.
|
||||
authenticated.put(
|
||||
[
|
||||
"/resource-policy/:resourcePolicyId/rules",
|
||||
"/public-resource-policy/:resourcePolicyId/rules"
|
||||
],
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules),
|
||||
@@ -701,7 +875,10 @@ authenticated.put(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/roles/add",
|
||||
[
|
||||
"/resource/:resourceId/roles/add",
|
||||
"/public-resource/:resourceId/roles/add"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
@@ -711,7 +888,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/roles/remove",
|
||||
[
|
||||
"/resource/:resourceId/roles/remove",
|
||||
"/public-resource/:resourceId/roles/remove"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
@@ -721,7 +901,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/users/add",
|
||||
[
|
||||
"/resource/:resourceId/users/add",
|
||||
"/public-resource/:resourceId/users/add"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
@@ -731,7 +914,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/users/remove",
|
||||
[
|
||||
"/resource/:resourceId/users/remove",
|
||||
"/public-resource/:resourceId/users/remove"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
@@ -741,7 +927,7 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/password`,
|
||||
[`/resource/:resourceId/password`, `/public-resource/:resourceId/password`],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
|
||||
@@ -750,7 +936,7 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/pincode`,
|
||||
[`/resource/:resourceId/pincode`, `/public-resource/:resourceId/pincode`],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
|
||||
@@ -759,7 +945,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/header-auth`,
|
||||
[
|
||||
`/resource/:resourceId/header-auth`,
|
||||
`/public-resource/:resourceId/header-auth`
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
|
||||
@@ -768,7 +957,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
[
|
||||
`/resource/:resourceId/whitelist`,
|
||||
`/public-resource/:resourceId/whitelist`
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
@@ -777,7 +969,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist/add`,
|
||||
[
|
||||
`/resource/:resourceId/whitelist/add`,
|
||||
`/public-resource/:resourceId/whitelist/add`
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
@@ -785,7 +980,10 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist/remove`,
|
||||
[
|
||||
`/resource/:resourceId/whitelist/remove`,
|
||||
`/public-resource/:resourceId/whitelist/remove`
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
@@ -793,14 +991,20 @@ authenticated.post(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
[
|
||||
`/resource/:resourceId/whitelist`,
|
||||
`/public-resource/:resourceId/whitelist`
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist),
|
||||
resource.getResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/access-token`,
|
||||
[
|
||||
`/resource/:resourceId/access-token`,
|
||||
`/public-resource/:resourceId/access-token`
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
|
||||
@@ -808,16 +1012,6 @@ authenticated.post(
|
||||
accessToken.generateAccessToken
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/session-token`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResourceSessionToken),
|
||||
logActionAudit(ActionsEnum.createResourceSessionToken),
|
||||
resource.createResourceSessionToken
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/access-token/:accessTokenId`,
|
||||
verifyApiKeyAccessTokenAccess,
|
||||
@@ -834,7 +1028,10 @@ authenticated.get(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/access-tokens`,
|
||||
[
|
||||
`/resource/:resourceId/access-tokens`,
|
||||
`/public-resource/:resourceId/access-tokens`
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listAccessTokens),
|
||||
accessToken.listAccessTokens
|
||||
@@ -1164,7 +1361,7 @@ authenticated.get(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/resource-names",
|
||||
["/org/:orgId/resource-names", "/org/:orgId/public-resource-names"],
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
||||
resource.listAllResourceNames
|
||||
|
||||
@@ -157,7 +157,8 @@ async function resolveAccessibleIdsUncached(
|
||||
.where(
|
||||
and(
|
||||
eq(userResources.userId, userId),
|
||||
eq(resources.orgId, orgId)
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.status, "approved")
|
||||
)
|
||||
),
|
||||
userRoleIds.length > 0
|
||||
@@ -171,7 +172,8 @@ async function resolveAccessibleIdsUncached(
|
||||
.where(
|
||||
and(
|
||||
inArray(roleResources.roleId, userRoleIds),
|
||||
eq(resources.orgId, orgId)
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.status, "approved")
|
||||
)
|
||||
)
|
||||
: Promise.resolve([]),
|
||||
@@ -183,7 +185,11 @@ async function resolveAccessibleIdsUncached(
|
||||
eq(effectiveResourcePolicyId, userPolicies.resourcePolicyId)
|
||||
)
|
||||
.where(
|
||||
and(eq(userPolicies.userId, userId), eq(resources.orgId, orgId))
|
||||
and(
|
||||
eq(userPolicies.userId, userId),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.status, "approved")
|
||||
)
|
||||
),
|
||||
userRoleIds.length > 0
|
||||
? db
|
||||
@@ -199,21 +205,48 @@ async function resolveAccessibleIdsUncached(
|
||||
.where(
|
||||
and(
|
||||
inArray(rolePolicies.roleId, userRoleIds),
|
||||
eq(resources.orgId, orgId)
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.status, "approved")
|
||||
)
|
||||
)
|
||||
: Promise.resolve([]),
|
||||
db
|
||||
.select({ siteResourceId: userSiteResources.siteResourceId })
|
||||
.from(userSiteResources)
|
||||
.where(eq(userSiteResources.userId, userId)),
|
||||
.innerJoin(
|
||||
siteResources,
|
||||
eq(
|
||||
userSiteResources.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(userSiteResources.userId, userId),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.status, "approved")
|
||||
)
|
||||
),
|
||||
userRoleIds.length > 0
|
||||
? db
|
||||
.select({
|
||||
siteResourceId: roleSiteResources.siteResourceId
|
||||
})
|
||||
.from(roleSiteResources)
|
||||
.where(inArray(roleSiteResources.roleId, userRoleIds))
|
||||
.innerJoin(
|
||||
siteResources,
|
||||
eq(
|
||||
roleSiteResources.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
inArray(roleSiteResources.roleId, userRoleIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.status, "approved")
|
||||
)
|
||||
)
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
|
||||
@@ -365,6 +398,7 @@ async function filterPublicResourceIdsByTextSearch(
|
||||
inArray(resources.resourceId, resourceIds),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.status, "approved"),
|
||||
textMatch
|
||||
)
|
||||
);
|
||||
@@ -402,6 +436,7 @@ async function filterSiteResourceIdsByTextSearch(
|
||||
inArray(siteResources.siteResourceId, siteResourceIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.status, "approved"),
|
||||
textMatch
|
||||
)
|
||||
);
|
||||
@@ -503,7 +538,8 @@ async function listSiteGroups(
|
||||
const publicConditions = [
|
||||
inArray(resources.resourceId, accessible.resourceIds),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true)
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.status, "approved")
|
||||
];
|
||||
if (searchPublic) {
|
||||
publicConditions.push(searchPublic);
|
||||
@@ -558,7 +594,8 @@ async function listSiteGroups(
|
||||
const siteConditions = [
|
||||
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true)
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.status, "approved")
|
||||
];
|
||||
if (searchSite) {
|
||||
siteConditions.push(searchSite);
|
||||
@@ -621,7 +658,8 @@ async function listSiteGroups(
|
||||
const noSitePublicConditions = [
|
||||
inArray(resources.resourceId, accessible.resourceIds),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true)
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.status, "approved")
|
||||
];
|
||||
if (searchPublic) {
|
||||
noSitePublicConditions.push(searchPublic);
|
||||
@@ -655,7 +693,8 @@ async function listSiteGroups(
|
||||
const noSiteSiteConditions = [
|
||||
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true)
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.status, "approved")
|
||||
];
|
||||
if (searchSite) {
|
||||
noSiteSiteConditions.push(searchSite);
|
||||
@@ -746,7 +785,8 @@ async function listLabelGroups(
|
||||
const publicConditions = [
|
||||
inArray(resources.resourceId, accessible.resourceIds),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true)
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.status, "approved")
|
||||
];
|
||||
const searchPublic = buildSearchConditionForPublic(query.query);
|
||||
if (searchPublic) {
|
||||
@@ -810,7 +850,8 @@ async function listLabelGroups(
|
||||
const siteConditions = [
|
||||
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true)
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.status, "approved")
|
||||
];
|
||||
const searchSite = buildSearchConditionForSiteResource(query.query);
|
||||
if (searchSite) {
|
||||
@@ -997,6 +1038,7 @@ async function mapPublicResources(
|
||||
inArray(resources.resourceId, resourceIds),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.status, "approved"),
|
||||
siteIdFilter != null
|
||||
? eq(sites.siteId, siteIdFilter)
|
||||
: undefined
|
||||
@@ -1088,6 +1130,7 @@ async function mapSiteResources(
|
||||
inArray(siteResources.siteResourceId, siteResourceIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.status, "approved"),
|
||||
siteIdFilter != null
|
||||
? eq(sites.siteId, siteIdFilter)
|
||||
: undefined
|
||||
@@ -1382,7 +1425,8 @@ async function collectAccessibleSites(
|
||||
const publicConditions = [
|
||||
inArray(resources.resourceId, accessible.resourceIds),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true)
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.status, "approved")
|
||||
];
|
||||
if (siteNameSearch) {
|
||||
publicConditions.push(siteNameSearch);
|
||||
@@ -1422,7 +1466,8 @@ async function collectAccessibleSites(
|
||||
const siteConditions = [
|
||||
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true)
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.status, "approved")
|
||||
];
|
||||
if (siteNameSearch) {
|
||||
siteConditions.push(siteNameSearch);
|
||||
@@ -1476,6 +1521,7 @@ async function collectAccessibleLabels(
|
||||
inArray(resources.resourceId, accessible.resourceIds),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.status, "approved"),
|
||||
eq(labels.orgId, orgId)
|
||||
];
|
||||
if (labelNameSearch) {
|
||||
@@ -1511,6 +1557,7 @@ async function collectAccessibleLabels(
|
||||
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.status, "approved"),
|
||||
eq(labels.orgId, orgId)
|
||||
];
|
||||
if (labelNameSearch) {
|
||||
|
||||
@@ -148,7 +148,12 @@ export async function buildClientConfigurationForNewtClient(
|
||||
.from(siteResources)
|
||||
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
|
||||
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
|
||||
.where(eq(siteNetworks.siteId, siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(siteNetworks.siteId, siteId),
|
||||
eq(siteResources.enabled, true)
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.map((r) => r.siteResources));
|
||||
|
||||
const targetsToSend: SubnetProxyTargetV2[] = [];
|
||||
|
||||
@@ -29,7 +29,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { publicKey, port, chainId } = message.data;
|
||||
const { publicKey, port, localEndpoints, chainId } = message.data;
|
||||
const siteId = newt.siteId;
|
||||
|
||||
// Get the current site data
|
||||
@@ -69,7 +69,10 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
||||
.update(sites)
|
||||
.set({
|
||||
publicKey,
|
||||
listenPort: port
|
||||
listenPort: port,
|
||||
localEndpoints: localEndpoints
|
||||
? JSON.stringify(localEndpoints)
|
||||
: null
|
||||
})
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.returning();
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
generateRemoteSubnets
|
||||
} from "@server/lib/ip";
|
||||
import logger from "@server/logger";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { addPeer, deletePeer } from "../newt/peers";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function buildSiteConfigurationForOlmClient(
|
||||
siteId: number;
|
||||
name?: string;
|
||||
endpoint?: string;
|
||||
localEndpoints?: string[];
|
||||
publicKey?: string;
|
||||
serverIP?: string | null;
|
||||
serverPort?: number | null;
|
||||
@@ -70,7 +71,13 @@ export async function buildSiteConfigurationForOlmClient(
|
||||
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
|
||||
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
|
||||
.where(
|
||||
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
|
||||
and(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
),
|
||||
eq(siteResources.enabled, true)
|
||||
)
|
||||
);
|
||||
|
||||
const siteResourcesBySiteId = new Map<number, SiteResource[]>();
|
||||
@@ -200,6 +207,9 @@ export async function buildSiteConfigurationForOlmClient(
|
||||
name: site.name,
|
||||
// relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing
|
||||
endpoint: site.endpoint,
|
||||
localEndpoints: site.localEndpoints
|
||||
? JSON.parse(site.localEndpoints)
|
||||
: undefined,
|
||||
publicKey: site.publicKey,
|
||||
serverIP: site.address,
|
||||
serverPort: site.listenPort,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { db, sites } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { clients, Olm } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { updatePeer as newtUpdatePeer } from "../newt/peers";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export const handleOlmLocalMessage: MessageHandler = async (context) => {
|
||||
const { message, client: c, sendToClient } = context;
|
||||
const olm = c as Olm;
|
||||
|
||||
logger.info("Handling local olm message!");
|
||||
|
||||
if (!olm) {
|
||||
logger.warn("Olm not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!olm.clientId) {
|
||||
logger.warn("Olm has no client!");
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = olm.clientId;
|
||||
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
logger.warn("Client not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
|
||||
if (!client.pubKey) {
|
||||
logger.warn("Client has no endpoint or listen port");
|
||||
return;
|
||||
}
|
||||
|
||||
const { siteId, chainId } = message.data;
|
||||
|
||||
// Get the site
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!site || !site.exitNodeId) {
|
||||
logger.warn("Site not found or has no exit node");
|
||||
return;
|
||||
}
|
||||
|
||||
// update the peer on the newt
|
||||
await newtUpdatePeer(siteId, client.pubKey, {
|
||||
endpoint: "" // this removes the endpoint so the newt knows to accept local
|
||||
});
|
||||
|
||||
// Just ack the message, we don't keep sending it
|
||||
return {
|
||||
message: {
|
||||
type: "olm/wg/peer/local",
|
||||
data: {
|
||||
siteId: siteId,
|
||||
chainId
|
||||
}
|
||||
},
|
||||
broadcast: false,
|
||||
excludeSender: false
|
||||
};
|
||||
};
|
||||
@@ -79,9 +79,9 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
||||
)
|
||||
);
|
||||
|
||||
// update the peer on the exit node
|
||||
// update the peer on the newt
|
||||
await newtUpdatePeer(siteId, client.pubKey, {
|
||||
endpoint: "" // this removes the endpoint so the exit node knows to relay
|
||||
endpoint: "" // this removes the endpoint so the newt knows to relay
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,24 +3,15 @@ import {
|
||||
db,
|
||||
networks,
|
||||
siteNetworks,
|
||||
siteResources,
|
||||
siteResources
|
||||
} from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import {
|
||||
clients,
|
||||
clientSitesAssociationsCache,
|
||||
Olm,
|
||||
sites
|
||||
} from "@server/db";
|
||||
import { clients, clientSitesAssociationsCache, Olm, sites } from "@server/db";
|
||||
import { and, eq, inArray, isNotNull, isNull } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import {
|
||||
generateAliasConfig,
|
||||
} from "@server/lib/ip";
|
||||
import { generateAliasConfig } from "@server/lib/ip";
|
||||
import { generateRemoteSubnets } from "@server/lib/ip";
|
||||
import {
|
||||
addPeer as newtAddPeer,
|
||||
} from "@server/routers/newt/peers";
|
||||
import { addPeer as newtAddPeer } from "@server/routers/newt/peers";
|
||||
|
||||
export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||
context
|
||||
@@ -135,10 +126,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||
clientSiteResourcesAssociationsCache.siteResourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
networks,
|
||||
eq(siteResources.networkId, networks.networkId)
|
||||
)
|
||||
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
and(
|
||||
@@ -147,10 +135,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||
)
|
||||
)
|
||||
.where(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
)
|
||||
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
|
||||
);
|
||||
|
||||
// Return connect message with all site configurations
|
||||
@@ -161,6 +146,9 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||
siteId: site.siteId,
|
||||
name: site.name,
|
||||
endpoint: site.endpoint,
|
||||
localEndpoints: site.localEndpoints
|
||||
? JSON.parse(site.localEndpoints)
|
||||
: undefined,
|
||||
publicKey: site.publicKey,
|
||||
serverIP: site.address,
|
||||
serverPort: site.listenPort,
|
||||
@@ -170,7 +158,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||
aliases: generateAliasConfig(
|
||||
allSiteResources.map(({ siteResources }) => siteResources)
|
||||
),
|
||||
chainId: chainId,
|
||||
chainId: chainId
|
||||
}
|
||||
},
|
||||
broadcast: false,
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { db, exitNodes, sites } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { clients, clientSitesAssociationsCache, Olm } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { updatePeer as newtUpdatePeer } from "../newt/peers";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export const handleOlmUnLocalMessage: MessageHandler = async (context) => {
|
||||
const { message, client: c, sendToClient } = context;
|
||||
const olm = c as Olm;
|
||||
|
||||
logger.info("Handling unlocal olm message!");
|
||||
|
||||
if (!olm) {
|
||||
logger.warn("Olm not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!olm.clientId) {
|
||||
logger.warn("Olm has no client!");
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = olm.clientId;
|
||||
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
logger.warn("Client not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
|
||||
if (!client.pubKey) {
|
||||
logger.warn("Client has no endpoint or listen port");
|
||||
return;
|
||||
}
|
||||
|
||||
const { siteId, chainId } = message.data;
|
||||
|
||||
// Get the site
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!site) {
|
||||
logger.warn("Site not found or has no exit node");
|
||||
return;
|
||||
}
|
||||
|
||||
const [clientSiteAssociation] = await db
|
||||
.select()
|
||||
.from(clientSitesAssociationsCache)
|
||||
.where(
|
||||
and(
|
||||
eq(clientSitesAssociationsCache.clientId, olm.clientId),
|
||||
eq(clientSitesAssociationsCache.siteId, siteId)
|
||||
)
|
||||
);
|
||||
|
||||
if (!clientSiteAssociation) {
|
||||
logger.warn("Client-Site association not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clientSiteAssociation.endpoint) {
|
||||
logger.warn("Client-Site association has no endpoint, cannot unrelay");
|
||||
return;
|
||||
}
|
||||
|
||||
// update the peer on the newt
|
||||
await newtUpdatePeer(siteId, client.pubKey, {
|
||||
endpoint: clientSiteAssociation.isRelayed
|
||||
? ""
|
||||
: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the newt
|
||||
});
|
||||
|
||||
return {
|
||||
message: {
|
||||
type: "olm/wg/peer/unlocal",
|
||||
data: {
|
||||
siteId: siteId,
|
||||
chainId
|
||||
}
|
||||
},
|
||||
broadcast: false,
|
||||
excludeSender: false
|
||||
};
|
||||
};
|
||||
@@ -77,9 +77,9 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// update the peer on the exit node
|
||||
// update the peer on the newt
|
||||
await newtUpdatePeer(siteId, client.pubKey, {
|
||||
endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the exit node
|
||||
endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the newt
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,3 +13,5 @@ export * from "./recoverOlmWithFingerprint";
|
||||
export * from "./handleOlmDisconnectingMessage";
|
||||
export * from "./handleOlmServerInitAddPeerHandshake";
|
||||
export * from "./offlineChecker";
|
||||
export * from "./handleOlmUnLocalMessage";
|
||||
export * from "./handleOlmLocalMessage";
|
||||
|
||||
@@ -18,6 +18,7 @@ export async function addPeer(
|
||||
serverPort: number | null;
|
||||
remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access
|
||||
aliases: Alias[];
|
||||
localEndpoints?: string[]; // optional, list of local endpoints for the peer
|
||||
},
|
||||
olmId?: string,
|
||||
version?: string | null
|
||||
@@ -44,6 +45,7 @@ export async function addPeer(
|
||||
name: peer.name,
|
||||
publicKey: peer.publicKey,
|
||||
endpoint: peer.endpoint,
|
||||
localEndpoints: peer.localEndpoints,
|
||||
relayEndpoint: peer.relayEndpoint,
|
||||
serverIP: peer.serverIP,
|
||||
serverPort: peer.serverPort,
|
||||
|
||||
@@ -168,7 +168,7 @@ registry.registerPath({
|
||||
path: "/org/{orgId}/resource-policy/{niceId}",
|
||||
description:
|
||||
"Get a resource policy by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string(),
|
||||
@@ -182,7 +182,20 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource-policy/{resourcePolicyId}",
|
||||
description: "Get a resource policy by its resourcePolicyId.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: z.object({
|
||||
resourcePolicyId: z.number()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource-policy/{resourcePolicyId}",
|
||||
description: "Get a resource policy by its resourcePolicyId.",
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: z.object({
|
||||
resourcePolicyId: z.number()
|
||||
|
||||
@@ -40,7 +40,66 @@ registry.registerPath({
|
||||
path: "/resource-policy/{resourceId}/access-control",
|
||||
description:
|
||||
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
|
||||
tags: [OpenAPITags.Policy, OpenAPITags.User],
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyAccessControlParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyAcccessControlBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource-policy/{resourceId}/access-control",
|
||||
description:
|
||||
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
|
||||
tags: [OpenAPITags.PublicResourcePolicy, OpenAPITags.User],
|
||||
request: {
|
||||
params: setResourcePolicyAccessControlParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyAcccessControlBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourceId}/access-control",
|
||||
description:
|
||||
"Set access control users for a resource policy, including SSO, users, roles, Identity provider. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyAccessControlParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyAcccessControlBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource-policy/{resourceId}/access-control",
|
||||
description:
|
||||
"Set access control users for a resource policy, including SSO, users, roles, Identity provider. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicy, OpenAPITags.User],
|
||||
request: {
|
||||
params: setResourcePolicyAccessControlParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -25,11 +25,70 @@ const setResourcePolicyHeaderAuthBodySchema = z.strictObject({
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
method: "post",
|
||||
path: "/resource-policy/{resourcePolicyId}/header-auth",
|
||||
description:
|
||||
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyHeaderAuthParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyHeaderAuthBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/header-auth",
|
||||
description:
|
||||
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.",
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyHeaderAuthParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyHeaderAuthBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/header-auth",
|
||||
description:
|
||||
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyHeaderAuthParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyHeaderAuthBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/header-auth",
|
||||
description:
|
||||
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyHeaderAuthParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -20,11 +20,70 @@ const setResourcePolicyPasswordBodySchema = z.strictObject({
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
method: "post",
|
||||
path: "/resource-policy/{resourcePolicyId}/password",
|
||||
description:
|
||||
"Set the password for a resource policy. Setting the password to null will remove it.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyPasswordParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyPasswordBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/password",
|
||||
description:
|
||||
"Set the password for a resource policy. Setting the password to null will remove it.",
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyPasswordParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyPasswordBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/password",
|
||||
description:
|
||||
"Set the password for a resource policy. Setting the password to null will remove it. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyPasswordParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyPasswordBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/password",
|
||||
description:
|
||||
"Set the password for a resource policy. Setting the password to null will remove it. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyPasswordParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -23,11 +23,70 @@ const setResourcePolicyPincodeBodySchema = z.strictObject({
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
method: "post",
|
||||
path: "/resource-policy/{resourcePolicyId}/pincode",
|
||||
description:
|
||||
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyPincodeParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyPincodeBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/pincode",
|
||||
description:
|
||||
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it.",
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyPincodeParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyPincodeBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/pincode",
|
||||
description:
|
||||
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyPincodeParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyPincodeBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/pincode",
|
||||
description:
|
||||
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyPincodeParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -43,11 +43,70 @@ const setResourcePolicyRulesParamsSchema = z.strictObject({
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
method: "post",
|
||||
path: "/resource-policy/{resourcePolicyId}/rules",
|
||||
description:
|
||||
"Set all rules for a resource policy at once. This will replace all existing rules.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyRulesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyRulesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/rules",
|
||||
description:
|
||||
"Set all rules for a resource policy at once. This will replace all existing rules.",
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyRulesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyRulesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/rules",
|
||||
description:
|
||||
"Set all rules for a resource policy at once. This will replace all existing rules. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyRulesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyRulesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/rules",
|
||||
description:
|
||||
"Set all rules for a resource policy at once. This will replace all existing rules. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyRulesParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -28,11 +28,70 @@ const setResourcePolicyWhitelistParamsSchema = z.strictObject({
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
method: "post",
|
||||
path: "/resource-policy/{resourcePolicyId}/whitelist",
|
||||
description:
|
||||
"Set email whitelist for a resource policy. This will replace all existing emails.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/whitelist",
|
||||
description:
|
||||
"Set email whitelist for a resource policy. This will replace all existing emails.",
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/whitelist",
|
||||
description:
|
||||
"Set email whitelist for a resource policy. This will replace all existing emails. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicyLegacy],
|
||||
request: {
|
||||
params: setResourcePolicyWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource-policy/{resourcePolicyId}/whitelist",
|
||||
description:
|
||||
"Set email whitelist for a resource policy. This will replace all existing emails. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: setResourcePolicyWhitelistParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -19,10 +19,66 @@ const updateResourcePolicyBodySchema = z.strictObject({
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
method: "post",
|
||||
path: "/resource-policy/{resourcePolicyId}",
|
||||
description: "Update a resource policy.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: updateResourcePolicyParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateResourcePolicyBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource-policy/{resourcePolicyId}",
|
||||
description: "Update a resource policy.",
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: updateResourcePolicyParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateResourcePolicyBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}",
|
||||
description: "Update a resource policy. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: updateResourcePolicyParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateResourcePolicyBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource-policy/{resourcePolicyId}",
|
||||
description: "Update a resource policy. Deprecated: use POST instead.",
|
||||
deprecated: true,
|
||||
tags: [OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: updateResourcePolicyParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -34,6 +34,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/whitelist/add",
|
||||
description: "Add a single email to the resource whitelist.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: addEmailToResourceWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: addEmailToResourceWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/whitelist/add",
|
||||
description: "Add a single email to the resource whitelist.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: addEmailToResourceWhitelistParamsSchema,
|
||||
@@ -144,10 +177,7 @@ export async function addEmailToResourceWhitelist(
|
||||
.from(resourcePolicyWhiteList)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
resourcePolicyWhiteList.resourcePolicyId,
|
||||
policyId
|
||||
),
|
||||
eq(resourcePolicyWhiteList.resourcePolicyId, policyId),
|
||||
eq(resourcePolicyWhiteList.email, email)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -28,6 +28,40 @@ const addRoleToResourceParamsSchema = z
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/roles/add",
|
||||
description:
|
||||
"Add a single role to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the role is added to the inline policy instead of directly to the resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: addRoleToResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: addRoleToResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/roles/add",
|
||||
description:
|
||||
"Add a single role to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the role is added to the inline policy instead of directly to the resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
|
||||
|
||||
@@ -28,6 +28,40 @@ const addUserToResourceParamsSchema = z
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/users/add",
|
||||
description:
|
||||
"Add a single user to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the user is added to the inline policy instead of directly to the resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: addUserToResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: addUserToResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/users/add",
|
||||
description:
|
||||
"Add a single user to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the user is added to the inline policy instead of directly to the resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.User],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { db } from "@server/db";
|
||||
import { Resource, resources } from "@server/db";
|
||||
import { Resource, resources, users } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -156,11 +156,42 @@ export async function authWithAccessToken(
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
let accessAuditUser: { username: string; userId: string } | undefined;
|
||||
if (tokenItem.userId) {
|
||||
const [associatedUser] = await db
|
||||
.select({
|
||||
userId: users.userId,
|
||||
username: users.username
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.userId, tokenItem.userId))
|
||||
.limit(1);
|
||||
if (associatedUser) {
|
||||
accessAuditUser = {
|
||||
userId: associatedUser.userId,
|
||||
username: associatedUser.username
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "accessToken",
|
||||
apiKey: accessAuditUser
|
||||
? undefined
|
||||
: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
},
|
||||
user: accessAuditUser,
|
||||
metadata: accessAuditUser
|
||||
? {
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
accessTokenTitle: tokenItem.title
|
||||
}
|
||||
: undefined,
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
@@ -153,6 +153,39 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/resource",
|
||||
description: "Create a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: createResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createHttpResourceSchema.or(createRawResourceSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/public-resource",
|
||||
description: "Create a resource.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: createResourceParamsSchema,
|
||||
|
||||
@@ -9,16 +9,14 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
isValidCIDR,
|
||||
isValidIP,
|
||||
isValidUrlGlobPattern
|
||||
RESOURCE_RULE_MATCH_TYPES,
|
||||
getResourceRuleValueValidationError
|
||||
} from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isValidRegionId } from "@server/db/regions";
|
||||
|
||||
const createResourceRuleSchema = z.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"]),
|
||||
match: z.enum(RESOURCE_RULE_MATCH_TYPES),
|
||||
value: z.string().min(1),
|
||||
priority: z.int(),
|
||||
enabled: z.boolean().optional()
|
||||
@@ -32,6 +30,39 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource/{resourceId}/rule",
|
||||
description: "Create a resource rule.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: createResourceRuleParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createResourceRuleSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource/{resourceId}/rule",
|
||||
description: "Create a resource rule.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: createResourceRuleParamsSchema,
|
||||
@@ -118,39 +149,14 @@ export async function createResourceRule(
|
||||
);
|
||||
}
|
||||
|
||||
if (match === "CIDR") {
|
||||
if (!isValidCIDR(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid CIDR provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "IP") {
|
||||
if (!isValidIP(value)) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided")
|
||||
);
|
||||
}
|
||||
} else if (match === "PATH") {
|
||||
if (!isValidUrlGlobPattern(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid URL glob pattern provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "REGION") {
|
||||
if (!isValidRegionId(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid region ID provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
const valueValidationError = getResourceRuleValueValidationError(
|
||||
match,
|
||||
value
|
||||
);
|
||||
if (valueValidationError) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, valueValidationError)
|
||||
);
|
||||
}
|
||||
|
||||
// Create the new resource rule
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources, users, userOrgs } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { createResourceSession } from "@server/auth/sessions/resource";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { createSession, generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { response } from "@server/lib/response";
|
||||
|
||||
const createResourceSessionTokenParams = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const createResourceSessionTokenBody = z.strictObject({
|
||||
userId: z.string().nonempty(),
|
||||
idpId: z.coerce.number().int().positive().optional()
|
||||
});
|
||||
|
||||
export type CreateResourceSessionTokenResponse = {
|
||||
requestToken: string;
|
||||
};
|
||||
|
||||
export async function createResourceSessionToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = createResourceSessionTokenParams.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = createResourceSessionTokenBody.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
const { userId, idpId } = parsedBody.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const candidates = await db
|
||||
.select({ userId: users.userId })
|
||||
.from(userOrgs)
|
||||
.innerJoin(users, eq(userOrgs.userId, users.userId))
|
||||
.where(
|
||||
and(
|
||||
eq(users.userId, userId),
|
||||
eq(userOrgs.orgId, resource.orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`User not found in the organization that owns this resource`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (candidates.length > 1) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Multiple users match this username (external users from different identity providers). Specify idpId to disambiguate."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const targetUserId = candidates[0].userId;
|
||||
|
||||
const appSessionToken = generateSessionToken();
|
||||
const appSession = await createSession(appSessionToken, targetUserId);
|
||||
|
||||
const requestToken = generateSessionToken();
|
||||
await createResourceSession({
|
||||
resourceId,
|
||||
token: requestToken,
|
||||
userSessionId: appSession.sessionId,
|
||||
isRequestToken: true,
|
||||
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
||||
sessionLength: 1000 * 30,
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
logger.debug("Resource session token created successfully");
|
||||
|
||||
return response<CreateResourceSessionTokenResponse>(res, {
|
||||
data: { requestToken },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource session token created successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,32 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource/{resourceId}",
|
||||
description: "Delete a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: deleteResourceSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/public-resource/{resourceId}",
|
||||
description: "Delete a resource.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: deleteResourceSchema
|
||||
|
||||
@@ -19,6 +19,32 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource/{resourceId}/rule/{ruleId}",
|
||||
description: "Delete a resource rule.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: deleteResourceRuleSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/public-resource/{resourceId}/rule/{ruleId}",
|
||||
description: "Delete a resource rule.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: deleteResourceRuleSchema
|
||||
|
||||
@@ -63,7 +63,7 @@ registry.registerPath({
|
||||
path: "/org/{orgId}/resource/{niceId}",
|
||||
description:
|
||||
"Get a resource by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string(),
|
||||
@@ -92,6 +92,34 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}",
|
||||
description: "Get a resource by resourceId.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: z.object({
|
||||
resourceId: z.number()
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}",
|
||||
description: "Get a resource by resourceId.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: z.object({
|
||||
|
||||
@@ -25,8 +25,21 @@ export type GetResourcePoliciesResponse = {
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/policies",
|
||||
description: "Get the inline and shared policies associated with a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Policy],
|
||||
description:
|
||||
"Get the inline and shared policies associated with a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: getResourcePoliciesParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}/policies",
|
||||
description:
|
||||
"Get the inline and shared policies associated with a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.PublicResourcePolicy],
|
||||
request: {
|
||||
params: getResourcePoliciesParamsSchema
|
||||
},
|
||||
|
||||
@@ -44,6 +44,32 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/whitelist",
|
||||
description: "Get the whitelist of emails for a specific resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: getResourceWhitelistSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}/whitelist",
|
||||
description: "Get the whitelist of emails for a specific resource.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: getResourceWhitelistSchema
|
||||
|
||||
@@ -42,9 +42,14 @@ export async function getResourceStatusHistory(
|
||||
|
||||
const entityType = "resource";
|
||||
const entityId = parsedParams.data.resourceId;
|
||||
const { days } = parsedQuery.data;
|
||||
const { days, tzOffsetMinutes } = parsedQuery.data;
|
||||
|
||||
const data = await getCachedStatusHistory(entityType, entityId, days);
|
||||
const data = await getCachedStatusHistory(
|
||||
entityType,
|
||||
entityId,
|
||||
days,
|
||||
tzOffsetMinutes
|
||||
);
|
||||
|
||||
return response<StatusHistoryResponse>(res, {
|
||||
data,
|
||||
|
||||
@@ -17,7 +17,6 @@ export * from "./getResourceWhitelist";
|
||||
export * from "./authWithWhitelist";
|
||||
export * from "./authWithAccessToken";
|
||||
export * from "./getExchangeToken";
|
||||
export * from "./createResourceSessionToken";
|
||||
export * from "./createResourceRule";
|
||||
export * from "./deleteResourceRule";
|
||||
export * from "./listResourceRules";
|
||||
|
||||
@@ -33,6 +33,34 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/resources-names",
|
||||
description: "List all resource names for an organization.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/public-resource-names",
|
||||
description: "List all resource names for an organization.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: z.object({
|
||||
|
||||
@@ -48,6 +48,32 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/roles",
|
||||
description: "List all roles for a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: listResourceRolesSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}/roles",
|
||||
description: "List all roles for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: listResourceRolesSchema
|
||||
|
||||
@@ -71,6 +71,33 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/rules",
|
||||
description: "List rules for a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: listResourceRulesParamsSchema,
|
||||
query: listResourceRulesSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}/rules",
|
||||
description: "List rules for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: listResourceRulesParamsSchema,
|
||||
|
||||
@@ -38,6 +38,32 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/users",
|
||||
description: "List all users for a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: listResourceUsersSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}/users",
|
||||
description: "List all users for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.User],
|
||||
request: {
|
||||
params: listResourceUsersSchema
|
||||
|
||||
@@ -138,6 +138,15 @@ const listResourcesSchema = z.strictObject({
|
||||
description:
|
||||
"When set, only resources that have at least one target on this site are returned"
|
||||
}),
|
||||
status: z
|
||||
.enum(["pending", "approved"])
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.openapi({
|
||||
type: "string",
|
||||
enum: ["pending", "approved"],
|
||||
description: "Filter by resource status"
|
||||
}),
|
||||
labels: z
|
||||
.preprocess((val) => {
|
||||
if (val === undefined || val === null || val === "") {
|
||||
@@ -400,6 +409,35 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/resources",
|
||||
description: "List resources for an organization.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
}),
|
||||
query: listResourcesSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/public-resources",
|
||||
description: "List resources for an organization.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: z.object({
|
||||
@@ -451,6 +489,7 @@ export async function listResources(
|
||||
sort_by,
|
||||
order,
|
||||
siteId,
|
||||
status,
|
||||
labels: labelFilter
|
||||
} = parsedQuery.data;
|
||||
|
||||
@@ -660,6 +699,10 @@ export async function listResources(
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof status !== "undefined") {
|
||||
conditions.push(eq(resources.status, status));
|
||||
}
|
||||
|
||||
if (siteId != null) {
|
||||
const resourcesWithSite = db
|
||||
.select({ resourceId: targets.resourceId })
|
||||
|
||||
@@ -45,18 +45,19 @@ function userResourceAliasesCacheKey(
|
||||
page: number,
|
||||
pageSize: number,
|
||||
includeLabels: boolean,
|
||||
labelFilter: string[]
|
||||
labelFilter: string[],
|
||||
status?: "pending" | "approved"
|
||||
) {
|
||||
const labelsKey =
|
||||
labelFilter.length > 0 ? labelFilter.slice().sort().join(",") : "all";
|
||||
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}`;
|
||||
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}:${status ?? "all"}`;
|
||||
}
|
||||
|
||||
const listUserResourceAliasesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listUserResourceAliasesQuerySchema = z.strictObject({
|
||||
const listUserResourceAliasesQuerySchema = z.object({
|
||||
pageSize: z.coerce
|
||||
.number<string>()
|
||||
.int()
|
||||
@@ -96,7 +97,16 @@ const listUserResourceAliasesQuerySchema = z.strictObject({
|
||||
type: "array",
|
||||
description:
|
||||
"Filter by resource labels. A resource matches when it has any of the given labels (OR)."
|
||||
})
|
||||
}),
|
||||
status: z
|
||||
.enum(["pending", "approved"])
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.openapi({
|
||||
type: "string",
|
||||
enum: ["pending", "approved"],
|
||||
description: "Filter by site resource status"
|
||||
})
|
||||
});
|
||||
|
||||
export type UserResourceAliasItem = {
|
||||
@@ -130,7 +140,8 @@ export async function listUserResourceAliases(
|
||||
page,
|
||||
pageSize,
|
||||
includeLabels,
|
||||
labels: labelFilter
|
||||
labels: labelFilter,
|
||||
status
|
||||
} = parsedQuery.data;
|
||||
|
||||
const parsedParams = listUserResourceAliasesParamsSchema.safeParse(
|
||||
@@ -172,7 +183,8 @@ export async function listUserResourceAliases(
|
||||
page,
|
||||
pageSize,
|
||||
includeLabels,
|
||||
labelFilter ?? []
|
||||
labelFilter ?? [],
|
||||
status
|
||||
);
|
||||
const cachedData: ListUserResourceAliasesResponse | undefined =
|
||||
await cache.get(cacheKey);
|
||||
@@ -257,6 +269,10 @@ export async function listUserResourceAliases(
|
||||
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
|
||||
];
|
||||
|
||||
if (typeof status !== "undefined") {
|
||||
whereConditions.push(eq(siteResources.status, status));
|
||||
}
|
||||
|
||||
if (labelFilter && labelFilter.length > 0) {
|
||||
whereConditions.push(
|
||||
inArray(
|
||||
|
||||
@@ -34,6 +34,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/whitelist/remove",
|
||||
description: "Remove a single email from the resource whitelist.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: removeEmailFromResourceWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: removeEmailFromResourceWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/whitelist/remove",
|
||||
description: "Remove a single email from the resource whitelist.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: removeEmailFromResourceWhitelistParamsSchema,
|
||||
@@ -143,10 +176,7 @@ export async function removeEmailFromResourceWhitelist(
|
||||
.from(resourcePolicyWhiteList)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
resourcePolicyWhiteList.resourcePolicyId,
|
||||
policyId
|
||||
),
|
||||
eq(resourcePolicyWhiteList.resourcePolicyId, policyId),
|
||||
eq(resourcePolicyWhiteList.email, email)
|
||||
)
|
||||
);
|
||||
@@ -164,10 +194,7 @@ export async function removeEmailFromResourceWhitelist(
|
||||
.delete(resourcePolicyWhiteList)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
resourcePolicyWhiteList.resourcePolicyId,
|
||||
policyId
|
||||
),
|
||||
eq(resourcePolicyWhiteList.resourcePolicyId, policyId),
|
||||
eq(resourcePolicyWhiteList.email, email)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -29,6 +29,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/roles/remove",
|
||||
description: "Remove a single role from a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: removeRoleFromResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: removeRoleFromResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/roles/remove",
|
||||
description: "Remove a single role from a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: removeRoleFromResourceParamsSchema,
|
||||
|
||||
@@ -29,6 +29,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/users/remove",
|
||||
description: "Remove a single user from a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: removeUserFromResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: removeUserFromResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/users/remove",
|
||||
description: "Remove a single user from a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.User],
|
||||
request: {
|
||||
params: removeUserFromResourceParamsSchema,
|
||||
|
||||
@@ -29,6 +29,40 @@ const setResourceAuthMethodsBodySchema = z.strictObject({
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/header-auth",
|
||||
description:
|
||||
"Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: setResourceAuthMethodsParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceAuthMethodsBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/header-auth",
|
||||
description:
|
||||
"Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
|
||||
@@ -27,6 +27,40 @@ const setResourceAuthMethodsBodySchema = z.strictObject({
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/password",
|
||||
description:
|
||||
"Set the password for a resource. Setting the password to null will remove it.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: setResourceAuthMethodsParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceAuthMethodsBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/password",
|
||||
description:
|
||||
"Set the password for a resource. Setting the password to null will remove it.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
|
||||
@@ -27,6 +27,40 @@ const setResourceAuthMethodsBodySchema = z.strictObject({
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/pincode",
|
||||
description:
|
||||
"Set the PIN code for a resource. Setting the PIN code to null will remove it.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: setResourceAuthMethodsParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceAuthMethodsBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/pincode",
|
||||
description:
|
||||
"Set the PIN code for a resource. Setting the PIN code to null will remove it.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
|
||||
@@ -21,6 +21,40 @@ const setResourceRolesParamsSchema = z.strictObject({
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/roles",
|
||||
description:
|
||||
"Set roles for a resource. This will replace all existing roles. When the resource has an inline policy defined (no shared resource policy assigned), roles are set on the inline policy instead of directly on the resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: setResourceRolesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceRolesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/roles",
|
||||
description:
|
||||
"Set roles for a resource. This will replace all existing roles. When the resource has an inline policy defined (no shared resource policy assigned), roles are set on the inline policy instead of directly on the resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
|
||||
|
||||
@@ -21,6 +21,40 @@ const setUserResourcesParamsSchema = z.strictObject({
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/users",
|
||||
description:
|
||||
"Set users for a resource. This will replace all existing users. When the resource has an inline policy defined (no shared resource policy assigned), users are set on the inline policy instead of directly on the resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: setUserResourcesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setUserResourcesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/users",
|
||||
description:
|
||||
"Set users for a resource. This will replace all existing users. When the resource has an inline policy defined (no shared resource policy assigned), users are set on the inline policy instead of directly on the resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.User],
|
||||
|
||||
@@ -35,6 +35,40 @@ const setResourceWhitelistParamsSchema = z.strictObject({
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/whitelist",
|
||||
description:
|
||||
"Set email whitelist for a resource. This will replace all existing emails.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: setResourceWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourceWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/whitelist",
|
||||
description:
|
||||
"Set email whitelist for a resource. This will replace all existing emails.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
|
||||
@@ -240,6 +240,42 @@ const updateRawResourceBodySchema = z
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}",
|
||||
description:
|
||||
"Update a resource. Policy fields (sso, mfa, pincode, password, whitelist) update the inline policy when no shared resource policy is assigned; when a shared policy is assigned those fields override the shared policy for this resource only.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: updateResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateHttpResourceBodySchema.and(
|
||||
updateRawResourceBodySchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}",
|
||||
description:
|
||||
"Update a resource. Policy fields (sso, mfa, pincode, password, whitelist) update the inline policy when no shared resource policy is assigned; when a shared policy is assigned those fields override the shared policy for this resource only.",
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
|
||||
@@ -9,12 +9,11 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
isValidCIDR,
|
||||
isValidIP,
|
||||
isValidUrlGlobPattern
|
||||
RESOURCE_RULE_MATCH_TYPES,
|
||||
getResourceRuleValueValidationError,
|
||||
ResourceRuleMatchType
|
||||
} from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isValidRegionId } from "@server/db/regions";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const updateResourceRuleParamsSchema = z.strictObject({
|
||||
@@ -22,15 +21,7 @@ const updateResourceRuleParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const resourceRuleMatchSchema = z.enum([
|
||||
"CIDR",
|
||||
"IP",
|
||||
"PATH",
|
||||
"COUNTRY",
|
||||
"COUNTRY_IS_NOT",
|
||||
"ASN",
|
||||
"REGION"
|
||||
]);
|
||||
const resourceRuleMatchSchema = z.enum(RESOURCE_RULE_MATCH_TYPES);
|
||||
|
||||
// Define Zod schema for request body validation
|
||||
const updateResourceRuleSchema = z
|
||||
@@ -49,6 +40,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/rule/{ruleId}",
|
||||
description: "Update a resource rule.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: updateResourceRuleParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateResourceRuleSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/public-resource/{resourceId}/rule/{ruleId}",
|
||||
description: "Update a resource rule.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: updateResourceRuleParamsSchema,
|
||||
@@ -140,14 +164,7 @@ export async function updateResourceRule(
|
||||
resource.resourcePolicyId === null &&
|
||||
resource.defaultResourcePolicyId !== null;
|
||||
|
||||
let existingMatch:
|
||||
| "CIDR"
|
||||
| "IP"
|
||||
| "PATH"
|
||||
| "COUNTRY"
|
||||
| "COUNTRY_IS_NOT"
|
||||
| "ASN"
|
||||
| "REGION";
|
||||
let existingMatch: ResourceRuleMatchType;
|
||||
|
||||
if (isInlinePolicy) {
|
||||
const policyId = resource.defaultResourcePolicyId!;
|
||||
@@ -231,42 +248,14 @@ export async function updateResourceRule(
|
||||
const { value } = updateData;
|
||||
|
||||
if (value !== undefined) {
|
||||
if (match === "CIDR") {
|
||||
if (!isValidCIDR(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid CIDR provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "IP") {
|
||||
if (!isValidIP(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid IP provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "PATH") {
|
||||
if (!isValidUrlGlobPattern(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid URL glob pattern provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "REGION") {
|
||||
if (!isValidRegionId(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid region ID provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
const valueValidationError = getResourceRuleValueValidationError(
|
||||
match,
|
||||
value
|
||||
);
|
||||
if (valueValidationError) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, valueValidationError)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
db,
|
||||
resources,
|
||||
siteNetworks,
|
||||
siteResources,
|
||||
sites,
|
||||
type Site,
|
||||
type SiteResource
|
||||
} from "@server/db";
|
||||
import { and, eq, inArray } 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 { OpenAPITags, registry } from "@server/openApi";
|
||||
import {
|
||||
getResourceIdsForSite,
|
||||
getSiteResourceIdsForSite
|
||||
} from "@server/lib/deleteSiteAssociatedResources";
|
||||
import {
|
||||
handleMessagingForUpdatedSiteResource,
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
waitForSiteResourceRebuildIdle
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const approveSiteParamsSchema = z.strictObject({
|
||||
siteId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site/{siteId}/approve",
|
||||
description:
|
||||
"Approve a pending site and approve (and enable when needed) its associated resources.",
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: approveSiteParamsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type SiteResourceEnableSideEffect = {
|
||||
existing: SiteResource;
|
||||
updated: SiteResource;
|
||||
siteIds: number[];
|
||||
};
|
||||
|
||||
export async function approveSite(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = approveSiteParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { siteId } = parsedParams.data;
|
||||
|
||||
const [existingSite] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingSite) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with ID ${siteId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!existingSite.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Site with ID ${siteId} has no organization`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgId = existingSite.orgId;
|
||||
let updatedSite: Site | undefined;
|
||||
const siteResourceEnableSideEffects: SiteResourceEnableSideEffect[] =
|
||||
[];
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
[updatedSite] = await trx
|
||||
.update(sites)
|
||||
.set({ status: "approved" })
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.returning();
|
||||
|
||||
const resourceIds = await getResourceIdsForSite(siteId, trx);
|
||||
const siteResourceIds = await getSiteResourceIdsForSite(
|
||||
siteId,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
|
||||
if (resourceIds.length > 0) {
|
||||
const pendingDisabledResources = await trx
|
||||
.select({ resourceId: resources.resourceId })
|
||||
.from(resources)
|
||||
.where(
|
||||
and(
|
||||
inArray(resources.resourceId, resourceIds),
|
||||
eq(resources.status, "pending"),
|
||||
eq(resources.enabled, false)
|
||||
)
|
||||
);
|
||||
|
||||
await trx
|
||||
.update(resources)
|
||||
.set({ status: "approved" })
|
||||
.where(inArray(resources.resourceId, resourceIds));
|
||||
|
||||
if (pendingDisabledResources.length > 0) {
|
||||
await trx
|
||||
.update(resources)
|
||||
.set({ enabled: true })
|
||||
.where(
|
||||
inArray(
|
||||
resources.resourceId,
|
||||
pendingDisabledResources.map(
|
||||
(r) => r.resourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (siteResourceIds.length > 0) {
|
||||
const existingSiteResources = await trx
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.where(
|
||||
inArray(siteResources.siteResourceId, siteResourceIds)
|
||||
);
|
||||
|
||||
const pendingDisabledSiteResources =
|
||||
existingSiteResources.filter(
|
||||
(sr) => sr.status === "pending" && !sr.enabled
|
||||
);
|
||||
|
||||
await trx
|
||||
.update(siteResources)
|
||||
.set({ status: "approved" })
|
||||
.where(
|
||||
inArray(siteResources.siteResourceId, siteResourceIds)
|
||||
);
|
||||
|
||||
if (pendingDisabledSiteResources.length > 0) {
|
||||
const enableIds = pendingDisabledSiteResources.map(
|
||||
(sr) => sr.siteResourceId
|
||||
);
|
||||
|
||||
const updatedEnabledSiteResources = await trx
|
||||
.update(siteResources)
|
||||
.set({ enabled: true })
|
||||
.where(inArray(siteResources.siteResourceId, enableIds))
|
||||
.returning();
|
||||
|
||||
for (const updated of updatedEnabledSiteResources) {
|
||||
const existing = pendingDisabledSiteResources.find(
|
||||
(sr) => sr.siteResourceId === updated.siteResourceId
|
||||
);
|
||||
if (!existing || !updated.networkId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const networkSites = await trx
|
||||
.select({ siteId: siteNetworks.siteId })
|
||||
.from(siteNetworks)
|
||||
.where(
|
||||
eq(siteNetworks.networkId, updated.networkId)
|
||||
);
|
||||
|
||||
siteResourceEnableSideEffects.push({
|
||||
existing,
|
||||
updated,
|
||||
siteIds: networkSites.map((s) => s.siteId)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const sideEffect of siteResourceEnableSideEffects) {
|
||||
rebuildClientAssociationsFromSiteResource(sideEffect.updated)
|
||||
.then(() =>
|
||||
waitForSiteResourceRebuildIdle(
|
||||
sideEffect.updated.siteResourceId
|
||||
)
|
||||
)
|
||||
.then(() =>
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
sideEffect.existing,
|
||||
sideEffect.updated,
|
||||
sideEffect.siteIds,
|
||||
sideEffect.siteIds
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild and handle messaging for site resource ${sideEffect.updated.siteResourceId} after site approval:`,
|
||||
e
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: updatedSite ?? null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Site approved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -159,15 +159,39 @@ export async function deleteSite(
|
||||
siteResources: []
|
||||
};
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (deleteResources) {
|
||||
if (deleteResources) {
|
||||
await db.transaction(async (trx) => {
|
||||
resourceSideEffects = await deleteAssociatedResourcesForSite(
|
||||
siteId,
|
||||
site.orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceSideEffects.resources.length > 0) {
|
||||
await usageService.add(
|
||||
site.orgId,
|
||||
LimitId.PUBLIC_RESOURCES,
|
||||
-resourceSideEffects.resources.length,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceSideEffects.siteResources.length > 0) {
|
||||
await usageService.add(
|
||||
site.orgId,
|
||||
LimitId.PRIVATE_RESOURCES,
|
||||
-resourceSideEffects.siteResources.length,
|
||||
trx
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await runDeleteSiteAssociatedResourcesSideEffects(
|
||||
resourceSideEffects
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (site.type == "wireguard") {
|
||||
if (site.pubKey) {
|
||||
await deletePeer(site.exitNodeId!, site.pubKey);
|
||||
@@ -180,12 +204,6 @@ export async function deleteSite(
|
||||
await usageService.add(site.orgId, LimitId.SITES, -1, trx);
|
||||
});
|
||||
|
||||
if (deleteResources) {
|
||||
await runDeleteSiteAssociatedResourcesSideEffects(
|
||||
resourceSideEffects
|
||||
);
|
||||
}
|
||||
|
||||
if (deletedNewt) {
|
||||
const payload = {
|
||||
type: `newt/wg/terminate`,
|
||||
|
||||
@@ -42,9 +42,14 @@ export async function getSiteStatusHistory(
|
||||
|
||||
const entityType = "site";
|
||||
const entityId = parsedParams.data.siteId;
|
||||
const { days } = parsedQuery.data;
|
||||
const { days, tzOffsetMinutes } = parsedQuery.data;
|
||||
|
||||
const data = await getCachedStatusHistory(entityType, entityId, days);
|
||||
const data = await getCachedStatusHistory(
|
||||
entityType,
|
||||
entityId,
|
||||
days,
|
||||
tzOffsetMinutes
|
||||
);
|
||||
|
||||
return response<StatusHistoryResponse>(res, {
|
||||
data,
|
||||
|
||||
@@ -3,6 +3,8 @@ export * from "./getStatusHistory";
|
||||
export * from "./createSite";
|
||||
export * from "./deleteSite";
|
||||
export * from "./updateSite";
|
||||
export * from "./approveSite";
|
||||
export * from "./rejectSite";
|
||||
export * from "./listSites";
|
||||
export * from "./listSiteRoles";
|
||||
export * from "./pickSiteDefaults";
|
||||
|
||||
@@ -177,7 +177,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/sites",
|
||||
description: "List all sites in an organization",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Site],
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: listSitesParamsSchema,
|
||||
query: listSitesSchema
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { newts, sites } from "@server/db";
|
||||
import { 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 { deletePeer } from "../gerbil/peers";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { cleanupSiteAssociations } from "@server/lib/rebuildClientAssociations";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { LimitId } from "@server/lib/billing";
|
||||
import {
|
||||
deletePendingAssociatedResourcesForSite,
|
||||
exceedsSiteAssociatedResourceDeleteLimit,
|
||||
getPendingAssociatedResourceCountForSite,
|
||||
runDeleteSiteAssociatedResourcesSideEffects,
|
||||
MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE,
|
||||
type DeleteSiteAssociatedResourcesSideEffects
|
||||
} from "@server/lib/deleteSiteAssociatedResources";
|
||||
|
||||
const rejectSiteParamsSchema = z.strictObject({
|
||||
siteId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site/{siteId}/reject",
|
||||
description:
|
||||
"Reject a pending site by deleting it and any associated resources that are still pending.",
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: rejectSiteParamsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function rejectSite(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = rejectSiteParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { siteId } = parsedParams.data;
|
||||
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!site) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with ID ${siteId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!site.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Site with ID ${siteId} has no organization`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const pendingAssociatedResourceCount =
|
||||
await getPendingAssociatedResourceCountForSite(siteId, site.orgId);
|
||||
|
||||
if (
|
||||
exceedsSiteAssociatedResourceDeleteLimit(
|
||||
pendingAssociatedResourceCount
|
||||
)
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Cannot reject site and associated pending resources when the site has more than ${MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE} pending resources`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [deletedNewt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
let resourceSideEffects: DeleteSiteAssociatedResourcesSideEffects = {
|
||||
resources: [],
|
||||
siteResources: []
|
||||
};
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
resourceSideEffects = await deletePendingAssociatedResourcesForSite(
|
||||
siteId,
|
||||
site.orgId,
|
||||
trx
|
||||
);
|
||||
|
||||
if (resourceSideEffects.resources.length > 0) {
|
||||
await usageService.add(
|
||||
site.orgId,
|
||||
LimitId.PUBLIC_RESOURCES,
|
||||
-resourceSideEffects.resources.length,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceSideEffects.siteResources.length > 0) {
|
||||
await usageService.add(
|
||||
site.orgId,
|
||||
LimitId.PRIVATE_RESOURCES,
|
||||
-resourceSideEffects.siteResources.length,
|
||||
trx
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await runDeleteSiteAssociatedResourcesSideEffects(resourceSideEffects);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (site.type == "wireguard") {
|
||||
if (site.pubKey) {
|
||||
await deletePeer(site.exitNodeId!, site.pubKey);
|
||||
}
|
||||
} else if (site.type == "newt") {
|
||||
await cleanupSiteAssociations(site, trx);
|
||||
}
|
||||
|
||||
await trx.delete(sites).where(eq(sites.siteId, siteId));
|
||||
await usageService.add(site.orgId, LimitId.SITES, -1, trx);
|
||||
});
|
||||
|
||||
if (deletedNewt) {
|
||||
const payload = {
|
||||
type: `newt/wg/terminate`,
|
||||
data: {}
|
||||
};
|
||||
sendToClient(deletedNewt.newtId, payload).catch((error) => {
|
||||
logger.error(
|
||||
"Failed to send termination message to newt:",
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Site rejected successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ const updateSiteBodySchema = z
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
dockerSocketEnabled: z.boolean().optional(),
|
||||
status: z.enum(["pending", "approved"]).optional(),
|
||||
autoUpdateEnabled: z.boolean().optional(),
|
||||
autoUpdateOverrideOrg: z.boolean().optional()
|
||||
})
|
||||
|
||||
@@ -31,6 +31,40 @@ const addClientToSiteResourceParamsSchema = z
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/clients/add",
|
||||
description:
|
||||
"Add a single client to a site resource. Clients with a userId cannot be added.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: addClientToSiteResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: addClientToSiteResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/clients/add",
|
||||
description:
|
||||
"Add a single client to a site resource. Clients with a userId cannot be added.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.Client],
|
||||
|
||||
@@ -33,6 +33,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/roles/add",
|
||||
description: "Add a single role to a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: addRoleToSiteResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: addRoleToSiteResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/roles/add",
|
||||
description: "Add a single role to a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: addRoleToSiteResourceParamsSchema,
|
||||
|
||||
@@ -33,6 +33,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/users/add",
|
||||
description: "Add a single user to a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: addUserToSiteResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: addUserToSiteResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/users/add",
|
||||
description: "Add a single user to a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.User],
|
||||
request: {
|
||||
params: addUserToSiteResourceParamsSchema,
|
||||
|
||||
@@ -38,6 +38,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/client/{clientId}/site-resources",
|
||||
description: "Add a machine client to multiple site resources at once.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: batchAddClientToSiteResourcesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: batchAddClientToSiteResourcesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/client/{clientId}/private-resources",
|
||||
description: "Add a machine client to multiple site resources at once.",
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
params: batchAddClientToSiteResourcesParamsSchema,
|
||||
|
||||
@@ -56,7 +56,6 @@ const createSiteResourceSchema = z
|
||||
siteId: z.number().int().positive().optional(), // DEPRECATED: for backward compatibility, we will convert this to siteIds array if provided
|
||||
destinationPort: z.int().positive().optional(),
|
||||
destination: z.string().min(1).nullish(),
|
||||
enabled: z.boolean().default(true),
|
||||
alias: z
|
||||
.string()
|
||||
.regex(
|
||||
@@ -208,6 +207,39 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/site-resource",
|
||||
description: "Create a new site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: createSiteResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createSiteResourceSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/private-resource",
|
||||
description: "Create a new site resource.",
|
||||
tags: [OpenAPITags.PrivateResource],
|
||||
request: {
|
||||
params: createSiteResourceParamsSchema,
|
||||
@@ -275,7 +307,6 @@ export async function createSiteResource(
|
||||
scheme,
|
||||
destinationPort,
|
||||
destination,
|
||||
enabled,
|
||||
ssl,
|
||||
alias,
|
||||
userIds,
|
||||
@@ -539,7 +570,6 @@ export async function createSiteResource(
|
||||
destination: destination, // the ssh can be null
|
||||
scheme,
|
||||
destinationPort,
|
||||
enabled,
|
||||
alias: alias ? alias.trim() : null,
|
||||
aliasAddress,
|
||||
tcpPortRangeString: tcpPortRangeStringAdjusted,
|
||||
|
||||
@@ -27,6 +27,32 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/site-resource/{siteResourceId}",
|
||||
description: "Delete a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: deleteSiteResourceParamsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/private-resource/{siteResourceId}",
|
||||
description: "Delete a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource],
|
||||
request: {
|
||||
params: deleteSiteResourceParamsSchema
|
||||
|
||||
@@ -57,6 +57,36 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/site-resource/{siteResourceId}",
|
||||
description: "Get a specific site resource by siteResourceId.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: z.object({
|
||||
siteResourceId: z.number(),
|
||||
siteId: z.number(),
|
||||
orgId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/private-resource/{siteResourceId}",
|
||||
description: "Get a specific site resource by siteResourceId.",
|
||||
tags: [OpenAPITags.PrivateResource],
|
||||
request: {
|
||||
params: z.object({
|
||||
|
||||
@@ -86,6 +86,15 @@ const listAllSiteResourcesByOrgQuerySchema = z.strictObject({
|
||||
description:
|
||||
"When set, only site resources associated with this site (via network) are returned"
|
||||
}),
|
||||
status: z
|
||||
.enum(["pending", "approved"])
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.openapi({
|
||||
type: "string",
|
||||
enum: ["pending", "approved"],
|
||||
description: "Filter by site resource status"
|
||||
}),
|
||||
labels: z
|
||||
.preprocess((val) => {
|
||||
if (val === undefined || val === null || val === "") {
|
||||
@@ -221,6 +230,33 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/site-resources",
|
||||
description: "List all site resources for an organization.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: listAllSiteResourcesByOrgParamsSchema,
|
||||
query: listAllSiteResourcesByOrgQuerySchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/private-resources",
|
||||
description: "List all site resources for an organization.",
|
||||
tags: [OpenAPITags.PrivateResource],
|
||||
request: {
|
||||
params: listAllSiteResourcesByOrgParamsSchema,
|
||||
@@ -283,6 +319,7 @@ export async function listAllSiteResourcesByOrg(
|
||||
sort_by,
|
||||
order,
|
||||
siteId,
|
||||
status,
|
||||
labels: labelFilter
|
||||
} = parsedQuery.data;
|
||||
|
||||
@@ -315,6 +352,10 @@ export async function listAllSiteResourcesByOrg(
|
||||
conditions.push(eq(siteResources.mode, mode));
|
||||
}
|
||||
|
||||
if (typeof status !== "undefined") {
|
||||
conditions.push(eq(siteResources.status, status));
|
||||
}
|
||||
|
||||
if (labelFilter && labelFilter.length > 0) {
|
||||
conditions.push(
|
||||
inArray(
|
||||
|
||||
@@ -39,6 +39,32 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/site-resource/{siteResourceId}/clients",
|
||||
description: "List all clients for a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: listSiteResourceClientsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/private-resource/{siteResourceId}/clients",
|
||||
description: "List all clients for a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.Client],
|
||||
request: {
|
||||
params: listSiteResourceClientsSchema
|
||||
|
||||
@@ -40,6 +40,32 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/site-resource/{siteResourceId}/roles",
|
||||
description: "List all roles for a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: listSiteResourceRolesSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/private-resource/{siteResourceId}/roles",
|
||||
description: "List all roles for a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: listSiteResourceRolesSchema
|
||||
|
||||
@@ -43,6 +43,32 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/site-resource/{siteResourceId}/users",
|
||||
description: "List all users for a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: listSiteResourceUsersSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/private-resource/{siteResourceId}/users",
|
||||
description: "List all users for a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.User],
|
||||
request: {
|
||||
params: listSiteResourceUsersSchema
|
||||
|
||||
@@ -47,6 +47,15 @@ const listSiteResourcesQuerySchema = z.strictObject({
|
||||
enum: ["asc", "desc"],
|
||||
default: "asc",
|
||||
description: "Sort order"
|
||||
}),
|
||||
status: z
|
||||
.enum(["pending", "approved"])
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.openapi({
|
||||
type: "string",
|
||||
enum: ["pending", "approved"],
|
||||
description: "Filter by site resource status"
|
||||
})
|
||||
});
|
||||
|
||||
@@ -58,6 +67,33 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/site/{siteId}/resources",
|
||||
description: "List site resources for a site.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: listSiteResourcesParamsSchema,
|
||||
query: listSiteResourcesQuerySchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/site/{siteId}/private-resources",
|
||||
description: "List site resources for a site.",
|
||||
tags: [OpenAPITags.PrivateResource],
|
||||
request: {
|
||||
params: listSiteResourcesParamsSchema,
|
||||
@@ -110,7 +146,7 @@ export async function listSiteResources(
|
||||
}
|
||||
|
||||
const { siteId, orgId } = parsedParams.data;
|
||||
const { limit, offset, sort_by, order } = parsedQuery.data;
|
||||
const { limit, offset, sort_by, order, status } = parsedQuery.data;
|
||||
|
||||
// Verify the site exists and belongs to the org
|
||||
const site = await db
|
||||
@@ -124,6 +160,15 @@ export async function listSiteResources(
|
||||
}
|
||||
|
||||
// Get site resources by joining networks to siteResources via siteNetworks
|
||||
const conditions = [
|
||||
eq(siteNetworks.siteId, siteId),
|
||||
eq(siteResources.orgId, orgId)
|
||||
];
|
||||
|
||||
if (typeof status !== "undefined") {
|
||||
conditions.push(eq(siteResources.status, status));
|
||||
}
|
||||
|
||||
const siteResourcesList = await db
|
||||
.select()
|
||||
.from(siteNetworks)
|
||||
@@ -132,12 +177,7 @@ export async function listSiteResources(
|
||||
siteResources,
|
||||
eq(siteResources.networkId, networks.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(siteNetworks.siteId, siteId),
|
||||
eq(siteResources.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.where(and(...conditions))
|
||||
.orderBy(
|
||||
sort_by
|
||||
? order === "asc"
|
||||
|
||||
@@ -31,6 +31,40 @@ const removeClientFromSiteResourceParamsSchema = z
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/clients/remove",
|
||||
description:
|
||||
"Remove a single client from a site resource. Clients with a userId cannot be removed.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: removeClientFromSiteResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: removeClientFromSiteResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/clients/remove",
|
||||
description:
|
||||
"Remove a single client from a site resource. Clients with a userId cannot be removed.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.Client],
|
||||
|
||||
@@ -33,6 +33,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/roles/remove",
|
||||
description: "Remove a single role from a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: removeRoleFromSiteResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: removeRoleFromSiteResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/roles/remove",
|
||||
description: "Remove a single role from a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: removeRoleFromSiteResourceParamsSchema,
|
||||
|
||||
@@ -33,6 +33,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/users/remove",
|
||||
description: "Remove a single user from a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: removeUserFromSiteResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: removeUserFromSiteResourceBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/users/remove",
|
||||
description: "Remove a single user from a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.User],
|
||||
request: {
|
||||
params: removeUserFromSiteResourceParamsSchema,
|
||||
|
||||
@@ -31,6 +31,40 @@ const setSiteResourceClientsParamsSchema = z
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/clients",
|
||||
description:
|
||||
"Set clients for a site resource. This will replace all existing clients. Clients with a userId cannot be added.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: setSiteResourceClientsParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setSiteResourceClientsBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/clients",
|
||||
description:
|
||||
"Set clients for a site resource. This will replace all existing clients. Clients with a userId cannot be added.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.Client],
|
||||
|
||||
@@ -32,6 +32,40 @@ const setSiteResourceRolesParamsSchema = z
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/roles",
|
||||
description:
|
||||
"Set roles for a site resource. This will replace all existing roles.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: setSiteResourceRolesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setSiteResourceRolesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/roles",
|
||||
description:
|
||||
"Set roles for a site resource. This will replace all existing roles.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.Role],
|
||||
|
||||
@@ -33,6 +33,40 @@ const setSiteResourceUsersParamsSchema = z
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}/users",
|
||||
description:
|
||||
"Set users for a site resource. This will replace all existing users.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: setSiteResourceUsersParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setSiteResourceUsersBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}/users",
|
||||
description:
|
||||
"Set users for a site resource. This will replace all existing users.",
|
||||
tags: [OpenAPITags.PrivateResource, OpenAPITags.User],
|
||||
|
||||
@@ -152,6 +152,11 @@ const updateSiteResourceSchema = z
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// this is a partial update; only enforce destination when the
|
||||
// caller is actually changing mode or destination
|
||||
if (data.mode === undefined && data.destination === undefined) {
|
||||
return true;
|
||||
}
|
||||
// destination is only optional for ssh mode with native authDaemonMode
|
||||
if (data.mode === "ssh" && data.authDaemonMode === "native") {
|
||||
return true;
|
||||
@@ -208,6 +213,39 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site-resource/{siteResourceId}",
|
||||
description: "Update a site resource.",
|
||||
tags: [OpenAPITags.PrivateResourceLegacy],
|
||||
request: {
|
||||
params: updateSiteResourceParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateSiteResourceSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/private-resource/{siteResourceId}",
|
||||
description: "Update a site resource.",
|
||||
tags: [OpenAPITags.PrivateResource],
|
||||
request: {
|
||||
params: updateSiteResourceParamsSchema,
|
||||
@@ -411,8 +449,10 @@ export async function updateSiteResource(
|
||||
: [];
|
||||
const existingSiteIds = existingSiteNetworks.map((sn) => sn.siteId);
|
||||
|
||||
let fullDomain: string | null = null;
|
||||
let finalSubdomain: string | null = null;
|
||||
// undefined means "leave unchanged" (partial update); only nulled out
|
||||
// when the mode is explicitly being changed away from http
|
||||
let fullDomain: string | null | undefined = undefined;
|
||||
let finalSubdomain: string | null | undefined = undefined;
|
||||
if (domainId) {
|
||||
// Validate domain and construct full domain
|
||||
const domainResult = await validateAndConstructDomain(
|
||||
@@ -448,6 +488,11 @@ export async function updateSiteResource(
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (mode !== undefined && mode !== "http") {
|
||||
// mode is explicitly changing away from http, so the resource
|
||||
// can no longer have a domain associated with it
|
||||
fullDomain = null;
|
||||
finalSubdomain = null;
|
||||
}
|
||||
|
||||
// make sure the alias is unique within the org if provided
|
||||
@@ -516,15 +561,28 @@ export async function updateSiteResource(
|
||||
destination: destination,
|
||||
destinationPort: destinationPort,
|
||||
enabled: enabled,
|
||||
alias: alias ? alias.trim() : null,
|
||||
alias:
|
||||
alias !== undefined
|
||||
? alias
|
||||
? alias.trim()
|
||||
: null
|
||||
: mode !== undefined &&
|
||||
mode !== "host" &&
|
||||
mode !== "ssh"
|
||||
? null
|
||||
: undefined,
|
||||
tcpPortRangeString: tcpPortRangeStringAdjusted,
|
||||
udpPortRangeString:
|
||||
mode == "http" || mode == "ssh"
|
||||
? ""
|
||||
: udpPortRangeString,
|
||||
disableIcmp:
|
||||
disableIcmp ||
|
||||
(mode == "http" || mode == "ssh" ? true : false),
|
||||
mode !== undefined
|
||||
? disableIcmp ||
|
||||
(mode == "http" || mode == "ssh"
|
||||
? true
|
||||
: false)
|
||||
: disableIcmp,
|
||||
domainId,
|
||||
subdomain: finalSubdomain,
|
||||
fullDomain,
|
||||
|
||||
@@ -93,6 +93,39 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource/{resourceId}/target",
|
||||
description: "Create a target for a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: createTargetParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createTargetSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource/{resourceId}/target",
|
||||
description: "Create a target for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Target],
|
||||
request: {
|
||||
params: createTargetParamsSchema,
|
||||
|
||||
@@ -92,6 +92,33 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/targets",
|
||||
description: "List targets for a resource.",
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: listTargetsParamsSchema,
|
||||
query: listTargetsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}/targets",
|
||||
description: "List targets for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Target],
|
||||
request: {
|
||||
params: listTargetsParamsSchema,
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function adminGeneratePasswordResetCode(
|
||||
});
|
||||
});
|
||||
|
||||
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${existingUser[0].email}&token=${token}`;
|
||||
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(existingUser[0].email!)}&token=${token}`;
|
||||
|
||||
logger.info(
|
||||
`Admin generated password reset code for user ${existingUser[0].email} (${userId})`
|
||||
|
||||
@@ -287,7 +287,7 @@ export async function inviteUser(
|
||||
)
|
||||
);
|
||||
|
||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
|
||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
|
||||
|
||||
if (doEmail) {
|
||||
await sendEmail(
|
||||
@@ -341,7 +341,7 @@ export async function inviteUser(
|
||||
.values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId })));
|
||||
});
|
||||
|
||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
|
||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
|
||||
|
||||
if (doEmail) {
|
||||
await sendEmail(
|
||||
|
||||
@@ -20,7 +20,9 @@ import {
|
||||
handleOlmServerPeerAddMessage,
|
||||
handleOlmUnRelayMessage,
|
||||
handleOlmDisconnectingMessage,
|
||||
handleOlmServerInitAddPeerHandshake
|
||||
handleOlmServerInitAddPeerHandshake,
|
||||
handleOlmLocalMessage,
|
||||
handleOlmUnLocalMessage
|
||||
} from "../olm";
|
||||
import { handleHealthcheckStatusMessage } from "../target";
|
||||
import { handleRoundTripMessage } from "./handleRoundTripMessage";
|
||||
@@ -32,6 +34,8 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
||||
"olm/wg/register": handleOlmRegisterMessage,
|
||||
"olm/wg/relay": handleOlmRelayMessage,
|
||||
"olm/wg/unrelay": handleOlmUnRelayMessage,
|
||||
"olm/wg/local": handleOlmLocalMessage,
|
||||
"olm/wg/unlocal": handleOlmUnLocalMessage,
|
||||
"olm/ping": handleOlmPingMessage,
|
||||
"olm/disconnecting": handleOlmDisconnectingMessage,
|
||||
"newt/disconnecting": handleNewtDisconnectingMessage,
|
||||
|
||||
Reference in New Issue
Block a user