aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2025-07-22 17:03:37 -0500
committerFrederick Muriuki Muriithi2025-07-22 17:03:37 -0500
commit7522a8eb4232a4fcb03c1af41b70e3f9c25702ca (patch)
tree626edd84870917ee5900251c97b03037d5bd0b9d
parent2fae0c6811fe53494e0cfbffc93b15450ecf5423 (diff)
downloadgn-libs-7522a8eb4232a4fcb03c1af41b70e3f9c25702ca.tar.gz
Begin working on simple DSL for privileges checking.
-rw-r--r--gn_libs/privileges.py28
-rw-r--r--tests/unit/test_privileges_checking.py16
-rw-r--r--tests/unit/test_privileges_spec_parsing.py141
3 files changed, 185 insertions, 0 deletions
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 (<operator> (<check>))"""
+ # 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