mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-18 11:36:30 +02:00
update remote node routes to support access token updates
This commit is contained in:
+1
-1
@@ -203,7 +203,7 @@
|
|||||||
"expireIn": "Expire In",
|
"expireIn": "Expire In",
|
||||||
"neverExpire": "Never expire",
|
"neverExpire": "Never expire",
|
||||||
"sharePersistSession": "Persist session after first use",
|
"sharePersistSession": "Persist session after first use",
|
||||||
"sharePersistSessionDescription": "When enabled, the first request with this token sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
|
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
|
||||||
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
|
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
|
||||||
"shareSeeOnce": "You will only be able to see this link once. Make sure to copy it.",
|
"shareSeeOnce": "You will only be able to see this link once. Make sure to copy it.",
|
||||||
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
|
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ import {
|
|||||||
resourceRules,
|
resourceRules,
|
||||||
resourcePolicyRules,
|
resourcePolicyRules,
|
||||||
userOrgRoles,
|
userOrgRoles,
|
||||||
roles
|
roles,
|
||||||
|
resourceAccessToken
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, and, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
|
import { eq, and, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
|
||||||
import { alias } from "@server/db";
|
import { alias } from "@server/db";
|
||||||
@@ -81,11 +82,16 @@ import config from "@server/lib/config";
|
|||||||
import { exchangeSession } from "@server/routers/badger";
|
import { exchangeSession } from "@server/routers/badger";
|
||||||
import {
|
import {
|
||||||
ResourceSessionValidationResult,
|
ResourceSessionValidationResult,
|
||||||
|
createResourceSession,
|
||||||
|
serializeResourceSessionCookie,
|
||||||
validateResourceSessionToken
|
validateResourceSessionToken
|
||||||
} from "@server/auth/sessions/resource";
|
} from "@server/auth/sessions/resource";
|
||||||
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
||||||
import { maxmindLookup } from "@server/db/maxmind";
|
import { maxmindLookup } from "@server/db/maxmind";
|
||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
||||||
|
import { getUserOrgRoles } from "@server/lib/userOrgRoles";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
||||||
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||||
@@ -178,6 +184,73 @@ const validateResourceAccessTokenBodySchema = z.strictObject({
|
|||||||
accessToken: z.string()
|
accessToken: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createAccessTokenSessionParamsSchema = z.strictObject({
|
||||||
|
resourceId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
const createAccessTokenSessionBodySchema = z.strictObject({
|
||||||
|
accessTokenId: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAccessTokenParamsSchema = z.strictObject({
|
||||||
|
accessTokenId: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
const logAccessAuditBodySchema = z.strictObject({
|
||||||
|
action: z.boolean(),
|
||||||
|
type: z.string(),
|
||||||
|
orgId: z.string(),
|
||||||
|
resourceId: z.number().optional(),
|
||||||
|
siteResourceId: z.number().optional(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
username: z.string(),
|
||||||
|
userId: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
apiKey: z
|
||||||
|
.object({
|
||||||
|
name: z.string().nullable(),
|
||||||
|
apiKeyId: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
metadata: z.any().optional(),
|
||||||
|
userAgent: z.string().optional(),
|
||||||
|
requestIp: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type AccessTokenUserData = {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
email: string | null;
|
||||||
|
name: string | null;
|
||||||
|
role: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function resolveAccessTokenUserData(
|
||||||
|
userId: string,
|
||||||
|
orgId: string
|
||||||
|
): Promise<AccessTokenUserData | undefined> {
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
|
||||||
|
return {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Certificates by domains query validation
|
// Certificates by domains query validation
|
||||||
const getCertificatesByDomainsQuerySchema = z.strictObject({
|
const getCertificatesByDomainsQuerySchema = z.strictObject({
|
||||||
// Accept domains as string or array (domains or domains[])
|
// Accept domains as string or array (domains or domains[])
|
||||||
@@ -1829,8 +1902,23 @@ hybridRouter.post(
|
|||||||
resourceId
|
resourceId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let userData: AccessTokenUserData | undefined;
|
||||||
|
if (
|
||||||
|
result.valid &&
|
||||||
|
result.tokenItem?.userId &&
|
||||||
|
result.tokenItem.orgId
|
||||||
|
) {
|
||||||
|
userData = await resolveAccessTokenUserData(
|
||||||
|
result.tokenItem.userId,
|
||||||
|
result.tokenItem.orgId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: result,
|
data: {
|
||||||
|
...result,
|
||||||
|
userData
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: result.valid
|
message: result.valid
|
||||||
@@ -1850,6 +1938,233 @@ hybridRouter.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create a resource session from a valid access token (for remote nodes)
|
||||||
|
hybridRouter.post(
|
||||||
|
"/resource/:resourceId/session/create-access-token",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedParams = createAccessTokenSessionParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = createAccessTokenSessionBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourceId } = parsedParams.data;
|
||||||
|
const { accessTokenId } = parsedBody.data;
|
||||||
|
|
||||||
|
const [tokenItem] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourceAccessToken)
|
||||||
|
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!tokenItem || tokenItem.resourceId !== resourceId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Access token not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenItem.persistSession) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Access token does not allow session persistence"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [resource] = await db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(eq(resources.resourceId, resourceId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!resource || !resource.fullDomain) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: { cookie },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Access token session created successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to create access token session"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resolve access token metadata for remote nodes (cookie session path)
|
||||||
|
hybridRouter.get(
|
||||||
|
"/resource/access-token/:accessTokenId",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedParams = getAccessTokenParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessTokenId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [tokenItem] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourceAccessToken)
|
||||||
|
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!tokenItem) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Access token not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let userData: AccessTokenUserData | undefined;
|
||||||
|
if (tokenItem.userId) {
|
||||||
|
userData = await resolveAccessTokenUserData(
|
||||||
|
tokenItem.userId,
|
||||||
|
tokenItem.orgId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: { tokenItem, userData },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Access token retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get access token"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Access audit log from remote nodes
|
||||||
|
hybridRouter.post(
|
||||||
|
"/logs/access",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedBody = logAccessAuditBodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteExitNode = req.remoteExitNode;
|
||||||
|
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Remote exit node not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
await checkExitNodeOrg(
|
||||||
|
remoteExitNode.exitNodeId,
|
||||||
|
parsedBody.data.orgId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Exit node not allowed for this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await logAccessAudit(parsedBody.data);
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Access audit log saved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to save access audit log"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const geoIpLookupParamsSchema = z.object({
|
const geoIpLookupParamsSchema = z.object({
|
||||||
ip: z.union([z.ipv4(), z.ipv6()])
|
ip: z.union([z.ipv4(), z.ipv6()])
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user