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

feat(ids-api): Add legal representative delegation type and return in delegation list. #15837

Merged
merged 23 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,17 @@ export const testCases: Record<string, TestCase> = {
],
},
),
// Returns available delegations for legal representatives
legalRepresentative1: new TestCase(
createClient({
clientId: clientId,
supportedDelegationTypes: [AuthDelegationType.LegalRepresentative],
}),
{
fromLegalRepresentative: [person1, person2],
protectedScopes: [],
expectedFrom: [person1, person2],
expectedTypes: [AuthDelegationType.LegalRepresentative],
},
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const customScope2 = 'cu2'
const customScopeOtherDomain = 'cu-od1'
const representativeScope1 = 'pr1'
const representativeScope2 = 'pr2'
const legalRepresentativeScope1 = 'lr1'
const legalRepresentativeScope2 = 'lr2'

export const legalGuardianScopes = [legalGuardianScope1, legalGuardianScope2]
export const procurationHolderScopes = [
Expand All @@ -37,12 +39,17 @@ export const procurationHolderScopes = [
]
export const customScopes = [customScope1, customScope2, customScopeOtherDomain]
export const representativeScopes = [representativeScope1, representativeScope2]
export const legalRepresentativeScopes = [
legalRepresentativeScope1,
legalRepresentativeScope2,
]

export interface ITestCaseOptions {
fromChildren?: string[]
fromCompanies?: string[]
fromCustom?: string[]
fromRepresentative?: string[]
fromLegalRepresentative?: string[]
scopes?: string[]
protectedScopes?: string[]
scopeAccess?: [string, string][]
Expand All @@ -59,6 +66,7 @@ export class TestCase {
fromCompanies: string[]
fromCustom: string[]
fromRepresentative: string[]
fromLegalRepresentative: string[]
scopes: string[]
protectedScopes: string[]
scopeAccess: [string, string][]
Expand All @@ -71,11 +79,13 @@ export class TestCase {
this.fromCompanies = options.fromCompanies ?? []
this.fromCustom = options.fromCustom ?? []
this.fromRepresentative = options.fromRepresentative ?? []
this.fromLegalRepresentative = options.fromLegalRepresentative ?? []
this.scopes = options.scopes ?? [
...legalGuardianScopes,
...procurationHolderScopes,
...customScopes,
...representativeScopes,
...legalRepresentativeScopes,
]
this.protectedScopes = options.protectedScopes ?? []
this.scopeAccess = options.scopeAccess ?? []
Expand Down Expand Up @@ -160,6 +170,9 @@ export class TestCase {
if (representativeScopes.includes(scopeName)) {
result.push(AuthDelegationType.PersonalRepresentative)
}
if (legalRepresentativeScopes.includes(scopeName)) {
result.push(AuthDelegationType.LegalRepresentative)
}
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { MergedDelegationDTO } from '@island.is/auth-api-lib'
import { RskRelationshipsClient } from '@island.is/clients-rsk-relationships'
import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2'
import { FixtureFactory } from '@island.is/services/auth/testing'
import {
AuthDelegationProvider,
AuthDelegationType,
} from '@island.is/shared/types'
import { createNationalRegistryUser } from '@island.is/testing/fixtures'
import { TestApp, truncate } from '@island.is/testing/nest'

Expand Down Expand Up @@ -86,6 +90,17 @@ describe('DelegationsController', () => {
),
)

await Promise.all(
testCase.fromLegalRepresentative.map((nationalId) =>
factory.createDelegationIndexRecord({
fromNationalId: nationalId,
toNationalId: testCase.user.nationalId,
type: AuthDelegationType.LegalRepresentative,
provider: AuthDelegationProvider.DistrictCommissionersRegistry,
}),
),
)
valurefugl marked this conversation as resolved.
Show resolved Hide resolved

jest
.spyOn(nationalRegistryApi, 'getCustodyChildren')
.mockImplementation(async () => testCase.fromChildren)
Expand Down
4 changes: 1 addition & 3 deletions apps/services/auth/ids-api/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ export const setupWithAuth = async ({
.useValue({
getValue: (feature: Features) =>
!features || features.includes(feature),
})
.overrideProvider(DelegationsIndexService)
.useClass(MockDelegationsIndexService),
}),
hooks: [
useAuth({ auth: user }),
useDatabase({ type: 'postgres', provider: SequelizeConfigService }),
Expand Down
9 changes: 8 additions & 1 deletion libs/api/domains/auth/src/lib/models/delegation.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
registerEnumType,
} from '@nestjs/graphql'

import { Identity } from '@island.is/api/domains/identity'
import {
AuthDelegationProvider,
AuthDelegationType,
} from '@island.is/clients/auth/delegation-api'
import { Identity } from '@island.is/api/domains/identity'

import { DelegationScope } from './delegationScope.model'

Expand All @@ -32,6 +32,8 @@ const exhaustiveCheck = (param: never) => {
return PersonalRepresentativeDelegation
case AuthDelegationType.Custom:
return CustomDelegation
case AuthDelegationType.LegalRepresentative:
return LegalRepresentativeDelegation
default:
exhaustiveCheck(delegation.type)
}
Expand Down Expand Up @@ -83,6 +85,11 @@ export class CustomDelegation extends Delegation {
domainName?: string
}

@ObjectType('AuthLegalRepresentativeDelegation', {
implements: Delegation,
})
export class LegalRepresentativeDelegation extends Delegation {}
saevarma marked this conversation as resolved.
Show resolved Hide resolved

@ObjectType('AuthMergedDelegation')
export class MergedDelegation {
@Field(() => [AuthDelegationType])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module.exports = {
up(queryInterface) {
return queryInterface.sequelize.query(`
BEGIN;
INSERT INTO delegation_provider
(id, name, description)
VALUES
('syslumenn', 'Sýslumenn', 'Provider for district commissioners registry');

INSERT INTO delegation_type
(id, provider, name, description)
VALUES
('LegalRepresentative', 'syslumenn', 'Legal Representative', 'Legal Representative delegation type');

INSERT INTO api_scope_delegation_types
(api_scope_name, delegation_type)
VALUES
('@island.is/documents', 'LegalRepresentative');

INSERT INTO client_delegation_types
(client_id, delegation_type)
VALUES
('@island.is/web', 'LegalRepresentative');

COMMIT;
`)
},

down(queryInterface) {
return queryInterface.sequelize.query(`
BEGIN;
DELETE FROM client_delegation_types
WHERE client_id = '@island.is/web' AND delegation_type = 'LegalRepresentative';

DELETE FROM api_scope_delegation_types
WHERE api_scope_name = '@island.is/documents' AND delegation_type = 'LegalRepresentative';

DELETE FROM delegation_type
WHERE id = 'LegalRepresentative';

DELETE FROM delegation_provider
WHERE id = 'syslumenn';

COMMIT;
`)
},
}
13 changes: 13 additions & 0 deletions libs/auth-api-lib/src/lib/delegations/delegation-dto.mapper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { AuthDelegationType } from '@island.is/shared/types'

import { DelegationRecordDTO } from './dto/delegation-index.dto'
import { DelegationDTO } from './dto/delegation.dto'
import { MergedDelegationDTO } from './dto/merged-delegation.dto'

Expand All @@ -13,4 +16,14 @@ export class DelegationDTOMapper {
scopes: dto.scopes,
}
}

public static recordToMergedDelegationDTO(
dto: DelegationRecordDTO,
): MergedDelegationDTO {
return {
fromNationalId: dto.fromNationalId,
toNationalId: dto.toNationalId,
types: [dto.type as AuthDelegationType],
valurefugl marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { BadRequestException, Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/sequelize'

import { User } from '@island.is/auth-nest-tools'
import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2'
import { FeatureFlagService, Features } from '@island.is/nest/feature-flags'
import {
AuthDelegationProvider,
AuthDelegationType,
} from '@island.is/shared/types'
import { isDefined } from '@island.is/shared/utils'

import { ClientAllowedScope } from '../clients/models/client-allowed-scope.model'
import { ClientDelegationType } from '../clients/models/client-delegation-type.model'
Expand All @@ -22,6 +25,8 @@ import { DelegationsIndexService } from './delegations-index.service'
import { DelegationDTO } from './dto/delegation.dto'
import { MergedDelegationDTO } from './dto/merged-delegation.dto'

const UNKNOWN_NAME = 'Óþekkt nafn'
valurefugl marked this conversation as resolved.
Show resolved Hide resolved

type ClientDelegationInfo = Pick<
Client,
'supportedDelegationTypes' | 'requireApiScopes'
Expand Down Expand Up @@ -58,6 +63,8 @@ export class DelegationsIncomingService {
private delegationsIncomingWardService: DelegationsIncomingWardService,
private delegationsIndexService: DelegationsIndexService,
private delegationProviderService: DelegationProviderService,
private nationalRegistryClient: NationalRegistryClientService,
private readonly featureFlagService: FeatureFlagService,
valurefugl marked this conversation as resolved.
Show resolved Hide resolved
) {}

async findAllValid(
Expand Down Expand Up @@ -116,20 +123,20 @@ export class DelegationsIncomingService {
const client = await this.getClientDelegationInfo(user)
if (!client?.supportedDelegationTypes) return []

const types: ClientDelegationType[] =
client.supportedDelegationTypes.filter(
const types: AuthDelegationType[] = client.supportedDelegationTypes
.filter(
(dt) =>
!delegationTypes ||
delegationTypes.includes(dt.delegationType as AuthDelegationType),
)
.map((t) => t.delegationType as AuthDelegationType)

if (types.length == 0) return []
saevarma marked this conversation as resolved.
Show resolved Hide resolved

const providers = await this.delegationProviderService.findProviders(
types.map((t) => t.delegationType),
)
const providers = await this.delegationProviderService.findProviders(types)

const clientAllowedApiScopes = await this.getClientAllowedApiScopes(user)
const clientAllowedApiScopes: ApiScopeInfo[] =
await this.getClientAllowedApiScopes(user)

const delegationPromises = []

Expand Down Expand Up @@ -187,6 +194,27 @@ export class DelegationsIncomingService {
)
}

if (
providers.includes(AuthDelegationProvider.DistrictCommissionersRegistry)
) {
const isLegalRepresentativeDelegationEnabled =
await this.featureFlagService.getValue(
Features.isLegalRepresentativeDelegationEnabled,
true,
user,
)
if (isLegalRepresentativeDelegationEnabled) {
delegationPromises.push(
this.getAvailableDistrictCommissionersRegistryDelegations(
user,
types,
clientAllowedApiScopes,
client.requireApiScopes,
),
)
}
}

const delegationSets = await Promise.all(delegationPromises)

let delegations = ([] as MergedDelegationDTO[])
Expand Down Expand Up @@ -224,6 +252,56 @@ export class DelegationsIncomingService {
return [...mergedDelegationMap.values()]
}

private async getAvailableDistrictCommissionersRegistryDelegations(
user: User,
types: AuthDelegationType[],
clientAllowedApiScopes: ApiScopeInfo[],
requireApiScopes?: boolean,
): Promise<MergedDelegationDTO[]> {
const records =
await this.delegationsIndexService.getAvailableDistrictCommissionersRegistryRecords(
user,
types,
clientAllowedApiScopes,
requireApiScopes,
)
const merged = records.map((d) =>
DelegationDTOMapper.recordToMergedDelegationDTO(d),
)

const persons = (
await Promise.all(
merged.map((d) =>
this.nationalRegistryClient
.getIndividual(d.fromNationalId)
.catch((error) => error),
),
)
)
.filter(this.isNotError)
.filter(isDefined)
.map((individual) => ({
nationalId: individual.nationalId,
name: individual.name ?? UNKNOWN_NAME,
}))

merged.forEach((d) => {
const person = persons.find((p) => p.nationalId === d.fromNationalId)
if (person) {
d.fromName = person.name
}
})
saevarma marked this conversation as resolved.
Show resolved Hide resolved

return merged
}
valurefugl marked this conversation as resolved.
Show resolved Hide resolved

/**
* Checks if item is not an instance of Error
*/
private isNotError<T>(item: T | Error): item is T {
return item instanceof Error === false
}

private getClientDelegationInfo(
user: User,
): Promise<ClientDelegationInfo | null> {
Expand Down
Loading
Loading