Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(auth-api): Delegation notifications #18106

Merged
merged 9 commits into from
Mar 3, 2025
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('*'),
}
227 changes: 86 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,51 @@ 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(
arr: 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 arr;
}

return arr.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 +206,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 +698,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()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const AuthDelegationApiClientConfig = defineConfig({
),
machineClientScope:
env.optionalJSON<string[]>('AUTH_DELEGATION_MACHINE_CLIENT_SCOPE') ??
[],
["@island.is/auth/delegations/index:system"],
}
},
})
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