@@ -32,9 +32,11 @@ scalar openfed__Scope
3232
3333The "scopes" argument requires an array (GraphQL List) of nested arrays.
3434
35- The outermost array represents a set of OR scopes.
35+ The ** outer array** represents a set of ** OR ** scopes—the token must satisfy ** at least one ** of the inner arrays .
3636
37- Each nested array sibling represents a set of AND scopes.
37+ Each ** inner array** represents a set of ** AND** scopes—the token must possess ** all** scopes within that array.
38+
39+ In other words, ` scopes: [["a", "b"], ["c"]] ` means: (` "a" ` AND ` "b" ` ) OR (` "c" ` ).
3840
3941Each element in the AND scopes array should be an ` openfed__Scope ` Scalar, which is an instance of permissions as
4042defined in your authentication token.
@@ -251,34 +253,43 @@ type Query {
251253}
252254```
253255
254- ### Combining scopes in the same subgraph (matrix multiplication)
255- Disparate sets of OR scopes affecting a single field will always combine through a multiplication matrix .
256- This is to ensure data cannot be accessed without all appropriate permissions .
256+ ### Combining scopes in the same subgraph
257+
258+ When scopes are defined in multiple places (for example, on a field and on its return type), each set of scopes is
259+ combined with every set from the other definition .
257260
258261<Warning >
259262 The maximum total number of scopes that can apply to a single field both directly and indirectly is 16.
260263</Warning >
261264
262- If one set of AND scopes are declared on a field definition and another set of AND scopes are defined on the
263- "type level" , the scopes will be multiplied together resulting in both sets of AND scopes being required .
265+ **Algorithm **
266+
267+ 1. Take each set of scopes from the first definition .
268+ 2. Combine it with each set from the second definition .
269+ 3. Merge the scopes in each pair into a single AND set .
270+
271+ The resulting scope sets are the Cartesian product of the two scope lists , with each pair merged into a single AND
272+ scope set .
264273
265- For instance , consider the following federated graph :
274+ #### Simple example
266275
267276```graphql
268277# federated graph
269278type Query {
270- scalars : [Scalar ! ]! @requiresScopes (scopes : [["read:query" ]]) # scopes defined in the field level
279+ scalars : [Scalar ! ]! @requiresScopes (scopes : [["read:query" ]]) # field- level scopes
271280}
272281
273- scalar Scalar @requiresScopes (scopes : [["read:scalar" ]]) # scopes defined on the type level
282+ scalar Scalar @requiresScopes (scopes : [["read:scalar" ]]) # type- level scopes
274283```
275284
276- Selecting the field ` Query . scalars ` would require the permissions ` "read:query" ` AND ` "read:scalar" `.
277- This is effectively the same as a single set of AND scopes defined as `[["read:query" , "read: scalar" ]]`.
285+ Field scopes : `[[ "read:query" ]]`
286+ Type scopes : `[["read:scalar" ]]`
278287
279- If there are multiple sets of AND scopes (multiple OR scopes), each set of AND scopes on the "field level" are
280- multiplied against each set of AND scopes on the "type level" .
281- Consider the following federated graph :
288+ Result : `[["read:query" , "read:scalar" ]]`
289+
290+ Selecting `Query .scalars ` requires both `"read:query" ` AND `"read:scalar" `.
291+
292+ #### Multiple OR scopes
282293
283294```graphql
284295# federated graph
@@ -289,16 +300,20 @@ type Query {
289300scalar Scalar @requiresScopes (scopes : [["read:scalar" ]])
290301```
291302
292- Selecting the field `Query .scalars ` would require the permissions (`"read:query" ` AND `"read:scalar" `) OR
293- (`"read:scalar" ` AND `"read:private" `).
294- This is effectively the same as a single set of OR scopes defined as
295- `[["read:query" , "read:scalar" ], ["read:private" , "read:scalar" ]]`.
303+ Field scopes : `[["read:query" ], ["read:private" ]]`
304+ Type scopes : `[["read:scalar" ]]`
305+
306+ Result :
307+ ```js
308+ [
309+ ["read:query" , "read:scalar" ],
310+ ["read:private" , "read:scalar" ]
311+ ]
312+ ```
296313
314+ An access token requires (` "read:query" ` AND ` "read:scalar" ` ) OR (` "read:private" ` AND ` "read:scalar" ` ).
297315
298- Each set of AND scopes on the "field level" will be added to each set of AND scopes on the "type level" .
299- Typically , the number of resultant scopes will be \<number of AND scopes on field level > *
300- \<number of AND scopes on type level >.
301- Consider the following federated graph :
316+ #### Full example
302317
303318``` graphql
304319# federated graph
@@ -309,23 +324,18 @@ type Query {
309324scalar Scalar @requiresScopes (scopes : [["read:scalar" , " read:custom" ], ["read:sensitive" ]])
310325```
311326
312- In the example above , each set of AND scopes defined at the "field level" would be ANDed together with each set of AND
313- scopes defined at the "type level" .
314- To break this down :
315- 1. `["read:query" , "read:field" ]` from the "field level" would be added to `["read:scalar" , "read:custom" ]` from the
316- "type level" , producing `["read:query" , "read:field" , "read:scalar" , "read:custom" ]`.
317- 2. `["read:query" , "read:field" ]` from the "field level" would be added to `["read:sensitive" ]` from the "type level" ,
318- producing `["read:query" , "read:field" , "read:sensitive" ]`.
319- 3. `["read:private" ]` from the "field level" would be added to `["read:scalar" , "read:custom" ]` from the
320- "type level" , producing `["read:private" , "read:scalar" , "read:custom" ]`.
321- 4. `["read:private" ]` from the "field level" would be added to `["read:sensitive" ]` from the "type level" , producing
322- `["read:private" , "read:sensitive" ]`.
323- 5. `["read:list" ]` from the "field level" would be added to `["read:scalar" , "read:custom" ]` from the
324- "type level" , producing `["read:list" , "read:scalar" , "read:custom" ]`.
325- 6. `["read:list" ]` from the "field level" would be added to `["read:sensitive" ]` from the "type level" , producing
326- `["read:list" , "read:sensitive" ]`.
327-
328- This is effectively the same as a single set of scopes defined as :
327+ Field scopes (3 sets) × Type scopes (2 sets) = 6 resulting sets :
328+
329+ | Field set | Type set | Merged result |
330+ | --- | --- | --- |
331+ | `["read:query" , "read:field" ]` | `["read:scalar" , "read:custom" ]` | `["read:query" , "read:field" , "read:scalar" , "read:custom" ]` |
332+ | `["read:query" , "read:field" ]` | `["read:sensitive" ]` | `["read:query" , "read:field" , "read:sensitive" ]` |
333+ | `["read:private" ]` | `["read:scalar" , "read:custom" ]` | `["read:private" , "read:scalar" , "read:custom" ]` |
334+ | `["read:private" ]` | `["read:sensitive" ]` | `["read:private" , "read:sensitive" ]` |
335+ | `["read:list" ]` | `["read:scalar" , "read:custom" ]` | `["read:list" , "read:scalar" , "read:custom" ]` |
336+ | `["read:list" ]` | `["read:sensitive" ]` | `["read:list" , "read:sensitive" ]` |
337+
338+ This is effectively the same as :
329339```js
330340[
331341 ["read:query" , "read:field" , "read:scalar" , "read:custom" ],
@@ -337,8 +347,10 @@ This is effectively the same as a single set of scopes defined as:
337347]
338348```
339349
340- ### Combining scopes across subgraphs (matrix multiplication)
341- If multiple instances of a field (shared field) or type define scopes, those scopes will also be multiplied together.
350+ ### Combining scopes across subgraphs
351+
352+ When multiple instances of a shared field or type define scopes across different subgraphs, the same combination
353+ rule applies—each set of scopes from one subgraph is combined with every set from the other.
342354
343355Consider the following subgraphs:
344356``` graphql
@@ -348,7 +360,7 @@ type Query @shareable {
348360 objects : [Object ! ]!
349361}
350362
351- type Object @shareable @requiresScopes (scopes : [["read:object" ]]] ) {
363+ type Object @shareable @requiresScopes (scopes : [["read:object" ]]) {
352364 id : ID !
353365}
354366```
@@ -364,8 +376,7 @@ type Object @shareable @requiresScopes(scopes: [["read:type"], ["read:private"]]
364376}
365377```
366378
367- The scopes defined by each instance of the field will be multiplied together , and the resulting product will be shown
368- in the federated graph :
379+ The scopes from each subgraph are combined , and the result is shown in the federated graph :
369380```graphql
370381type Query {
371382 ids : [ID ! ]! @requiresScopes (scopes : [
@@ -385,11 +396,14 @@ type Object @requiresScopes(scopes: [
385396}
386397```
387398
388- Note that the federated graph will show the combined result of scopes for a specific field or type .
399+ Note that the federated graph shows the combined result of scopes for a specific field or type .
389400The _effective_ scopes , _i .e ._ , the "type level" scopes affecting a field , will not be reflected in the federated graph .
390401
391- ### Combining scopes (superset scope reduction)
392- In the event that combining scopes would produce superfluous scopes , they will be removed .
402+ ### Combining scopes (superset reduction)
403+
404+ After scopes are combined , any scope set that is a superset of another set is removed . A superset is
405+ redundant because the smaller set already grants access with fewer permissions .
406+
393407Consider the following subgraphs :
394408
395409```graphql
@@ -406,29 +420,23 @@ type Query @shareable {
406420}
407421```
408422
409- One might assume the scopes would merge thus :
410- ```graphql
411- # federated graph
412- type Query {
413- ids : [ID ! ]! @requiresScopes (scopes : [
414- ["read:id" ],
415- ["read:id" , " read:field" ],
416- ["read:field" , " read:id" ],
417- ["read:field" ],
418- ["read:private" , " read:id" ],
419- ["read:private" , " read:field" ]
420- ])
421- }
422- ```
423+ Combining the scopes produces 6 sets :
424+
425+ | subgraph -a set | subgraph -b set | Merged result |
426+ | --- | --- | --- |
427+ | `["read:id" ]` | `["read:id" ]` | `["read:id" ]` |
428+ | `["read:id" ]` | `["read:field" ]` | `["read:id" , "read:field" ]` |
429+ | `["read:field" ]` | `["read:id" ]` | `["read:field" , "read:id" ]` |
430+ | `["read:field" ]` | `["read:field" ]` | `["read:field" ]` |
431+ | `["read:private" ]` | `["read:id" ]` | `["read:private" , "read:id" ]` |
432+ | `["read:private" ]` | `["read:field" ]` | `["read:private" , "read:field" ]` |
433+
434+ After reduction :
435+ - `["read:id" , "read:field" ]` and `["read:field" , "read:id" ]` are duplicates and supersets of both `["read:id" ]` and `["read:field" ]` - removed .
436+ - `["read:private" , "read:id" ]` is a superset of `["read:id" ]` - removed .
437+ - `["read:private" , "read:field" ]` is a superset of `["read:field" ]` - removed .
423438
424- However , upon closer inspection , it becomes apparent that many of these scopes are either repeated or supersets of
425- other scopes .
426- Consequently , the merge result remove any scopes that are supersets of other scopes .
427- For example , the scope `"read:private" ` now only exists with either `"read:id" ` or `"read:field" `.
428- This makes `"read:private" ` redundant because if one has either the `"read:id" ` or `"read:field" ` permissions , this is
429- sufficient to access the field .
430- This the same case for the combined scopes of `"read:id" ` and `"read:field" `; just one is sufficient .
431- And so , the resulting federated graph is actually shown below (note that `"read:private" ` is removed):
439+ The resulting federated graph :
432440```graphql
433441# federated graph
434442type Query {
0 commit comments