Skip to content

Commit

Permalink
Merge pull request #199 from permitio/omer/per-10804-is-allowed-url
Browse files Browse the repository at this point in the history
Add all check proxies external data store
  • Loading branch information
omer9564 authored Oct 15, 2024
2 parents c205313 + 501f886 commit 43f299b
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 20 deletions.
54 changes: 34 additions & 20 deletions horizon/enforcer/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,24 @@ async def _is_allowed(query: BaseSchema, request: Request, policy_package: str):
return await post_to_opa(request, path, opa_input)


async def conditional_is_allowed(
query: BaseSchema,
request: Request,
*,
policy_package: str = MAIN_POLICY_PACKAGE,
external_data_manager_path: str = "/check",
) -> dict:
if sidecar_config.ENABLE_EXTERNAL_DATA_MANAGER:
response = await _is_allowed_data_manager(query, request, path=external_data_manager_path)
raw_result = json.loads(response.body)
log_query_result(query, response, is_inner=True)
else:
response = await _is_allowed(query, request, policy_package)
raw_result = json.loads(response.body).get("result", {})
log_query_result(query, response)
return raw_result


async def _is_allowed_data_manager(
query: BaseSchema, request: Request, *, path: str = "/check"
):
Expand Down Expand Up @@ -505,10 +523,17 @@ async def is_allowed_all_tenants(
query: AuthorizationQuery,
x_permit_sdk_language: Optional[str] = Depends(notify_seen_sdk),
):
response = await _is_allowed(query, request, ALL_TENANTS_POLICY_PACKAGE)
log_query_result(query, response)
try:
if sidecar_config.ENABLE_EXTERNAL_DATA_MANAGER:
response = await _is_allowed_data_manager(
query, request, path="/check/all-tenants"
)
raw_result = json.loads(response.body)
log_query_result(query, response, is_inner=True)
else:
response = await _is_allowed(query, request, ALL_TENANTS_POLICY_PACKAGE)
raw_result = json.loads(response.body).get("result", {})
log_query_result(query, response)
try:
processed_query = (
get_v1_processed_query(raw_result)
or get_v2_processed_query(raw_result)
Expand Down Expand Up @@ -582,18 +607,12 @@ async def is_allowed(
raise HTTPException(
status_code=status.HTTP_421_MISDIRECTED_REQUEST,
detail="Mismatch between client version and PDP version,"
" required v2 request body, got v1. "
"hint: try to update your client version to v2",
" required v2 request body, got v1. "
"hint: try to update your client version to v2",
)
query = cast(AuthorizationQuery, query)
if sidecar_config.ENABLE_EXTERNAL_DATA_MANAGER:
response = await _is_allowed_data_manager(query, request)
raw_result = json.loads(response.body)
log_query_result(query, response, is_inner=True)
else:
response = await _is_allowed(query, request, MAIN_POLICY_PACKAGE)
raw_result = json.loads(response.body).get("result", {})
log_query_result(query, response)

raw_result = await conditional_is_allowed(query, request)
try:
processed_query = (
get_v1_processed_query(raw_result)
Expand Down Expand Up @@ -636,10 +655,8 @@ async def is_allowed_nginx(
resource=Resource(type=permit_resource_type, tenant=permit_tenant_id),
)

response = await _is_allowed(query, request, MAIN_POLICY_PACKAGE)
log_query_result(query, response)
raw_result = await conditional_is_allowed(query, request)
try:
raw_result = json.loads(response.body).get("result", {})
processed_query = (
get_v1_processed_query(raw_result)
or get_v2_processed_query(raw_result)
Expand Down Expand Up @@ -672,7 +689,7 @@ async def is_allowed_kong(request: Request, query: KongAuthorizationQuery):
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Kong integration is disabled. "
"Please set the PDP_KONG_INTEGRATION variable to true to enable it.",
"Please set the PDP_KONG_INTEGRATION variable to true to enable it.",
)

await PersistentStateHandler.get_instance().seen_sdk("kong")
Expand Down Expand Up @@ -720,11 +737,8 @@ async def is_allowed_kong(request: Request, query: KongAuthorizationQuery):
action=query.input.request.http.method.lower(),
),
request,
MAIN_POLICY_PACKAGE,
)
log_query_result_kong(query.input, response)
try:
raw_result = json.loads(response.body).get("result", {})
result = {
"result": raw_result.get("allow", False),
}
Expand Down
185 changes: 185 additions & 0 deletions horizon/tests/test_enforcer_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
from aioresponses import aioresponses
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.testclient import TestClient
from opal_client.client import OpalClient
from opal_client.config import opal_client_config
Expand Down Expand Up @@ -164,6 +165,89 @@ async def pdp_api_client() -> TestClient:
# TODO: Add Kong
]

ALLOWED_ENDPOINTS_DATASYNC = [
(
"/allowed",
"/check",
AuthorizationQuery(
user=User(key="user1"),
action="read",
resource=Resource(type="resource1"),
),
None,
{"allow": True},
{"allow": True},
),
(
"/allowed_url",
"/check",
UrlAuthorizationQuery(
user=User(key="user1"),
http_method="DELETE",
url="https://some.url/important_resource",
tenant="default",
),
None,
{"allow": True},
{"allow": True},
),
(
"/nginx_allowed",
"/check",
None,
{
"permit-user-key": "user1",
"permit-tenant-id": "default",
"permit-action": "read",
"permit-resource-type": "resource1",
},
{"allow": True},
{"allow": True},
),
(
"/allowed/all-tenants",
"/check/all-tenants",
AuthorizationQuery(
user=User(key="user1"),
action="read",
resource=Resource(type="resource1"),
),
None,
{
"allowed_tenants": [
{
"tenant": {"key": "default", "attributes": {}},
"allow": True,
"result": True,
}
]
},
{
"allowed_tenants": [
{
"tenant": {"key": "default", "attributes": {}},
"allow": True,
"result": True,
}
]
},
),
(
"/allowed/bulk",
"/check/bulk",
[
AuthorizationQuery(
user=User(key="user1"),
action="read",
resource=Resource(type="resource1"),
)
],
None,
{"allow": [{"allow": True, "result": True}]},
{"allow": [{"allow": True, "result": True}]},
),
]


@pytest.mark.parametrize(
"endpoint, opa_endpoint, query, opa_response, expected_response",
Expand Down Expand Up @@ -342,3 +426,104 @@ def post_endpoint():
response = post_endpoint()
assert response.status_code == 504
assert "OPA request timed out" in response.text


@pytest.mark.parametrize(
("endpoint", "datasync_endpoint", "query", "headers", "datasync_response", "expected_response"),
ALLOWED_ENDPOINTS_DATASYNC,
)
def test_enforce_endpoint_datasync(
endpoint: str,
datasync_endpoint: str,
query: list[BaseModel] | BaseModel | None,
headers: dict | None,
datasync_response: dict,
expected_response: dict,
):
sidecar_config.ENABLE_EXTERNAL_DATA_MANAGER = True
_client = TestClient(sidecar._app)

def post_endpoint():
return _client.post(
endpoint,
headers={"authorization": f"Bearer {sidecar_config.API_KEY}"} | (headers or {}),
json=jsonable_encoder(query) if query else None,
)

with aioresponses() as m:
datasync_url = (
f"{sidecar_config.DATA_MANAGER_SERVICE_URL}/v1/authz{datasync_endpoint}"
)

# Test valid response from OPA
m.post(
datasync_url,
status=200,
payload=datasync_response,
)

if endpoint == "/allowed_url":
# allowed_url gonna first call the mapping rules endpoint then the normal OPA allow endpoint
m.post(
url=f"{opal_client_config.POLICY_STORE_URL}/v1/data/mapping_rules",
status=200,
payload={
"result": {
"all": [
{
"url": "https://some.url/important_resource",
"http_method": "delete",
"action": "delete",
"resource": "resource1",
}
]
}
},
repeat=True,
)

response = post_endpoint()
assert response.status_code == 200
print(response.json())
if isinstance(expected_response, list):
assert response.json() == expected_response
elif isinstance(expected_response, dict):
for k, v in expected_response.items():
assert (
response.json()[k] == v
), f"Expected {k} to be {v} but got {response.json()[k]}"
else:
raise TypeError(
f"Unexpected expected response type, expected one of list, dict and got {type(expected_response)}"
)

# Test bad status from OPA
bad_status = random.choice([401, 404, 400, 500, 503])
m.post(
datasync_url,
status=bad_status,
payload=datasync_response,
)
response = post_endpoint()
assert response.status_code == 502
assert "Data Manager request failed" in response.text
assert f"status: {bad_status}" in response.text

# Test connection error
m.post(
datasync_url,
exception=aiohttp.ClientConnectionError("don't want to connect"),
)
response = post_endpoint()
assert response.status_code == 502
assert "Data Manager request failed" in response.text
assert "don't want to connect" in response.text

# Test timeout - not working yet
m.post(
datasync_url,
exception=asyncio.exceptions.TimeoutError(),
)
response = post_endpoint()
assert response.status_code == 504
assert "Data Manager request timed out" in response.text

0 comments on commit 43f299b

Please sign in to comment.