mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(admin): accept domain entries in the email blocklist (#3129)
This commit is contained in:
@@ -46,6 +46,20 @@ const IS_EMAIL_BANNED_QUERY = BannedEmails.select({
|
||||
where: BannedEmails.where.eq('email_lower'),
|
||||
});
|
||||
const LOAD_ALL_BANNED_EMAILS_QUERY = BannedEmails.select();
|
||||
|
||||
function getEmailBlocklistKeys(email: string): Array<string> {
|
||||
const emailLower = email.trim().toLowerCase();
|
||||
const atIndex = emailLower.lastIndexOf('@');
|
||||
if (atIndex <= 0) {
|
||||
return [emailLower];
|
||||
}
|
||||
const labels = emailLower.slice(atIndex + 1).split('.');
|
||||
const keys = [emailLower];
|
||||
for (let index = 0; index < labels.length - 1; index++) {
|
||||
keys.push(`@${labels.slice(index).join('.')}`);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
const IS_PHRASE_BANNED_QUERY = BannedPhrases.select({
|
||||
where: BannedPhrases.where.eq('phrase'),
|
||||
});
|
||||
@@ -224,11 +238,15 @@ export class AdminRepository implements IAdminRepository {
|
||||
}
|
||||
|
||||
async isEmailBanned(email: string): Promise<boolean> {
|
||||
const emailLower = email.toLowerCase();
|
||||
const result = await fetchOne<{
|
||||
email_lower: string;
|
||||
}>(IS_EMAIL_BANNED_QUERY.bind({email_lower: emailLower}));
|
||||
return !!result;
|
||||
for (const key of getEmailBlocklistKeys(email)) {
|
||||
const result = await fetchOne<{
|
||||
email_lower: string;
|
||||
}>(IS_EMAIL_BANNED_QUERY.bind({email_lower: key}));
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async banEmail(email: string): Promise<void> {
|
||||
|
||||
@@ -67,7 +67,8 @@ const BLOCKLIST_CATALOG = [
|
||||
},
|
||||
{
|
||||
list_type: 'email' as const,
|
||||
description: 'Email addresses that cannot be used to register or be set on an account.',
|
||||
description:
|
||||
'Email addresses that cannot be used to register or be set on an account. An entry written as @example.com covers every address at that domain and its subdomains.',
|
||||
value_field: 'email',
|
||||
fields: [],
|
||||
scoped: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import {
|
||||
clearTestEmails,
|
||||
createAuthHarness,
|
||||
@@ -36,7 +37,11 @@ describe('Email blocklist at signup and email change', () => {
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
await clearTestEmails(harness);
|
||||
admin = await setUserACLs(harness, await createTestAccount(harness), ['admin:authenticate', 'ban:email:add']);
|
||||
admin = await setUserACLs(harness, await createTestAccount(harness), [
|
||||
'admin:authenticate',
|
||||
'ban:email:add',
|
||||
'ban:email:check',
|
||||
]);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
@@ -74,6 +79,52 @@ describe('Email blocklist at signup and email change', () => {
|
||||
expectGenericEmailError(json, 'email');
|
||||
});
|
||||
|
||||
function register(email: string) {
|
||||
return createBuilder<ValidationErrorBody>(harness, '')
|
||||
.post('/auth/register')
|
||||
.body({
|
||||
email,
|
||||
username: createUniqueUsername('domain'),
|
||||
global_name: TEST_USER_DATA.DEFAULT_GLOBAL_NAME,
|
||||
password: TEST_CREDENTIALS.STRONG_PASSWORD,
|
||||
date_of_birth: TEST_USER_DATA.DEFAULT_DATE_OF_BIRTH,
|
||||
consent: true,
|
||||
});
|
||||
}
|
||||
|
||||
it('refuses registration at a blocklisted domain and its subdomains', async () => {
|
||||
const domain = `${randomUUID()}.test`;
|
||||
await blocklist(`@${domain.toUpperCase()}`);
|
||||
for (const email of [`someone@${domain}`, `SOMEONE@MAIL.${domain.toUpperCase()}`]) {
|
||||
const {json} = await register(email).expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY').executeWithResponse();
|
||||
expectGenericEmailError(json, 'email');
|
||||
}
|
||||
});
|
||||
|
||||
it('does not extend a domain entry to unrelated domains', async () => {
|
||||
const domain = `${randomUUID()}.test`;
|
||||
await blocklist(`@${domain}`);
|
||||
await register(`someone@not${domain}`).execute();
|
||||
await register(`someone@${domain}.example`).execute();
|
||||
});
|
||||
|
||||
it('reports a domain entry through the blocklist check', async () => {
|
||||
const domain = `${randomUUID()}.test`;
|
||||
await blocklist(`@${domain}`);
|
||||
const {banned} = await createBuilder<{banned: boolean}>(harness, admin.token)
|
||||
.get(`/admin/blocklists/email/entries/${encodeURIComponent(`@${domain}`)}`)
|
||||
.execute();
|
||||
expect(banned).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a malformed domain entry', async () => {
|
||||
await createBuilder(harness, admin.token)
|
||||
.post('/admin/blocklists/email/entries')
|
||||
.body({email: '@not a domain'})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('still registers an address that is not blocklisted', async () => {
|
||||
await blocklist(createUniqueEmail('blocked-other'));
|
||||
await createTestAccount(harness);
|
||||
|
||||
Reference in New Issue
Block a user