Notfallzugang gilt auch im Worker
Der Voll-Abgleich hat den Administrator alle fünf Minuten hinausgeworfen: enforceMembership kannte BOOTSTRAP_ADMIN_IDS nicht und sah nur, dass keine seiner Rollen zur Anmeldung berechtigt. Solange das Rollen-Mapping noch leer ist, trifft das zwangsläufig zu — der Notfallzugang war damit genau in der Lage wirkungslos, für die es ihn gibt. Gefunden beim ersten Lauf gegen den echten Discord-Server, nicht im Test.
This commit is contained in:
@@ -7,6 +7,14 @@ export type WorkerEnv = {
|
|||||||
databaseUrl: string
|
databaseUrl: string
|
||||||
discordBotToken: string
|
discordBotToken: string
|
||||||
discordGuildId: string
|
discordGuildId: string
|
||||||
|
/**
|
||||||
|
* Notfallzugang. Diese Nutzer behalten ihre Sitzung, auch wenn keine ihrer
|
||||||
|
* Rollen zur Anmeldung berechtigt. Ohne das wirft der Voll-Abgleich den
|
||||||
|
* Administrator alle fünf Minuten hinaus — also genau dann, wenn er das
|
||||||
|
* Rollen-Mapping erst noch einrichten will.
|
||||||
|
* Siehe docs/03-auth-und-rechte.md.
|
||||||
|
*/
|
||||||
|
bootstrapAdminIds: string[]
|
||||||
/** Voll-Abgleich als Sicherheitsnetz, siehe docs/02-architektur.md. */
|
/** Voll-Abgleich als Sicherheitsnetz, siehe docs/02-architektur.md. */
|
||||||
fullSyncIntervalMs: number
|
fullSyncIntervalMs: number
|
||||||
heartbeatIntervalMs: number
|
heartbeatIntervalMs: number
|
||||||
@@ -24,6 +32,10 @@ export function loadEnv(): WorkerEnv {
|
|||||||
databaseUrl: required('DATABASE_URL'),
|
databaseUrl: required('DATABASE_URL'),
|
||||||
discordBotToken: required('DISCORD_BOT_TOKEN'),
|
discordBotToken: required('DISCORD_BOT_TOKEN'),
|
||||||
discordGuildId: required('DISCORD_GUILD_ID'),
|
discordGuildId: required('DISCORD_GUILD_ID'),
|
||||||
|
bootstrapAdminIds: (process.env.BOOTSTRAP_ADMIN_IDS ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean),
|
||||||
fullSyncIntervalMs: Number(process.env.WORKER_FULL_SYNC_MS ?? 5 * 60_000),
|
fullSyncIntervalMs: Number(process.env.WORKER_FULL_SYNC_MS ?? 5 * 60_000),
|
||||||
heartbeatIntervalMs: Number(process.env.WORKER_HEARTBEAT_MS ?? 30_000),
|
heartbeatIntervalMs: Number(process.env.WORKER_HEARTBEAT_MS ?? 30_000),
|
||||||
adminChannelId: process.env.DISCORD_ADMIN_CHANNEL_ID,
|
adminChannelId: process.env.DISCORD_ADMIN_CHANNEL_ID,
|
||||||
|
|||||||
+3
-3
@@ -35,7 +35,7 @@ async function main(): Promise<void> {
|
|||||||
for (const member of members.values()) {
|
for (const member of members.values()) {
|
||||||
if (member.user.bot) continue
|
if (member.user.bot) continue
|
||||||
if (await syncMember(db, member)) changed++
|
if (await syncMember(db, member)) changed++
|
||||||
await enforceMembership(db, member.id)
|
await enforceMembership(db, member.id, env.bootstrapAdminIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wer nicht mehr auf dem Server ist, hat auch keine Sitzung mehr.
|
// Wer nicht mehr auf dem Server ist, hat auch keine Sitzung mehr.
|
||||||
@@ -61,7 +61,7 @@ async function main(): Promise<void> {
|
|||||||
if (member.guild.id !== env.discordGuildId || member.user.bot) return
|
if (member.guild.id !== env.discordGuildId || member.user.bot) return
|
||||||
void (async () => {
|
void (async () => {
|
||||||
await syncMember(db, member)
|
await syncMember(db, member)
|
||||||
await enforceMembership(db, member.id)
|
await enforceMembership(db, member.id, env.bootstrapAdminIds)
|
||||||
})().catch((error: unknown) => console.error('[worker] guildMemberAdd', error))
|
})().catch((error: unknown) => console.error('[worker] guildMemberAdd', error))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ async function main(): Promise<void> {
|
|||||||
if (member.guild.id !== env.discordGuildId || member.user.bot) return
|
if (member.guild.id !== env.discordGuildId || member.user.bot) return
|
||||||
void (async () => {
|
void (async () => {
|
||||||
await syncMember(db, member)
|
await syncMember(db, member)
|
||||||
await enforceMembership(db, member.id)
|
await enforceMembership(db, member.id, env.bootstrapAdminIds)
|
||||||
})().catch((error: unknown) => console.error('[worker] guildMemberUpdate', error))
|
})().catch((error: unknown) => console.error('[worker] guildMemberUpdate', error))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -124,7 +124,16 @@ export async function syncMember(db: PrismaClient, member: GuildMember): Promise
|
|||||||
* Request des Betroffenen griffe — und genau darauf kann man sich nicht
|
* Request des Betroffenen griffe — und genau darauf kann man sich nicht
|
||||||
* verlassen, wenn jemandem gerade der Zugriff entzogen wurde.
|
* verlassen, wenn jemandem gerade der Zugriff entzogen wurde.
|
||||||
*/
|
*/
|
||||||
export async function enforceMembership(db: PrismaClient, userId: string): Promise<boolean> {
|
export async function enforceMembership(
|
||||||
|
db: PrismaClient,
|
||||||
|
userId: string,
|
||||||
|
bootstrapAdminIds: readonly string[] = [],
|
||||||
|
): Promise<boolean> {
|
||||||
|
// Der Notfallzugang gilt auch hier. Sonst wirft der Voll-Abgleich den
|
||||||
|
// Administrator alle fünf Minuten hinaus, solange noch keine Rolle zur
|
||||||
|
// Anmeldung berechtigt — und einrichten kann er es dann nie.
|
||||||
|
if (bootstrapAdminIds.includes(userId)) return false
|
||||||
|
|
||||||
const roles = await db.userRole.findMany({
|
const roles = await db.userRole.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
select: { discordRoleId: true },
|
select: { discordRoleId: true },
|
||||||
|
|||||||
@@ -82,6 +82,20 @@ suite('Sitzungen beim Rollenentzug', () => {
|
|||||||
expect(nutzer?.isActive).toBe(false)
|
expect(nutzer?.isActive).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('wirft den Notfallzugang nicht hinaus, auch ohne AfiS-Rolle', async () => {
|
||||||
|
// Sonst wirft der Voll-Abgleich den Administrator alle fünf Minuten
|
||||||
|
// hinaus, solange noch keine Rolle zur Anmeldung berechtigt — und
|
||||||
|
// einrichten kann er das Rollen-Mapping dann nie.
|
||||||
|
const id = `${marke}-notfall`
|
||||||
|
await nutzerMitSitzung(id, [rolleGast])
|
||||||
|
|
||||||
|
const beendet = await enforceMembership(db, id, [id])
|
||||||
|
|
||||||
|
expect(beendet).toBe(false)
|
||||||
|
expect(await db.session.count({ where: { userId: id } })).toBe(1)
|
||||||
|
expect((await db.user.findUnique({ where: { id } }))?.isActive).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('beendet Sitzungen beim Serveraustritt und behält den Nutzer', async () => {
|
it('beendet Sitzungen beim Serveraustritt und behält den Nutzer', async () => {
|
||||||
const id = `${marke}-austritt`
|
const id = `${marke}-austritt`
|
||||||
await nutzerMitSitzung(id, [rolleAfis])
|
await nutzerMitSitzung(id, [rolleAfis])
|
||||||
|
|||||||
Reference in New Issue
Block a user