Skip to content

Commit

Permalink
fix(auth-api): Delegation notifications (#18106)
Browse files Browse the repository at this point in the history
* refactors how we fetch delegation records and adds a filter feature flagg

* remove extra space

* remove unused import

* fix broken tests

* remove default value

* rename from arr to delegations

* add comment for special case of feature flag filter
  • Loading branch information
magnearun authored Mar 3, 2025
1 parent b81e548 commit 001552d
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,30 @@ export const scopes: Record<string, Scope> = {
legalGuardian: {
name: '@lg1',
grantToLegalGuardians: true,
supportedDelegationTypes: [AuthDelegationType.LegalGuardian],
},
procurationHolder: {
name: '@ph1',
grantToProcuringHolders: true,
supportedDelegationTypes: [AuthDelegationType.ProcurationHolder],
},
custom: {
name: '@cu1',
allowExplicitDelegationGrant: true,
supportedDelegationTypes: [AuthDelegationType.Custom],
},
custom2: {
name: '@cu2',
allowExplicitDelegationGrant: true,
supportedDelegationTypes: [AuthDelegationType.Custom],
},
representative: {
name: '@pr1',
grantToPersonalRepresentatives: true,
personalRepresentativeRightTypePermissions: [
personalRepresentativeRightTypeCodePostholf,
],
supportedDelegationTypes: [PersonalRepresentativeDelegationType.PersonalRepresentativePostholf],
},
all: {
name: '@all',
Expand All @@ -112,9 +117,16 @@ export const scopes: Record<string, Scope> = {
personalRepresentativeRightTypePermissions: [
personalRepresentativeRightTypeCodePostholf,
],
supportedDelegationTypes: [
AuthDelegationType.LegalGuardian,
AuthDelegationType.ProcurationHolder,
AuthDelegationType.Custom,
AuthDelegationType.PersonalRepresentative,
],
},
none: {
name: '@none',
supportedDelegationTypes: [],
},
}

Expand Down Expand Up @@ -156,6 +168,7 @@ export class TestCase {
grantToProcuringHolders: s.grantToProcuringHolders,
allowExplicitDelegationGrant: s.allowExplicitDelegationGrant,
grantToPersonalRepresentatives: s.grantToPersonalRepresentatives,
supportedDelegationTypes: s.supportedDelegationTypes,
}))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const FeatureFlagServiceMock = {
getValue: () => true,
getValue: () => Promise.resolve('*'),
}
230 changes: 89 additions & 141 deletions libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
getPersonalRepresentativeDelegationType,
} from '@island.is/shared/types'

import { PersonalRepresentativeScopePermissionService } from '../personal-representative/services/personal-representative-scope-permission.service'
import {
PersonalRepresentativeScopePermissionService,
} from '../personal-representative/services/personal-representative-scope-permission.service'
import { ApiScope } from '../resources/models/api-scope.model'
import { UserIdentitiesService } from '../user-identities/user-identities.service'
import { IncomingDelegationsCompanyService } from './delegations-incoming-company.service'
Expand All @@ -30,17 +32,12 @@ import { DelegationDTO } from './dto/delegation.dto'
import { DelegationIndexMeta } from './models/delegation-index-meta.model'
import { DelegationIndex } from './models/delegation-index.model'
import { DelegationDirection } from './types/delegationDirection'
import {
DelegationRecordType,
PersonalRepresentativeDelegationType,
} from './types/delegationRecord'
import {
delegationProviderTypeMap,
validateDelegationTypeAndProvider,
validateToAndFromNationalId,
} from './utils/delegations'
import { DelegationRecordType, PersonalRepresentativeDelegationType } from './types/delegationRecord'
import { validateDelegationTypeAndProvider, validateToAndFromNationalId } from './utils/delegations'
import { getXBirthday } from './utils/getXBirthday'
import { isUnderXAge } from './utils/isUnderXAge'
import { ApiScopeDelegationType } from '../resources/models/api-scope-delegation-type.model'
import { FeatureFlagService, Features } from '@island.is/nest/feature-flags'

const TEN_MINUTES = 1000 * 60 * 10
const ONE_WEEK = 1000 * 60 * 60 * 24 * 7
Expand Down Expand Up @@ -142,6 +139,8 @@ export class DelegationsIndexService {
constructor(
@InjectModel(ApiScope)
private apiScopeModel: typeof ApiScope,
@InjectModel(ApiScopeDelegationType)
private apiScopeDelegationTypeModel: typeof ApiScopeDelegationType,
@InjectModel(DelegationIndex)
private delegationIndexModel: typeof DelegationIndex,
@InjectModel(DelegationIndexMeta)
Expand All @@ -152,11 +151,54 @@ export class DelegationsIndexService {
private delegationsIncomingWardService: DelegationsIncomingWardService,
private personalRepresentativeScopePermissionService: PersonalRepresentativeScopePermissionService,
private userIdentitiesService: UserIdentitiesService,
private featureFlagService: FeatureFlagService,

@Inject(LOGGER_PROVIDER)
private logger: Logger,
private auditService: AuditService,
) {}

private async filterByFeatureFlaggedDelegationTypes(
delegations: DelegationRecordDTO[],
): Promise<DelegationRecordDTO[]> {
const featureFlaggedDelegationTypes = await this.featureFlagService
.getValue(Features.delegationTypesWithNotificationsEnabled, '')
.then((types): Set<string> | '*' | undefined => {
if (!types?.trim()) return undefined; // Empty value means no delegation types allowed
if (types?.trim() === '*') return '*'; // All delegation types allowed
return new Set(types?.split(',').map((type) => type.trim()));
});

// Case: No allowed delegation types
if (!featureFlaggedDelegationTypes) {
return [];
}

// Case: All delegation types are allowed
if (featureFlaggedDelegationTypes === '*') {
return delegations;
}

// Special case: Custom and GeneralMandate delegation types are stored with a ":person" or ":company" suffix,
// indicating if the value is allowing for delegations of the type from a person or a company.
return delegations.filter((delegation) => {

if (delegation.type === AuthDelegationType.Custom || delegation.type === AuthDelegationType.GeneralMandate) {
const isFromPerson =
featureFlaggedDelegationTypes.has(`${delegation.type}:person`) &&
kennitala.isPerson(delegation.fromNationalId)

const isFromCompany =
featureFlaggedDelegationTypes.has(`${delegation.type}:company`) &&
kennitala.isCompany(delegation.fromNationalId)

return isFromPerson || isFromCompany;
}

return featureFlaggedDelegationTypes.has(delegation.type);
});
}

/* Lookup delegations in index for user for specific scope */
async getDelegationRecords({
scope,
Expand All @@ -167,38 +209,56 @@ export class DelegationsIndexService {
nationalId: string
direction: DelegationDirection
}): Promise<PaginatedDelegationRecordDTO> {

const apiScope = await this.apiScopeModel.findOne({
where: {
name: scope,
},
})

if (!apiScope) {
throw new BadRequestException('Invalid scope')
}

const delegationTypesSupportedByScope = await this.apiScopeDelegationTypeModel.findAll({
where: {
apiScopeName: apiScope.name,
},
}).then(x => x.map((d) => d.delegationType))

if (!kennitala.isValid(nationalId)) {
throw new BadRequestException('Invalid national id')
}

const delegations = await Promise.all([
this.getCustomDelegationRecords({
scope: apiScope,
nationalId,
direction,
}),
this.getRepresentativeDelegationRecords({
scope: apiScope,
nationalId,
direction,
}),
this.getCompanyDelegationRecords({
scope: apiScope,
nationalId,
direction,
}),
this.getWardDelegationRecords({ scope: apiScope, nationalId, direction }),
]).then((d) => d.flat())
const supportsCustom = delegationTypesSupportedByScope.includes(AuthDelegationType.Custom)

const where = {
...(direction === DelegationDirection.INCOMING
? { toNationalId: nationalId }
: { fromNationalId: nationalId }),
validTo: { [Op.or]: [{ [Op.gte]: new Date() }, { [Op.is]: null }] },
}

const delegations = await this.delegationIndexModel
.findAll({
where: {
[Op.or]: [
{
...where,
type: {
[Op.in]: delegationTypesSupportedByScope.filter(
(d) => d !== AuthDelegationType.Custom,
),
},
},
supportsCustom ? {
...where,
type: AuthDelegationType.Custom,
customDelegationScopes: { [Op.contains]: [apiScope.name] },
} : {},
],
},
})
.then((d) => d.flat().map((d) => d.toDTO())).then((d) => this.filterByFeatureFlaggedDelegationTypes(d))

// For now, we don't implement pagination but still return the paginated response
return {
Expand Down Expand Up @@ -641,116 +701,4 @@ export class DelegationsIndexService {
)
}

private async getCustomDelegationRecords({
scope,
nationalId,
direction,
}: FetchDelegationRecordsArgs): Promise<DelegationRecordDTO[]> {
if (!scope.allowExplicitDelegationGrant) {
return []
}

return this.delegationIndexModel
.findAll({
where: {
...(direction === DelegationDirection.INCOMING
? { toNationalId: nationalId }
: { fromNationalId: nationalId }),
type: AuthDelegationType.Custom,
provider: AuthDelegationProvider.Custom,
customDelegationScopes: { [Op.contains]: [scope.name] },
validTo: { [Op.or]: [{ [Op.gte]: new Date() }, { [Op.is]: null }] },
},
})
.then((d) => d.map((d) => d.toDTO()))
}

private async getRepresentativeDelegationRecords({
scope,
nationalId,
direction,
}: FetchDelegationRecordsArgs): Promise<DelegationRecordDTO[]> {
if (!scope.grantToPersonalRepresentatives) {
return []
}

// Get all personal representative right types that are permitted for the scope and construct the delegation types
const permittedDelegationTypes =
await this.personalRepresentativeScopePermissionService
.getScopePermissionsAsync(scope.name)
.then((scopePermission) =>
scopePermission.map((rightType) =>
getPersonalRepresentativeDelegationType(rightType.rightTypeCode),
),
)

return this.delegationIndexModel
.findAll({
where: {
...(direction === DelegationDirection.INCOMING
? { toNationalId: nationalId }
: { fromNationalId: nationalId }),
type: {
[Op.in]: permittedDelegationTypes,
},
provider: AuthDelegationProvider.PersonalRepresentativeRegistry,
validTo: { [Op.or]: [{ [Op.gte]: new Date() }, { [Op.is]: null }] },
},
})
.then((d) => d.map((d) => d.toDTO()))
}

private async getCompanyDelegationRecords({
scope,
nationalId,
direction,
}: FetchDelegationRecordsArgs): Promise<DelegationRecordDTO[]> {
if (!scope.grantToProcuringHolders) {
return []
}

return this.delegationIndexModel
.findAll({
where: {
...(direction === DelegationDirection.INCOMING
? { toNationalId: nationalId }
: { fromNationalId: nationalId }),
type: {
[Op.in]:
delegationProviderTypeMap[AuthDelegationProvider.CompanyRegistry],
},
provider: AuthDelegationProvider.CompanyRegistry,
validTo: { [Op.or]: [{ [Op.gte]: new Date() }, { [Op.is]: null }] },
},
})
.then((d) => d.map((d) => d.toDTO()))
}

private async getWardDelegationRecords({
scope,
nationalId,
direction,
}: FetchDelegationRecordsArgs): Promise<DelegationRecordDTO[]> {
if (!scope.grantToLegalGuardians) {
return []
}

return this.delegationIndexModel
.findAll({
where: {
...(direction === DelegationDirection.INCOMING
? { toNationalId: nationalId }
: { fromNationalId: nationalId }),
type: {
[Op.in]:
delegationProviderTypeMap[
AuthDelegationProvider.NationalRegistry
],
},
provider: AuthDelegationProvider.NationalRegistry,
validTo: { [Op.or]: [{ [Op.gte]: new Date() }, { [Op.is]: null }] },
},
})
.then((d) => d.map((d) => d.toDTO()))
}
}
2 changes: 2 additions & 0 deletions libs/feature-flags/src/lib/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export enum Features {

// Should auth api use national registry v3 for checking deceased status
isNationalRegistryV3DeceasedStatusEnabled = 'isNationalRegistryV3DeceasedStatusEnabled',

delegationTypesWithNotificationsEnabled = 'delegationTypesWithNotificationsEnabled',
}

export enum ServerSideFeature {
Expand Down

0 comments on commit 001552d

Please sign in to comment.