1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
|
from __future__ import annotations
from types import SimpleNamespace
from uuid import UUID
import pytest
from globus_sdk.scopes.consents import ConsentForest, ConsentTreeConstructionError
from tests.common import ConsentTest, ScopeRepr
_zero_uuid = str(UUID(int=0))
def _uuid_of(char: str) -> str:
if len(char) != 1:
raise ValueError(f"char must be a single character, got {char!r}")
return _zero_uuid.replace("0", char)
Clients = SimpleNamespace(
Zero=_uuid_of("0"),
One=_uuid_of("1"),
Two=_uuid_of("2"),
Three=_uuid_of("3"),
)
Scopes = SimpleNamespace(
A=ScopeRepr(_uuid_of("A"), "A"),
B=ScopeRepr(_uuid_of("B"), "B"),
C=ScopeRepr(_uuid_of("C"), "C"),
D=ScopeRepr(_uuid_of("D"), "D"),
)
def test_consent_forest_creation():
root = ConsentTest.of(Clients.Zero, Scopes.A)
node1 = ConsentTest.of(Clients.One, Scopes.B, parent=root)
node2 = ConsentTest.of(Clients.Two, Scopes.C, parent=node1)
forest = ConsentForest([root, node1, node2])
assert len(forest.trees) == 1
tree = forest.trees[0]
assert tree.root == root
assert tree.max_depth == 3
assert tree.edges[tree.root.id] == {node1.id}
assert tree.edges[node1.id] == {node2.id}
assert tree.edges[node2.id] == set()
assert tree.get_node(root.id) == root
assert tree.get_node(node1.id) == node1
assert tree.get_node(node2.id) == node2
def test_consent_forest_scope_requirement_evaluation():
root = ConsentTest.of(Clients.Zero, Scopes.A)
node1 = ConsentTest.of(Clients.One, Scopes.B, parent=root)
node2 = ConsentTest.of(Clients.Two, Scopes.C, parent=node1)
forest = ConsentForest([root, node1, node2])
assert forest.meets_scope_requirements("A")
assert forest.meets_scope_requirements("A[B[C]]")
assert not forest.meets_scope_requirements("B")
assert not forest.meets_scope_requirements("A[C]")
def test_consent_forest_scope_requirement_with_sibling_dependent_scopes():
root = ConsentTest.of(Clients.Zero, Scopes.A)
node1 = ConsentTest.of(Clients.One, Scopes.B, parent=root)
node2 = ConsentTest.of(Clients.Two, Scopes.C, parent=root)
forest = ConsentForest([root, node1, node2])
assert forest.meets_scope_requirements("A")
assert forest.meets_scope_requirements("A[B]")
assert forest.meets_scope_requirements("A[C]")
assert forest.meets_scope_requirements("A[B C]")
assert not forest.meets_scope_requirements("A[B[C]]")
assert not forest.meets_scope_requirements("A[C[B]]")
@pytest.mark.parametrize("atomically_revocable", (True, False))
def test_consent_forest_scope_requirement_with_optional_dependent_scopes(
atomically_revocable: bool,
):
"""
Dependent scope optionality is intentionally ignored for this implementation.
In formal terms, the scope "A[*B]" is only satisfied by a tree matching the shape
A -> B where B is "atomically revocable".
We've decided that this is an auth service concern, not a concern for local clients
to be making decisions about; so we intentionally ignore this distinction in order
to give standard users a simpler verification mechanism to ask "will my request
work with the current set of consents?".
"""
root = ConsentTest.of(Clients.Zero, Scopes.A)
child = ConsentTest.of(
Clients.One, Scopes.B, parent=root, atomically_revocable=atomically_revocable
)
forest = ConsentForest([root, child])
assert forest.meets_scope_requirements("A[B]")
assert forest.meets_scope_requirements("A[*B]")
def test_consent_forest_with_disjoint_consents_with_duplicate_scopes():
"""
Strange state to reproduce in practice but this test case simulates the forest of
Tree 1: A (Client Zero) -> B (Client Zero)
Tree 2: B (Client Zero) -> C (Client Zero)
In this situation, A[B] and B[C] are both satisfied, but A[B[C]] is not.
"""
root1 = ConsentTest.of(Clients.Zero, Scopes.A)
child1 = ConsentTest.of(Clients.Zero, Scopes.B, parent=root1)
root2 = ConsentTest.of(Clients.Zero, Scopes.B)
child2 = ConsentTest.of(Clients.Zero, Scopes.C, parent=root2)
forest = ConsentForest([root1, child1, root2, child2])
assert forest.meets_scope_requirements("A[B]")
assert forest.meets_scope_requirements("B[C]")
assert not forest.meets_scope_requirements("A[B[C]]")
def test_consent_forest_with_missing_intermediary_nodes():
"""
Simulate a situation in which we didn't receive the full list of consents from
Auth. So the tree has holes
Tree: A -> <B should be here but isn't> -> C
"""
root = ConsentTest.of(Clients.Zero, Scopes.A)
node1 = ConsentTest.of(Clients.One, Scopes.B, parent=root)
node2 = ConsentTest.of(Clients.Two, Scopes.C, parent=node1)
# Only add the first and last node to the forest.
# The last node (C) references the middle node (B) and so forest loading should
# fail.
with pytest.raises(
ConsentTreeConstructionError, match=rf"Missing parent node: {node1.id}"
):
ConsentForest([root, node2])
|