Merge remote-tracking branch 'origin/develop' into more-fixes
This commit is contained in:
@@ -70,7 +70,9 @@ const systemACLPrefix = "system:";
|
||||
|
||||
export type SystemACL = Array<(typeof systemACLs)[number]>;
|
||||
|
||||
export type ValidACLItems = Array<SystemACL[number] | UserACL[number]>;
|
||||
export type GlobalACL =
|
||||
| `${typeof systemACLPrefix}${(typeof systemACLs)[number]}`
|
||||
| `${typeof userACLPrefix}${(typeof userACLs)[number]}`;
|
||||
|
||||
class ACLManager {
|
||||
private getAuthorizationToken(request: MinimumRequestObject) {
|
||||
@@ -175,6 +177,38 @@ class ACLManager {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async fetchAllACLs(
|
||||
request: MinimumRequestObject,
|
||||
): Promise<GlobalACL[] | undefined> {
|
||||
const userSession = await sessionHandler.getSession(request);
|
||||
if (!userSession) {
|
||||
const authorizationToken = this.getAuthorizationToken(request);
|
||||
if (!authorizationToken) return undefined;
|
||||
const token = await prisma.aPIToken.findUnique({
|
||||
where: { token: authorizationToken },
|
||||
});
|
||||
if (!token) return undefined;
|
||||
return token.acls as GlobalACL[];
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userSession.userId },
|
||||
select: {
|
||||
admin: true,
|
||||
},
|
||||
});
|
||||
if (!user)
|
||||
throw new Error("User session without user - did something break?");
|
||||
|
||||
const acls = userACLs.map((e) => `${userACLPrefix}${e}`);
|
||||
|
||||
if (user.admin) {
|
||||
acls.push(...systemACLs.map((e) => `${systemACLPrefix}${e}`));
|
||||
}
|
||||
|
||||
return acls as GlobalACL[];
|
||||
}
|
||||
}
|
||||
|
||||
export const aclManager = new ACLManager();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { EnumDictionary } from "../utils/types";
|
||||
import https from "https";
|
||||
import { useCertificateAuthority } from "~/server/plugins/ca";
|
||||
import prisma from "../db/database";
|
||||
import { ClientCapabilities } from "~/prisma/client";
|
||||
|
||||
@@ -17,7 +15,7 @@ export enum InternalClientCapability {
|
||||
export const validCapabilities = Object.values(InternalClientCapability);
|
||||
|
||||
export type CapabilityConfiguration = {
|
||||
[InternalClientCapability.PeerAPI]: { endpoints: string[] };
|
||||
[InternalClientCapability.PeerAPI]: object;
|
||||
[InternalClientCapability.UserStatus]: object;
|
||||
[InternalClientCapability.CloudSaves]: object;
|
||||
};
|
||||
@@ -27,6 +25,7 @@ class CapabilityManager {
|
||||
InternalClientCapability,
|
||||
(configuration: object) => Promise<boolean>
|
||||
> = {
|
||||
/*
|
||||
[InternalClientCapability.PeerAPI]: async (rawConfiguration) => {
|
||||
const configuration =
|
||||
rawConfiguration as CapabilityConfiguration[InternalClientCapability.PeerAPI];
|
||||
@@ -71,12 +70,13 @@ class CapabilityManager {
|
||||
valid = true;
|
||||
break;
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
},
|
||||
*/
|
||||
[InternalClientCapability.PeerAPI]: async () => true,
|
||||
[InternalClientCapability.UserStatus]: async () => true, // No requirements for user status
|
||||
[InternalClientCapability.CloudSaves]: async () => true, // No requirements for cloud saves
|
||||
};
|
||||
@@ -92,7 +92,7 @@ class CapabilityManager {
|
||||
|
||||
async upsertClientCapability(
|
||||
capability: InternalClientCapability,
|
||||
rawCapability: object,
|
||||
rawCapabilityConfiguration: object,
|
||||
clientId: string,
|
||||
) {
|
||||
const upsertFunctions: EnumDictionary<
|
||||
@@ -100,8 +100,7 @@ class CapabilityManager {
|
||||
() => Promise<void> | void
|
||||
> = {
|
||||
[InternalClientCapability.PeerAPI]: async function () {
|
||||
const configuration =
|
||||
rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
|
||||
// const configuration =rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
|
||||
|
||||
const currentClient = await prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
@@ -110,6 +109,7 @@ class CapabilityManager {
|
||||
},
|
||||
});
|
||||
if (!currentClient) throw new Error("Invalid client ID");
|
||||
/*
|
||||
if (currentClient.capabilities.includes(ClientCapabilities.PeerAPI)) {
|
||||
await prisma.clientPeerAPIConfiguration.update({
|
||||
where: { clientId },
|
||||
@@ -126,6 +126,7 @@ class CapabilityManager {
|
||||
endpoints: configuration.endpoints,
|
||||
},
|
||||
});
|
||||
*/
|
||||
|
||||
await prisma.client.update({
|
||||
where: { id: clientId },
|
||||
|
||||
@@ -2,10 +2,13 @@ import { randomUUID } from "node:crypto";
|
||||
import prisma from "../db/database";
|
||||
import type { Platform } from "~/prisma/client";
|
||||
import { useCertificateAuthority } from "~/server/plugins/ca";
|
||||
import type { CapabilityConfiguration, InternalClientCapability } from "./capabilities";
|
||||
import capabilityManager from "./capabilities";
|
||||
|
||||
export interface ClientMetadata {
|
||||
name: string;
|
||||
platform: Platform;
|
||||
capabilities: Partial<CapabilityConfiguration>;
|
||||
}
|
||||
|
||||
export class ClientHandler {
|
||||
@@ -75,7 +78,7 @@ export class ClientHandler {
|
||||
if (!metadata) throw new Error("Invalid client ID");
|
||||
if (!metadata.userId) throw new Error("Un-authorized client ID");
|
||||
|
||||
return await prisma.client.create({
|
||||
const client = await prisma.client.create({
|
||||
data: {
|
||||
id: id,
|
||||
userId: metadata.userId,
|
||||
@@ -87,6 +90,20 @@ export class ClientHandler {
|
||||
lastConnected: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
for (const [capability, configuration] of Object.entries(
|
||||
metadata.data.capabilities,
|
||||
)) {
|
||||
await capabilityManager.upsertClientCapability(
|
||||
capability as InternalClientCapability,
|
||||
configuration,
|
||||
client.id,
|
||||
);
|
||||
}
|
||||
|
||||
this.temporaryClientTable.delete(id);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
async removeClient(id: string) {
|
||||
|
||||
@@ -306,7 +306,7 @@ class LibraryManager {
|
||||
title: `'${game.mName}' ('${versionName}') finished importing.`,
|
||||
description: `Drop finished importing version ${versionName} for ${game.mName}.`,
|
||||
actions: [`View|/admin/library/${gameId}`],
|
||||
requiredPerms: ["import:game:new"],
|
||||
acls: ["system:import:version:read"],
|
||||
});
|
||||
|
||||
progress(100);
|
||||
|
||||
@@ -8,28 +8,35 @@ Design goals:
|
||||
|
||||
import type { Notification } from "~/prisma/client";
|
||||
import prisma from "../db/database";
|
||||
import type { GlobalACL } from "../acls";
|
||||
|
||||
// type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
// TODO: document notification action format
|
||||
export type NotificationCreateArgs = Pick<
|
||||
Notification,
|
||||
"title" | "description" | "actions" | "nonce" | "requiredPerms"
|
||||
>;
|
||||
"title" | "description" | "actions" | "nonce"
|
||||
> & { acls: Array<GlobalACL> };
|
||||
|
||||
class NotificationSystem {
|
||||
// userId to acl to listenerId
|
||||
private listeners = new Map<
|
||||
string,
|
||||
Map<string, (notification: Notification) => void>
|
||||
Map<
|
||||
string,
|
||||
{ callback: (notification: Notification) => void; acls: GlobalACL[] }
|
||||
>
|
||||
>();
|
||||
|
||||
listen(
|
||||
userId: string,
|
||||
acls: Array<GlobalACL>,
|
||||
id: string,
|
||||
callback: (notification: Notification) => void,
|
||||
) {
|
||||
this.listeners.set(userId, new Map());
|
||||
this.listeners.get(userId)?.set(id, callback);
|
||||
if (!this.listeners.has(userId)) this.listeners.set(userId, new Map());
|
||||
// eslint-disable-next-line @typescript-eslint/no-extra-non-null-assertion
|
||||
this.listeners.get(userId)!!.set(id, { callback, acls });
|
||||
|
||||
this.catchupListener(userId, id);
|
||||
}
|
||||
@@ -39,23 +46,27 @@ class NotificationSystem {
|
||||
}
|
||||
|
||||
private async catchupListener(userId: string, id: string) {
|
||||
const callback = this.listeners.get(userId)?.get(id);
|
||||
if (!callback)
|
||||
const listener = this.listeners.get(userId)?.get(id);
|
||||
if (!listener)
|
||||
throw new Error("Failed to catch-up listener: callback does not exist");
|
||||
const notifications = await prisma.notification.findMany({
|
||||
where: { userId: userId },
|
||||
where: { userId: userId, acls: { hasSome: listener.acls } },
|
||||
orderBy: {
|
||||
created: "asc", // Oldest first, because they arrive in reverse order
|
||||
},
|
||||
});
|
||||
for (const notification of notifications) {
|
||||
await callback(notification);
|
||||
await listener.callback(notification);
|
||||
}
|
||||
}
|
||||
|
||||
private async pushNotification(userId: string, notification: Notification) {
|
||||
for (const listener of this.listeners.get(userId) ?? []) {
|
||||
await listener[1](notification);
|
||||
for (const [_, listener] of this.listeners.get(userId) ?? []) {
|
||||
const hasSome =
|
||||
notification.acls.findIndex(
|
||||
(e) => listener.acls.findIndex((v) => v === e) != -1,
|
||||
) != -1;
|
||||
if (hasSome) await listener.callback(notification);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,25 +111,7 @@ class NotificationSystem {
|
||||
}
|
||||
|
||||
async systemPush(notificationCreateArgs: NotificationCreateArgs) {
|
||||
await this.push("system", notificationCreateArgs);
|
||||
}
|
||||
|
||||
async pushAllAdmins(notificationCreateArgs: NotificationCreateArgs) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
admin: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const res: Promise<void>[] = [];
|
||||
for (const user of users) {
|
||||
res.push(this.push(user.id, notificationCreateArgs));
|
||||
}
|
||||
// wait for all notifications to pass
|
||||
await Promise.all(res);
|
||||
return await this.pushAll(notificationCreateArgs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import prisma from "../db/database";
|
||||
import type { User } from "~/prisma/client";
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import objectHandler from "../objects";
|
||||
import type { Readable } from "stream";
|
||||
@@ -12,10 +13,15 @@ interface OIDCWellKnown {
|
||||
scopes_supported: string[];
|
||||
}
|
||||
|
||||
interface OIDCAuthSessionOptions {
|
||||
redirect: string | undefined;
|
||||
}
|
||||
|
||||
interface OIDCAuthSession {
|
||||
redirectUrl: string;
|
||||
callbackUrl: string;
|
||||
state: string;
|
||||
options: OIDCAuthSessionOptions;
|
||||
}
|
||||
|
||||
interface OIDCUserInfo {
|
||||
@@ -132,7 +138,7 @@ export class OIDCManager {
|
||||
};
|
||||
}
|
||||
|
||||
generateAuthSession(): OIDCAuthSession {
|
||||
generateAuthSession(options?: OIDCAuthSessionOptions): OIDCAuthSession {
|
||||
const stateKey = randomUUID();
|
||||
|
||||
const normalisedUrl = new URL(
|
||||
@@ -148,12 +154,16 @@ export class OIDCManager {
|
||||
redirectUrl: finalUrl,
|
||||
callbackUrl: redirectUrl,
|
||||
state: stateKey,
|
||||
options: options ?? { redirect: undefined },
|
||||
};
|
||||
this.signinStateTable[stateKey] = session;
|
||||
return session;
|
||||
}
|
||||
|
||||
async authorize(code: string, state: string) {
|
||||
async authorize(
|
||||
code: string,
|
||||
state: string,
|
||||
): Promise<{ user: User; options: OIDCAuthSessionOptions } | string> {
|
||||
const session = this.signinStateTable[state];
|
||||
if (!session) return "Invalid state parameter";
|
||||
|
||||
@@ -191,7 +201,9 @@ export class OIDCManager {
|
||||
|
||||
const user = await this.fetchOrCreateUser(userinfo);
|
||||
|
||||
return user;
|
||||
if (typeof user === "string") return user;
|
||||
|
||||
return { user, options: session.options };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return `Request to identity provider failed: ${e}`;
|
||||
|
||||
Reference in New Issue
Block a user