Skip to content

Commit b24d853

Browse files
committed
feat: Return required claims in need_info error when possible
1 parent aee675b commit b24d853

24 files changed

Lines changed: 90 additions & 388 deletions

packages/uma/.componentsignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"Buffer",
77
"Error",
88
"EventEmitter",
9+
"ForbiddenHttpError",
910
"Map",
1011
"NodeJS.Dict",
1112
"Permission",

packages/uma/config/tickets/strategy/claim-elimination.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

packages/uma/src/credentials/Requirements.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/uma/src/dialog/BaseNegotiator.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { BadRequestHttpError, ForbiddenHttpError, HttpErrorClass, KeyValueStorage } from '@solid/community-server';
22
import { getLoggerFor } from 'global-logger-factory';
33
import { randomUUID } from 'node:crypto';
4-
import { ClaimSet } from '../credentials/ClaimSet';
54
import { Verifier } from '../credentials/verify/Verifier';
6-
import { NeedInfoError } from '../errors/NeedInfoError';
5+
import { NeedInfoError, RequiredClaim } from '../errors/NeedInfoError';
76
import { getOperationLogger } from '../logging/OperationLogger';
87
import { serializePolicyInstantiation } from '../logging/OperationSerializer';
98
import { TicketingStrategy } from '../ticketing/strategy/TicketingStrategy';
@@ -73,21 +72,18 @@ export class BaseNegotiator implements Negotiator {
7372
}
7473

7574
// ... on failure, deny if no solvable requirements
76-
this.denyRequest(ticket);
75+
this.denyRequest(ticket, resolved.value);
7776
}
7877

7978
// TODO:
80-
protected denyRequest(ticket: Ticket): never {
81-
const requiredClaims = ticket.required.map(req => Object.keys(req));
82-
if (requiredClaims.length === 0) throw new ForbiddenHttpError();
79+
protected denyRequest(ticket: Ticket, requirements: RequiredClaim[]): never {
80+
if (requirements.length === 0) throw new ForbiddenHttpError('Request denied');
8381

8482
// ... require more info otherwise
8583
const id = randomUUID();
8684
this.ticketStore.set(id, ticket);
8785
throw new NeedInfoError('Need more info to authorize request ...', id, {
88-
required_claims: {
89-
claim_token_format: requiredClaims,
90-
},
86+
required_claims: requirements,
9187
});
9288
}
9389

packages/uma/src/dialog/ContractNegotiator.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createErrorMessage, KeyValueStorage } from '@solid/community-server';
22
import { getLoggerFor } from 'global-logger-factory';
3-
import { Requirements } from '../credentials/Requirements';
43
import { Verifier } from '../credentials/verify/Verifier';
4+
import { RequiredClaim } from '../errors/NeedInfoError';
55
import { ContractManager } from '../policies/contracts/ContractManager';
66
import { TicketingStrategy } from '../ticketing/strategy/TicketingStrategy';
77
import { Ticket } from '../ticketing/Ticket';
@@ -70,7 +70,7 @@ export class ContractNegotiator extends BaseNegotiator {
7070
}
7171

7272
// ... on failure, deny if no solvable requirements
73-
this.denyRequest(ticket);
73+
this.denyRequest(ticket, result.value);
7474
}
7575

7676
/**
@@ -81,8 +81,8 @@ export class ContractNegotiator extends BaseNegotiator {
8181
* In case the ticket is not resolved,
8282
* the needed requirements will be returned as Failure.
8383
*/
84-
protected async toContract(ticket: Ticket): Promise<Result<ODRLContract, Requirements[]>> {
85-
let result : Result<ODRLContract, Requirements[]>;
84+
protected async toContract(ticket: Ticket): Promise<Result<ODRLContract, RequiredClaim[]>> {
85+
let result : Result<ODRLContract, RequiredClaim[]>;
8686
let contract: ODRLContract | undefined;
8787

8888
// Check contract availability

packages/uma/src/errors/NeedInfoError.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@ import { ForbiddenHttpError } from '@solid/community-server';
33
export type RedirectUserInfo = {
44
redirect_user: string
55
}
6+
7+
export type RequiredClaim = {
8+
claim_token_format?: string,
9+
claim_type?: string,
10+
friendly_name?: string,
11+
issuer?: string,
12+
name?: string,
13+
derivation_resource_id?: string,
14+
resource_scopes?: string[],
15+
}
16+
617
export type RequiredClaimsInfo = {
7-
required_claims: { claim_token_format: string[][] }
18+
required_claims: RequiredClaim[]
819
}
920

1021
/**

packages/uma/src/policies/authorizers/AllAuthorizer.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getLoggerFor } from 'global-logger-factory';
22
import { ClaimSet } from '../../credentials/ClaimSet';
3-
import { Requirements } from '../../credentials/Requirements';
43
import { Permission } from '../../views/Permission';
54
import { ANY_RESOURCE, ANY_SCOPE, Authorizer } from './Authorizer';
65

@@ -28,10 +27,4 @@ export class AllAuthorizer implements Authorizer {
2827

2928
return [{ resource_id: ANY_RESOURCE, resource_scopes: [ ANY_SCOPE ] }];
3029
}
31-
32-
/** @inheritdoc */
33-
public async credentials(permissions: Permission[]): Promise<Requirements[]> {
34-
this.logger.info(`Skipping credentials. ${JSON.stringify(permissions)}`);
35-
return [{}];
36-
}
3730
}
Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Requirements } from '../../credentials/Requirements';
21
import { ClaimSet } from '../../credentials/ClaimSet';
32
import { Permission } from '../../views/Permission';
43

@@ -17,18 +16,4 @@ export abstract class Authorizer {
1716
* @return {Promise<Permission[]>} - An Array of available Permissions.
1817
*/
1918
public abstract permissions(claims: ClaimSet, query?: Partial<Permission>[]): Promise<Permission[]>;
20-
21-
/**
22-
* Calculates the required Credentials to achieve a set of given Permissions.
23-
*
24-
* @param {Permissions[]} permissions - The requested Permissions.
25-
* @param {Requirements} query - An optional query to constrain the calculated Requirements.
26-
*
27-
* @return {Promise<Requirements>} An object containing ClaimDescriptions.
28-
*/
29-
public abstract credentials(permissions: Permission[], query?: Requirements): Promise<Requirements[]>;
30-
// TODO:
31-
// * @throws {ForbiddenHttpError} When no Credentials can be found (within the query limits)
32-
// * that would grant the requested Permissions.
33-
3419
}

packages/uma/src/policies/authorizers/NamespacedAuthorizer.ts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getLoggerFor } from 'global-logger-factory';
22
import { ClaimSet } from '../../credentials/ClaimSet';
3-
import { Requirements } from '../../credentials/Requirements';
43
import { RegistrationStore } from '../../util/RegistrationStore';
54
import { Permission } from '../../views/Permission';
65
import { Authorizer } from './Authorizer';
@@ -52,30 +51,6 @@ export class NamespacedAuthorizer implements Authorizer {
5251
return authorizer.permissions(claims, query);
5352
}
5453

55-
/** @inheritdoc */
56-
public async credentials(permissions: Permission[], query?: Requirements): Promise<Requirements[]> {
57-
this.logger.info(`Calculating credentials. ${JSON.stringify({ permissions, query })}`);
58-
59-
// No requirements if no requested permissions
60-
if (!permissions || permissions.length === 0) return [];
61-
62-
// Base namespace on first resource
63-
const ns = await this.findNamespace(permissions[0].resource_id);
64-
65-
// Check namespaces of other resources
66-
for (let i = 1; i < permissions.length; ++i) {
67-
if (await this.findNamespace(permissions[i].resource_id) !== ns) {
68-
this.logger.warn(`Cannot calculate credentials over multiple namespaces at once.`);
69-
return [];
70-
}
71-
}
72-
73-
// Find applicable authorizer
74-
const authorizer = (typeof ns === 'string' && this.authorizers[ns]) || this.fallback;
75-
76-
return authorizer.credentials(permissions, query);
77-
}
78-
7954
/**
8055
* Finds the applicable authorizer to use based on the input query.
8156
*/
Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getLoggerFor } from 'global-logger-factory';
22
import { ClaimSet } from '../../credentials/ClaimSet';
3-
import { Requirements } from '../../credentials/Requirements';
43
import { Permission } from '../../views/Permission';
54
import { Authorizer } from './Authorizer';
65

@@ -14,11 +13,4 @@ export class NoneAuthorizer implements Authorizer {
1413
public async permissions(claims: ClaimSet, query?: Partial<Permission>[]): Promise<Permission[]> {
1514
return [];
1615
}
17-
18-
/** @inheritdoc */
19-
public async credentials(permissions: Permission[], query?: Requirements): Promise<Requirements[]> {
20-
this.logger.info(`Skipping credentials. ${JSON.stringify({ permissions, query })}`);
21-
// throw new ForbiddenHttpError(); // TODO: indicating impossibility to RS would save roundtrip
22-
return [];
23-
}
2416
}

0 commit comments

Comments
 (0)