|
1 | 1 | import { assert, expect } from 'chai'; |
| 2 | +import sinon from 'sinon'; |
2 | 3 | import { Client as TDF } from '../../tdf3/src/index.js'; |
3 | 4 | import { DecoratedReadableStream } from '../../tdf3/src/client/DecoratedReadableStream.js'; |
| 5 | +import { findEntryInCache } from '../../tdf3/src/client/index.js'; |
| 6 | +import { getMocks } from '../mocks/index.js'; |
| 7 | +import { Algorithm, Value } from '../../src/platform/policy/objects_pb.js'; |
| 8 | +import { create } from '@bufbuild/protobuf'; |
| 9 | +import { GetAttributeValuesByFqnsResponseSchema } from '../../src/platform/policy/attributes/attributes_pb.js'; |
| 10 | +import { base64 } from '../../src/encodings/index.js'; |
| 11 | +import { Attribute } from 'src/policy/attributes.js'; |
4 | 12 |
|
5 | 13 | describe('client wrapper tests', function () { |
6 | 14 | it('client params safe from updating', function () { |
@@ -132,6 +140,178 @@ describe('client wrapper tests', function () { |
132 | 140 | assert.ok(expected); |
133 | 141 | } |
134 | 142 | }); |
| 143 | + |
| 144 | + it('encrypt autoconfigure hydrates fqns via getAttributeValuesByFqns', async function () { |
| 145 | + const Mocks = getMocks(); |
| 146 | + const authProvider = { |
| 147 | + updateClientPublicKey: async () => {}, |
| 148 | + withCreds: async (httpReq: TDF.HttpRequest) => ({ |
| 149 | + ...httpReq, |
| 150 | + headers: { ...httpReq.headers, Authorization: 'Bearer dummy-auth-token' }, |
| 151 | + }), |
| 152 | + }; |
| 153 | + |
| 154 | + const kasValueUri = 'https://kas.value.example/kas'; |
| 155 | + const kasAttributeUri = 'https://kas.attribute.example/kas'; |
| 156 | + |
| 157 | + const attributeValueFqn = 'http://example.com/attr/value-keys/value/one'; |
| 158 | + const attributeOnlyFqn = 'http://example.com/attr/attr-keys/value/two'; |
| 159 | + |
| 160 | + const valueWithAttribute: Value = { |
| 161 | + $typeName: 'policy.Value', |
| 162 | + fqn: attributeValueFqn, |
| 163 | + kasKeys: [ |
| 164 | + { |
| 165 | + $typeName: 'policy.SimpleKasKey', |
| 166 | + kasId: 'kas-value-1', |
| 167 | + kasUri: kasValueUri, |
| 168 | + publicKey: { |
| 169 | + $typeName: 'policy.SimpleKasPublicKey', |
| 170 | + pem: Mocks.kasPublicKey, |
| 171 | + kid: 'value-kid-1', |
| 172 | + algorithm: Algorithm.RSA_2048, |
| 173 | + }, |
| 174 | + }, |
| 175 | + ], |
| 176 | + id: 'value-id-1', |
| 177 | + attribute: { |
| 178 | + $typeName: 'policy.Attribute', |
| 179 | + id: 'attr-id-value', |
| 180 | + name: 'value-keys', |
| 181 | + fqn: 'http://example.com/attr/value-keys', |
| 182 | + rule: 0, |
| 183 | + values: [], |
| 184 | + grants: [], |
| 185 | + kasKeys: [], |
| 186 | + namespace: { |
| 187 | + $typeName: 'policy.Namespace', |
| 188 | + id: 'ns-id-value', |
| 189 | + name: 'example.com', |
| 190 | + fqn: 'http://example.com', |
| 191 | + grants: [], |
| 192 | + kasKeys: [], |
| 193 | + rootCerts: [], |
| 194 | + }, |
| 195 | + }, |
| 196 | + value: 'one', |
| 197 | + grants: [], |
| 198 | + active: true, |
| 199 | + subjectMappings: [], |
| 200 | + resourceMappings: [], |
| 201 | + obligations: [], |
| 202 | + }; |
| 203 | + |
| 204 | + const attributeOnly: Attribute = { |
| 205 | + $typeName: 'policy.Attribute', |
| 206 | + id: 'attr-id-attr', |
| 207 | + name: 'attr-keys', |
| 208 | + fqn: 'http://example.com/attr/attr-keys', |
| 209 | + rule: 0, |
| 210 | + values: [], |
| 211 | + grants: [], |
| 212 | + kasKeys: [ |
| 213 | + { |
| 214 | + $typeName: 'policy.SimpleKasKey', |
| 215 | + kasId: 'kas-attr-1', |
| 216 | + kasUri: kasAttributeUri, |
| 217 | + publicKey: { |
| 218 | + $typeName: 'policy.SimpleKasPublicKey', |
| 219 | + pem: Mocks.kasPublicKey, |
| 220 | + kid: 'attr-kid-1', |
| 221 | + algorithm: Algorithm.RSA_2048, |
| 222 | + }, |
| 223 | + }, |
| 224 | + ], |
| 225 | + namespace: { |
| 226 | + $typeName: 'policy.Namespace', |
| 227 | + id: 'ns-id-attr', |
| 228 | + name: 'example.com', |
| 229 | + fqn: 'http://example.com', |
| 230 | + grants: [], |
| 231 | + kasKeys: [], |
| 232 | + rootCerts: [], |
| 233 | + }, |
| 234 | + }; |
| 235 | + |
| 236 | + const getAttributeValuesByFqnsResponse = create(GetAttributeValuesByFqnsResponseSchema, { |
| 237 | + fqnAttributeValues: { |
| 238 | + [attributeValueFqn]: { |
| 239 | + value: valueWithAttribute, |
| 240 | + attribute: valueWithAttribute.attribute, |
| 241 | + }, |
| 242 | + [attributeOnlyFqn]: { |
| 243 | + attribute: attributeOnly, |
| 244 | + }, |
| 245 | + }, |
| 246 | + }); |
| 247 | + |
| 248 | + const fetchStub = sinon.stub(globalThis, 'fetch').callsFake(async (input) => { |
| 249 | + const url = typeof input === 'string' ? input : input.toString(); |
| 250 | + if (url.includes('GetAttributeValuesByFqns')) { |
| 251 | + return new Response(JSON.stringify(getAttributeValuesByFqnsResponse), { |
| 252 | + status: 200, |
| 253 | + headers: { 'Content-Type': 'application/json' }, |
| 254 | + }); |
| 255 | + } |
| 256 | + throw new Error(`unexpected fetch: ${url}`); |
| 257 | + }); |
| 258 | + |
| 259 | + const client = new TDF.Client({ |
| 260 | + kasEndpoint: 'https://kas.default.example/kas', |
| 261 | + clientId: 'id', |
| 262 | + dpopKeys: Mocks.entityKeyPair(), |
| 263 | + authProvider, |
| 264 | + platformUrl: 'http://example.com', |
| 265 | + }); |
| 266 | + |
| 267 | + const encryptParams = { |
| 268 | + ...new TDF.EncryptParamsBuilder().withStringSource('hello world').withAutoconfigure().build(), |
| 269 | + scope: { |
| 270 | + attributes: [attributeValueFqn, attributeOnlyFqn], |
| 271 | + }, |
| 272 | + }; |
| 273 | + |
| 274 | + try { |
| 275 | + const stream = await client.encrypt(encryptParams); |
| 276 | + assert.ok(stream); |
| 277 | + assert.equal(fetchStub.callCount, 1, 'fetch should only be called for FQN hydration'); |
| 278 | + |
| 279 | + const cachedValueKey = findEntryInCache( |
| 280 | + client.kasKeyInfoCache, |
| 281 | + kasValueUri, |
| 282 | + 'rsa:2048', |
| 283 | + 'value-kid-1' |
| 284 | + ); |
| 285 | + const cachedAttributeKey = findEntryInCache( |
| 286 | + client.kasKeyInfoCache, |
| 287 | + kasAttributeUri, |
| 288 | + 'rsa:2048', |
| 289 | + 'attr-kid-1' |
| 290 | + ); |
| 291 | + assert(cachedValueKey !== null, 'value-level key should be cached'); |
| 292 | + assert(cachedAttributeKey !== null, 'attribute-level key should be cached'); |
| 293 | + const policy = JSON.parse(base64.decode(stream.manifest.encryptionInformation.policy)); |
| 294 | + const dataAttributes = policy?.body?.dataAttributes ?? []; |
| 295 | + assert.deepEqual( |
| 296 | + dataAttributes.map((attr: { attribute: string }) => attr.attribute).sort(), |
| 297 | + [attributeValueFqn, attributeOnlyFqn].sort(), |
| 298 | + 'policy should include both attributes' |
| 299 | + ); |
| 300 | + |
| 301 | + const keyAccess = stream.manifest.encryptionInformation.keyAccess; |
| 302 | + assert.equal(keyAccess.length, 2, 'manifest should include both key access objects'); |
| 303 | + assert.deepInclude( |
| 304 | + keyAccess.map((kao) => ({ url: kao.url, kid: kao.kid })), |
| 305 | + { url: kasValueUri, kid: 'value-kid-1' } |
| 306 | + ); |
| 307 | + assert.deepInclude( |
| 308 | + keyAccess.map((kao) => ({ url: kao.url, kid: kao.kid })), |
| 309 | + { url: kasAttributeUri, kid: 'attr-kid-1' } |
| 310 | + ); |
| 311 | + } finally { |
| 312 | + fetchStub.restore(); |
| 313 | + } |
| 314 | + }); |
135 | 315 | }); |
136 | 316 |
|
137 | 317 | describe('tdf stream tests', function () { |
|
0 commit comments