Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logical operations on Patterns and Named patterns + new test #294

Merged
merged 3 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions testsuite/policy/authorization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def asdict(self):


@dataclass
class Rule:
class Pattern:
"""
Data class for rules represented by simple pattern-matching expressions.
Args:
Expand All @@ -60,6 +60,33 @@ class Rule:
value: str


@dataclass
class AnyPattern:
"""Dataclass specifying *OR* operation on patterns. Any one needs to pass for this block to pass."""

any: list["Rule"]


@dataclass
class AllPattern:
"""Dataclass specifying *AND* operation on patterns. All need to pass for this block to pass."""

all: list["Rule"]


@dataclass
class PatternRef:
"""
Dataclass that references other pattern-matching expression by name.
Use authorization.add_patterns() function to define named pattern-matching expression.
"""

patternRef: str


Rule = Pattern | AnyPattern | AllPattern | PatternRef


@dataclass
class ABCValue(abc.ABC):
"""
Expand Down Expand Up @@ -158,10 +185,3 @@ class Cache:

ttl: int
key: ABCValue


@dataclass
class PatternRef:
"""Dataclass for specifying Pattern reference in Authorization"""

patternRef: str
10 changes: 9 additions & 1 deletion testsuite/policy/authorization/auth_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from testsuite.utils import asdict
from testsuite.openshift import OpenShiftObject, modify
from testsuite.openshift.client import OpenShiftClient
from .sections import AuthorizationSection, IdentitySection, MetadataSection, ResponseSection, Rule
from .sections import AuthorizationSection, IdentitySection, MetadataSection, ResponseSection
from . import Rule, Pattern


class AuthConfig(OpenShiftObject):
Expand Down Expand Up @@ -75,3 +76,10 @@ def add_rule(self, when: list[Rule]):
"""Add rule for the skip of entire AuthConfig"""
self.auth_section.setdefault("when", [])
self.auth_section["when"].extend([asdict(x) for x in when])

@modify
def add_patterns(self, patterns: dict[str, list[Pattern]]):
"""Add named pattern-matching expressions to be referenced in other "when" rules."""
self.model.spec.setdefault("patterns", {})
for key, value in patterns.items():
self.model.spec["patterns"].update({key: [asdict(x) for x in value]})
5 changes: 3 additions & 2 deletions testsuite/policy/authorization/sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Selector,
Credentials,
Rule,
Pattern,
ABCValue,
ValueFrom,
JsonResponse,
Expand Down Expand Up @@ -261,8 +262,8 @@ def add_role_rule(self, name: str, role: str, path: str, **common_features):
:param role: name of role
:param path: path to apply this rule to
"""
rule = Rule("auth.identity.realm_access.roles", "incl", role)
when = Rule("context.request.http.path", "matches", path)
rule = Pattern("auth.identity.realm_access.roles", "incl", role)
when = Pattern("context.request.http.path", "matches", path)
common_features.setdefault("when", [])
common_features["when"].append(when)
self.add_auth_rules(name, [rule], **common_features)
Expand Down
4 changes: 2 additions & 2 deletions testsuite/policy/rate_limit_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import openshift as oc

from testsuite.policy.authorization import Rule
from testsuite.policy.authorization import Pattern
from testsuite.utils import asdict
from testsuite.gateway import Referencable
from testsuite.openshift.client import OpenShiftClient
Expand Down Expand Up @@ -40,7 +40,7 @@ def create_instance(cls, openshift: OpenShiftClient, name, target: Referencable,
return cls(model, context=openshift.context)

@modify
def add_limit(self, name, limits: Iterable[Limit], when: Iterable[Rule] = None, counters: list[str] = None):
def add_limit(self, name, limits: Iterable[Limit], when: Iterable[Pattern] = None, counters: list[str] = None):
"""Add another limit"""
limit: dict = {
"rates": [asdict(limit) for limit in limits],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Test condition to skip the authorization section of AuthConfig"""
import pytest

from testsuite.policy.authorization import Rule
from testsuite.policy.authorization import Pattern


@pytest.fixture(scope="module")
def authorization(authorization):
"""Add to the AuthConfig authorization with opa policy that will always reject POST requests"""
when_post = [Rule("context.request.http.method", "eq", "POST")]
when_post = [Pattern("context.request.http.method", "eq", "POST")]
authorization.authorization.add_opa_policy("opa", "allow { false }", when=when_post)
return authorization

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Test condition to skip the identity section of AuthConfig"""
import pytest

from testsuite.policy.authorization import Rule
from testsuite.policy.authorization import Pattern
from testsuite.httpx.auth import HeaderApiKeyAuth


Expand All @@ -20,7 +20,7 @@ def auth(api_key):
@pytest.fixture(scope="module")
def authorization(authorization, api_key):
"""Add to the AuthConfig API key identity, which can only be used on requests to the /get path"""
when_get = [Rule("context.request.http.path", "eq", "/get")]
when_get = [Pattern("context.request.http.path", "eq", "/get")]
authorization.identity.add_api_key("api-key", selector=api_key.selector, when=when_get)
return authorization

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Test condition to skip the metadata section of AuthConfig"""
import pytest

from testsuite.policy.authorization import Rule
from testsuite.policy.authorization import Pattern


@pytest.fixture(scope="module")
Expand All @@ -17,7 +17,7 @@ def authorization(authorization, mockserver_expectation):
Add to the AuthConfig metadata evaluator with get http request to the mockserver,
which will be only triggered on POST requests to the endpoint
"""
when_post = [Rule("context.request.http.method", "eq", "POST")]
when_post = [Pattern("context.request.http.method", "eq", "POST")]
authorization.metadata.add_http("mock", mockserver_expectation, "GET", when=when_post)
return authorization

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""Test condition to skip the response section of AuthConfig"""
import pytest

from testsuite.policy.authorization import Rule, Value, JsonResponse
from testsuite.policy.authorization import Pattern, Value, JsonResponse
from testsuite.utils import extract_response


@pytest.fixture(scope="module")
def authorization(authorization):
"""Add to the AuthConfig response, which will only trigger on POST requests"""
authorization.responses.add_success_header(
"simple", JsonResponse({"data": Value("response")}), when=[Rule("context.request.http.method", "eq", "POST")]
"simple", JsonResponse({"data": Value("response")}), when=[Pattern("context.request.http.method", "eq", "POST")]
)
return authorization

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Test patterns reference functionality and All/Any logical expressions."""
import pytest

from testsuite.policy.authorization import Pattern, PatternRef, AnyPattern, AllPattern


@pytest.fixture(scope="module")
def authorization(authorization):
"""
Add multiple named patterns to AuthConfig to be referenced in later authorization rules.
Create authorization rule which:
1. For a GET requests allows only paths "/anything/dog" and "/anything/cat"
2. For a POST requests allows only paths "/anything/apple" and "/anything/pear"
3. For requests that contain header "x-special" it will get authorized regardless.
"""
authorization.add_patterns(
{
"apple": [Pattern("context.request.http.path", "eq", "/anything/apple")],
"pear": [Pattern("context.request.http.path", "eq", "/anything/pear")],
"dog": [Pattern("context.request.http.path", "eq", "/anything/dog")],
"cat": [Pattern("context.request.http.path", "eq", "/anything/cat")],
"get": [Pattern("context.request.http.method", "eq", "GET")],
"post": [Pattern("context.request.http.method", "eq", "POST")],
}
)

authorization.authorization.add_auth_rules(
"auth_rules",
[
AnyPattern(
[
AllPattern([AnyPattern([PatternRef("dog"), PatternRef("cat")]), PatternRef("get")]),
AllPattern([AnyPattern([PatternRef("apple"), PatternRef("pear")]), PatternRef("post")]),
Pattern("context.request.http.headers.@keys", "incl", "x-special"),
]
)
],
)

return authorization


@pytest.mark.parametrize(
"path, expected_code",
[
("/get", 403),
("/anything/rock", 403),
("/anything/apple", 403),
("/anything/pear", 403),
("/anything/dog", 200),
("/anything/cat", 200),
],
)
def test_get_rule(client, auth, path, expected_code):
"""Test if doing GET request adheres to specified auth rule."""
assert client.get(path, auth=auth).status_code == expected_code


@pytest.mark.parametrize(
"path, expected_code",
[
("/post", 403),
("/anything/rock", 403),
("/anything/apple", 200),
("/anything/pear", 200),
("/anything/dog", 403),
("/anything/cat", 403),
],
)
def test_post_rule(client, auth, path, expected_code):
"""Test if doing POST request adheres to specified auth rule."""
assert client.post(path, auth=auth).status_code == expected_code


def test_special_header_rule(client, auth):
"""Test if using the "x-special" header adheres to specified auth rule."""
assert client.get("/get", auth=auth, headers={"x-special": "value"}).status_code == 200
assert client.post("/post", auth=auth, headers={"x-special": "value"}).status_code == 200
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Test condition to skip the entire AuthConfig"""
import pytest

from testsuite.policy.authorization import Rule
from testsuite.policy.authorization import Pattern


@pytest.fixture(scope="module")
def authorization(authorization, module_label):
"""Add rule to the AuthConfig to skip entire authn/authz with certain request header"""
authorization.add_rule([Rule("context.request.http.headers.key", "neq", module_label)])
authorization.add_rule([Pattern("context.request.http.headers.key", "neq", module_label)])
return authorization


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""https://github.com/Kuadrant/authorino/blob/main/docs/user-guides/token-normalization.md"""
import pytest
from testsuite.policy.authorization import Rule, Value, ValueFrom
from testsuite.policy.authorization import Pattern, Value, ValueFrom
from testsuite.httpx.auth import HeaderApiKeyAuth, HttpxOidcClientAuth


Expand Down Expand Up @@ -45,8 +45,8 @@ def authorization(authorization, rhsso, api_key):
defaults_properties={"roles": Value(["admin"])},
)

rule = Rule(selector="auth.identity.roles", operator="incl", value="admin")
when = Rule(selector="context.request.http.method", operator="eq", value="DELETE")
rule = Pattern(selector="auth.identity.roles", operator="incl", value="admin")
when = Pattern(selector="context.request.http.method", operator="eq", value="DELETE")
authorization.authorization.add_auth_rules("only-admins-can-delete", rules=[rule], when=[when])
averevki marked this conversation as resolved.
Show resolved Hide resolved
return authorization

Expand Down
4 changes: 2 additions & 2 deletions testsuite/tests/kuadrant/authorino/metadata/test_user_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest

from testsuite.httpx.auth import HttpxOidcClientAuth
from testsuite.policy.authorization import Rule
from testsuite.policy.authorization import Pattern


@pytest.fixture(scope="module")
Expand All @@ -22,7 +22,7 @@ def authorization(authorization, rhsso):
"""
authorization.metadata.add_user_info("user-info", "rhsso")
authorization.authorization.add_auth_rules(
"rule", [Rule("auth.metadata.user-info.email", "eq", rhsso.user.properties["email"])]
"rule", [Pattern("auth.metadata.user-info.email", "eq", rhsso.user.properties["email"])]
)
return authorization

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

from testsuite.certificates import CertInfo
from testsuite.utils import cert_builder
from testsuite.policy.authorization import Rule
from testsuite.policy.authorization import Pattern


@pytest.fixture(scope="module", autouse=True)
def authorization(authorization, blame, selector, cert_attributes):
"""Create AuthConfig with mtls identity and pattern matching rule"""
authorization.identity.add_mtls(blame("mtls"), selector=selector)

rule_organization = Rule("auth.identity.Organization", "incl", cert_attributes["O"])
rule_organization = Pattern("auth.identity.Organization", "incl", cert_attributes["O"])
authorization.authorization.add_auth_rules(blame("redhat"), [rule_organization])

return authorization
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Tests on mTLS authentication with multiple attributes"""
import pytest

from testsuite.policy.authorization import Rule
from testsuite.policy.authorization import Pattern


@pytest.fixture(scope="module", autouse=True)
def authorization(authorization, blame, cert_attributes):
"""Add second pattern matching rule to the AuthConfig"""
rule_country = Rule("auth.identity.Country", "incl", cert_attributes["C"])
rule_country = Pattern("auth.identity.Country", "incl", cert_attributes["C"])
authorization.authorization.add_auth_rules(blame("redhat"), [rule_country])

return authorization
Expand Down
10 changes: 5 additions & 5 deletions testsuite/tests/kuadrant/authorino/operator/tls/test_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import openshift as oc
from openshift import OpenShiftPythonException

from testsuite.policy.authorization import Rule, ValueFrom
from testsuite.policy.authorization import Pattern, ValueFrom
from testsuite.certificates import CertInfo
from testsuite.policy.authorization.auth_config import AuthConfig
from testsuite.utils import cert_builder
Expand Down Expand Up @@ -82,8 +82,8 @@ def authorization(authorization, openshift, module_label, authorino_domain) -> A
user_value = ValueFrom("auth.identity.username")

when = [
Rule("auth.authorization.features.allow", "eq", "true"),
Rule("auth.authorization.features.verb", "eq", "CREATE"),
Pattern("auth.authorization.features.allow", "eq", "true"),
Pattern("auth.authorization.features.verb", "eq", "CREATE"),
]
kube_attrs = {
"namespace": {"value": openshift.project},
Expand All @@ -97,8 +97,8 @@ def authorization(authorization, openshift, module_label, authorino_domain) -> A
)

when = [
Rule("auth.authorization.features.allow", "eq", "true"),
Rule("auth.authorization.features.verb", "eq", "DELETE"),
Pattern("auth.authorization.features.allow", "eq", "true"),
Pattern("auth.authorization.features.verb", "eq", "DELETE"),
]
kube_attrs = {
"namespace": {"value": openshift.project},
Expand Down
4 changes: 2 additions & 2 deletions testsuite/tests/kuadrant/authorino/response/test_deny_with.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from json import loads
import pytest

from testsuite.policy.authorization import Rule, Value, ValueFrom, DenyResponse
from testsuite.policy.authorization import Pattern, Value, ValueFrom, DenyResponse

HEADERS = {
"x-string-header": Value("abc"),
Expand Down Expand Up @@ -35,7 +35,7 @@ def authorization(authorization):
)
)
# Authorize only when url path is "/allow"
authorization.authorization.add_auth_rules("Whitelist", [Rule("context.request.http.path", "eq", "/allow")])
authorization.authorization.add_auth_rules("Whitelist", [Pattern("context.request.http.path", "eq", "/allow")])
return authorization


Expand Down