Skip to content
Open
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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ podmonitors.monitoring.coreos.com,$\
apiservices.apiregistration.k8s.io,$\
horizontalpodautoscalers.autoscaling,$\
oidcpolicies.extensions.kuadrant.io,$\
planpolicies.extensions.kuadrant.io
planpolicies.extensions.kuadrant.io,$\
pipelinepolicies.extensions.kuadrant.io

clean: ## Clean all objects on cluster created by running this testsuite. Set the env variable USER to delete after someone else
@echo "Deleting objects for user: $(USER)"
Expand Down
2 changes: 2 additions & 0 deletions config/settings.local.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# password: "testPassword"
# spicedb:
# image: "SPICEDB_IMAGE"
# pipeline_policy_extension_service:
# image: "PIPELINE_POLICY_EXTENSION_SERVICE_IMAGE"
# auth0:
# client_id: "CLIENT_ID"
# client_secret: "CLIENT_SECRET"
Expand Down
2 changes: 2 additions & 0 deletions config/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ default:
image: "quay.io/rhn_support_azgabur/mockserver:latest"
spicedb:
image: "quay.io/authzed/spicedb:latest"
pipeline_policy_extension_service:
image: "quay.io/kuadrant/threat-assessment-service:latest"
prometheus:
project: "openshift-monitoring"
service: "thanos-querier"
Expand Down
133 changes: 133 additions & 0 deletions testsuite/kuadrant/extensions/pipeline_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Module containing classes related to PipelinePolicy"""

from __future__ import annotations

from functools import cached_property
from typing import Dict, List, Optional

from testsuite.gateway import Referencable
from testsuite.kubernetes import modify
from testsuite.kubernetes.client import KubernetesClient
from testsuite.kuadrant.policy import Policy


class ActionSection:
"""Section for request/response actions in a PipelinePolicy, mirrors ActionSpec from the Go API"""

def __init__(self, obj: "PipelinePolicy", section_name: str) -> None:
self.obj = obj
self.section_name = section_name

def modify_and_apply(self, modifier_func, retries=2, cmd_args=None):
"""Delegates modify_and_apply to the parent PipelinePolicy"""

def _new_modifier(obj):
modifier_func(ActionSection(obj, self.section_name))

return self.obj.modify_and_apply(_new_modifier, retries, cmd_args)

@property
def committed(self):
"""Delegates committed check to the parent PipelinePolicy"""
return self.obj.committed

@property
def section(self):
"""Returns the action list for this section"""
return self.obj.model.spec.setdefault(self.section_name, [])

@modify
def add_grpc_method(self, method: str, var: Optional[str] = None, predicate: Optional[str] = None):
"""Add a grpc_method action that calls an upstream"""
action: Dict = {"type": "grpc_method", "method": method}
if var:
action["var"] = var
if predicate:
action["predicate"] = predicate
self.section.append(action)

@modify
def add_deny(
self,
predicate: Optional[str] = None,
with_status: Optional[int] = None,
with_headers: Optional[str] = None,
with_body: Optional[str] = None,
):
"""Add a deny action"""
action: Dict = {"type": "deny"}
if predicate:
action["predicate"] = predicate
if with_status:
action["withStatus"] = with_status
if with_headers:
action["withHeaders"] = with_headers
if with_body:
action["withBody"] = with_body
self.section.append(action)

@modify
def add_fail(self, log_message: str, predicate: Optional[str] = None):
"""Add a fail action"""
action: Dict = {"type": "fail", "logMessage": log_message}
if predicate:
action["predicate"] = predicate
self.section.append(action)

@modify
def add_headers(self, headers: List[List[str]], predicate: Optional[str] = None):
"""Add an add_headers action"""
action: Dict = {"type": "add_headers", "headersToAdd": str(headers)}
if predicate:
action["predicate"] = predicate
self.section.append(action)


class PipelinePolicy(Policy):
"""PipelinePolicy for defining declarative action pipelines (request/response actions) on routes"""

@classmethod
def create_instance(
cls,
cluster: KubernetesClient,
name: str,
target: Referencable,
labels: Dict[str, str] = None,
section_name: str = None,
):
"""Creates base instance"""
model: Dict = {
"apiVersion": "extensions.kuadrant.io/v1alpha1",
"kind": "PipelinePolicy",
"metadata": {"name": name, "namespace": cluster.project, "labels": labels},
"spec": {
"targetRef": target.reference,
},
}
if section_name:
model["spec"]["targetRef"]["sectionName"] = section_name

return cls(model, context=cluster.context)

@cached_property
def on_http_request(self) -> ActionSection:
"""Gives access to request actions"""
return ActionSection(self, "request")

@cached_property
def on_http_response(self) -> ActionSection:
"""Gives access to response actions"""
return ActionSection(self, "response")

@modify
def add_action_method(self, name: str, url: str, service: str, method: str, message_template: str):
"""Add a gRPC upstream action method definition"""
self.model.spec.setdefault("actionMethods", []).append(
{
"name": name,
"url": url,
"service": service,
"method": method,
"messageTemplate": message_template,
}
)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Shared fixtures for PipelinePolicy testing."""

import pytest

from testsuite.kubernetes import Selector
from testsuite.kubernetes.deployment import Deployment
from testsuite.kubernetes.service import Service, ServicePort
from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy


@pytest.fixture(scope="session", autouse=True)
def check_pipeline_policy_crd(cluster, skip_or_fail):
"""Skip all PipelinePolicy tests if the CRD is not installed on the cluster."""
try:
cluster.do_action("get", "crd/pipelinepolicies.extensions.kuadrant.io")
except Exception: # pylint: disable=broad-except
skip_or_fail("PipelinePolicy CRD is not installed on the cluster")


@pytest.fixture(scope="module")
def pipeline_policy(cluster, blame, route):
"""PipelinePolicy targeting the test HTTPRoute"""
return PipelinePolicy.create_instance(cluster, blame("pipeline"), route)


@pytest.fixture(scope="module")
def threat_assessment_service(request, cluster, blame, module_label, testconfig):
"""Deploys the ThreatAssessmentService gRPC backend"""
name = blame("threat")
match_labels = {"app": module_label, "deployment": name}

deployment = Deployment.create_instance(
cluster,
name,
container_name="threat-assessment",
image=testconfig["pipeline_policy_extension_service"]["image"],
ports={"grpc": 8080},
selector=Selector(matchLabels=match_labels),
labels={"app": module_label},
)
request.addfinalizer(deployment.delete)
deployment.commit()
deployment.wait_for_ready()

service = Service.create_instance(
cluster,
name,
selector=match_labels,
ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")],
labels={"app": module_label},
)
request.addfinalizer(service.delete)
service.commit()
return service


@pytest.fixture(scope="module", autouse=True)
def commit(request, pipeline_policy):
"""Commit and wait for PipelinePolicy to be ready."""
for component in [pipeline_policy]:
request.addfinalizer(component.delete)
component.commit()
component.wait_for_ready()
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Fixtures for PipelinePolicy interaction tests with AuthPolicy and RateLimitPolicy."""

import pytest

from testsuite.httpx.auth import HttpxOidcClientAuth
from testsuite.kuadrant.policy.rate_limit import Limit


@pytest.fixture(scope="module")
def authorization(authorization, oidc_provider):
"""AuthPolicy with OIDC identity verification."""
authorization.identity.add_oidc("default", oidc_provider.well_known["issuer"])
return authorization


@pytest.fixture(scope="module")
def auth(oidc_provider):
"""Valid OIDC authentication for requests."""
return HttpxOidcClientAuth(oidc_provider.get_token, "authorization")


@pytest.fixture(scope="module")
def rate_limit(rate_limit):
"""RateLimitPolicy with a low limit for testing."""
rate_limit.add_limit("basic", [Limit(3, "10s")])
return rate_limit
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Tests for PipelinePolicy interaction with AuthPolicy."""

import pytest

pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions]


@pytest.fixture(scope="module")
def pipeline_policy(pipeline_policy):
"""PipelinePolicy with deny action and response header."""
pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403)
pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]])
return pipeline_policy


@pytest.fixture(scope="module", autouse=True)
def commit(request, authorization, pipeline_policy):
"""Commit AuthPolicy and PipelinePolicy."""
for component in [authorization, pipeline_policy]:
request.addfinalizer(component.delete)
component.commit()
component.wait_for_ready()


def test_auth_and_pipeline_allowed(client, auth):
"""Authenticated request to allowed path passes both AuthPolicy and PipelinePolicy."""
response = client.get("/get", auth=auth)
assert response.status_code == 200
assert response.headers.get("x-pipeline-policy") == "active"


def test_auth_and_pipeline_unauthorized(client):
"""Unauthenticated request is rejected by AuthPolicy before PipelinePolicy runs."""
response = client.get("/get")
assert response.status_code == 401
assert response.headers.get("x-pipeline-policy") is None


@pytest.mark.issue("https://github.com/Kuadrant/wasm-shim/issues/371")
@pytest.mark.xfail(reason="https://github.com/Kuadrant/wasm-shim/issues/371")
def test_auth_and_pipeline_blocked_path(client, auth):
"""Authenticated request to blocked path is denied by PipelinePolicy deny action."""
response = client.get("/blocked", auth=auth)
assert response.status_code == 403
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Tests for PipelinePolicy interaction with both AuthPolicy and RateLimitPolicy."""

import pytest

pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions]


@pytest.fixture(scope="module")
def pipeline_policy(pipeline_policy):
"""PipelinePolicy with deny action and response header."""
pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403)
pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]])
return pipeline_policy


@pytest.fixture(scope="module", autouse=True)
def commit(request, authorization, rate_limit, pipeline_policy):
"""Commit AuthPolicy, RateLimitPolicy, and PipelinePolicy."""
for component in [authorization, rate_limit, pipeline_policy]:
request.addfinalizer(component.delete)
component.commit()
component.wait_for_ready()


@pytest.mark.flaky(reruns=3, reruns_delay=15)
def test_all_policies_rate_limited(client, auth):
"""Rate limit is enforced alongside AuthPolicy and PipelinePolicy."""
responses = client.get_many("/get", 3, auth=auth)
responses.assert_all(status_code=200)
for resp in responses:
assert resp.headers.get("x-pipeline-policy") == "active"

response = client.get("/get", auth=auth)
assert response.status_code == 429
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Tests for PipelinePolicy interaction with RateLimitPolicy."""

import pytest

pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions]


@pytest.fixture(scope="module")
def pipeline_policy(pipeline_policy):
"""PipelinePolicy with response header."""
pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]])
return pipeline_policy


@pytest.fixture(scope="module", autouse=True)
def commit(request, rate_limit, pipeline_policy):
"""Commit RateLimitPolicy and PipelinePolicy."""
for component in [rate_limit, pipeline_policy]:
request.addfinalizer(component.delete)
component.commit()
component.wait_for_ready()


@pytest.mark.flaky(reruns=3, reruns_delay=15)
def test_rate_limit_and_pipeline(client):
"""Rate limit is enforced alongside PipelinePolicy actions."""
responses = client.get_many("/get", 3)
responses.assert_all(status_code=200)
for resp in responses:
assert resp.headers.get("x-pipeline-policy") == "active"

response = client.get("/get")
assert response.status_code == 429
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Basic happy-path tests for PipelinePolicy: deny action and response headers."""

import pytest

pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions]


@pytest.fixture(scope="module")
def pipeline_policy(pipeline_policy):
"""Configure PipelinePolicy with deny action and response headers."""
pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403)
pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]])
return pipeline_policy


def test_allowed_path(client):
"""Request to an allowed path returns 200 with the custom response header."""
response = client.get("/get")
assert response.status_code == 200
assert response.headers.get("x-pipeline-policy") == "active"


def test_blocked_path(client):
"""Request to /blocked is denied by the deny action."""
response = client.get("/blocked")
assert response.status_code == 403
Loading
Loading