Skip to content

Commit

Permalink
Merge pull request #20988 from abpframework/issue-20342
Browse files Browse the repository at this point in the history
Angular - Checking `Property Policy` for the Extensions
  • Loading branch information
masum-ulu authored Oct 8, 2024
2 parents 37465e4 + 96c3e1b commit 8129dae
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Injectable, inject } from '@angular/core';

import { Observable } from 'rxjs';
import { tap, map } from 'rxjs/operators';

import { ConfigStateService, IAbpGuard } from '@abp/ng.core';
import { ConfigStateService, IAbpGuard, PermissionService } from '@abp/ng.core';
import {
ExtensionsService,
getObjectExtensionEntitiesFromStore,
Expand All @@ -23,6 +21,7 @@ import { eAccountComponents } from '../enums/components';
@Injectable()
export class AccountExtensionsGuard implements IAbpGuard {
protected readonly configState = inject(ConfigStateService);
protected readonly permmission = inject(PermissionService);
protected readonly extensions = inject(ExtensionsService);

canActivate(): Observable<boolean> {
Expand All @@ -34,7 +33,7 @@ export class AccountExtensionsGuard implements IAbpGuard {
map(entities => ({
[eAccountComponents.PersonalSettings]: entities.User,
})),
mapEntitiesToContributors(this.configState, 'AbpIdentity'),
mapEntitiesToContributors(this.configState, this.permmission, 'AbpIdentity'),
tap(objectExtensionContributors => {
mergeWithDefaultProps(
this.extensions.editFormProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { inject } from '@angular/core';
import { ConfigStateService } from '@abp/ng.core';
import { ResolveFn } from '@angular/router';
import { map, tap } from 'rxjs';
import { ConfigStateService, PermissionService } from '@abp/ng.core';
import {
ExtensionsService,
getObjectExtensionEntitiesFromStore,
Expand All @@ -9,10 +10,10 @@ import {
} from '@abp/ng.components/extensible';
import { eAccountComponents } from '../enums';
import { ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS, DEFAULT_ACCOUNT_FORM_PROPS } from '../tokens';
import { ResolveFn } from '@angular/router';

export const accountExtensionsResolver: ResolveFn<any> = () => {
const configState = inject(ConfigStateService);
const permission = inject(PermissionService);
const extensions = inject(ExtensionsService);

const config = { optional: true };
Expand All @@ -23,7 +24,7 @@ export const accountExtensionsResolver: ResolveFn<any> = () => {
map(entities => ({
[eAccountComponents.PersonalSettings]: entities.User,
})),
mapEntitiesToContributors(configState, 'AbpIdentity'),
mapEntitiesToContributors(configState, permission, 'AbpIdentity'),
tap(objectExtensionContributors => {
mergeWithDefaultProps(
extensions.editFormProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ export interface ExtensionPropertyDto {
configuration: Record<string, any>;
defaultValue: any;
formText?: string;
policy?: Policy;
}

export interface BaseDefinition {
requiresAll: boolean;
}
export interface FeatureDefinition extends BaseDefinition {
features?: string[];
}
export interface PermissionDefinition extends BaseDefinition {
permissionNames?: string[];
}

export interface Policy {
globalFeatures: FeatureDefinition;
features: FeatureDefinition;
permissions: PermissionDefinition;
}

export interface ExtensionPropertyUiDto {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ export function generateFormFromProps<R = any>(data: PropData<R>) {
props.forEach(({ value: prop }) => {
const name = prop.name;
const isExtraProperty = prop.isExtra || name in extraProperties;
let value = isExtraProperty ? extraProperties[name] : name in record ? record[name] : undefined;

let value = undefined;

if (isExtraProperty) {
value = extraProperties[name];
} else if (name in record) {
value = record[name];
}

if (typeof value === 'undefined') value = prop.defaultValue;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConfigStateService, PermissionService } from '@abp/ng.core';
import { Observable, of } from 'rxjs';
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties';
import {
Expand All @@ -22,6 +23,8 @@ import {
PropList,
PropsFactory,
} from '../models/props';
import { Policy } from '../models/internal/object-extensions';
import { ObjectExtensions } from '../models/object-extensions';

export function createExtraPropertyValueResolver<T>(
name: string,
Expand All @@ -47,6 +50,51 @@ export function mergeWithDefaultProps<F extends PropsFactory<any>>(
);
});
}

export function checkPolicies(
properties: ObjectExtensions.EntityExtensionProperties,
configState: ConfigStateService,
permissionService: PermissionService,
) {
const props = Object.entries(properties);

const checkPolicy = (policy: Policy): boolean => {
const { permissions, globalFeatures, features } = policy;

const checks = [
{
items: permissions?.permissionNames,
requiresAll: permissions?.requiresAll,
check: (item: string) => permissionService.getGrantedPolicy(item),
},
{
items: globalFeatures?.features,
requiresAll: globalFeatures?.requiresAll,
check: (item: string) => configState.getGlobalFeatureIsEnabled(item),
},
{
items: features?.features,
requiresAll: features?.requiresAll,
check: (item: string) => configState.getFeatureIsEnabled(item),
},
];

return checks.every(({ items, requiresAll, check }) => {
if (!items?.length) {
return true;
}

return requiresAll ? items.every(check) : items.some(check);
});
};

props.forEach(([name, property]) => {
if (property.policy && !checkPolicy(property.policy)) {
delete properties[name];
}
});
}

type InferredPropDefaults<F> =
F extends EntityPropsFactory<infer RE>
? EntityPropDefaults<RE>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ExtensionEnumDto,
ExtensionPropertyUiLookupDto,
ObjectExtensionsDto,
PermissionService,
} from '@abp/ng.core';
import { Observable, pipe, zip } from 'rxjs';
import { filter, map, switchMap, take } from 'rxjs/operators';
Expand All @@ -15,7 +16,7 @@ import { ObjectExtensions } from '../models/object-extensions';
import { PropCallback } from '../models/props';
import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util';
import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util';
import { createExtraPropertyValueResolver } from './props.util';
import { checkPolicies, createExtraPropertyValueResolver } from './props.util';
import {
createTypeaheadDisplayNameGenerator,
createTypeaheadOptions,
Expand All @@ -39,15 +40,18 @@ function selectEnums(
): Observable<Record<string, ExtensionEnumDto>> {
return selectObjectExtensions(configState).pipe(
map((extensions: ObjectExtensionsDto) =>
Object.keys(extensions.enums).reduce((acc, key) => {
const { fields, localizationResource } = extensions.enums[key];
acc[key] = {
fields,
localizationResource,
transformed: createEnum(fields),
};
return acc;
}, {} as Record<string, ObjectExtensions.ExtensionEnumDto>),
Object.keys(extensions.enums).reduce(
(acc, key) => {
const { fields, localizationResource } = extensions.enums[key];
acc[key] = {
fields,
localizationResource,
transformed: createEnum(fields),
};
return acc;
},
{} as Record<string, ObjectExtensions.ExtensionEnumDto>,
),
),
);
}
Expand All @@ -71,6 +75,7 @@ export function getObjectExtensionEntitiesFromStore(

export function mapEntitiesToContributors<T = any>(
configState: ConfigStateService,
permissionService: PermissionService,
resource: string,
) {
return pipe(
Expand All @@ -86,10 +91,16 @@ export function mapEntitiesToContributors<T = any>(
acc.editForm[key] = [];

const entity: ObjectExtensions.EntityExtensionDto = entities[key];
if (!entity) return acc;
if (!entity) {
return acc;
}

const properties = entity.properties;
if (!properties) return acc;
if (!properties) {
return acc;
}

checkPolicies(properties, configState, permissionService);

const mapPropertiesToContributors = createPropertiesToContributorsMapper<T>(
generateDisplayName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import {ConfigStateService} from '@abp/ng.core';
import {firstValueFrom, of} from 'rxjs';
import {take} from 'rxjs/operators';
import {ePropType} from '../lib/enums/props.enum';
import {EntityPropList} from '../lib/models/entity-props';
import {FormPropList} from '../lib/models/form-props';
import {ObjectExtensions} from '../lib/models/object-extensions';
import { ConfigStateService, PermissionService } from '@abp/ng.core';
import { firstValueFrom, lastValueFrom, of } from 'rxjs';
import { take } from 'rxjs/operators';
import { ePropType } from '../lib/enums/props.enum';
import { EntityPropList } from '../lib/models/entity-props';
import { FormPropList } from '../lib/models/form-props';
import { ObjectExtensions } from '../lib/models/object-extensions';
import {
getObjectExtensionEntitiesFromStore,
mapEntitiesToContributors,
} from '../lib/utils/state.util';

const fakeAppConfigService = {get: () => of(createMockState())} as any;
const fakeLocalizationService = {get: () => of(createMockState())} as any;
const fakeAppConfigService = { get: () => of(createMockState()) } as any;
const fakeLocalizationService = { get: () => of(createMockState()) } as any;
const configState = new ConfigStateService(fakeAppConfigService, fakeLocalizationService, false);
configState.refreshAppState();
const permissionService = new PermissionService(configState);

describe('State Utils', () => {
describe('#getObjectExtensionEntitiesFromStore', () => {
it('should return observable entities of an existing module', async () => {

const objectExtensionEntitiesFromStore$ = getObjectExtensionEntitiesFromStore(
configState,
'Identity',
)
);

const entities = await firstValueFrom(objectExtensionEntitiesFromStore$)
const entities = await firstValueFrom(objectExtensionEntitiesFromStore$);
expect('Role' in entities).toBe(true);
});

Expand All @@ -48,9 +48,12 @@ describe('State Utils', () => {

describe('#mapEntitiesToContributors', () => {
it('should return contributors from given entities', async () => {
const contributors = await of(createMockEntities())
.pipe(mapEntitiesToContributors(configState, 'AbpIdentity'), take(1))
.toPromise();
const contributors = await lastValueFrom(
of(createMockEntities()).pipe(
mapEntitiesToContributors(configState, permissionService, 'AbpIdentity'),
take(1),
),
);

const propList = new EntityPropList();
contributors.prop.Role.forEach(callback => callback(propList));
Expand Down Expand Up @@ -118,7 +121,7 @@ function createMockState() {
},
defaultResourceName: 'Default',
currentCulture: {
cultureName: 'en'
cultureName: 'en',
},
languages: [],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-co
import { AbpApplicationLocalizationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-localization.service';
import {
ApplicationConfigurationDto,
ApplicationFeatureConfigurationDto,
ApplicationGlobalFeatureConfigurationDto,
} from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/models';
import { INCUDE_LOCALIZATION_RESOURCES_TOKEN } from '../tokens/include-localization-resources.token';
Expand Down Expand Up @@ -160,6 +161,18 @@ export class ConfigStateService {
});
}

private isFeatureEnabled(key: string, features: ApplicationFeatureConfigurationDto) {
return features.values[key] === 'true';
}

getFeatureIsEnabled(key: string) {
return this.isFeatureEnabled(key, this.store.state.features);
}

getFeatureIsEnabled$(key: string) {
return this.store.sliceState(state => this.isFeatureEnabled(key, state.features));
}

getSetting(key: string) {
return this.store.state.setting?.values?.[key];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Injectable, inject } from '@angular/core';

import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';

import { ConfigStateService, IAbpGuard } from '@abp/ng.core';
import {
ExtensionsService,
getObjectExtensionEntitiesFromStore,
mapEntitiesToContributors,
mergeWithDefaultActions,
mergeWithDefaultProps,
} from '@abp/ng.components/extensible';
import { ConfigStateService, IAbpGuard, PermissionService } from '@abp/ng.core';
import { Injectable, inject } from '@angular/core';

import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';

import { eIdentityComponents } from '../enums/components';
import {
Expand All @@ -32,6 +31,7 @@ import {
@Injectable()
export class IdentityExtensionsGuard implements IAbpGuard {
protected readonly configState = inject(ConfigStateService);
protected readonly permission = inject(PermissionService);
protected readonly extensions = inject(ExtensionsService);

canActivate(): Observable<boolean> {
Expand All @@ -48,7 +48,7 @@ export class IdentityExtensionsGuard implements IAbpGuard {
[eIdentityComponents.Roles]: entities.Role,
[eIdentityComponents.Users]: entities.User,
})),
mapEntitiesToContributors(this.configState, 'AbpIdentity'),
mapEntitiesToContributors(this.configState, this.permission, 'AbpIdentity'),
tap(objectExtensionContributors => {
mergeWithDefaultActions(
this.extensions.entityActions,
Expand Down
Loading

0 comments on commit 8129dae

Please sign in to comment.