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

refactor(jans-cedarling): make tokens optional #10555

Merged
merged 10 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
37 changes: 18 additions & 19 deletions docs/cedarling/cedarling-authz.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,24 @@ this is a sample request from a hypothetical application:

```js
input = {
"tokens": {
"access_token": "eyJhbGc....",
"id_token": "eyJjbGc...",
"userinfo_token": "eyJjbGc...",
},
"action": "View",
"resource": {
"id": "ticket-10101",
"type" : "Ticket",
"owner": "[email protected]",
"org_id": "Acme"
},
"context": {
"ip_address": "54.9.21.201",
"network_type": "VPN",
"user_agent": "Chrome 125.0.6422.77 (Official Build) (arm64)",
"time": "1719266610.98636",
}
}
"access_token": "eyJhbGc....",
"id_token": "eyJjbGc...",
"userinfo_token": "eyJjbGc...",
"tx_token": "eyJjbGc...",
"action": "View",
"resource": {
"id": "ticket-10101",
"type" : "Ticket",
"owner": "[email protected]",
"org_id": "Acme"
},
"context": {
"ip_address": "54.9.21.201",
"network_type": "VPN",
"user_agent": "Chrome 125.0.6422.77 (Official Build) (arm64)",
"time": "1719266610.98636",
}
}

decision_result = authz(input)
```
Expand Down
8 changes: 5 additions & 3 deletions docs/cedarling/cedarling-policy-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,18 @@ This record contains the information needed to validate tokens from this issuer:
- **description** : (*String*) A brief description of the trusted issuer, providing context for administrators.
- **openid_configuration_endpoint** : (*String*) The HTTPS URL for the OpenID Connect configuration endpoint (usually found at `/.well-known/openid-configuration`).
- **identity_source** : (*Object*, *optional*) Metadata related to the tokens issued by this issuer.
- **`access_tokens`, `id_tokens`, `userinfo_tokens`, and `tx_tokens`**: See: [Token Metadata Schema](#token-metadata-schema).

**Notes**:

- The `access_tokens`, `id_tokens`, `userinfo_tokens`, and `tx_tokens` fields will follow the [Token Metadata Schema](#token-metadata-schema).
- The `access_tokens` will contain a `trusted` and `principal_identifier` field in addition to the fields from the `Token Metadata Schema`.

### Token Metadata Schema

The Token Entity Metadata Schema defines how tokens are mapped, parsed, and transformed within Cedarling. It allows you to specify how to extract user IDs, roles, and other claims from a token using customizable parsers.

```json
{
"trusted": bool,
"principal_identifier": "str",
"user_id": "<field name in token (e.g., 'email', 'sub', 'uid', etc.) or '' if not used>",
"role_mapping": "<field for role assignment (e.g., 'role', 'memberOf', etc.) or '' if not used>",
"claim_mapping": {
Expand Down
14 changes: 8 additions & 6 deletions docs/cedarling/python/sidecar.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The sidecar is a containerized Flask project that uses the `cedarling_python` bi
- Ensure that you have installed [docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/install/).
- Clone the [Janssen](https://github.com/JanssenProject/jans) repository
- Navigate to `jans/jans-cedarling/flask-sidecar`
- Edit the provided `secrets/bootstrap.json` file to your specifications. The configuration keys are described [here](https://github.com/JanssenProject/jans/blob/ffe9f493e4a5c6b05f2adeeb8a6eba7eb83b103e/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi#L9).
- Edit the provided `secrets/bootstrap.json` file to your specifications. The configuration keys are described [here](https://github.com/JanssenProject/jans/blob/main/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi#L10).
- Run `docker compose up`
- For cloud deployments, please use the provided Dockerfile and pass your bootstrap configuration via the environment variable `CEDARLING_BOOTSTRAP_CONFIG_FILE`.
- The sidecar runs on port 5000. OpenAPI documentation is available at `http://0.0.0.0:5000/swagger-ui`
Expand All @@ -28,8 +28,13 @@ Example request to the evaluation endpoint:
```
{
"subject": {
"type": "string",
"id": "string"
"type": "JWT",
"id": "cedarling",
"properties": {
"access_token": "",
"id_token": "",
"userinfo_token": ""
}
},
"resource": {
"type": "Jans::Application",
Expand All @@ -48,9 +53,6 @@ Example request to the evaluation endpoint:
"name": "Jans::Action::\"Read\""
},
"context": {
"access_token": "...",
"id_token": "...",
"userinfo_token": "...",
"device_health": [
"Healthy"
],
Expand Down
5 changes: 3 additions & 2 deletions jans-cedarling/flask-sidecar/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ RUN pip3 install "poetry==$POETRY_VERSION" gunicorn \
# ===============
# Project setup
# ===============
ENV JANS_SOURCE_VERSION=9610bc15908331e8344dfaed16ee8a397bd999d5
ENV JANS_SOURCE_VERSION=92cc167ade06492e25425fa14426722c9882b0ec

COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh
Expand Down Expand Up @@ -70,7 +70,8 @@ RUN poetry add /api/cedarling_python-$(cat /api/cedarling_version)-cp310-cp310-m

ENV FLASK_APP=main.core:app \
GUNICORN_LOG_LEVEL=${GUNICORN_LOG_LEVEL:-debug} \
CEDARLING_BOOTSTRAP_CONFIG_FILE=${CEDARLING_BOOTSTRAP_CONFIG_FILE:-/api/bootstrap.json}
CEDARLING_BOOTSTRAP_CONFIG_FILE=${CEDARLING_BOOTSTRAP_CONFIG_FILE:-/api/bootstrap.json} \
SIDECAR_DEBUG_RESPOSE=${SIDECAR_DEBUG_RESPONSE:-False}

EXPOSE 5000
LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/cedarling-flask-sidecar" \
Expand Down
47 changes: 40 additions & 7 deletions jans-cedarling/flask-sidecar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,44 @@ This is a Flask API that implements the [AuthZen](https://openid.github.io/authz
To run the API:

- Install [poetry](https://python-poetry.org/docs/#installation)
- Clone the [Janssen](https://github.com/JanssenProject/jans) repository
- Clone the [Janssen](https://github.com/JanssenProject/jans) repository:
```
git clone --filter blob:none --no-checkout https://github.com/JanssenProject/jans
```
```
cd jans
```
```
git clone --filter blob:none --no-checkout https://github.com/JanssenProject/jans /tmp/jans \
&& cd /tmp/jans \
&& git sparse-checkout init --cone \
&& git checkout main \
&& git sparse-checkout set jans-cedarling
git sparse-checkout init --cone
```
```
git checkout main
```

```
- Navigate to `jans/jans-cedarling/flask-sidecar/main`
git sparse-checkout set jans-cedarling
```
- Navigate to `jans-cedarling/flask-sidecar`
- Run `poetry install` to install dependencies
- Navigate to `main/`
- Run `poetry run flask run` to run the API on `http://127.0.0.1:5000`

## Configuration

For running via poetry, the sidecar supports the following environment variables:

| Variable name | Default value | Supported value(s) |
| ------------- | ------------- | ------------------ |
| APP_MODE | testing | development, testing, production |
| CEDARLING_BOOTSTRAP_CONFIG_FILE | None | Path to your configuration |
| SIDECAR_DEBUG_RESPONSE | False | True, False |

- Navigate to `jans/jans-cedarling/flask-sidecar/main` and create a file named `.env`
- Set environment variables like so:
```
APP_MODE=development
```

## Tests

Not yet implemented
Expand All @@ -28,6 +54,13 @@ Not yet implemented
- Clone the [Janssen](https://github.com/JanssenProject/jans) repository
- Navigate to `jans/jans-cedarling/flask-sidecar/`
- Modify the `secrets/bootstrap.json` file to your specifications. Configuration values are described [here](https://github.com/JanssenProject/jans/blob/main/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi).
- The default configuration expects you to provide a URL to a policy store file via `CEDARLING_POLICY_STORE_URI`. If you want to use a local policy store via `CEDARLING_POLICY_STORE_FN`, you need to mount it inside the docker image. Place your policy store file in the `secrets` folder and edit the Dockerfile at line 46 to add this line:

```
...
COPY --chown=1000:1000 ./secrets/<policy store file>.json /api/
...
```
- Run `docker compose up`
- The service is running on `http://0.0.0.0:5000`. OpenAPI documentation is available at `/swagger-ui`

Expand Down
1 change: 1 addition & 0 deletions jans-cedarling/flask-sidecar/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
- FLASK_APP=main.core:app
- APP_MODE=${APP_MODE:-development}
- CEDARLING_BOOTSTRAP_CONFIG_FILE=/run/secrets/cedarling_bootstrap_config_file
- SIDECAR_DEBUG_RESPOSE={SIDECAR_DEBUG_RESPOSE:-False}
secrets:
- cedarling_bootstrap_config_file
secrets:
Expand Down
75 changes: 49 additions & 26 deletions jans-cedarling/flask-sidecar/main/base/cedarling/cedarling.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from cedarling_python import BootstrapConfig
from cedarling_python import Cedarling
from cedarling_python import ResourceData, Request, AuthorizeResult, AuthorizeResultResponse
from cedarling_python import (
ResourceData,
Request,
AuthorizeResultResponse,
Tokens
)
from main.logger import logger
from flask import Flask
import json
Expand All @@ -19,6 +24,7 @@ def __init__(self, app=None):

def init_app(self, app: Flask):
self._bootstrap_config = app.config.get("CEDARLING_BOOTSTRAP_CONFIG", "{}")
self.debug_response: bool = app.config.get("SIDECAR_DEBUG_RESPONSE", False)
app.extensions = getattr(app, "extensions", {})
app.extensions["cedarling_client"] = self
self.initialize_cedarling()
Expand All @@ -42,11 +48,17 @@ def generate_resource(self, resource: DictType) -> ResourceData:
resource_entity = ResourceData.from_dict(resource_entity_dict)
return resource_entity

def validate_context(self, context: DictType) -> bool:
for key in KEYS_LIST:
if context.get(key, None) is None:
return False
return True
def validate_subject(self, subject: DictType) -> bool:
if "properties" not in subject:
return False
count = 0
i = 0
while count == 0 and i < len(KEYS_LIST):
key = KEYS_LIST[i]
if subject["properties"].get(key, None) is not None:
count += 1
i += 1
return True if count > 0 else False

def generate_report(self, authorize_response: AuthorizeResultResponse | None, report: str) -> _t.List[str]:
result = []
Expand All @@ -59,6 +71,13 @@ def generate_report(self, authorize_response: AuthorizeResultResponse | None, re
for error in diagnostic.errors:
result.append(error.error)
return result

def get_reason(self, authorize_response: AuthorizeResultResponse | None) -> _t.List[str]:
result = []
if authorize_response is not None:
for reason in authorize_response.diagnostics.reason:
result.append(reason)
return result

def authorize(self,
subject: DictType,
Expand All @@ -68,7 +87,7 @@ def authorize(self,
result_dict = {}
action_entity = action.get("name", "")
resource_entity = self.generate_resource(resource)
if not self.validate_context(context):
if not self.validate_subject(subject):
result_dict["decision"] = False
result_dict["context"] = {
"id": "-1",
Expand All @@ -77,13 +96,12 @@ def authorize(self,
}
}
return result_dict
access_token = context.get("access_token", "")
id_token = context.get("id_token", "")
userinfo_token = context.get("userinfo_token", "")
for key in KEYS_LIST:
context.pop(key)
access_token = subject["properties"].get("access_token", None)
id_token = subject["properties"].get("id_token", None)
userinfo_token = subject["properties"].get("userinfo_token", None)
try:
request = Request(access_token, id_token, userinfo_token, action_entity, resource_entity, context)
tokens = Tokens(access_token, id_token, userinfo_token)
request = Request(tokens, action_entity, resource_entity, context)
authorize_result = self._cedarling.authorize(request)
except Exception as e:
result_dict["decision"] = False
Expand All @@ -108,18 +126,23 @@ def authorize(self,
person_value = person_result.decision.value
if workload_result is not None:
workload_value = workload_result.decision.value
person_diagnostic = self.generate_report(person_result, "reason")
person_error = self.generate_report(person_result, "error")
workload_diagnostic = self.generate_report(workload_result, "reason")
workload_error = self.generate_report(workload_result, "error")
result_dict["context"] = {
"reason_admin": {
"person evaluation": person_value,
"person diagnostics": person_diagnostic,
"person error": person_error,
"workload evaluation": workload_value,
"workload diagnostics": workload_diagnostic,
"workload_error": workload_error
if self.debug_response:
person_diagnostic = self.generate_report(person_result, "reason")
person_error = self.generate_report(person_result, "error")
person_reason = self.get_reason(person_result)
workload_diagnostic = self.generate_report(workload_result, "reason")
workload_error = self.generate_report(workload_result, "error")
workload_reason = self.get_reason(workload_result)
result_dict["context"] = {
"reason_admin": {
"person evaluation": person_value,
"person diagnostics": person_diagnostic,
"person error": person_error,
"person reason": person_reason,
"workload evaluation": workload_value,
"workload diagnostics": workload_diagnostic,
"workload error": workload_error,
"workload reason": workload_reason
}
}
}
return result_dict
5 changes: 5 additions & 0 deletions jans-cedarling/flask-sidecar/main/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ class BaseConfig:
exit()
with open(CEDARLING_BOOTSTRAP_CONFIG_FILE, "r") as f:
CEDARLING_BOOTSTRAP_CONFIG = f.read()
SIDECAR_DEBUG_RESPONSE = os.getenv("SIDECAR_DEBUG_RESPONSE", "False")
if SIDECAR_DEBUG_RESPONSE == "True":
SIDECAR_DEBUG_RESPONSE = True
else:
SIDECAR_DEBUG_RESPONSE = False

class TestingConfig(BaseConfig):
TESTING = True
Expand Down
Loading
Loading