diff --git a/packages/dbx-form/src/lib/formly/field/selection/pickable/pickable.field.directive.ts b/packages/dbx-form/src/lib/formly/field/selection/pickable/pickable.field.directive.ts index ceb7b4a91..8a73c51d2 100644 --- a/packages/dbx-form/src/lib/formly/field/selection/pickable/pickable.field.directive.ts +++ b/packages/dbx-form/src/lib/formly/field/selection/pickable/pickable.field.directive.ts @@ -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'; diff --git a/packages/dbx-form/src/lib/formly/field/selection/searchable/searchable.field.directive.ts b/packages/dbx-form/src/lib/formly/field/selection/searchable/searchable.field.directive.ts index c1fbe93f5..fc6c11e0f 100644 --- a/packages/dbx-form/src/lib/formly/field/selection/searchable/searchable.field.directive.ts +++ b/packages/dbx-form/src/lib/formly/field/selection/searchable/searchable.field.directive.ts @@ -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'; diff --git a/packages/firebase/src/lib/common/firestore/snapshot/snapshot.field.ts b/packages/firebase/src/lib/common/firestore/snapshot/snapshot.field.ts index c60a9e0c0..42a32b247 100644 --- a/packages/firebase/src/lib/common/firestore/snapshot/snapshot.field.ts +++ b/packages/firebase/src/lib/common/firestore/snapshot/snapshot.field.ts @@ -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 { @@ -90,6 +90,9 @@ export function optionalFirestoreUID() { return optionalFirestoreString(); } +export const firestoreModelKey = firestoreString(); +export const firestoreModelId = firestoreString(); + export type FirestoreDateFieldConfig = DefaultMapConfiguredFirestoreFieldConfig & { saveDefaultAsNow?: boolean; }; @@ -275,8 +278,8 @@ export function firestoreMap(config: FirestoreMapF * * Filters out models with no/null roles by default. */ -export function firestoreModelKeyGrantedRoleMap() { - return firestoreMap({ +export function firestoreModelKeyGrantedRoleMap() { + return firestoreMap({ mapFilter: KeyValueTypleValueFilter.EMPTY }); } @@ -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> = firestoreModelKeyGrantedRoleMap; /** * FirestoreField configuration for a map-type object with array values. @@ -310,8 +313,8 @@ export function firestoreArrayMap(config: Firestor * * Filters empty roles/arrays by default. */ -export function firestoreModelKeyGrantedRoleArrayMap() { - return firestoreArrayMap({ +export function firestoreModelKeyGrantedRoleArrayMap() { + return firestoreArrayMap({ mapFieldValues: filterEmptyValues }); } @@ -321,7 +324,7 @@ export function firestoreModelKeyGrantedRoleArrayMap() { * * Filters empty roles/arrays by default. */ -export const firestoreModelIdGrantedRoleArrayMap = firestoreModelKeyGrantedRoleArrayMap; +export const firestoreModelIdGrantedRoleArrayMap: () => FirestoreModelFieldMapFunctionsConfig, FirestoreMapFieldType> = firestoreModelKeyGrantedRoleArrayMap; // MARK: Deprecated export type FirestoreSetFieldConfig = DefaultMapConfiguredFirestoreFieldConfig, T[]>; diff --git a/packages/firebase/src/lib/common/model/context.ts b/packages/firebase/src/lib/common/model/context.ts index aada070fd..d0f756bd6 100644 --- a/packages/firebase/src/lib/common/model/context.ts +++ b/packages/firebase/src/lib/common/model/context.ts @@ -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 extends FirebaseModelContext { readonly app: C; diff --git a/packages/firebase/src/lib/common/model/model.service.spec.ts b/packages/firebase/src/lib/common/model/model.service.spec.ts index 6536c8064..ef24e6c66 100644 --- a/packages/firebase/src/lib/common/model/model.service.spec.ts +++ b/packages/firebase/src/lib/common/model/model.service.spec.ts @@ -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).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).auth = { - uid: 'test', - isAdmin: () => false - } as any; - - const result = await mockFirebaseModelServices('mockItem', context).roleMapForModel(item); - expect(isFullAccessRoleMap(result.roleMap)).toBe(false); - }); - }); }); }); }); diff --git a/packages/firebase/src/lib/common/model/permission/permission.service.ts b/packages/firebase/src/lib/common/model/permission/permission.service.ts index ef2c7eb5d..60a5d991f 100644 --- a/packages/firebase/src/lib/common/model/permission/permission.service.ts +++ b/packages/firebase/src/lib/common/model/permission/permission.service.ts @@ -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'; @@ -31,14 +31,6 @@ export class FirebaseModelPermissionServiceInstance, 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) { return output.exists; } @@ -53,3 +45,69 @@ export type InContextFirebaseModelPermissionService = FirestoreDocument, R extends string = string> = InModelContextModelPermissionService> & InModelContextFirebaseModelLoader; + +// MARK: Utility +export const grantFullAccessIfAdmin: GeneralGrantRolesIfFunction = grantModelRolesIfAdminFunction(fullAccessRoleMap); + +export function grantModelRolesIfAdmin(context: FirebaseModelContext, rolesToGrantToAdmin: GetterOrValue>, otherwise?: () => GrantedRoleMap): GrantedRoleMap { + 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(rolesToGrantToAdmin: GetterOrValue>): GrantRolesIfFunction { + return grantModelRolesIfFunction(isAdminInFirebaseModelContext, rolesToGrantToAdmin); +} + +/** + * DecisionFunction for a FirebaseModelContext that checks if the current user is an admin. + * + * @param context + * @returns + */ +export const isAdminInFirebaseModelContext: DecisionFunction = (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 = (context: C) => GrantedRoleMap; +export type GeneralGrantRolesOnlyIfFunction = (context: C) => GrantedRoleMap; + +/** + * Creates a GrantRolesOnlyIfFunction + * + * @param grantIf + * @param grantedRoles + * @returns + */ +export function grantModelRolesOnlyIfFunction(grantIf: DecisionFunction, grantedRoles: GetterOrValue>): GrantRolesOnlyIfFunction { + const fn = grantModelRolesIfFunction(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 = (context: C, otherwise?: Getter>) => GrantedRoleMap; +export type GeneralGrantRolesIfFunction = (context: C, otherwise?: Getter>) => GrantedRoleMap; + +/** + * Creates a GrantRolesIfFunction. + * + * @param grantIf + * @param grantedRoles + * @returns + */ +export function grantModelRolesIfFunction(grantIf: DecisionFunction, grantedRoles: GetterOrValue>): GrantRolesIfFunction { + return (context: C, otherwise: Getter> = noAccessRoleMap) => { + const decision = grantIf(context); + const results = decision ? getValueFromGetter(grantedRoles) : otherwise(); + return results; + }; +} diff --git a/packages/model/src/lib/service/permission/role.ts b/packages/model/src/lib/service/permission/role.ts index 2e7438ef2..3f388a3fc 100644 --- a/packages/model/src/lib/service/permission/role.ts +++ b/packages/model/src/lib/service/permission/role.ts @@ -45,7 +45,7 @@ export function noAccessRoleMap(): NoAccessRoleMap { }; } -export function isNoAccessRoleMap(input: GrantedRoleMap | NoAccessRoleMap): input is NoAccessRoleMap { +export function isNoAccessRoleMap(input: GrantedRoleMap | NoAccessRoleMap): input is NoAccessRoleMap { return (input as NoAccessRoleMap)[NO_ACCESS_ROLE_KEY] === true; } @@ -59,17 +59,17 @@ export function fullAccessRoleMap(): FullAccessRoleMap { }; } -export function isFullAccessRoleMap(input: GrantedRoleMap | FullAccessRoleMap): input is FullAccessRoleMap { +export function isFullAccessRoleMap(input: GrantedRoleMap | FullAccessRoleMap): input is FullAccessRoleMap { return (input as FullAccessRoleMap)[FULL_ACCESS_ROLE_KEY] === true; } -export type GrantedRoleMap = NoAccessRoleMap | FullAccessRoleMap | GrantedRoleKeysMap; +export type GrantedRoleMap = NoAccessRoleMap | FullAccessRoleMap | GrantedRoleKeysMap; -export type GrantedRoleKeysMap = { - [key in T]?: Maybe; +export type GrantedRoleKeysMap = { + [key in R]?: Maybe; }; -export interface GrantedRoleMapReader { +export interface GrantedRoleMapReader { /** * Returns true if no access has been given. */ @@ -78,35 +78,35 @@ export interface GrantedRoleMapReader { /** * 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): boolean; + hasRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue): boolean; /** * Returns true if the map explicitly contains the role. */ - containsRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue): boolean; + containsRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue): boolean; } -export function grantedRoleMapReader(map: GrantedRoleMap): GrantedRoleMapReader { +export function grantedRoleMapReader(map: GrantedRoleMap): GrantedRoleMapReader { return new GrantedRoleMapReaderInstance(map); } -export class GrantedRoleMapReaderInstance implements GrantedRoleMapReader { - constructor(private readonly _map: GrantedRoleMap) {} +export class GrantedRoleMapReaderInstance implements GrantedRoleMapReader { + constructor(private readonly _map: GrantedRoleMap) {} 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): boolean { + hasRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue): boolean { if ((this._map as FullAccessRoleMap)[FULL_ACCESS_ROLE_KEY]) { return true; } else { @@ -114,7 +114,7 @@ export class GrantedRoleMapReaderInstance implem } } - containsRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue): boolean { + containsRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue): boolean { const roles = asArray(inputRoles); if (setIncludes === 'any') { diff --git a/packages/util/src/lib/auth/auth.role.claims.spec.ts b/packages/util/src/lib/auth/auth.role.claims.spec.ts index 791e91b5a..6db17d788 100644 --- a/packages/util/src/lib/auth/auth.role.claims.spec.ts +++ b/packages/util/src/lib/auth/auth.role.claims.spec.ts @@ -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; diff --git a/packages/util/src/lib/value/map.ts b/packages/util/src/lib/value/map.ts index 66a7dac94..58e483681 100644 --- a/packages/util/src/lib/value/map.ts +++ b/packages/util/src/lib/value/map.ts @@ -77,3 +77,9 @@ export function mapFunctionOutput(output: O, inpu } }); } + +// MARK: MapTypes +/** + * A map function that derives a boolean from the input. + */ +export type DecisionFunction = MapFunction;