Skip to content

Commit

Permalink
Merge branch 'master' into feat/translation-poeditor-integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Ayush8923 authored Nov 25, 2024
2 parents 1e4700f + 7115646 commit 9af3ea9
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 46 deletions.
49 changes: 30 additions & 19 deletions src/app/core/permissions/ability/ability.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { get } from "lodash-es";
import { LatestEntityLoader } from "../../entity/latest-entity-loader";
import { SessionInfo, SessionSubject } from "../../session/auth/session-info";
import { CurrentUserSubject } from "../../session/current-user-subject";
import { merge } from "rxjs";
import { filter, firstValueFrom, merge } from "rxjs";
import { map } from "rxjs/operators";

/**
Expand Down Expand Up @@ -52,26 +52,23 @@ export class AbilityService extends LatestEntityLoader<Config<DatabaseRules>> {

private async updateAbilityWithUserRules(rules: DatabaseRules): Promise<any> {
// If rules object is empty, everything is allowed
const userRules: DatabaseRule[] = rules
? await this.getRulesForUser(rules)
const rawUserRules: DatabaseRule[] = rules
? this.getRulesForUser(rules)
: [{ action: "manage", subject: "all" }];

if (userRules.length === 0) {
// No rules or only default rules defined
const user = this.sessionInfo.value;
Logging.warn(
`no rules found for user "${user?.name}" with roles "${user?.roles}"`,
);
}
const userRules: DatabaseRule[] =
await this.interpolateUserVariables(rawUserRules);

this.ability.update(userRules);
return this.permissionEnforcer.enforcePermissionsOnLocalData(userRules);
}

private async getRulesForUser(rules: DatabaseRules): Promise<DatabaseRule[]> {
private getRulesForUser(rules: DatabaseRules): DatabaseRule[] {
const sessionInfo = this.sessionInfo.value;
if (!sessionInfo) {
return rules.public ?? [];
}

const rawUserRules: DatabaseRule[] = [];
if (rules.default) {
rawUserRules.push(...rules.default);
Expand All @@ -80,39 +77,53 @@ export class AbilityService extends LatestEntityLoader<Config<DatabaseRules>> {
const rulesForRole = rules[role] || [];
rawUserRules.push(...rulesForRole);
});
return this.interpolateUser(rawUserRules, sessionInfo);

if (rawUserRules.length === 0 && sessionInfo) {
// No rules or only default rules defined
Logging.warn(
`no rules found for user "${sessionInfo.name}" with roles "${sessionInfo.roles}"`,
);
}

return rawUserRules;
}

private interpolateUser(
private async interpolateUserVariables(
rules: DatabaseRule[],
sessionInfo: SessionInfo,
): DatabaseRule[] {
const user = this.currentUser.value;
): Promise<DatabaseRule[]> {
const sessionInfo: SessionInfo = this.sessionInfo.value;

const user = await firstValueFrom(
// only emit once user entity is loaded (or "null" for user account without entity)
this.currentUser.pipe(filter((x) => x !== undefined)),
);
if (user && user["projects"]) {
sessionInfo.projects = user["projects"];
} else {
sessionInfo.projects = [];
}

const dynamicPlaceholders = {
user: sessionInfo,
};
return JSON.parse(JSON.stringify(rules), (_that, rawValue) => {
if (rawValue[0] !== "$") {
return rawValue;
}

let name = rawValue.slice(2, -1);
let name = rawValue.slice(2, -1); // extract name from "${name}"
if (name === "user.name") {
// the user account related entity (assured with prefix) is now stored in user.entityId
// mapping the previously valid ${user.name} here for backwards compatibility
name = "user.entityId";
}
const value = get({ user: sessionInfo }, name);
const value = get(dynamicPlaceholders, name);

if (typeof value === "undefined") {
throw new ReferenceError(`Variable ${name} is not defined`);
}

return value;
});
}) as DatabaseRule[];
}
}
8 changes: 7 additions & 1 deletion src/app/core/session/current-user-subject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { Injectable } from "@angular/core";

/**
* The Entity linked to the currently logged-in user, which can be used to pre-fill forms or customize workflows.
* This might be undefined even when logged in. E.g. when using an administrative support account.
*
* This value is
* - an entity object from the database, if the user account is linked to a valid entity (via the "exact_username" attribute)
* - `undefined even not logged in (e.g. when using a public form as an anonymous visitor)
* - `null` when no entity is linked to the current user or the linked entityId is invalid (doc not found in database)
*
* This distinction between "undefined" and "null" helps navigate some special cases, e.g. in the AbilityService.
*/
@Injectable()
export class CurrentUserSubject extends BehaviorSubject<Entity> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
/*
* This file is part of ndb-core.
*
* ndb-core is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ndb-core is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
*/

import { SessionManagerService } from "./session-manager.service";
import { LoginState } from "../session-states/login-state.enum";
import {
Expand Down Expand Up @@ -152,7 +135,6 @@ describe("SessionManagerService", () => {
entityId: adminUser.getId(),
});
await service.remoteLogin();
expect(currentUser.value).toBeUndefined();

// user entity available -> user should be set
await entityMapper.save(adminUser);
Expand All @@ -171,7 +153,7 @@ describe("SessionManagerService", () => {

expect(loadSpy).not.toHaveBeenCalled();
expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
expect(TestBed.inject(CurrentUserSubject).value).toBeUndefined();
expect(TestBed.inject(CurrentUserSubject).value).toBeNull();
});

it("should allow other entities to log in", async () => {
Expand Down
17 changes: 10 additions & 7 deletions src/app/core/session/session-service/session-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,22 @@ export class SessionManagerService {
await this.initializeDatabaseForCurrentUser(session);
this.sessionInfo.next(session);
this.loginStateSubject.next(LoginState.LOGGED_IN);
if (session.entityId) {
this.configService.configUpdates.pipe(take(1)).subscribe(() =>
// requires initial config to be loaded first!
this.initUserEntity(session.entityId),
);
}
this.configService.configUpdates.pipe(take(1)).subscribe(() =>
// requires initial config to be loaded first!
this.initUserEntity(session.entityId),
);
}

private initUserEntity(entityId: string) {
if (!entityId) {
this.currentUser.next(null);
return;
}

const entityType = Entity.extractTypeFromId(entityId);
this.entityMapper
.load(entityType, entityId)
.catch(() => undefined)
.catch(() => null) // see CurrentUserSubject: emits "null" for non-existing user entity
.then((res) => this.currentUser.next(res));
this.updateSubscription = this.entityMapper
.receiveUpdates(entityType)
Expand Down

0 comments on commit 9af3ea9

Please sign in to comment.