Skip to content

Commit f8e54e5

Browse files
authored
feat(sdk): sdk to use connect rpc calls (#596)
* feat: refactored access.ts http calls * feat: refactor api fqns calls * chore: fix roundtrip * fix: refactor api fqns calls types * chore: increase text coverage for rpc calls * fix: platformUrl guess from kasEndpoint * feat: call legacy api in case of rpc call failure for support with legacy servers
1 parent 94298b2 commit f8e54e5

25 files changed

+745
-467
lines changed

cli/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/cli.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,11 @@ export const handleArgs = (args: string[]) => {
367367
type: 'string',
368368
description: 'URL to non-default KAS instance (https://mykas.net)',
369369
})
370+
.option('platformUrl', {
371+
group: 'Server Endpoints:',
372+
type: 'string',
373+
description: 'Location of policy service and KAS (https://opentdf.demo)',
374+
})
370375
.option('oidcEndpoint', {
371376
group: 'Server Endpoints:',
372377
type: 'string',
@@ -585,6 +590,7 @@ export const handleArgs = (args: string[]) => {
585590
async (argv) => {
586591
log('DEBUG', 'Running inspect command');
587592
const ct = new OpenTDF({
593+
platformUrl: argv.platformUrl || guessPolicyUrl(argv),
588594
authProvider: new InvalidAuthProvider(),
589595
});
590596
try {
@@ -635,7 +641,7 @@ export const handleArgs = (args: string[]) => {
635641
},
636642
disableDPoP: !argv.dpop,
637643
policyEndpoint: guessedPolicyEndpoint,
638-
platformUrl: guessedPolicyEndpoint,
644+
platformUrl: argv.platformUrl || guessedPolicyEndpoint,
639645
});
640646
try {
641647
log('SILLY', `Initialized client`);
@@ -685,14 +691,16 @@ export const handleArgs = (args: string[]) => {
685691
log('DEBUG', 'Running encrypt command');
686692
const authProvider = await processAuth(argv);
687693
log('DEBUG', `Initialized auth provider ${JSON.stringify(authProvider)}`);
694+
const guessedPolicyEndpoint = guessPolicyUrl(argv);
688695

689696
const client = new OpenTDF({
690697
authProvider,
691698
defaultCreateOptions: {
692699
defaultKASEndpoint: argv.kasEndpoint,
693700
},
694701
disableDPoP: !argv.dpop,
695-
policyEndpoint: guessPolicyUrl(argv),
702+
policyEndpoint: guessedPolicyEndpoint,
703+
platformUrl: argv.platformUrl || guessedPolicyEndpoint,
696704
});
697705
try {
698706
log('SILLY', `Initialized client`);

lib/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"lint": "eslint ./src/**/*.ts ./tdf3/**/*.ts ./tests/**/*.ts",
7171
"prepack": "npm run build",
7272
"test": "npm run build && npm run test:with-server",
73+
"mock:platform": "npm run build && node dist/web/tests/server.js",
7374
"test:with-server": "node dist/web/tests/server.js & trap \"node dist/web/tests/stopServer.js\" EXIT; npm run test:mocha && npm run test:wtr && npm run test:browser && npm run coverage:merge",
7475
"test:browser": "npx webpack --config webpack.test.config.cjs && npx karma start karma.conf.cjs",
7576
"test:mocha": "c8 --exclude=\"dist/web/tests/**/*\" --report-dir=./coverage/mocha mocha 'dist/web/tests/mocha/**/*.spec.js' && npx c8 report --reporter=json --report-dir=./coverage/mocha",

lib/src/access.ts

Lines changed: 54 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
import { type AuthProvider } from './auth/auth.js';
2-
import {
3-
ConfigurationError,
4-
InvalidFileError,
5-
NetworkError,
6-
PermissionDeniedError,
7-
ServiceError,
8-
UnauthenticatedError,
9-
} from './errors.js';
10-
import { pemToCryptoPublicKey, validateSecureUrl } from './utils.js';
2+
import { ServiceError } from './errors.js';
3+
import { RewrapResponse } from './platform/kas/kas_pb.js';
4+
import { getPlatformUrlFromKasEndpoint, validateSecureUrl } from './utils.js';
5+
6+
import { fetchKeyAccessServers as fetchKeyAccessServersRpc } from './access/access-rpc.js';
7+
import { fetchKeyAccessServers as fetchKeyAccessServersLegacy } from './access/access-fetch.js';
8+
import { fetchWrappedKey as fetchWrappedKeysRpc } from './access/access-rpc.js';
9+
import { fetchWrappedKey as fetchWrappedKeysLegacy } from './access/access-fetch.js';
10+
import { fetchKasPubKey as fetchKasPubKeyRpc } from './access/access-rpc.js';
11+
import { fetchKasPubKey as fetchKasPubKeyLegacy } from './access/access-fetch.js';
1112

1213
export type RewrapRequest = {
1314
signedRequestToken: string;
1415
};
1516

16-
export type RewrapResponse = {
17-
metadata: Record<string, unknown>;
18-
entityWrappedKey: string;
19-
sessionPublicKey: string;
20-
schemaVersion: string;
21-
};
22-
2317
/**
2418
* Get a rewrapped access key to the document, if possible
2519
* @param url Key access server rewrap endpoint
@@ -29,59 +23,20 @@ export type RewrapResponse = {
2923
*/
3024
export async function fetchWrappedKey(
3125
url: string,
32-
requestBody: RewrapRequest,
33-
authProvider: AuthProvider,
34-
clientVersion: string
26+
signedRequestToken: string,
27+
authProvider: AuthProvider
3528
): Promise<RewrapResponse> {
36-
const req = await authProvider.withCreds({
37-
url,
38-
method: 'POST',
39-
headers: {
40-
'Content-Type': 'application/json',
41-
},
42-
body: JSON.stringify(requestBody),
43-
});
44-
45-
let response: Response;
46-
47-
try {
48-
response = await fetch(req.url, {
49-
method: req.method,
50-
mode: 'cors', // no-cors, *cors, same-origin
51-
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
52-
credentials: 'same-origin', // include, *same-origin, omit
53-
headers: req.headers,
54-
redirect: 'follow', // manual, *follow, error
55-
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
56-
body: req.body as BodyInit,
57-
});
58-
} catch (e) {
59-
throw new NetworkError(`unable to fetch wrapped key from [${url}]`, e);
60-
}
61-
62-
if (!response.ok) {
63-
switch (response.status) {
64-
case 400:
65-
throw new InvalidFileError(
66-
`400 for [${req.url}]: rewrap bad request [${await response.text()}]`
67-
);
68-
case 401:
69-
throw new UnauthenticatedError(`401 for [${req.url}]; rewrap auth failure`);
70-
case 403:
71-
throw new PermissionDeniedError(`403 for [${req.url}]; rewrap permission denied`);
72-
default:
73-
if (response.status >= 500) {
74-
throw new ServiceError(
75-
`${response.status} for [${req.url}]: rewrap failure due to service error [${await response.text()}]`
76-
);
77-
}
78-
throw new NetworkError(
79-
`${req.method} ${req.url} => ${response.status} ${response.statusText}`
80-
);
81-
}
82-
}
83-
84-
return response.json();
29+
const platformUrl = getPlatformUrlFromKasEndpoint(url);
30+
31+
return await tryPromisesUntilFirstSuccess(
32+
() => fetchWrappedKeysRpc(platformUrl, signedRequestToken, authProvider),
33+
() =>
34+
fetchWrappedKeysLegacy(
35+
url,
36+
{ signedRequestToken },
37+
authProvider
38+
) as unknown as Promise<RewrapResponse>
39+
);
8540
}
8641

8742
export type KasPublicKeyAlgorithm = 'ec:secp256r1' | 'rsa:2048';
@@ -145,7 +100,7 @@ export type KasPublicKeyInfo = {
145100
key: Promise<CryptoKey>;
146101
};
147102

148-
async function noteInvalidPublicKey(url: URL, r: Promise<CryptoKey>): Promise<CryptoKey> {
103+
export async function noteInvalidPublicKey(url: URL, r: Promise<CryptoKey>): Promise<CryptoKey> {
149104
try {
150105
return await r;
151106
} catch (e) {
@@ -160,49 +115,10 @@ export async function fetchKeyAccessServers(
160115
platformUrl: string,
161116
authProvider: AuthProvider
162117
): Promise<OriginAllowList> {
163-
let nextOffset = 0;
164-
const allServers = [];
165-
do {
166-
const req = await authProvider.withCreds({
167-
url: `${platformUrl}/key-access-servers?pagination.offset=${nextOffset}`,
168-
method: 'GET',
169-
headers: {
170-
'Content-Type': 'application/json',
171-
},
172-
});
173-
let response: Response;
174-
try {
175-
response = await fetch(req.url, {
176-
method: req.method,
177-
headers: req.headers,
178-
body: req.body as BodyInit,
179-
mode: 'cors',
180-
cache: 'no-cache',
181-
credentials: 'same-origin',
182-
redirect: 'follow',
183-
referrerPolicy: 'no-referrer',
184-
});
185-
} catch (e) {
186-
throw new NetworkError(`unable to fetch kas list from [${req.url}]`, e);
187-
}
188-
// if we get an error from the kas registry, throw an error
189-
if (!response.ok) {
190-
throw new ServiceError(
191-
`unable to fetch kas list from [${req.url}], status: ${response.status}`
192-
);
193-
}
194-
const { keyAccessServers = [], pagination = {} } = await response.json();
195-
allServers.push(...keyAccessServers);
196-
nextOffset = pagination.nextOffset || 0;
197-
} while (nextOffset > 0);
198-
199-
const serverUrls = allServers.map((server) => server.uri);
200-
// add base platform kas
201-
if (!serverUrls.includes(`${platformUrl}/kas`)) {
202-
serverUrls.push(`${platformUrl}/kas`);
203-
}
204-
205-
return new OriginAllowList(serverUrls, false);
118+
return await tryPromisesUntilFirstSuccess(
119+
() => fetchKeyAccessServersRpc(platformUrl, authProvider),
120+
() => fetchKeyAccessServersLegacy(platformUrl, authProvider)
121+
);
206122
}
207123

208124
/**
@@ -217,64 +133,10 @@ export async function fetchKasPubKey(
217133
kasEndpoint: string,
218134
algorithm?: KasPublicKeyAlgorithm
219135
): Promise<KasPublicKeyInfo> {
220-
if (!kasEndpoint) {
221-
throw new ConfigurationError('KAS definition not found');
222-
}
223-
// Logs insecure KAS. Secure is enforced in constructor
224-
validateSecureUrl(kasEndpoint);
225-
226-
// Parse kasEndpoint to URL, then append to its path and update its query parameters
227-
let pkUrlV2: URL;
228-
try {
229-
pkUrlV2 = new URL(kasEndpoint);
230-
} catch (e) {
231-
throw new ConfigurationError(`KAS definition invalid: [${kasEndpoint}]`, e);
232-
}
233-
if (!pkUrlV2.pathname.endsWith('kas_public_key')) {
234-
if (!pkUrlV2.pathname.endsWith('/')) {
235-
pkUrlV2.pathname += '/';
236-
}
237-
pkUrlV2.pathname += 'v2/kas_public_key';
238-
}
239-
pkUrlV2.searchParams.set('algorithm', algorithm || 'rsa:2048');
240-
if (!pkUrlV2.searchParams.get('v')) {
241-
pkUrlV2.searchParams.set('v', '2');
242-
}
243-
244-
let kasPubKeyResponseV2: Response;
245-
try {
246-
kasPubKeyResponseV2 = await fetch(pkUrlV2);
247-
} catch (e) {
248-
throw new NetworkError(`unable to fetch public key from [${pkUrlV2}]`, e);
249-
}
250-
if (!kasPubKeyResponseV2.ok) {
251-
switch (kasPubKeyResponseV2.status) {
252-
case 404:
253-
throw new ConfigurationError(`404 for [${pkUrlV2}]`);
254-
case 401:
255-
throw new UnauthenticatedError(`401 for [${pkUrlV2}]`);
256-
case 403:
257-
throw new PermissionDeniedError(`403 for [${pkUrlV2}]`);
258-
default:
259-
throw new NetworkError(
260-
`${pkUrlV2} => ${kasPubKeyResponseV2.status} ${kasPubKeyResponseV2.statusText}`
261-
);
262-
}
263-
}
264-
const jsonContent = await kasPubKeyResponseV2.json();
265-
const { publicKey, kid }: KasPublicKeyInfo = jsonContent;
266-
if (!publicKey) {
267-
throw new NetworkError(
268-
`invalid response from public key endpoint [${JSON.stringify(jsonContent)}]`
269-
);
270-
}
271-
return {
272-
key: noteInvalidPublicKey(pkUrlV2, pemToCryptoPublicKey(publicKey)),
273-
publicKey,
274-
url: kasEndpoint,
275-
algorithm: algorithm || 'rsa:2048',
276-
...(kid && { kid }),
277-
};
136+
return await tryPromisesUntilFirstSuccess(
137+
() => fetchKasPubKeyRpc(kasEndpoint, algorithm),
138+
() => fetchKasPubKeyLegacy(kasEndpoint, algorithm)
139+
);
278140
}
279141

280142
const origin = (u: string): string => {
@@ -301,3 +163,25 @@ export class OriginAllowList {
301163
return this.origins.includes(origin(url));
302164
}
303165
}
166+
167+
/**
168+
* Tries two promise-returning functions in order and returns the first successful result.
169+
* If both fail, throws the error from the second.
170+
* @param first First function returning a promise to try.
171+
* @param second Second function returning a promise to try if the first fails.
172+
*/
173+
async function tryPromisesUntilFirstSuccess<T>(
174+
first: () => Promise<T>,
175+
second: () => Promise<T>
176+
): Promise<T> {
177+
try {
178+
return await first();
179+
} catch (e1) {
180+
console.info('v2 request error', e1);
181+
try {
182+
return await second();
183+
} catch (err) {
184+
throw err;
185+
}
186+
}
187+
}

0 commit comments

Comments
 (0)