|
3 | 3 | assertCompositionSuccess, |
4 | 4 | composeAsFed2Subgraphs, |
5 | 5 | } from "./testHelper"; |
6 | | -import {InterfaceType} from "@apollo/federation-internals"; |
| 6 | +import {InterfaceType, ObjectType} from "@apollo/federation-internals"; |
7 | 7 |
|
8 | 8 | describe('authorization tests', () => { |
9 | 9 | describe("@requires", () => { |
@@ -474,6 +474,55 @@ describe('authorization tests', () => { |
474 | 474 | ); |
475 | 475 | }) |
476 | 476 |
|
| 477 | + it('verifies access control on chain of requires', () => { |
| 478 | + const subgraph1 = { |
| 479 | + name: 'Subgraph1', |
| 480 | + url: 'https://Subgraph1', |
| 481 | + typeDefs: gql` |
| 482 | + type Query { |
| 483 | + t: T |
| 484 | + } |
| 485 | +
|
| 486 | + type T @key(fields: "id") { |
| 487 | + id: ID |
| 488 | + extra: String @external |
| 489 | + requiresExtra: String @requires(fields: "extra") |
| 490 | + } |
| 491 | + ` |
| 492 | + } |
| 493 | + |
| 494 | + const subgraph2 = { |
| 495 | + name: 'Subgraph2', |
| 496 | + url: 'https://Subgraph2', |
| 497 | + typeDefs: gql` |
| 498 | + type T @key(fields: "id") { |
| 499 | + id: ID |
| 500 | + secret: String @external |
| 501 | + extra: String @requires(fields: "secret") |
| 502 | + } |
| 503 | + ` |
| 504 | + } |
| 505 | + |
| 506 | + const subgraph3 = { |
| 507 | + name: 'Subgraph3', |
| 508 | + url: 'https://Subgraph3', |
| 509 | + typeDefs: gql` |
| 510 | + type T @key(fields: "id") { |
| 511 | + id: ID |
| 512 | + secret: String @authenticated @inaccessible |
| 513 | + } |
| 514 | + ` |
| 515 | + } |
| 516 | + |
| 517 | + const result = composeAsFed2Subgraphs([subgraph1, subgraph2, subgraph3]); |
| 518 | + expect(result.schema).toBeUndefined(); |
| 519 | + expect(result.errors?.length).toBe(1); |
| 520 | + expect(result.errors?.[0].message).toBe( |
| 521 | + '[Subgraph2] Field "T.extra" does not specify necessary @authenticated, @requiresScopes and/or ' + |
| 522 | + '@policy auth requirements to access the transitive field "T.secret" data from @requires selection set.' |
| 523 | + ); |
| 524 | + }) |
| 525 | + |
477 | 526 | it('works with chain of requires', () => { |
478 | 527 | const subgraph1 = { |
479 | 528 | name: 'Subgraph1', |
@@ -517,6 +566,140 @@ describe('authorization tests', () => { |
517 | 566 | const result = composeAsFed2Subgraphs([subgraph1, subgraph2, subgraph3]); |
518 | 567 | assertCompositionSuccess(result); |
519 | 568 | }) |
| 569 | + |
| 570 | + it('works with interface objects', () => { |
| 571 | + const subgraph1 = { |
| 572 | + name: 'Subgraph1', |
| 573 | + url: 'https://Subgraph1', |
| 574 | + typeDefs: gql` |
| 575 | + type Query { |
| 576 | + i: I |
| 577 | + } |
| 578 | +
|
| 579 | + type I @interfaceObject @key(fields: "id") { |
| 580 | + id: ID! |
| 581 | + extra: String @external |
| 582 | + requiresExtra: String @requires(fields: "extra") @authenticated |
| 583 | + } |
| 584 | + ` |
| 585 | + } |
| 586 | + |
| 587 | + const subgraph2 = { |
| 588 | + name: 'Subgraph2', |
| 589 | + url: 'https://Subgraph2', |
| 590 | + typeDefs: gql` |
| 591 | + interface I @key(fields: "id") { |
| 592 | + id: ID! |
| 593 | + extra: String |
| 594 | + } |
| 595 | +
|
| 596 | + type T @key(fields: "id") { |
| 597 | + id: ID |
| 598 | + extra: String @authenticated |
| 599 | + } |
| 600 | + ` |
| 601 | + } |
| 602 | + |
| 603 | + const result = composeAsFed2Subgraphs([subgraph1, subgraph2]); |
| 604 | + assertCompositionSuccess(result); |
| 605 | + const interfaceI = result.schema.type("I") as InterfaceType; |
| 606 | + expect(interfaceI).toBeDefined(); |
| 607 | + const requiresExtraField = interfaceI.field('requiresExtra'); |
| 608 | + expect(requiresExtraField).toBeDefined(); |
| 609 | + expect(requiresExtraField?.appliedDirectivesOf("authenticated")).toBeDefined(); |
| 610 | + }) |
| 611 | + |
| 612 | + it('works with interface object chains', () => { |
| 613 | + const subgraph1 = { |
| 614 | + name: 'Subgraph1', |
| 615 | + url: 'https://Subgraph1', |
| 616 | + typeDefs: gql` |
| 617 | + type Query { |
| 618 | + i: I |
| 619 | + } |
| 620 | +
|
| 621 | + type I @interfaceObject @key(fields: "id") { |
| 622 | + id: ID! |
| 623 | + extra: String @external |
| 624 | + requiresExtra: String @requires(fields: "extra") @authenticated |
| 625 | + } |
| 626 | + ` |
| 627 | + } |
| 628 | + |
| 629 | + const subgraph2 = { |
| 630 | + name: 'Subgraph2', |
| 631 | + url: 'https://Subgraph2', |
| 632 | + typeDefs: gql` |
| 633 | + type I @interfaceObject @key(fields: "id") { |
| 634 | + id: ID! |
| 635 | + secret: String @external |
| 636 | + extra: String @requires(fields: "secret") @authenticated |
| 637 | + } |
| 638 | + ` |
| 639 | + } |
| 640 | + |
| 641 | + const subgraph3 = { |
| 642 | + name: 'Subgraph3', |
| 643 | + url: 'https://Subgraph3', |
| 644 | + typeDefs: gql` |
| 645 | + interface I @key(fields: "id") { |
| 646 | + id: ID! |
| 647 | + secret: String |
| 648 | + } |
| 649 | +
|
| 650 | + type T implements I @key(fields: "id") { |
| 651 | + id: ID! |
| 652 | + secret: String @authenticated |
| 653 | + } |
| 654 | + ` |
| 655 | + } |
| 656 | + |
| 657 | + const result = composeAsFed2Subgraphs([subgraph1, subgraph2, subgraph3]); |
| 658 | + assertCompositionSuccess(result); |
| 659 | + }) |
| 660 | + |
| 661 | + it('verifies requires on interface objects without auth', () => { |
| 662 | + const subgraph1 = { |
| 663 | + name: 'Subgraph1', |
| 664 | + url: 'https://Subgraph1', |
| 665 | + typeDefs: gql` |
| 666 | + type Query { |
| 667 | + i: I |
| 668 | + } |
| 669 | +
|
| 670 | + type I @interfaceObject @key(fields: "id") { |
| 671 | + id: ID! |
| 672 | + extra: String @external |
| 673 | + requiresExtra: String @requires(fields: "extra") |
| 674 | + } |
| 675 | + ` |
| 676 | + } |
| 677 | + |
| 678 | + const subgraph2 = { |
| 679 | + name: 'Subgraph2', |
| 680 | + url: 'https://Subgraph2', |
| 681 | + typeDefs: gql` |
| 682 | + interface I @key(fields: "id") { |
| 683 | + id: ID! |
| 684 | + extra: String |
| 685 | + } |
| 686 | + |
| 687 | + type T implements I @key(fields: "id") { |
| 688 | + id: ID! |
| 689 | + extra: String @authenticated |
| 690 | + } |
| 691 | + ` |
| 692 | + } |
| 693 | + |
| 694 | + const result = composeAsFed2Subgraphs([subgraph1, subgraph2]); |
| 695 | + console.log(result.supergraphSdl); |
| 696 | + expect(result.schema).toBeUndefined(); |
| 697 | + expect(result.errors?.length).toBe(1); |
| 698 | + expect(result.errors?.[0].message).toBe( |
| 699 | + '[Subgraph1] Field "I.requiresExtra" does not specify necessary @authenticated, @requiresScopes and/or @policy' + |
| 700 | + ' auth requirements to access the transitive field "T.extra" data from @requires selection set.' |
| 701 | + ); |
| 702 | + }) |
520 | 703 | }); |
521 | 704 |
|
522 | 705 | describe("@context", () => { |
@@ -802,7 +985,7 @@ describe('authorization tests', () => { |
802 | 985 | assertCompositionSuccess(result); |
803 | 986 | expect( |
804 | 987 | result.schema.type('I')?.appliedDirectivesOf("authenticated")?.[0] |
805 | | - ); |
| 988 | + ).toBeDefined(); |
806 | 989 | }) |
807 | 990 |
|
808 | 991 | it('propagates @requiresScopes from type', () => { |
@@ -1139,5 +1322,157 @@ describe('authorization tests', () => { |
1139 | 1322 | ] |
1140 | 1323 | ); |
1141 | 1324 | }) |
| 1325 | + |
| 1326 | + it('works with interface objects', () => { |
| 1327 | + const subgraph1 = { |
| 1328 | + name: 'Subgraph1', |
| 1329 | + url: 'https://Subgraph1', |
| 1330 | + typeDefs: gql` |
| 1331 | + type Query { |
| 1332 | + i: I |
| 1333 | + } |
| 1334 | +
|
| 1335 | + type I @interfaceObject @key(fields: "id") { |
| 1336 | + id: ID! |
| 1337 | + secret: String @requiresScopes(scopes: [["S1"]]) |
| 1338 | + } |
| 1339 | + ` |
| 1340 | + } |
| 1341 | + |
| 1342 | + const subgraph2 = { |
| 1343 | + name: 'Subgraph2', |
| 1344 | + url: 'https://Subgraph2', |
| 1345 | + typeDefs: gql` |
| 1346 | + interface I @key(fields: "id") { |
| 1347 | + id: ID! |
| 1348 | + extra: String |
| 1349 | + } |
| 1350 | +
|
| 1351 | + type T implements I @key(fields: "id") { |
| 1352 | + id: ID! |
| 1353 | + extra: String @authenticated |
| 1354 | + } |
| 1355 | + |
| 1356 | + type U implements I @key(fields: "id") { |
| 1357 | + id: ID! |
| 1358 | + extra: String |
| 1359 | + } |
| 1360 | + ` |
| 1361 | + } |
| 1362 | + |
| 1363 | + const result = composeAsFed2Subgraphs([subgraph1, subgraph2]); |
| 1364 | + assertCompositionSuccess(result); |
| 1365 | + }) |
| 1366 | + |
| 1367 | + it('works with shareable interface object fields', () => { |
| 1368 | + const subgraph1 = { |
| 1369 | + name: 'Subgraph1', |
| 1370 | + url: 'https://Subgraph1', |
| 1371 | + typeDefs: gql` |
| 1372 | + type Query { |
| 1373 | + i: I |
| 1374 | + } |
| 1375 | +
|
| 1376 | + type I @interfaceObject @key(fields: "id") { |
| 1377 | + id: ID! |
| 1378 | + secret: String @requiresScopes(scopes: [["S1"]]) @shareable |
| 1379 | + } |
| 1380 | + ` |
| 1381 | + } |
| 1382 | + |
| 1383 | + const subgraph2 = { |
| 1384 | + name: 'Subgraph2', |
| 1385 | + url: 'https://Subgraph2', |
| 1386 | + typeDefs: gql` |
| 1387 | + interface I @key(fields: "id") { |
| 1388 | + id: ID! |
| 1389 | + extra: String |
| 1390 | + } |
| 1391 | +
|
| 1392 | + type T implements I @key(fields: "id") { |
| 1393 | + id: ID! |
| 1394 | + extra: String @authenticated |
| 1395 | + } |
| 1396 | +
|
| 1397 | + type U implements I @key(fields: "id") { |
| 1398 | + id: ID! |
| 1399 | + extra: String |
| 1400 | + } |
| 1401 | + ` |
| 1402 | + } |
| 1403 | + |
| 1404 | + const subgraph3 = { |
| 1405 | + name: 'Subgraph3', |
| 1406 | + url: 'https://Subgraph3', |
| 1407 | + typeDefs: gql` |
| 1408 | + type T @key(fields: "id") { |
| 1409 | + id: ID! |
| 1410 | + secret: String @requiresScopes(scopes: [["S2"]]) @shareable |
| 1411 | + } |
| 1412 | + ` |
| 1413 | + } |
| 1414 | + |
| 1415 | + const result = composeAsFed2Subgraphs([subgraph1, subgraph2, subgraph3]); |
| 1416 | + assertCompositionSuccess(result); |
| 1417 | + // interface I { |
| 1418 | + // id: ID! |
| 1419 | + // secret: String @requiresScopes(scopes: [["S1", "S2"]]) |
| 1420 | + // extra: String @authenticated |
| 1421 | + // } |
| 1422 | + const i = result.schema.type("I"); |
| 1423 | + expect(i).toBeDefined(); |
| 1424 | + expect(i).toBeInstanceOf(InterfaceType); |
| 1425 | + const secretI = (i as InterfaceType).field("secret"); |
| 1426 | + expect(secretI?.appliedDirectivesOf("requiresScopes") |
| 1427 | + ?.[0]?.arguments()?.["scopes"]).toStrictEqual( |
| 1428 | + [ |
| 1429 | + ['S1', 'S2'], |
| 1430 | + ] |
| 1431 | + ); |
| 1432 | + const extraI = (i as InterfaceType).field("extra"); |
| 1433 | + expect(extraI?.appliedDirectivesOf("authenticated") |
| 1434 | + ?.[0] |
| 1435 | + ).toBeDefined(); |
| 1436 | + |
| 1437 | + // type T implements I { |
| 1438 | + // id: ID! |
| 1439 | + // extra: String @authenticated |
| 1440 | + // secret: String @requiresScopes(scopes: [["S1", "S2"]]) |
| 1441 | + // } |
| 1442 | + const t = result.schema.type("T"); |
| 1443 | + expect(t).toBeDefined(); |
| 1444 | + expect(t).toBeInstanceOf(ObjectType); |
| 1445 | + const secretT = (t as ObjectType).field("secret"); |
| 1446 | + expect(secretT?.appliedDirectivesOf("requiresScopes") |
| 1447 | + ?.[0]?.arguments()?.["scopes"]).toStrictEqual( |
| 1448 | + [ |
| 1449 | + ['S1', 'S2'], |
| 1450 | + ] |
| 1451 | + ); |
| 1452 | + const extraT = (t as ObjectType).field("extra"); |
| 1453 | + expect(extraT?.appliedDirectivesOf("authenticated") |
| 1454 | + ?.[0] |
| 1455 | + ).toBeDefined(); |
| 1456 | + |
| 1457 | + // type U implements I { |
| 1458 | + // id: ID! |
| 1459 | + // extra: String |
| 1460 | + // secret: String @requiresScopes(scopes: [["S1", "S2"]]) |
| 1461 | + // } |
| 1462 | + const u = result.schema.type("U"); |
| 1463 | + expect(u).toBeDefined(); |
| 1464 | + expect(u).toBeInstanceOf(ObjectType); |
| 1465 | + const secretU = (u as ObjectType).field("secret"); |
| 1466 | + expect(secretU?.appliedDirectivesOf("requiresScopes") |
| 1467 | + ?.[0]?.arguments()?.["scopes"]).toStrictEqual( |
| 1468 | + [ |
| 1469 | + ['S1', 'S2'], |
| 1470 | + ] |
| 1471 | + ); |
| 1472 | + const extraU = (u as ObjectType).field("extra"); |
| 1473 | + expect(extraU?.appliedDirectivesOf("authenticated") |
| 1474 | + ?.[0] |
| 1475 | + ).toBeUndefined(); |
| 1476 | + }) |
1142 | 1477 | }); |
1143 | 1478 | }); |
0 commit comments