Skip to content

Commit f5afd19

Browse files
authored
Merge branch 'master' into bugfix/oded/fix-docker
2 parents 3cdd0bd + c95bccc commit f5afd19

25 files changed

Lines changed: 472 additions & 173 deletions

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ indent_size=2
99
trim_trailing_whitespace=true
1010

1111
[*.py]
12-
indent_size=4
12+
indent_size=4

.github/workflows/dockerhub_push.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
tags: |
4141
type=ref,event=branch
4242
type=semver,pattern={{version}}
43-
-
43+
-
4444
name: Echo published tags
4545
run: |
4646
echo "Published docker tags: ${{ steps.meta.outputs.tags }}"

.github/workflows/pre-commit.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: pre-commit
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [master, main]
7+
8+
jobs:
9+
pre-commit:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v3
13+
- uses: actions/setup-python@v3
14+
- uses: pre-commit/action@v2.0.3

.isort.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[settings]
2+
profile=black

.pre-commit-config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.2.0
4+
hooks:
5+
- id: check-yaml
6+
- id: end-of-file-fixer
7+
- id: trailing-whitespace
8+
- repo: https://github.com/psf/black
9+
rev: 22.3.0
10+
hooks:
11+
- id: black
12+
- repo: https://github.com/pycqa/isort
13+
rev: 5.10.1
14+
hooks:
15+
- id: isort

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
recursive-include horizon/static *
1+
recursive-include horizon/static *

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ you must declare the environment variable `READ_ONLY_GITHUB_TOKEN` for this comm
3636
```
3737
DEV_MODE_CLIENT_TOKEN=<CLIENT_TOKEN> make run
3838
```
39-
you must declare the environment variable `DEV_MODE_CLIENT_TOKEN` for this command to work.
39+
you must declare the environment variable `DEV_MODE_CLIENT_TOKEN` for this command to work.

horizon/authentication.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from fastapi import Header, HTTPException, status
2+
3+
from horizon.config import sidecar_config
4+
5+
6+
def enforce_pdp_token(authorization=Header(None)):
7+
if authorization is None:
8+
raise HTTPException(
9+
status.HTTP_401_UNAUTHORIZED, detail="Missing Authorization header"
10+
)
11+
schema, token = authorization.split(" ")
12+
13+
if schema.strip().lower() != "bearer" or token.strip() != sidecar_config.API_KEY:
14+
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Invalid PDP token")

horizon/config.py

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44

55

66
class SidecarConfig(Confi):
7-
CONTROL_PLANE = confi.str("CONTROL_PLANE", "http://localhost:8000", description="URL to the control plane that manages this PDP, typically Permit.io cloud (api.permit.io)")
7+
CONTROL_PLANE = confi.str(
8+
"CONTROL_PLANE",
9+
"http://localhost:8000",
10+
description="URL to the control plane that manages this PDP, typically Permit.io cloud (api.permit.io)",
11+
)
812

913
# backend api url, where proxy requests go
10-
BACKEND_SERVICE_URL = confi.str("BACKEND_SERVICE_URL", confi.delay("{CONTROL_PLANE}/v1"))
11-
BACKEND_LEGACY_URL = confi.str("BACKEND_LEGACY_URL", confi.delay("{CONTROL_PLANE}/sdk"))
14+
BACKEND_SERVICE_URL = confi.str(
15+
"BACKEND_SERVICE_URL", confi.delay("{CONTROL_PLANE}/v1")
16+
)
17+
BACKEND_LEGACY_URL = confi.str(
18+
"BACKEND_LEGACY_URL", confi.delay("{CONTROL_PLANE}/sdk")
19+
)
1220

1321
# backend route to fetch policy data topics
1422
REMOTE_CONFIG_ENDPOINT = confi.str("REMOTE_CONFIG_ENDPOINT", "pdps/me/config")
@@ -26,55 +34,93 @@ class SidecarConfig(Confi):
2634
ENABLE_MONITORING = confi.bool("ENABLE_MONITORING", False)
2735

2836
# centralized logging
29-
CENTRAL_LOG_DRAIN_URL = confi.str("CENTRAL_LOG_DRAIN_URL", "https://listener.logz.io:8071")
37+
CENTRAL_LOG_DRAIN_URL = confi.str(
38+
"CENTRAL_LOG_DRAIN_URL", "https://listener.logz.io:8071"
39+
)
3040
CENTRAL_LOG_DRAIN_TIMEOUT = confi.int("CENTRAL_LOG_DRAIN_TIMEOUT", 5)
3141
CENTRAL_LOG_TOKEN = confi.str("CENTRAL_LOG_TOKEN", None)
3242
CENTRAL_LOG_ENABLED = confi.bool("CENTRAL_LOG_ENABLED", False)
3343

3444
# internal OPA config
35-
OPA_CONFIG_FILE_PATH = confi.str("OPA_CONFIG_FILE_PATH", "~/opa/config.yaml", description="the path on the container for OPA config file")
36-
OPA_AUTH_POLICY_FILE_PATH = confi.str("OPA_AUTH_POLICY_FILE_PATH", "~/opa/basic-authz.rego", description="the path on the container for OPA authorization policy (rego file)")
37-
OPA_BEARER_TOKEN_REQUIRED = confi.bool("OPA_BEARER_TOKEN_REQUIRED", True, description="if true, all API calls to OPA must provide a bearer token (the value of CLIENT_TOKEN)")
38-
OPA_DECISION_LOG_ENABLED = confi.bool("OPA_DECISION_LOG_ENABLED", True, description="if true, OPA decision logs will be uploaded to the Permit.io cloud console")
39-
OPA_DECISION_LOG_CONSOLE = confi.bool("OPA_DECISION_LOG_CONSOLE", False, description="if true, OPA decision logs will also be printed to console (only relevant if `OPA_DECISION_LOG_ENABLED` is true)")
40-
OPA_DECISION_LOG_INGRESS_ROUTE = confi.str("OPA_DECISION_LOG_INGRESS_ROUTE", "/v1/decision_logs/ingress", description="the route on the backend the decision logs will be uploaded to")
41-
OPA_DECISION_LOG_MIN_DELAY = confi.int("OPA_DECISION_LOG_MIN_DELAY", 1, description="min amount of time (in seconds) to wait between decision log uploads")
42-
OPA_DECISION_LOG_MAX_DELAY = confi.int("OPA_DECISION_LOG_MAX_DELAY", 10, description="max amount of time (in seconds) to wait between decision log uploads")
45+
OPA_CONFIG_FILE_PATH = confi.str(
46+
"OPA_CONFIG_FILE_PATH",
47+
"~/opa/config.yaml",
48+
description="the path on the container for OPA config file",
49+
)
50+
OPA_AUTH_POLICY_FILE_PATH = confi.str(
51+
"OPA_AUTH_POLICY_FILE_PATH",
52+
"~/opa/basic-authz.rego",
53+
description="the path on the container for OPA authorization policy (rego file)",
54+
)
55+
OPA_BEARER_TOKEN_REQUIRED = confi.bool(
56+
"OPA_BEARER_TOKEN_REQUIRED",
57+
True,
58+
description="if true, all API calls to OPA must provide a bearer token (the value of CLIENT_TOKEN)",
59+
)
60+
OPA_DECISION_LOG_ENABLED = confi.bool(
61+
"OPA_DECISION_LOG_ENABLED",
62+
True,
63+
description="if true, OPA decision logs will be uploaded to the Permit.io cloud console",
64+
)
65+
OPA_DECISION_LOG_CONSOLE = confi.bool(
66+
"OPA_DECISION_LOG_CONSOLE",
67+
False,
68+
description="if true, OPA decision logs will also be printed to console (only relevant if `OPA_DECISION_LOG_ENABLED` is true)",
69+
)
70+
OPA_DECISION_LOG_INGRESS_ROUTE = confi.str(
71+
"OPA_DECISION_LOG_INGRESS_ROUTE",
72+
"/v1/decision_logs/ingress",
73+
description="the route on the backend the decision logs will be uploaded to",
74+
)
75+
OPA_DECISION_LOG_MIN_DELAY = confi.int(
76+
"OPA_DECISION_LOG_MIN_DELAY",
77+
1,
78+
description="min amount of time (in seconds) to wait between decision log uploads",
79+
)
80+
OPA_DECISION_LOG_MAX_DELAY = confi.int(
81+
"OPA_DECISION_LOG_MAX_DELAY",
82+
10,
83+
description="max amount of time (in seconds) to wait between decision log uploads",
84+
)
4385

4486
# temp log format (until cloud config is received)
45-
TEMP_LOG_FORMAT = confi.str("TEMP_LOG_FORMAT", "<green>{time}</green> | {process} | <blue>{name: <40}</blue>|<level>{level:^6} | {message}</level>")
87+
TEMP_LOG_FORMAT = confi.str(
88+
"TEMP_LOG_FORMAT",
89+
"<green>{time}</green> | {process} | <blue>{name: <40}</blue>|<level>{level:^6} | {message}</level>",
90+
)
4691

4792
# non configurable values -------------------------------------------------
4893

4994
# redoc configuration (openapi schema)
5095
OPENAPI_TAGS_METADATA = [
5196
{
5297
"name": "Authorization API",
53-
"description": "Authorization queries to OPA. These queries are answered locally by OPA " + \
54-
"and do not require the cloud service. Latency should be very low (< 20ms per query)"
98+
"description": "Authorization queries to OPA. These queries are answered locally by OPA "
99+
+ "and do not require the cloud service. Latency should be very low (< 20ms per query)",
55100
},
56101
{
57102
"name": "Local Queries",
58-
"description": "These queries are done locally against the sidecar and do not " + \
59-
"involve a network round-trip to Permit.io cloud API. Therefore they are safe " + \
60-
"to use with reasonable performance (i.e: with negligible latency) in the context of a user request.",
103+
"description": "These queries are done locally against the sidecar and do not "
104+
+ "involve a network round-trip to Permit.io cloud API. Therefore they are safe "
105+
+ "to use with reasonable performance (i.e: with negligible latency) in the context of a user request.",
61106
},
62107
{
63108
"name": "Policy Updater",
64-
"description": "API to manually trigger and control the local policy caching and refetching."
109+
"description": "API to manually trigger and control the local policy caching and refetching.",
65110
},
66111
{
67112
"name": "Cloud API Proxy",
68-
"description": "These endpoints proxy the Permit.io cloud api, and therefore **incur high-latency**. " + \
69-
"You should not use the cloud API in the standard request flow of users, i.e in places where the incurred " + \
70-
"added latency will affect your entire api. A good place to call the cloud API will be in one-time user events " + \
71-
"such as user registration (i.e: calling sync user, assigning initial user roles, etc.). " + \
72-
"The sidecar will proxy to the cloud every request prefixed with '/sdk'.",
113+
"description": "These endpoints proxy the Permit.io cloud api, and therefore **incur high-latency**. "
114+
+ "You should not use the cloud API in the standard request flow of users, i.e in places where the incurred "
115+
+ "added latency will affect your entire api. A good place to call the cloud API will be in one-time user events "
116+
+ "such as user registration (i.e: calling sync user, assigning initial user roles, etc.). "
117+
+ "The sidecar will proxy to the cloud every request prefixed with '/sdk'.",
73118
"externalDocs": {
74119
"description": "The cloud api complete docs are located here:",
75120
"url": "https://api.permit.io/redoc",
76121
},
77-
}
122+
},
78123
]
79124

80-
sidecar_config = SidecarConfig(prefix="PDP_")
125+
126+
sidecar_config = SidecarConfig(prefix="PDP_")

horizon/enforcer/api.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import json
2-
from typing import Optional, Dict
2+
from typing import Dict, Optional
33

4-
from fastapi import APIRouter, status, Response
5-
from opal_client.policy_store import BasePolicyStoreClient, DEFAULT_POLICY_STORE_GETTER
6-
from opal_client.policy_store.opa_client import fail_silently
4+
from fastapi import APIRouter, Depends, Response, status
75
from opal_client.logger import logger
8-
from horizon.config import sidecar_config
6+
from opal_client.policy_store.base_policy_store_client import BasePolicyStoreClient
7+
from opal_client.policy_store.opa_client import fail_silently
8+
from opal_client.policy_store.policy_store_client_factory import (
9+
DEFAULT_POLICY_STORE_GETTER,
10+
)
911

12+
from horizon.authentication import enforce_pdp_token
13+
from horizon.config import sidecar_config
1014
from horizon.enforcer.schemas import AuthorizationQuery, AuthorizationResult
1115

12-
def init_enforcer_api_router(policy_store:BasePolicyStoreClient=None):
16+
17+
def init_enforcer_api_router(policy_store: BasePolicyStoreClient = None):
1318
policy_store = policy_store or DEFAULT_POLICY_STORE_GETTER()
14-
router = APIRouter()
19+
router = APIRouter(dependencies=[Depends(enforce_pdp_token)])
1520

1621
def log_query_and_result(query: AuthorizationQuery, response: Response):
1722
params = "({}, {}, {})".format(query.user, query.action, query.resource.type)
@@ -22,12 +27,20 @@ def log_query_and_result(query: AuthorizationQuery, response: Response):
2227
granting_role = None
2328
if allowed:
2429
granting_permissions = result.get("granting_permission", [])
25-
granting_permission = {} if len(granting_permissions) == 0 else granting_permissions[0]
30+
granting_permission = (
31+
{} if len(granting_permissions) == 0 else granting_permissions[0]
32+
)
2633
permission = granting_permission.get("permission", {})
27-
granting_role: Optional[Dict] = granting_permission.get("granting_role", None)
34+
granting_role: Optional[Dict] = granting_permission.get(
35+
"granting_role", None
36+
)
2837
if granting_role:
2938
role_id = granting_role.get("id", "__NO_ID__")
30-
roles = [r for r in result.get("user_roles", []) if r.get("id", "") == role_id]
39+
roles = [
40+
r
41+
for r in result.get("user_roles", [])
42+
if r.get("id", "") == role_id
43+
]
3144
granting_role = granting_role if not roles else roles[0]
3245

3346
debug = {
@@ -50,53 +63,65 @@ def log_query_and_result(query: AuthorizationQuery, response: Response):
5063
allowed=allowed,
5164
api_params=params,
5265
input=query.dict(),
53-
debug=debug
66+
debug=debug,
5467
)
5568
except:
5669
try:
5770
body = str(response.body, "utf-8")
5871
except:
5972
body = None
60-
data = {} if body is None else {"response_body" : body}
61-
logger.info("is allowed",
73+
data = {} if body is None else {"response_body": body}
74+
logger.info(
75+
"is allowed",
6276
params=params,
6377
query=query.dict(),
6478
response_status=response.status_code,
6579
**data
6680
)
6781

68-
69-
@router.post("/allowed", response_model=AuthorizationResult, status_code=status.HTTP_200_OK, response_model_exclude_none=True)
82+
@router.post(
83+
"/allowed",
84+
response_model=AuthorizationResult,
85+
status_code=status.HTTP_200_OK,
86+
response_model_exclude_none=True,
87+
)
7088
async def is_allowed(query: AuthorizationQuery):
7189
async def _is_allowed():
7290
return await policy_store.get_data_with_input(path="rbac", input=query)
7391

7492
fallback_response = dict(result=dict(allow=False, debug="OPA not responding"))
75-
is_allowed_with_fallback = fail_silently(fallback=fallback_response)(_is_allowed)
93+
is_allowed_with_fallback = fail_silently(fallback=fallback_response)(
94+
_is_allowed
95+
)
7696
response = await is_allowed_with_fallback()
7797
log_query_and_result(query, response)
7898
try:
7999
raw_result = json.loads(response.body).get("result", {})
80100
processed_query = raw_result.get("authorization_query", {})
81101
result = {
82102
"allow": raw_result.get("allow", False),
83-
"result": raw_result.get("allow", False), # fallback for older sdks (TODO: remove)
103+
"result": raw_result.get(
104+
"allow", False
105+
), # fallback for older sdks (TODO: remove)
84106
"query": {
85107
"user": processed_query.get("user", {"id": query.user}),
86108
"action": processed_query.get("action", query.action),
87-
"resource": processed_query.get("resource", query.resource.dict(exclude_none=True)),
109+
"resource": processed_query.get(
110+
"resource", query.resource.dict(exclude_none=True)
111+
),
88112
},
89113
"debug": {
90114
"warnings": raw_result.get("debug", []),
91115
"user_roles": raw_result.get("user_roles", []),
92116
"granting_permission": raw_result.get("granting_permission", []),
93117
"user_permissions": raw_result.get("user_permissions", []),
94-
}
118+
},
95119
}
96120
except:
97121
result = dict(allow=False, result=False)
98-
logger.warning("is allowed (fallback response)", reason="cannot decode opa response")
122+
logger.warning(
123+
"is allowed (fallback response)", reason="cannot decode opa response"
124+
)
99125
return result
100126

101127
return router
102-

0 commit comments

Comments
 (0)