Each tenant's page carries its legacy protocols switch (LP-9 to LP-18)
A card above the tenant form: whether legacy mail protocols are on or off
for the organization, a one-click "Turn legacy protocols back on", and
"Turn off legacy protocols…", which opens -- before anything can change --
the impact panel with the tenant's own people (LP-15), the statement at
tenant scope and the typed confirmation (LP-16, LP-17). The statement reads
"everyone in {organization}" and names no ports and no firewall: a
tenant's switch closes nothing, other tenants share the ports (LP-13).
It reads and turns inbuxa:TenantProtocolPolicy, with the domain
permissions the server checks for it. It shows on the read-only tenant page
too: a tenant administrator reads its tenant without changing it (MT-12)
and may still turn the switch. Turning it back on while the server has
legacy protocols off is refused by the server, and the refusal is shown.
The banner (LP-18) falls back to the tenant's switch where the server's
can't be read -- inside a tenant -- and reads "off for your
organization".
The impact panel, statement and confirmation move to parts.tsx, shared by
Hardening and the tenant card; the statement takes its scope.
Checked by hand against a local server with a tenant, two of its users and
the admin signed in over IMAP: the tenant card's panel lists the tenant's
two users and not the admin; the statement reads "everyone in Example Co"
with no ports or firewall; turning it off makes the tenant's user get "NO
[ALERT] Your organization allows only INBUXA webmail and JMAP apps" over
IMAP; one click turns it back on; Hardening's panel lists all three. Not
checked by hand: the banner as a tenant administrator sees it.
This commit is contained in:
@@ -256,3 +256,65 @@ export function phraseMatches(typed: string): boolean {
|
||||
export function describeListener(l: PolicyListener): string {
|
||||
return l.ports.length > 0 ? `${l.name} (${l.ports.join(', ')})` : l.name;
|
||||
}
|
||||
|
||||
// ---- A tenant's switch: inbuxa:TenantProtocolPolicy (LP-9 to LP-14) ----
|
||||
|
||||
const TENANT_OBJECT = 'inbuxa:TenantProtocolPolicy';
|
||||
|
||||
export interface TenantPolicy {
|
||||
/** The tenant's id, which is also the policy's. */
|
||||
id: string;
|
||||
legacyProtocols: 'enabled' | 'disabled';
|
||||
/** Milliseconds since the epoch. */
|
||||
changedAt: number | null;
|
||||
/** The tenant's own people who used a legacy mail app lately (LP-15), or null from an older server. */
|
||||
recentLegacyUse: RecentUse[] | null;
|
||||
}
|
||||
|
||||
export function parseTenantPolicy(raw: Record<string, unknown>): TenantPolicy {
|
||||
return {
|
||||
id: typeof raw.id === 'string' ? raw.id : '',
|
||||
legacyProtocols: raw.legacyProtocols === 'disabled' ? 'disabled' : 'enabled',
|
||||
changedAt: typeof raw.changedAt === 'number' ? raw.changedAt : null,
|
||||
recentLegacyUse: Array.isArray(raw.recentLegacyUse) ? parseRecent(raw.recentLegacyUse) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A tenant's switch. With no id, the caller's own tenant's -- which is how a
|
||||
* tenant administrator reads it; a server administrator names the tenant.
|
||||
*/
|
||||
export async function fetchTenantPolicy(tenantId: string | null, signal?: AbortSignal): Promise<TenantPolicy> {
|
||||
const accountId = getAccountId('x:Domain');
|
||||
const responses = await jmapRequest(
|
||||
[[`${TENANT_OBJECT}/get`, { accountId, ids: tenantId ? [tenantId] : null }, '0']],
|
||||
signal,
|
||||
[INBUXA_CAPABILITY],
|
||||
);
|
||||
const [name, result] = responses[0] ?? [];
|
||||
if (name !== `${TENANT_OBJECT}/get`) {
|
||||
const type = (result as { type?: string } | undefined)?.type;
|
||||
if (type === 'unknownMethod' || type === 'unknownCapability') throw new PolicyUnavailable(type);
|
||||
throw new Error((result as { description?: string } | undefined)?.description ?? type ?? 'Request failed');
|
||||
}
|
||||
const list = (result as { list?: Record<string, unknown>[] }).list ?? [];
|
||||
// A tenant admin's /get with no ids holds exactly its own tenant's.
|
||||
if (!list[0] || (!tenantId && list.length !== 1)) throw new PolicyUnavailable('notFound');
|
||||
return parseTenantPolicy(list[0]);
|
||||
}
|
||||
|
||||
/** Turns a tenant's switch. The server refuses turning it on while its own is off (LP-9). */
|
||||
export async function updateTenantPolicy(tenantId: string, legacyProtocols: 'enabled' | 'disabled'): Promise<void> {
|
||||
const accountId = getAccountId('x:Domain');
|
||||
const responses = await jmapRequest(
|
||||
[[`${TENANT_OBJECT}/set`, { accountId, update: { [tenantId]: { legacyProtocols } } }, '0']],
|
||||
undefined,
|
||||
[INBUXA_CAPABILITY],
|
||||
);
|
||||
const [name, result] = responses[0] ?? [];
|
||||
if (name !== `${TENANT_OBJECT}/set`) {
|
||||
throw new Error((result as { description?: string } | undefined)?.description ?? 'Request failed');
|
||||
}
|
||||
const failed = (result as { notUpdated?: Record<string, JmapSetError> | null }).notUpdated?.[tenantId];
|
||||
if (failed) throw new Error(failed.description ?? failed.type);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user