enum condition filters

This commit is contained in:
timonrieger
2026-07-25 16:11:41 +02:00
parent d26b6151e0
commit 6a728b7159
2 changed files with 99 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
import { UnauthorizedException } from '@nestjs/common';
import { AssetVisibility } from 'src/enum';
import { applyLockedVisibilityPolicy } from 'src/utils/search-filter';
import { AuthFactory } from 'test/factories/auth.factory';
import { describe, expect, it } from 'vitest';
const { Locked, Timeline, Archive } = AssetVisibility;
const elevatedAuth = () => AuthFactory.from().session({ hasElevatedPermission: true }).build();
const unelevatedAuth = () => AuthFactory.from().session().build();
describe(applyLockedVisibilityPolicy.name, () => {
it('should let an elevated session query locked assets', () => {
const filter = { visibility: { eq: Locked } };
expect(applyLockedVisibilityPolicy(elevatedAuth(), filter)).toBe(filter);
});
it('should reject an unelevated session when any operator permits locked', () => {
for (const visibility of [{ eq: Locked }, { ne: Timeline }, { in: [Locked, Timeline] }, { notIn: [Timeline] }]) {
expect(() => applyLockedVisibilityPolicy(unelevatedAuth(), { visibility })).toThrow(UnauthorizedException);
}
});
it('should keep a filter whose top-level visibility excludes locked', () => {
for (const visibility of [{ eq: Timeline }, { ne: Locked }, { in: [Timeline, Archive] }, { notIn: [Locked] }]) {
const filter = { visibility };
expect(applyLockedVisibilityPolicy(unelevatedAuth(), filter)).toBe(filter);
}
});
it('should let a safe top-level visibility decide over could-match branches', () => {
const filter = { visibility: { ne: Locked }, or: [{ visibility: { eq: Locked } }] };
expect(applyLockedVisibilityPolicy(unelevatedAuth(), filter)).toBe(filter);
});
it('should reject a branch that permits locked when the top level has no visibility', () => {
const filter = { or: [{ city: { eq: 'Oslo' } }, { visibility: { in: [Locked, Timeline] } }] };
expect(() => applyLockedVisibilityPolicy(unelevatedAuth(), filter)).toThrow(UnauthorizedException);
});
it('should otherwise inject visibility != locked without mutating the input', () => {
const filter = { city: { eq: 'Oslo' }, visibility: undefined };
expect(applyLockedVisibilityPolicy(unelevatedAuth(), filter)).toEqual({
city: { eq: 'Oslo' },
visibility: { ne: Locked },
});
expect(filter.visibility).toBeUndefined();
const branched = { or: [{ isFavorite: { eq: true } }, { visibility: { eq: Timeline } }] };
expect(applyLockedVisibilityPolicy(unelevatedAuth(), branched).visibility).toEqual({ ne: Locked });
});
});
+47
View File
@@ -0,0 +1,47 @@
import { AuthDto } from 'src/dtos/auth.dto';
import { SearchFilter, SearchFilterBranch } from 'src/dtos/search.dto';
import { AssetVisibility } from 'src/enum';
import { requireElevatedPermission } from 'src/utils/access';
type EnumField = 'type' | 'visibility';
type EnumValue = NonNullable<NonNullable<SearchFilterBranch[EnumField]>['eq']>;
type EnumCondition = { eq?: EnumValue; ne?: EnumValue; in?: EnumValue[]; notIn?: EnumValue[] };
/** Whether a row with `value` can satisfy the condition. A missing operator allows any value. */
const canMatch = (condition: EnumCondition, value: EnumValue): boolean =>
(condition.eq === undefined || condition.eq === value) &&
(condition.ne === undefined || condition.ne !== value) &&
(condition.in === undefined || condition.in.includes(value)) &&
(condition.notIn === undefined || !condition.notIn.includes(value));
/**
* The conditions that decide which `field` values the filter can return. A top-level condition
* decides alone, otherwise each branch itself.
*/
const decidingConditions = (filter: SearchFilter, field: EnumField): EnumCondition[] => {
if (filter[field] !== undefined) {
return [filter[field]];
}
return (filter.or ?? []).map((branch) => branch[field]).filter((condition) => condition !== undefined);
};
/**
* Keeps locked assets out of search results unless the session is elevated: a filter that asks for
* them is rejected with 401, and any other filter gets `visibility != locked` ANDed in.
*/
export const applyLockedVisibilityPolicy = (auth: AuthDto, filter: SearchFilter): SearchFilter => {
if (auth.session?.hasElevatedPermission) {
return filter;
}
if (decidingConditions(filter, 'visibility').some((condition) => canMatch(condition, AssetVisibility.Locked))) {
requireElevatedPermission(auth);
}
if (filter.visibility !== undefined) {
return filter;
}
return { ...filter, visibility: { ne: AssetVisibility.Locked } };
};