feat(admission): add group VPN exceptions
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { isRequestTimestampFresh, resolveEffectiveGroup, verifyHashedToken } from "@minecraft-account-manager/auth";
|
||||
import { gameAdmissionDenialReason, isRequestTimestampFresh, resolveEffectiveGroup, verifyHashedToken } from "@minecraft-account-manager/auth";
|
||||
import { problemDetails, velocityAccessRequestSchema } from "@minecraft-account-manager/contracts";
|
||||
import {
|
||||
appSettings,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@minecraft-account-manager/database";
|
||||
import { and, eq, isNull, lt, sql } from "drizzle-orm";
|
||||
import { NextResponse } from "next/server";
|
||||
import { admissionDenialMessage, DEFAULT_ADMISSION_MESSAGES } from "@/lib/admission-settings";
|
||||
import { db } from "@/lib/database";
|
||||
import { isUniqueConstraintViolation } from "@/lib/database-errors";
|
||||
import { getIpIntelligence, toAuditIpData } from "@/lib/ip-intelligence";
|
||||
@@ -20,7 +21,6 @@ import { logger } from "@/lib/logger";
|
||||
import { problemInstance, problemResponse } from "@/lib/problem-response";
|
||||
|
||||
const MAX_CLOCK_SKEW_MS = 45_000;
|
||||
const DEFAULT_DENIAL_MESSAGE = "Please register your Minecraft account before joining.";
|
||||
|
||||
function methodNotAllowed(request: Request) {
|
||||
const response = problemResponse(problemDetails(
|
||||
@@ -111,35 +111,13 @@ async function handleVelocityAccess(request: Request) {
|
||||
}
|
||||
|
||||
const [settings] = await db.select().from(appSettings).where(eq(appSettings.id, "default")).limit(1);
|
||||
const denialMessage = settings?.registrationMessage ?? DEFAULT_DENIAL_MESSAGE;
|
||||
const admissionMessages = {
|
||||
registrationMessage: settings?.registrationMessage ?? DEFAULT_ADMISSION_MESSAGES.registrationMessage,
|
||||
groupAccessDeniedMessage: settings?.groupAccessDeniedMessage ?? DEFAULT_ADMISSION_MESSAGES.groupAccessDeniedMessage,
|
||||
vpnDeniedMessage: settings?.vpnDeniedMessage ?? DEFAULT_ADMISSION_MESSAGES.vpnDeniedMessage,
|
||||
};
|
||||
|
||||
let [knownAccount] = await db
|
||||
.select({ id: minecraftAccounts.id })
|
||||
.from(minecraftAccounts)
|
||||
.where(
|
||||
and(
|
||||
eq(minecraftAccounts.minecraftUuid, input.minecraftUuid),
|
||||
isNull(minecraftAccounts.deletedAt),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!knownAccount) {
|
||||
[knownAccount] = await db
|
||||
.select({ id: minecraftAccounts.id })
|
||||
.from(minecraftAccounts)
|
||||
.where(
|
||||
and(
|
||||
isNull(minecraftAccounts.minecraftUuid),
|
||||
sql`lower(${minecraftAccounts.username}) = lower(${input.username})`,
|
||||
isNull(minecraftAccounts.deletedAt),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
}
|
||||
|
||||
const intelligence = knownAccount
|
||||
? await getIpIntelligence(input.ipAddress)
|
||||
: { classification: "unknown" as const, provider: null };
|
||||
const intelligence = await getIpIntelligence(input.ipAddress);
|
||||
const auditIpData = toAuditIpData(intelligence);
|
||||
|
||||
const decision = await db.transaction(async (tx) => {
|
||||
@@ -209,23 +187,24 @@ async function handleVelocityAccess(request: Request) {
|
||||
classification: intelligence.classification,
|
||||
observedAt: occurredAt,
|
||||
});
|
||||
return { allowed: false as const, message: denialMessage };
|
||||
return { allowed: false as const, message: admissionDenialMessage("not_registered", admissionMessages) };
|
||||
}
|
||||
|
||||
const [explicitGroup] = await tx
|
||||
.select({ id: groups.id, name: groups.name, accessEnabled: groups.accessEnabled })
|
||||
.select({ id: groups.id, name: groups.name, accessEnabled: groups.accessEnabled, anonymizedNetworksAllowed: groups.anonymizedNetworksAllowed })
|
||||
.from(userGroupMemberships)
|
||||
.innerJoin(groups, eq(groups.id, userGroupMemberships.groupId))
|
||||
.where(eq(userGroupMemberships.userId, account.userId))
|
||||
.limit(1);
|
||||
const [defaultGroup] = await tx
|
||||
.select({ id: groups.id, name: groups.name, accessEnabled: groups.accessEnabled })
|
||||
.select({ id: groups.id, name: groups.name, accessEnabled: groups.accessEnabled, anonymizedNetworksAllowed: groups.anonymizedNetworksAllowed })
|
||||
.from(groups)
|
||||
.where(eq(groups.isDefault, true))
|
||||
.limit(1);
|
||||
const effectiveGroup = resolveEffectiveGroup(explicitGroup ?? null, defaultGroup ?? null);
|
||||
|
||||
if (!effectiveGroup?.accessEnabled) {
|
||||
const denialReason = gameAdmissionDenialReason(effectiveGroup ?? null, intelligence.classification);
|
||||
if (denialReason) {
|
||||
await tx.insert(events).values({
|
||||
id: randomUUID(),
|
||||
source: `/velocity/${input.serverId}`,
|
||||
@@ -235,7 +214,9 @@ async function handleVelocityAccess(request: Request) {
|
||||
actorUserId: account.userId,
|
||||
data: {
|
||||
username: input.username,
|
||||
reason: "group_access_disabled",
|
||||
reason: denialReason,
|
||||
accessGroup: effectiveGroup?.name ?? null,
|
||||
accessGroupId: effectiveGroup?.id ?? null,
|
||||
ipIntelligence: auditIpData,
|
||||
},
|
||||
ipAddress: input.ipAddress,
|
||||
@@ -251,9 +232,14 @@ async function handleVelocityAccess(request: Request) {
|
||||
classification: intelligence.classification,
|
||||
observedAt: occurredAt,
|
||||
});
|
||||
return { allowed: false as const, message: "Your account group does not currently have server access." };
|
||||
return {
|
||||
allowed: false as const,
|
||||
message: admissionDenialMessage(denialReason, admissionMessages),
|
||||
};
|
||||
}
|
||||
|
||||
if (!effectiveGroup) throw new Error("Effective access group is unavailable after admission approval");
|
||||
|
||||
if (account.minecraftUuid !== input.minecraftUuid || account.username !== input.username) {
|
||||
await tx
|
||||
.update(minecraftAccounts)
|
||||
@@ -304,6 +290,7 @@ async function handleVelocityAccess(request: Request) {
|
||||
uuidBackfilled: account.minecraftUuid === null,
|
||||
ipIntelligence: auditIpData,
|
||||
accessGroup: effectiveGroup.name,
|
||||
accessGroupId: effectiveGroup.id,
|
||||
},
|
||||
ipAddress: input.ipAddress,
|
||||
correlationId: input.requestId,
|
||||
|
||||
Reference in New Issue
Block a user