From 7522a8eb4232a4fcb03c1af41b70e3f9c25702ca Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Tue, 22 Jul 2025 17:03:37 -0500 Subject: Begin working on simple DSL for privileges checking. --- gn_libs/privileges.py | 28 ++++++ tests/unit/test_privileges_checking.py | 16 ++++ tests/unit/test_privileges_spec_parsing.py | 141 +++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 gn_libs/privileges.py create mode 100644 tests/unit/test_privileges_checking.py create mode 100644 tests/unit/test_privileges_spec_parsing.py diff --git a/gn_libs/privileges.py b/gn_libs/privileges.py new file mode 100644 index 0000000..47cb735 --- /dev/null +++ b/gn_libs/privileges.py @@ -0,0 +1,28 @@ +"""Utilities for handling privileges.""" +from typing import Union, TypeAlias + +PrivilegesList = tuple[str, ...] +CheckObj = tuple[str, PrivilegesList] # where the first item is either "OR" or "AND" +Checks: TypeAlias = Union[ + CheckObj, + tuple[str, tuple['Checks', ...]]] + + +class SpecificationValueError(ValueError): + """Raised when there is an error in the specification string.""" + pass + + +def parse(spec: str) -> Checks: + """Parse a string specification for privileges and return a tree of data + objects of the form ( ())""" + # if(spec.strip() == ""): + # raise SpecificationValueError( + # "You passed an empty specification. I do not know what to do.") + + return tuple() + + +def check(spec: str, privileges: tuple[str, ...]) -> bool: + """Check that the sequence of `privileges` satisfies `spec`.""" + return False diff --git a/tests/unit/test_privileges_checking.py b/tests/unit/test_privileges_checking.py new file mode 100644 index 0000000..460344f --- /dev/null +++ b/tests/unit/test_privileges_checking.py @@ -0,0 +1,16 @@ +"""Tests to verify the privileges check works.""" +import pytest + +from gn_libs.privileges import check + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec,privileges,expected", + ()) +def test_check(spec, privileges, expected): + """ + GIVEN: A privileges-check specification, and a tuple of privileges + WHEN: A check is performed + THEN: Verify that the check returns the expected value + """ + assert check(spec, privileges) == expected diff --git a/tests/unit/test_privileges_spec_parsing.py b/tests/unit/test_privileges_spec_parsing.py new file mode 100644 index 0000000..7b563e1 --- /dev/null +++ b/tests/unit/test_privileges_spec_parsing.py @@ -0,0 +1,141 @@ +"""Tests for parsing the privileges checks specification.""" +import pytest + +from gn_libs.privileges import parse, SpecificationValueError + + +## NOTE: Should we limit depth of nesting of checks, e.g. don't do more than +## 3 levels or so? + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec", + ("", + "(AND)", + "(AND (OR))", + "(OR (AND))", + "(OR (AND (OR (AND ))))")) +def test_empty_spec(spec): + """ + GIVEN: An effectively empty specification + WHEN: The specification is parsed + THEN: Raise a `SpecificationValueError` + """ + with pytest.raises(SpecificationValueError): + parse(spec) + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec,expected", + (("(AND priv1)", ("AND", ("priv1",))), + ("(AND priv1 priv2)", ("AND", ("priv1", "priv2"))), + ("(AND priv1 priv2 priv3)", ("AND", ("priv1", "priv2", "priv3"))), + ("(and priv1)", ("AND", ("priv1",))), + ("(and priv1 priv2)", ("AND", ("priv1", "priv2"))), + ("(and priv1 priv2 priv3)", ("AND", ("priv1", "priv2", "priv3"))), + ("(and priv1 priv2 (and priv3 priv4))", + ("AND", ("priv1", "priv2", "priv3", "priv4"))))) +def test_and(spec, expected): + """ + GIVEN: A simple 'AND' privileges check specification `spec` + WHEN: The specification is parsed + THEN: Verify the parsed output gives an 'AND' check object + """ + assert parse(spec) == expected + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec,expected", + (("(OR priv1)", ("OR", ("priv1",))), + ("(OR priv1 priv2)", ("OR", ("priv1", "priv2"))), + ("(OR priv1 priv2 priv3)", ("OR", ("priv1", "priv2", "priv3"))), + ("(or priv1)", ("OR", ("priv1",))), + ("(or priv1 priv2)", ("OR", ("priv1", "priv2"))), + ("(or priv1 priv2 priv3)", ("OR", ("priv1", "priv2", "priv3"))))) +def test_or(spec, expected): + """ + GIVEN: A simple 'OR' privileges check specification `spec` + WHEN: The specification is parsed + THEN: Verify the parsed output gives an 'OR' check object + """ + assert parse(spec) == expected + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec,expected", + (("(or priv1 priv2 (or priv3 priv4))", + ("OR", ("priv1", "priv2", "priv3", "priv4"))), + ("(and priv1 priv2 (and priv3 priv4))", + ("AND", ("priv1", "priv2", "priv3", "priv4"))))) +def test_merging(spec, expected): + """ + GIVEN: + - A nested specification where 2 or more of subsequent operators are + - the same + WHEN: The specification is parsed + THEN: Verify the parsed output merges the checks into a single object + """ + # NOTE: The "given-when-then" description above does not please me. + assert parse(spec) == expected + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec,expected", + ()) +def test_and_or(spec, expected): + """ + GIVEN: + - A specification beginning with an "AND" operator followed by an "OR" + - operator + WHEN: The specification is parsed + THEN: Verify the parsed output is correct + """ + assert parse(spec) == expected + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec,expected", + ()) +def test_or_and(spec, expected): + """ + GIVEN: + - A specification beginning with an "OR" operator followed by an "AND" + - operator + WHEN: The specification is parsed + THEN: Verify the parsed output is correct + """ + assert parse(spec) == expected + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec", + ()) +def test_invalid(spec): + """ + GIVEN: An invalid specification + WHEN: The specification is parsed + THEN: Verify that the `SpecificationValueError` is raised + """ + # NOTE: Maybe use hypothesis to generate random strings? + with pytest.raises(SpecificationValueError): + assert parse(spec) + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "spec,expected", + ()) +def test_complex(spec, expected): + """ + GIVEN: An valid, but more complex specification + WHEN: The specification is parsed + THEN: Verify that the specification parses correctly + """ + assert parse(spec) == expected -- cgit v1.2.3