Skip to content

Commit

Permalink
feat(core) new EntityPermissionGuard to check access using EntityAbil…
Browse files Browse the repository at this point in the history
…ity (#2125)

---------
This functionality has been developed for the project “codo”.
codo is developed under the projects “Landungsbrücken – Patenschaften in Hamburg stärken” and “openTransfer Patenschaften”. It is funded through the program “Menschen stärken Menschen” by the German Federal Ministry of Family Affairs, Senior Citizens, Women and Youth.
More information at https://github.com/codo-mentoring

“Landungsbrücken – Patenschaften in Hamburg stärken” is a project of BürgerStiftung Hamburg in cooperation with the Mentor.Ring Hamburg. With a mix of networking opportunities, capacity building and financial support the project strengthens Hamburg’s scene of mentoring projects since its founding in 2016.

The “Stiftung Bürgermut” foundation since 2007 supports the digital and real exchange of experiences and connections of active citizens. Within the federal program “Menschen stärken Menschen” the foundation as part of its program “openTransfer Patenschaften” offers support services for connecting, spreading and upskilling mentoring organisations across Germany.

Diese Funktion wurde entwickelt für das Projekt codo.
codo wird entwickelt im Rahmen der Projekte Landungsbrücken – Patenschaften in Hamburg stärken und openTransfer Patenschaften. Er ist gefördert durch das Bundesprogramm Menschen stärken Menschen des Bundesministeriums für Familie, Senioren, Frauen und Jugend.
Mehr Informationen unter https://github.com/codo-mentoring

“Landungsbrücken – Patenschaften in Hamburg stärken” ist ein Projekt der BürgerStiftung Hamburg in Kooperation mit dem Mentor.Ring Hamburg. Mit einer Mischung aus Vernetzungsangeboten, Qualifizierungsmaßnahmen und finanzieller Förderung stärkt das Projekt die Hamburger Szene der Patenschaftsprojekte seit der Gründung im Jahr 2016.

Die Stiftung Bürgermut fördert seit 2007 den digitalen und realen Erfahrungsaustausch und die Vernetzung von engagierten Bürger:innen. Innerhalb des Bundesprogramms „Menschen stärken Menschen” bietet die Stiftung im Rahmen ihres Programms openTransfer Patenschaften Unterstützungsleistungen zur Vernetzung, Verbreitung und Qualifizierung von Patenschafts- und Mentoringorganisationen bundesweit.

Co-authored-by: codo-mentoring <117934638+codo-mentoring@users.noreply.github.com>
  • Loading branch information
sleidig and codo-mentoring authored Dec 13, 2023
1 parent 1d2c877 commit 9d650ec
Show file tree
Hide file tree
Showing 20 changed files with 353 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,25 @@ import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
import { MatDialog } from "@angular/material/dialog";
import { of } from "rxjs";
import { EntityAbility } from "../../../permissions/ability/entity-ability";

describe("EnumDropdownComponent", () => {
let component: EnumDropdownComponent;
let fixture: ComponentFixture<EnumDropdownComponent>;
let mockDialog: jasmine.SpyObj<MatDialog>;
let mockAbility: jasmine.SpyObj<EntityAbility>;

beforeEach(async () => {
mockDialog = jasmine.createSpyObj(["open"]);
mockAbility = jasmine.createSpyObj(["can"]);
mockAbility.can.and.returnValue(true);

await TestBed.configureTestingModule({
imports: [EnumDropdownComponent, MockedTestingModule.withState()],
providers: [{ provide: MatDialog, useValue: mockDialog }],
providers: [
{ provide: MatDialog, useValue: mockDialog },
{ provide: EntityAbility, useValue: mockAbility },
],
}).compileComponents();

fixture = TestBed.createComponent(EnumDropdownComponent);
Expand Down
3 changes: 3 additions & 0 deletions src/app/core/config/config-fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export const defaultJsonConfig = {
"view:note": {
"component": "NotesManager",
"config": {
"entity": "Note",
"title": $localize`:Title for notes overview:Notes & Reports`,
"includeEventNotes": false,
"showEventNotesToggle": true,
Expand Down Expand Up @@ -279,6 +280,7 @@ export const defaultJsonConfig = {
}
]
},
"requiredPermissionOperation": "update",
"permittedUserRoles": ["admin_app"]
},
"view:admin": {
Expand Down Expand Up @@ -411,6 +413,7 @@ export const defaultJsonConfig = {
"view:child": {
"component": "ChildrenList",
"config": {
"entity": "Child",
"columns": [
{
"viewComponent": "ChildBlock",
Expand Down
11 changes: 6 additions & 5 deletions src/app/core/config/dynamic-routing/router.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { NotFoundComponent } from "./not-found/not-found.component";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { AuthGuard } from "../../session/auth.guard";
import { RoutedViewComponent } from "../../ui/routed-view/routed-view.component";
import { EntityPermissionGuard } from "../../permissions/permission-guard/entity-permission.guard";

class TestComponent extends Component {}

Expand Down Expand Up @@ -66,20 +67,20 @@ describe("RouterService", () => {
component: RoutedViewComponent,
data: { component: "ChildrenList" },
canDeactivate: [jasmine.any(Function)],
canActivate: [AuthGuard],
canActivate: [AuthGuard, EntityPermissionGuard],
},
{
path: "child/:id",
component: RoutedViewComponent,
data: { component: "EntityDetails", config: testViewConfig },
canDeactivate: [jasmine.any(Function)],
canActivate: [AuthGuard],
canActivate: [AuthGuard, EntityPermissionGuard],
},
{
path: "list",
component: RoutedViewComponent,
data: { component: "EntityList", permittedUserRoles: ["user_app"] },
canActivate: [AuthGuard, UserRoleGuard],
canActivate: [AuthGuard, EntityPermissionGuard, UserRoleGuard],
canDeactivate: [jasmine.any(Function)],
},
];
Expand Down Expand Up @@ -108,7 +109,7 @@ describe("RouterService", () => {
{
path: "other",
component: TestComponent,
canActivate: [AuthGuard, UserRoleGuard],
canActivate: [AuthGuard, EntityPermissionGuard, UserRoleGuard],
canDeactivate: [jasmine.any(Function)],
data: { permittedUserRoles: ["admin_app"] },
},
Expand Down Expand Up @@ -166,7 +167,7 @@ describe("RouterService", () => {
path: "list",
component: RoutedViewComponent,
data: { component: "EntityList", permittedUserRoles: ["admin"] },
canActivate: [AuthGuard, UserRoleGuard],
canActivate: [AuthGuard, EntityPermissionGuard, UserRoleGuard],
canDeactivate: [jasmine.any(Function)],
},
];
Expand Down
3 changes: 2 additions & 1 deletion src/app/core/config/dynamic-routing/router.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { NotFoundComponent } from "./not-found/not-found.component";
import { AuthGuard } from "../../session/auth.guard";
import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service";
import { RoutedViewComponent } from "../../ui/routed-view/routed-view.component";
import { EntityPermissionGuard } from "../../permissions/permission-guard/entity-permission.guard";

/**
* The RouterService dynamically sets up Angular routing from config loaded through the {@link ConfigService}.
Expand Down Expand Up @@ -86,7 +87,7 @@ export class RouterService {

private generateRouteFromConfig(view: ViewConfig, route: Route): Route {
route.data = route.data ?? {};
route.canActivate = [AuthGuard];
route.canActivate = [AuthGuard, EntityPermissionGuard];
route.canDeactivate = [
() => inject(UnsavedChangesService).checkUnsavedChanges(),
];
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/demo-data/demo-data.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ import { DemoSiteSettingsGeneratorService } from "../site-settings/demo-site-set
import { DemoReportConfigGeneratorService } from "../../features/reporting/demo-report-config-generator.service";

const demoDataGeneratorProviders = [
...DemoConfigGeneratorService.provider(),
...DemoPermissionGeneratorService.provider(),
...DemoConfigGeneratorService.provider(),
...DemoSiteSettingsGeneratorService.provider(),
...DemoPublicFormGeneratorService.provider(),
...DemoUserGeneratorService.provider(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ describe("EntityDetailsComponent", () => {
mockEntityRemoveService = jasmine.createSpyObj(["remove"]);
mockChildrenService.queryRelationsOf.and.resolveTo([]);
mockChildrenService.getAserResultsOfChild.and.resolveTo([]);
mockAbility = jasmine.createSpyObj(["cannot", "update"]);
mockAbility = jasmine.createSpyObj(["cannot", "update", "on"]);
mockAbility.cannot.and.returnValue(false);
mockAbility.on.and.returnValue(() => true);

TestBed.configureTestingModule({
imports: [EntityDetailsComponent, MockedTestingModule.withState()],
providers: [
Expand Down
6 changes: 6 additions & 0 deletions src/app/core/entity-details/form/form.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@ describe("FormComponent", () => {
});

it("calls router once a new child is saved", async () => {
const entityFormService = TestBed.inject(EntityFormService);
spyOn(entityFormService, "saveChanges").and.resolveTo();

const testChild = new Child();
const router = fixture.debugElement.injector.get(Router);
spyOn(router, "navigate");

component.creatingNew = true;
component.entity = testChild;
await component.saveClicked();

expect(entityFormService.saveChanges).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(["", testChild.getId()]);
});

Expand Down
17 changes: 12 additions & 5 deletions src/app/core/entity/latest-entity-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,35 @@ export abstract class LatestEntityLoader<T extends Entity> {
* Initialize the loader to make the entity available and emit continuous updates
* through the `entityUpdated` property
*/
startLoading() {
this.loadOnce();
async startLoading() {
const initialValue = await this.loadOnce();
this.entityMapper
.receiveUpdates(this.entityCtor)
.pipe(filter(({ entity }) => entity.getId() === this.entityID))
.subscribe(({ entity }) => this.entityUpdated.next(entity));
.subscribe(({ entity }) => {
this.entityUpdated.next(entity);
});
return initialValue;
}

/**
* Do an initial load of the entity to be available through the `entityUpdated` property
* (without watching for continuous updates).
*/
loadOnce() {
loadOnce(): Promise<T | undefined> {
return this.entityMapper
.load(this.entityCtor, this.entityID)
.then((entity) => this.entityUpdated.next(entity))
.then((entity) => {
this.entityUpdated.next(entity);
return entity;
})
.catch((err) => {
if (err?.status !== HttpStatusCode.NotFound) {
this.logger.error(
`Loading entity "${this.entityCtor.ENTITY_TYPE}:${this.entityID}" failed: ${err} `,
);
}
return undefined;
});
}
}
Loading

0 comments on commit 9d650ec

Please sign in to comment.