Skip to content

Commit

Permalink
feat: added grantModelRolesIfFunction and related types
Browse files Browse the repository at this point in the history
- removed adminGetsAllowAllRoles as for most cases complete admin access is not desirable
  • Loading branch information
dereekb committed Jun 8, 2022
1 parent 71e3cac commit 5432fab
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PrimativeKey, convertMaybeToArray, findUnique, makeValuesGroupMap, Mayb
import { Directive, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FormControl, AbstractControl } from '@angular/forms';
import { MatInput } from '@angular/material/input';
import { FieldTypeConfig, FormlyFieldConfig, FormlyFieldProps } from '@ngx-formly/core';
import { FieldTypeConfig, FormlyFieldProps } from '@ngx-formly/core';
import { FieldType } from '@ngx-formly/material';
import { BehaviorSubject, combineLatest, Observable, of, filter, map, debounceTime, distinctUntilChanged, switchMap, startWith, shareReplay, mergeMap, first, delay } from 'rxjs';
import { PickableValueFieldDisplayFn, PickableValueFieldDisplayValue, PickableValueFieldFilterFn, PickableValueFieldHashFn, PickableValueFieldLoadValuesFn, PickableValueFieldValue } from './pickable';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DbxInjectionComponentConfig, mergeDbxInjectionComponentConfigs } from '
import { filterMaybe, SubscriptionObject, LoadingState, LoadingStateContextInstance, successResult, startWithBeginLoading } from '@dereekb/rxjs';
import { ChangeDetectorRef, Directive, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { AbstractControl, FormControl, ValidatorFn } from '@angular/forms';
import { FieldTypeConfig, FormlyFieldConfig, FormlyFieldProps } from '@ngx-formly/core';
import { FieldTypeConfig, FormlyFieldProps } from '@ngx-formly/core';
import { FieldType } from '@ngx-formly/material';
import { debounceTime, distinctUntilChanged, first, map, mergeMap, shareReplay, startWith, switchMap, BehaviorSubject, of, Observable } from 'rxjs';
import { SearchableValueFieldHashFn, SearchableValueFieldStringSearchFn, SearchableValueFieldDisplayFn, SearchableValueFieldDisplayValue, SearchableValueFieldValue, SearchableValueFieldAnchorFn, ConfiguredSearchableValueFieldDisplayValue } from './searchable';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GrantedRole } from '@dereekb/model';
import { FirestoreModelKey } from '../collection/collection';
import { nowISODateString, toISODateString, toJsDate } from '@dereekb/date';
import { ModelFieldMapFunctionsConfig, GetterOrValue, Maybe, ModelFieldMapConvertFunction, passThrough, PrimativeKey, ReadKeyFunction, makeFindUniqueFunction, ModelFieldMapFunctionsWithDefaultsConfig, filterMaybeValues, MaybeSo, FindUniqueFunction, FindUniqueStringsTransformConfig, findUniqueTransform, MapFunction, FilterKeyValueTuplesInput, KeyValueTypleValueFilter, filterFromPOJOFunction, copyObject, CopyObjectFunction, mapObjectMapFunction, filterEmptyValues } from '@dereekb/util';
import { ModelFieldMapFunctionsConfig, GetterOrValue, Maybe, ModelFieldMapConvertFunction, passThrough, PrimativeKey, ReadKeyFunction, makeFindUniqueFunction, ModelFieldMapFunctionsWithDefaultsConfig, filterMaybeValues, MaybeSo, FindUniqueFunction, FindUniqueStringsTransformConfig, findUniqueTransform, MapFunction, FilterKeyValueTuplesInput, KeyValueTypleValueFilter, filterFromPOJOFunction, copyObject, CopyObjectFunction, mapObjectMapFunction, filterEmptyValues, ModelKey } from '@dereekb/util';
import { FIRESTORE_EMPTY_VALUE } from './snapshot';

export interface BaseFirestoreFieldConfig<V, D = unknown> {
Expand Down Expand Up @@ -90,6 +90,9 @@ export function optionalFirestoreUID() {
return optionalFirestoreString();
}

export const firestoreModelKey = firestoreString();
export const firestoreModelId = firestoreString();

export type FirestoreDateFieldConfig = DefaultMapConfiguredFirestoreFieldConfig<Date, string> & {
saveDefaultAsNow?: boolean;
};
Expand Down Expand Up @@ -275,8 +278,8 @@ export function firestoreMap<T, K extends string = string>(config: FirestoreMapF
*
* Filters out models with no/null roles by default.
*/
export function firestoreModelKeyGrantedRoleMap() {
return firestoreMap<GrantedRole, FirestoreModelKey>({
export function firestoreModelKeyGrantedRoleMap<R extends GrantedRole>() {
return firestoreMap<R, FirestoreModelKey>({
mapFilter: KeyValueTypleValueFilter.EMPTY
});
}
Expand All @@ -286,7 +289,7 @@ export function firestoreModelKeyGrantedRoleMap() {
*
* Filters out models with no/null roles by default.
*/
export const firestoreModelIdGrantedRoleMap = firestoreModelKeyGrantedRoleMap;
export const firestoreModelIdGrantedRoleMap: () => FirestoreModelFieldMapFunctionsConfig<FirestoreMapFieldType<ModelKey, string>, FirestoreMapFieldType<ModelKey, string>> = firestoreModelKeyGrantedRoleMap;

/**
* FirestoreField configuration for a map-type object with array values.
Expand All @@ -310,8 +313,8 @@ export function firestoreArrayMap<T, K extends string = string>(config: Firestor
*
* Filters empty roles/arrays by default.
*/
export function firestoreModelKeyGrantedRoleArrayMap() {
return firestoreArrayMap<GrantedRole, FirestoreModelKey>({
export function firestoreModelKeyGrantedRoleArrayMap<R extends GrantedRole>() {
return firestoreArrayMap<R, FirestoreModelKey>({
mapFieldValues: filterEmptyValues
});
}
Expand All @@ -321,7 +324,7 @@ export function firestoreModelKeyGrantedRoleArrayMap() {
*
* Filters empty roles/arrays by default.
*/
export const firestoreModelIdGrantedRoleArrayMap = firestoreModelKeyGrantedRoleArrayMap;
export const firestoreModelIdGrantedRoleArrayMap: () => FirestoreModelFieldMapFunctionsConfig<FirestoreMapFieldType<ModelKey[], string>, FirestoreMapFieldType<ModelKey[], string>> = firestoreModelKeyGrantedRoleArrayMap;

// MARK: Deprecated
export type FirestoreSetFieldConfig<T extends string | number> = DefaultMapConfiguredFirestoreFieldConfig<Set<T>, T[]>;
Expand Down
9 changes: 1 addition & 8 deletions packages/firebase/src/lib/common/model/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,7 @@ import { FirebasePermissionContext, FirebasePermissionErrorContext } from './per
/**
* A base model context that contains info about what is current occuring.
*/
export interface FirebaseModelContext extends FirebasePermissionContext, FirebasePermissionErrorContext, FirebaseAuthContext {
/**
* Whether or not to return all role checks for models as true if the auth context shows the current user as an admin.
*
* Is false by default.
*/
readonly adminGetsAllowAllRoles?: boolean;
}
export interface FirebaseModelContext extends FirebasePermissionContext, FirebasePermissionErrorContext, FirebaseAuthContext {}

export interface FirebaseAppModelContext<C> extends FirebaseModelContext {
readonly app: C;
Expand Down
25 changes: 0 additions & 25 deletions packages/firebase/src/lib/common/model/model.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,31 +282,6 @@ describe('firebaseModelsService', () => {
expect(result.roleMap).toBeDefined();
expect(isNoAccessRoleMap(result.roleMap)).toBe(true);
});

describe('with adminGetsAllowAllRoles=true', () => {
beforeEach(() => {
(context as any).adminGetsAllowAllRoles = true;
});

it('should return fullAccessor if the user is an admin', async () => {
(context as Building<typeof context>).auth = {
isAdmin: () => true
} as any;

const result = await mockFirebaseModelServices('mockItem', context).roleMapForModel(item);
expect(isFullAccessRoleMap(result.roleMap)).toBe(true);
});

it('should return normal roles if the user is not an admin.', async () => {
(context as Building<typeof context>).auth = {
uid: 'test',
isAdmin: () => false
} as any;

const result = await mockFirebaseModelServices('mockItem', context).roleMapForModel(item);
expect(isFullAccessRoleMap(result.roleMap)).toBe(false);
});
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FirestoreDocument } from './../../firestore';
import { AbstractModelPermissionService, fullAccessGrantedModelRoles, GrantedRoleMap, InContextModelPermissionService, InModelContextModelPermissionService, ModelPermissionService } from '@dereekb/model';
import { Maybe, PromiseOrValue } from '@dereekb/util';
import { AbstractModelPermissionService, fullAccessRoleMap, GrantedRoleMap, InContextModelPermissionService, InModelContextModelPermissionService, ModelPermissionService, noAccessRoleMap } from '@dereekb/model';
import { DecisionFunction, Getter, GetterOrValue, getValueFromGetter, Maybe, PromiseOrValue } from '@dereekb/util';
import { FirebaseModelLoader, InModelContextFirebaseModelLoader } from '../model/model.loader';
import { FirebaseModelContext } from '../context';
import { FirebasePermissionServiceModel } from './permission';
Expand Down Expand Up @@ -31,14 +31,6 @@ export class FirebaseModelPermissionServiceInstance<C extends FirebaseModelConte
return model;
}

protected override async getRoleMapForOutput(output: FirebasePermissionServiceModel<T, D>, context: C, model: D) {
if (context.adminGetsAllowAllRoles && context.auth?.isAdmin?.()) {
return fullAccessGrantedModelRoles(context, output);
} else {
return super.getRoleMapForOutput(output, context, model);
}
}

protected override isUsableOutputForRoles(output: FirebasePermissionServiceModel<T, D>) {
return output.exists;
}
Expand All @@ -53,3 +45,69 @@ export type InContextFirebaseModelPermissionService<C, T, D extends FirestoreDoc

// MARK: InModelContext
export type InModelContextFirebaseModelPermissionService<C, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> = InModelContextModelPermissionService<C, D, R, FirebasePermissionServiceModel<T, D>> & InModelContextFirebaseModelLoader<T, D>;

// MARK: Utility
export const grantFullAccessIfAdmin: GeneralGrantRolesIfFunction = grantModelRolesIfAdminFunction(fullAccessRoleMap);

export function grantModelRolesIfAdmin<R extends string = string>(context: FirebaseModelContext, rolesToGrantToAdmin: GetterOrValue<GrantedRoleMap<R>>, otherwise?: () => GrantedRoleMap<R>): GrantedRoleMap<R> {
return grantModelRolesIfAdminFunction(rolesToGrantToAdmin)(context, otherwise);
}

/**
* Convenience function that checks the input context if the user is an admin or not and grants pre-set admin roles if they are.
*
* @param context
* @param rolesToGrantToAdmin
* @param otherwise
* @returns
*/
export function grantModelRolesIfAdminFunction<R extends string = string>(rolesToGrantToAdmin: GetterOrValue<GrantedRoleMap<R>>): GrantRolesIfFunction<R> {
return grantModelRolesIfFunction(isAdminInFirebaseModelContext, rolesToGrantToAdmin);
}

/**
* DecisionFunction for a FirebaseModelContext that checks if the current user is an admin.
*
* @param context
* @returns
*/
export const isAdminInFirebaseModelContext: DecisionFunction<FirebaseModelContext> = (context: FirebaseModelContext) => context.auth?.isAdmin() ?? false;

/**
* Grants the configured roles if the decision is made about the context. Otherwise, returns a NoAccessRoleMap.
*/
export type GrantRolesOnlyIfFunction<R extends string = string, C extends FirebaseModelContext = FirebaseModelContext> = (context: C) => GrantedRoleMap<R>;
export type GeneralGrantRolesOnlyIfFunction = <R extends string = string, C extends FirebaseModelContext = FirebaseModelContext>(context: C) => GrantedRoleMap<R>;

/**
* Creates a GrantRolesOnlyIfFunction
*
* @param grantIf
* @param grantedRoles
* @returns
*/
export function grantModelRolesOnlyIfFunction<C extends FirebaseModelContext, R extends string = string>(grantIf: DecisionFunction<C>, grantedRoles: GetterOrValue<GrantedRoleMap<R>>): GrantRolesOnlyIfFunction<R, C> {
const fn = grantModelRolesIfFunction<C, R>(grantIf, grantedRoles);
return (context: C) => fn(context);
}

/**
* Grants the configured roles if the decision is made about the context. Otherwise, invokes the otherwise function if available, or returns a NoAccessRoleMap.
*/
export type GrantRolesIfFunction<R extends string = string, C extends FirebaseModelContext = FirebaseModelContext> = (context: C, otherwise?: Getter<GrantedRoleMap<R>>) => GrantedRoleMap<R>;
export type GeneralGrantRolesIfFunction = <R extends string = string, C extends FirebaseModelContext = FirebaseModelContext>(context: C, otherwise?: Getter<GrantedRoleMap<R>>) => GrantedRoleMap<R>;

/**
* Creates a GrantRolesIfFunction.
*
* @param grantIf
* @param grantedRoles
* @returns
*/
export function grantModelRolesIfFunction<C extends FirebaseModelContext, R extends string = string>(grantIf: DecisionFunction<C>, grantedRoles: GetterOrValue<GrantedRoleMap<R>>): GrantRolesIfFunction<R, C> {
return (context: C, otherwise: Getter<GrantedRoleMap<R>> = noAccessRoleMap) => {
const decision = grantIf(context);
const results = decision ? getValueFromGetter(grantedRoles) : otherwise();
return results;
};
}
30 changes: 15 additions & 15 deletions packages/model/src/lib/service/permission/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function noAccessRoleMap(): NoAccessRoleMap {
};
}

export function isNoAccessRoleMap<T extends string = string>(input: GrantedRoleMap<T> | NoAccessRoleMap): input is NoAccessRoleMap {
export function isNoAccessRoleMap<R extends string = string>(input: GrantedRoleMap<R> | NoAccessRoleMap): input is NoAccessRoleMap {
return (input as NoAccessRoleMap)[NO_ACCESS_ROLE_KEY] === true;
}

Expand All @@ -59,17 +59,17 @@ export function fullAccessRoleMap(): FullAccessRoleMap {
};
}

export function isFullAccessRoleMap<T extends string = string>(input: GrantedRoleMap<T> | FullAccessRoleMap): input is FullAccessRoleMap {
export function isFullAccessRoleMap<R extends string = string>(input: GrantedRoleMap<R> | FullAccessRoleMap): input is FullAccessRoleMap {
return (input as FullAccessRoleMap)[FULL_ACCESS_ROLE_KEY] === true;
}

export type GrantedRoleMap<T extends GrantedRole = string> = NoAccessRoleMap | FullAccessRoleMap | GrantedRoleKeysMap<T>;
export type GrantedRoleMap<R extends GrantedRole = string> = NoAccessRoleMap | FullAccessRoleMap | GrantedRoleKeysMap<R>;

export type GrantedRoleKeysMap<T extends GrantedRole = string> = {
[key in T]?: Maybe<boolean>;
export type GrantedRoleKeysMap<R extends GrantedRole = string> = {
[key in R]?: Maybe<boolean>;
};

export interface GrantedRoleMapReader<T extends GrantedRole = string> {
export interface GrantedRoleMapReader<R extends GrantedRole = string> {
/**
* Returns true if no access has been given.
*/
Expand All @@ -78,43 +78,43 @@ export interface GrantedRoleMapReader<T extends GrantedRole = string> {
/**
* Returns true if the role is granted.
*/
hasRole(role: T): boolean;
hasRole(role: R): boolean;

/**
* Returns true if the roles are granted.
*/
hasRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue<T>): boolean;
hasRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue<R>): boolean;

/**
* Returns true if the map explicitly contains the role.
*/
containsRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue<T>): boolean;
containsRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue<R>): boolean;
}

export function grantedRoleMapReader<T extends GrantedRole = string>(map: GrantedRoleMap<T>): GrantedRoleMapReader<T> {
export function grantedRoleMapReader<R extends GrantedRole = string>(map: GrantedRoleMap<R>): GrantedRoleMapReader<R> {
return new GrantedRoleMapReaderInstance(map);
}

export class GrantedRoleMapReaderInstance<T extends GrantedRole = string> implements GrantedRoleMapReader<T> {
constructor(private readonly _map: GrantedRoleMap<T>) {}
export class GrantedRoleMapReaderInstance<R extends GrantedRole = string> implements GrantedRoleMapReader<R> {
constructor(private readonly _map: GrantedRoleMap<R>) {}

hasNoAccess(): boolean {
return (this._map as NoAccessRoleMap)[NO_ACCESS_ROLE_KEY];
}

hasRole(role: T): boolean {
hasRole(role: R): boolean {
return this.hasRoles('any', role);
}

hasRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue<T>): boolean {
hasRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue<R>): boolean {
if ((this._map as FullAccessRoleMap)[FULL_ACCESS_ROLE_KEY]) {
return true;
} else {
return this.containsRoles(setIncludes, inputRoles);
}
}

containsRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue<T>): boolean {
containsRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue<R>): boolean {
const roles = asArray(inputRoles);

if (setIncludes === 'any') {
Expand Down
2 changes: 1 addition & 1 deletion packages/util/src/lib/auth/auth.role.claims.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AUTH_USER_ROLE, Maybe, objectHasKey } from '@dereekb/util';
import { containsAllValues, hasDifferentValues } from '../set';
import { AuthRoleSet, AUTH_ADMIN_ROLE } from './auth.role';
import { AuthClaimsObject, AuthRoleClaimsFactoryConfig, AuthRoleClaimsService, authRoleClaimsService, AUTH_ROLE_CLAIMS_DEFAULT_CLAIM_VALUE, AUTH_ROLE_CLAIMS_DEFAULT_EMPTY_VALUE } from './auth.role.claims';
import { AuthClaimsObject, AuthRoleClaimsService, authRoleClaimsService, AUTH_ROLE_CLAIMS_DEFAULT_CLAIM_VALUE, AUTH_ROLE_CLAIMS_DEFAULT_EMPTY_VALUE } from './auth.role.claims';

type TestClaims = {
test: string;
Expand Down
6 changes: 6 additions & 0 deletions packages/util/src/lib/value/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,9 @@ export function mapFunctionOutput<O extends object, I = unknown>(output: O, inpu
}
});
}

// MARK: MapTypes
/**
* A map function that derives a boolean from the input.
*/
export type DecisionFunction<I> = MapFunction<I, boolean>;

0 comments on commit 5432fab

Please sign in to comment.