Skip to content

Commit 12d7835

Browse files
committed
refactor: Reduce negotiator duplication
1 parent 43c1bce commit 12d7835

File tree

2 files changed

+91
-148
lines changed

2 files changed

+91
-148
lines changed

packages/uma/src/dialog/BaseNegotiator.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ export class BaseNegotiator implements Negotiator {
8080
}
8181

8282
// ... on failure, deny if no solvable requirements
83+
this.denyRequest(ticket);
84+
}
85+
86+
// TODO:
87+
protected denyRequest(ticket: Ticket): never {
8388
const requiredClaims = ticket.required.map(req => Object.keys(req));
8489
if (requiredClaims.length === 0) throw new ForbiddenHttpError();
8590

@@ -101,7 +106,7 @@ export class BaseNegotiator implements Negotiator {
101106
*
102107
* @returns The Ticket describing the dialog at hand.
103108
*/
104-
private async getTicket(input: DialogInput): Promise<Ticket> {
109+
protected async getTicket(input: DialogInput): Promise<Ticket> {
105110
const { ticket, permissions } = input;
106111

107112
if (ticket) {
@@ -128,7 +133,7 @@ export class BaseNegotiator implements Negotiator {
128133
*
129134
* @returns An updated Ticket in which the Credentials have been validated.
130135
*/
131-
private async processCredentials(input: DialogInput, ticket: Ticket): Promise<Ticket> {
136+
protected async processCredentials(input: DialogInput, ticket: Ticket): Promise<Ticket> {
132137
const { claim_token: token, claim_token_format: format } = input;
133138

134139
if (token || format) {
@@ -152,7 +157,7 @@ export class BaseNegotiator implements Negotiator {
152157
* @throws An Error constructed with the provided constructor with the
153158
* provided message
154159
*/
155-
private error(constructor: HttpErrorClass, message: string): never {
160+
protected error(constructor: HttpErrorClass, message: string): never {
156161
this.logger.warn(message);
157162
throw new constructor(message);
158163
}
Lines changed: 83 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
1-
import {
2-
BadRequestHttpError,
3-
createErrorMessage,
4-
ForbiddenHttpError,
5-
getLoggerFor,
6-
HttpErrorClass,
7-
KeyValueStorage
8-
} from '@solid/community-server';
9-
import { v4 } from 'uuid';
10-
import { AccessToken, Permission, Requirements } from '..';
1+
import { createErrorMessage, getLoggerFor, KeyValueStorage } from '@solid/community-server';
2+
import { Requirements } from '../credentials/Requirements';
113
import { Verifier } from '../credentials/verify/Verifier';
12-
import { NeedInfoError } from '../errors/NeedInfoError';
134
import { ContractManager } from '../policies/contracts/ContractManager';
145
import { TicketingStrategy } from '../ticketing/strategy/TicketingStrategy';
156
import { Ticket } from '../ticketing/Ticket';
7+
import { AccessToken } from '../tokens/AccessToken';
168
import { TokenFactory } from '../tokens/TokenFactory';
179
import { processRequestPermission, switchODRLandCSSPermission } from '../util/rdf/RequestProcessing';
1810
import { Result, Success } from '../util/Result';
1911
import { reType } from '../util/ReType';
2012
import { convertStringOrJsonLdIdentifierToString, ODRLContract, StringOrJsonLdIdentifier } from '../views/Contract';
13+
import { Permission } from '../views/Permission';
14+
import { BaseNegotiator } from './BaseNegotiator';
2115
import { DialogInput } from './Input';
22-
import { Negotiator } from './Negotiator';
2316
import { DialogOutput } from './Output';
2417

25-
2618
/**
2719
* A mocked Negotiator for demonstration purposes to display contract negotiation
2820
*/
29-
export class ContractNegotiator implements Negotiator {
21+
export class ContractNegotiator extends BaseNegotiator {
3022
protected readonly logger = getLoggerFor(this);
3123

3224
// protected readonly operationLogger = getOperationLogger();
@@ -45,7 +37,8 @@ export class ContractNegotiator implements Negotiator {
4537
protected ticketingStrategy: TicketingStrategy,
4638
protected tokenFactory: TokenFactory,
4739
) {
48-
this.logger.warn('The Contract Negotiator is for demonstration purposes only! DO NOT USE THIS IN PRODUCTION !!!')
40+
super(verifier, ticketStore, ticketingStrategy, tokenFactory);
41+
this.logger.warn('The Contract Negotiator is for demonstration purposes only! DO NOT USE THIS IN PRODUCTION !!!');
4942
}
5043

5144
/**
@@ -68,12 +61,33 @@ export class ContractNegotiator implements Negotiator {
6861
const updatedTicket = await this.processCredentials(input, ticket);
6962
this.logger.debug(`Processed credentials ${JSON.stringify(updatedTicket)}`);
7063

71-
let result : Result<ODRLContract, Requirements[]>
64+
// TODO:
65+
const result = await this.toContract(updatedTicket);
66+
67+
if (result.success) {
68+
// TODO:
69+
return this.toResponse(result.value);
70+
}
71+
72+
// ... on failure, deny if no solvable requirements
73+
this.denyRequest(ticket);
74+
}
75+
76+
/**
77+
* Generates a contract based on the given ticket,
78+
* or returns one previously made,
79+
* and returns it as Success.
80+
*
81+
* In case the ticket is not resolved,
82+
* the needed requirements will be returned as Failure.
83+
*/
84+
protected async toContract(ticket: Ticket): Promise<Result<ODRLContract, Requirements[]>> {
85+
let result : Result<ODRLContract, Requirements[]>;
7286
let contract: ODRLContract | undefined;
7387

7488
// Check contract availability
7589
try {
76-
contract = this.contractManager.findContract(updatedTicket)
90+
contract = this.contractManager.findContract(ticket)
7791
} catch (e) {
7892
this.logger.debug(`Error: ${createErrorMessage(e)}`);
7993
}
@@ -86,7 +100,7 @@ export class ContractNegotiator implements Negotiator {
86100
} else {
87101
this.logger.debug(`No existing contract discovered. Attempting to resolve ticket.`)
88102

89-
const resolved = await this.ticketingStrategy.resolveTicket(updatedTicket);
103+
const resolved = await this.ticketingStrategy.resolveTicket(ticket);
90104
this.logger.debug(`Resolved ticket. ${JSON.stringify(resolved)}`);
91105

92106
if (resolved.success) {
@@ -103,144 +117,68 @@ export class ContractNegotiator implements Negotiator {
103117
result = resolved
104118
}
105119
}
120+
return result;
121+
}
106122

107-
if (result.success) {
108-
let contract : ODRLContract = result.value
109-
110-
this.logger.debug(JSON.stringify(contract, null, 2))
111-
112-
// todo: set resource scopes according to contract!
113-
// Using a map first as the contract could return multiple entries for the same resource_id
114-
// as it only allows 1 action per entry.
115-
const permissionMap: Record<string, Permission> = {};
116-
for (const permission of contract.permission) {
117-
const id = convertStringOrJsonLdIdentifierToString(permission.target as StringOrJsonLdIdentifier);
118-
if (!permissionMap[id]) {
119-
permissionMap[id] = {
120-
// We do not accept AssetCollections as targets of an UMA access request formatted as an ODRL request!
121-
resource_id: id,
122-
resource_scopes: [ // mapping from ODRL to internal CSS read permission
123-
switchODRLandCSSPermission(convertStringOrJsonLdIdentifierToString(permission.action))
124-
]
125-
};
126-
} else {
127-
permissionMap[id].resource_scopes.push(
123+
// TODO: name
124+
protected async toResponse(contract: ODRLContract): Promise<DialogOutput> {
125+
126+
this.logger.debug(JSON.stringify(contract, null, 2))
127+
128+
// todo: set resource scopes according to contract!
129+
// Using a map first as the contract could return multiple entries for the same resource_id
130+
// as it only allows 1 action per entry.
131+
const permissionMap: Record<string, Permission> = {};
132+
for (const permission of contract.permission) {
133+
const id = convertStringOrJsonLdIdentifierToString(permission.target as StringOrJsonLdIdentifier);
134+
if (!permissionMap[id]) {
135+
permissionMap[id] = {
136+
// We do not accept AssetCollections as targets of an UMA access request formatted as an ODRL request!
137+
resource_id: id,
138+
resource_scopes: [ // mapping from ODRL to internal CSS read permission
128139
switchODRLandCSSPermission(convertStringOrJsonLdIdentifierToString(permission.action))
129-
);
130-
}
140+
]
141+
};
142+
} else {
143+
permissionMap[id].resource_scopes.push(
144+
switchODRLandCSSPermission(convertStringOrJsonLdIdentifierToString(permission.action))
145+
);
131146
}
132-
let permissions: Permission[] = Object.values(permissionMap);
133-
this.logger.debug(`granting permissions: ${JSON.stringify(permissions)}`);
134-
135-
// Create response
136-
const tokenContents: AccessToken = { permissions, contract }
137-
138-
this.logger.debug(`resolved result ${JSON.stringify(result)}`);
139-
140-
const { token, tokenType } = await this.tokenFactory.serialize(tokenContents);
141-
142-
this.logger.debug(`Minted token ${JSON.stringify(token)}`);
143-
144-
// TODO:: test logging
145-
// this.operationLogger.addLogEntry(serializePolicyInstantiation())
146-
147-
// Store created instantiated policy (above contract variable) in the pod storage as an instantiated policy
148-
// todo: dynamic URL
149-
// todo: fix instantiated from url
150-
// contract['http://www.w3.org/ns/prov#wasDerivedFrom'] = [ 'urn:ucp:be-gov:policy:d81b8118-af99-4ab3-b2a7-63f8477b6386 ']
151-
// TODO: test-private error: this container does not exist and unauth does not have append perms
152-
const instantiatedPolicyContainer = 'http://localhost:3000/ruben/settings/policies/instantiated/';
153-
const policyCreationResponse = await fetch(instantiatedPolicyContainer, {
154-
method: 'POST',
155-
headers: { 'content-type': 'application/ld+json' },
156-
body: JSON.stringify(contract, null, 2)
157-
});
158-
159-
if (policyCreationResponse.status !== 201) { this.logger.warn('Adding a policy did not succeed...') }
160-
161-
// TODO:: dynamic contract link to stored signed contract.
162-
// If needed we can always embed here directly into the return JSON
163-
return ({
164-
access_token: token,
165-
token_type: tokenType,
166-
});
167147
}
148+
let permissions: Permission[] = Object.values(permissionMap);
149+
this.logger.debug(`granting permissions: ${JSON.stringify(permissions)}`);
168150

169-
// ... on failure, deny if no solvable requirements
170-
const requiredClaims = ticket.required.map(req => Object.keys(req));
171-
if (requiredClaims.length === 0) throw new ForbiddenHttpError();
172-
173-
// ... require more info otherwise
174-
const id = v4();
175-
this.ticketStore.set(id, ticket);
176-
throw new NeedInfoError('Need more info to authorize request ...', id, {
177-
required_claims: {
178-
claim_token_format: requiredClaims,
179-
},
180-
});
181-
}
151+
// Create response
152+
const tokenContents: AccessToken = { permissions, contract }
182153

183-
/**
184-
* Helper function that retrieves a Ticket from the TicketStore if it exists,
185-
* or initializes a new one otherwise.
186-
*
187-
* @param input - The input of the negotiation dialog.
188-
*
189-
* @returns The Ticket describing the dialog at hand.
190-
*/
191-
private async getTicket(input: DialogInput): Promise<Ticket> {
192-
const { ticket, permission, permissions } = input;
193-
194-
if (ticket) {
195-
const stored = await this.ticketStore.get(ticket);
196-
if (!stored) this.error(BadRequestHttpError, 'The provided ticket is not valid.');
154+
this.logger.debug(`resolved result ${JSON.stringify(contract)}`);
197155

198-
await this.ticketStore.delete(ticket);
199-
return stored;
200-
}
156+
const { token, tokenType } = await this.tokenFactory.serialize(tokenContents);
201157

202-
if (!permissions) {
203-
this.error(BadRequestHttpError, 'A token request without existing ticket should include requested permissions.');
204-
}
158+
this.logger.debug(`Minted token ${JSON.stringify(token)}`);
205159

206-
return await this.ticketingStrategy.initializeTicket(permissions);
207-
}
160+
// TODO:: test logging
161+
// this.operationLogger.addLogEntry(serializePolicyInstantiation())
208162

209-
/**
210-
* Helper function that checks for the presence of Credentials and, if present,
211-
* verifies them and validates them in context of the provided Ticket.
212-
*
213-
* @param input - The input of the negotiation dialog.
214-
* @param ticket - The Ticket against which to validate any Credentials.
215-
*
216-
* @returns An updated Ticket in which the Credentials have been validated.
217-
*/
218-
private async processCredentials(input: DialogInput, ticket: Ticket): Promise<Ticket> {
219-
const { claim_token: token, claim_token_format: format } = input;
220-
221-
if (token || format) {
222-
if (!token) this.error(BadRequestHttpError, 'Request with a "claim_token_format" must contain a "claim_token".');
223-
if (!format) this.error(BadRequestHttpError, 'Request with a "claim_token" must contain a "claim_token_format".');
224-
225-
const claims = await this.verifier.verify({ token, format });
226-
227-
return await this.ticketingStrategy.validateClaims(ticket, claims);
228-
}
163+
// Store created instantiated policy (above contract variable) in the pod storage as an instantiated policy
164+
// todo: dynamic URL
165+
// todo: fix instantiated from url
166+
// contract['http://www.w3.org/ns/prov#wasDerivedFrom'] = [ 'urn:ucp:be-gov:policy:d81b8118-af99-4ab3-b2a7-63f8477b6386 ']
167+
// TODO: test-private error: this container does not exist and unauth does not have append perms
168+
const instantiatedPolicyContainer = 'http://localhost:3000/ruben/settings/policies/instantiated/';
169+
const policyCreationResponse = await fetch(instantiatedPolicyContainer, {
170+
method: 'POST',
171+
headers: { 'content-type': 'application/ld+json' },
172+
body: JSON.stringify(contract, null, 2)
173+
});
229174

230-
return ticket;
231-
}
175+
if (policyCreationResponse.status !== 201) { this.logger.warn('Adding a policy did not succeed...') }
232176

233-
/**
234-
* Logs and throws an error
235-
*
236-
* @param {HttpErrorClass} constructor - The error constructor.
237-
* @param {string} message - The error message.
238-
*
239-
* @throws An Error constructed with the provided constructor with the
240-
* provided message
241-
*/
242-
private error(constructor: HttpErrorClass, message: string): never {
243-
this.logger.warn(message);
244-
throw new constructor(message);
177+
// TODO:: dynamic contract link to stored signed contract.
178+
// If needed we can always embed here directly into the return JSON
179+
return ({
180+
access_token: token,
181+
token_type: tokenType,
182+
});
245183
}
246184
}

0 commit comments

Comments
 (0)