src/app/core/permissions/permission-enforcer/permission-enforcer.service.ts
This service checks whether the relevant rules for the current user changed. If it detects a change, all Entity types that have read restrictions are collected. All entities of these entity types are loaded and checked whether the currently logged-in user has read permissions. If one entity is found for which the user does not have read permissions, then the local database is destroyed and a new sync has to start.
Properties |
|
Methods |
constructor(sessionInfo: SessionSubject, ability: EntityAbility, entityMapper: EntityMapperService, dbResolver: DatabaseResolverService, analyticsService: AnalyticsService, entities: EntityRegistry, location: Location, configService: ConfigService)
|
|||||||||||||||||||||||||||
Parameters :
|
Async enforcePermissionsOnLocalData | ||||||
enforcePermissionsOnLocalData(userRules: DatabaseRule[])
|
||||||
Parameters :
Returns :
Promise<void>
|
Static Readonly LOCALSTORAGE_KEY |
Type : string
|
Default value : "RULES"
|
This is a suffix used to persist the user-relevant rules in local storage to later check for changes. |
import { Inject, Injectable } from "@angular/core";
import { DatabaseRule } from "../permission-types";
import { EntityConstructor } from "../../entity/model/entity";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { LOCATION_TOKEN } from "../../../utils/di-tokens";
import { AnalyticsService } from "../../analytics/analytics.service";
import { EntityAbility } from "../ability/entity-ability";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { ConfigService } from "../../config/config.service";
import { firstValueFrom } from "rxjs";
import { SessionSubject } from "../../session/auth/session-info";
import { DatabaseResolverService } from "../../database/database-resolver.service";
/**
* This service checks whether the relevant rules for the current user changed.
* If it detects a change, all Entity types that have read restrictions are collected.
* All entities of these entity types are loaded and checked whether the currently logged-in user has read permissions.
* If one entity is found for which the user does **not** have read permissions, then the local database is destroyed and a new sync has to start.
*/
@Injectable({ providedIn: "root" })
export class PermissionEnforcerService {
/**
* This is a suffix used to persist the user-relevant rules in local storage to later check for changes.
*/
static readonly LOCALSTORAGE_KEY = "RULES";
constructor(
private sessionInfo: SessionSubject,
private ability: EntityAbility,
private entityMapper: EntityMapperService,
private dbResolver: DatabaseResolverService,
private analyticsService: AnalyticsService,
private entities: EntityRegistry,
@Inject(LOCATION_TOKEN) private location: Location,
private configService: ConfigService,
) {}
async enforcePermissionsOnLocalData(
userRules: DatabaseRule[],
): Promise<void> {
const userRulesString = JSON.stringify(userRules);
if (!this.sessionInfo.value || !this.userRulesChanged(userRulesString)) {
return;
}
const subjects = this.getSubjectsWithReadRestrictions(userRules);
if (await this.dbHasEntitiesWithoutPermissions(subjects)) {
this.analyticsService.eventTrack(
"destroying local db due to lost permissions",
{ category: "Migration" },
);
// TODO: is it enough to destroy the default DB or could other DBs also be affected?
await this.dbResolver.destroyDatabases();
this.location.reload();
}
window.localStorage.setItem(this.getUserStorageKey(), userRulesString);
}
private userRulesChanged(newRules: string): boolean {
const storedRules = window.localStorage.getItem(this.getUserStorageKey());
return storedRules !== newRules;
}
private getUserStorageKey() {
return `${this.sessionInfo.value.id}-${PermissionEnforcerService.LOCALSTORAGE_KEY}`;
}
private getSubjectsWithReadRestrictions(
rules: DatabaseRule[],
): EntityConstructor[] {
const subjects = new Set<string>(this.entities.keys());
rules
.filter((rule) => this.isReadRule(rule))
.forEach((rule) => this.collectSubjectsFromRule(rule, subjects));
return [...subjects].map((subj) => this.entities.get(subj));
}
private collectSubjectsFromRule(rule: DatabaseRule, subjects: Set<string>) {
const relevantSubjects = this.getRelevantSubjects(rule);
if (rule.inverted || rule.conditions) {
// Add subject if the rule can prevent someone from having access
relevantSubjects.forEach((subject) => subjects.add(subject));
} else {
// Remove subject if rule gives access
relevantSubjects.forEach((subject) => subjects.delete(subject));
}
}
private isReadRule(rule: DatabaseRule): boolean {
return (
rule.action === "read" ||
rule.action.includes("read") ||
rule.action === "manage"
);
}
private getRelevantSubjects(rule: DatabaseRule): string[] {
let subjects: string[];
if (rule.subject === "all") {
subjects = [...this.entities.keys()];
} else if (Array.isArray(rule.subject)) {
subjects = rule.subject;
} else {
subjects = [rule.subject];
}
// Only return valid entities
return subjects.filter((sub) => this.entities.has(sub));
}
private async dbHasEntitiesWithoutPermissions(
subjects: EntityConstructor[],
): Promise<boolean> {
// wait for config service to be ready before using the entity mapper
await firstValueFrom(this.configService.configUpdates);
for (const subject of subjects) {
const entities = await this.entityMapper.loadType(subject);
if (entities.some((entity) => this.ability.cannot("read", entity))) {
return true;
}
}
return false;
}
}