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 a Cedar policy engine plugin #461

Merged
merged 15 commits into from
May 10, 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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "cedar-agent"]
path = cedar-agent
url = https://github.com/permitio/cedar-agent.git
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ install-develop:
docker-build-client:
@docker build -t permitio/opal-client --target client -f docker/Dockerfile .

docker-build-client-cedar:
@docker build -t permitio/opal-client-cedar --target client-cedar -f docker/Dockerfile .

docker-build-client-standalone:
@docker build -t permitio/opal-client-standalone --target client-standalone -f docker/Dockerfile .

Expand Down
1 change: 1 addition & 0 deletions cedar-agent
Submodule cedar-agent added at b5cdcc
36 changes: 35 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ COPY ./packages/opal-server/requires.txt ./server_requires.txt
# install python deps
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r ./base_requires.txt -r ./common_requires.txt -r ./client_requires.txt -r ./server_requires.txt

# CEDAR AGENT BUILD STAGE ---------------------------
# split this stage to save time and reduce image size
# ---------------------------------------------------
FROM rust:1.69.0 as cedar-builder
COPY cedar-agent /tmp/cedar-agent/
ARG cargo_flags="-r"
RUN cd /tmp/cedar-agent && \
cargo build ${cargo_flags} && \
cp /tmp/cedar-agent/target/*/cedar-agent /

# COMMON IMAGE --------------------------------------
# ---------------------------------------------------
FROM python:3.10-slim as common
Expand Down Expand Up @@ -70,6 +80,8 @@ RUN mkdir -p /opal/backup
VOLUME /opal/backup


# IMAGE to extract OPA from official image ----------
# ---------------------------------------------------
FROM alpine:latest as opa-extractor
USER root

Expand All @@ -85,7 +97,7 @@ RUN skopeo copy "docker://${opa_image}:${opa_tag}" docker-archive:./image.tar &&
rm -r image image.tar


# CLIENT IMAGE --------------------------------------
# OPA CLIENT IMAGE ----------------------------------
# Using standalone image as base --------------------
# ---------------------------------------------------
FROM client-standalone as client
Expand All @@ -104,6 +116,28 @@ ENV OPAL_INLINE_OPA_ENABLED=true
EXPOSE 8181
USER opal

# CEDAR CLIENT IMAGE --------------------------------
# Using standalone image as base --------------------
# ---------------------------------------------------
FROM client-standalone as client-cedar

# Temporarily move back to root for additional setup
USER root

RUN apt-get update && apt-get install -y netcat jq && apt-get clean

# Copy cedar from its build stage
COPY --from=cedar-builder /cedar-agent /bin/cedar-agent

# enable inline Cedar agent
ENV OPAL_POLICY_STORE_TYPE=CEDAR
ENV OPAL_INLINE_CEDAR_ENABLED=true
ENV OPAL_INLINE_CEDAR_CONFIG='{"addr": "0.0.0.0:8180"}'
ENV OPAL_POLICY_STORE_URL=http://localhost:8180
# expose cedar port
EXPOSE 8180
USER opal

# SERVER IMAGE --------------------------------------
# ---------------------------------------------------
FROM common as server
Expand Down
71 changes: 71 additions & 0 deletions docker/docker-compose-exmple-cedar.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
version: "3.8"
services:
# When scaling the opal-server to multiple nodes and/or multiple workers, we use
# a *broadcast* channel to sync between all the instances of opal-server.
# Under the hood, this channel is implemented by encode/broadcaster (see link below).
# At the moment, the broadcast channel can be either: postgresdb, redis or kafka.
# The format of the broadcaster URI string (the one we pass to opal server as `OPAL_BROADCAST_URI`) is specified here:
# https://github.com/encode/broadcaster#available-backends
broadcast_channel:
image: postgres:alpine
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
opal_server:
# by default we run opal-server from latest official image
image: permitio/opal-server:latest
environment:
# the broadcast backbone uri used by opal server workers (see comments above for: broadcast_channel)
- OPAL_BROADCAST_URI=postgres://postgres:postgres@broadcast_channel:5432/postgres
# number of uvicorn workers to run inside the opal-server container
- UVICORN_NUM_WORKERS=4
# the git repo hosting our policy
# - if this repo is not public, you can pass an ssh key via `OPAL_POLICY_REPO_SSH_KEY`)
# - the repo we pass in this example is *public* and acts as an example repo with dummy rego policy
# - for more info, see: https://docs.opal.ac/tutorials/track_a_git_repo
- OPAL_POLICY_REPO_URL=https://github.com/permitio/opal-example-policy-repo-cedar
# in this example we will use a polling interval of 30 seconds to check for new policy updates (git commits affecting the rego policy).
# however, it is better to utilize a git *webhook* to trigger the server to check for changes only when the repo has new commits.
# for more info see: https://docs.opal.ac/tutorials/track_a_git_repo
- OPAL_POLICY_REPO_POLLING_INTERVAL=30
# configures from where the opal client should initially fetch data (when it first goes up, after disconnection, etc).
# the data sources represents from where the opal clients should get a "complete picture" of the data they need.
# after the initial sources are fetched, the client will subscribe only to update notifications sent by the server.
- OPAL_DATA_CONFIG_SOURCES={"config":{"entries":[{"url":"http://opal_server:7002/policy-data","topics":["policy_data"],"dst_path":""}]}}
# By default, the OPAL server looks for OPA .rego files. Configure it to look for .cedar files.
- OPAL_FILTER_FILE_EXTENSIONS=.cedar
- OPAL_POLICY_REPO_POLICY_EXTENSIONS=.cedar
- OPAL_LOG_FORMAT_INCLUDE_PID=true
ports:
# exposes opal server on the host machine, you can access the server at: http://localhost:7002
- "7002:7002"
depends_on:
- broadcast_channel
opal_client:
# by default we run opal-client from latest official image
image: permitio/opal-client-cedar:latest
environment:
- OPAL_SERVER_URL=http://opal_server:7002
- OPAL_LOG_FORMAT_INCLUDE_PID=true

# Uncomment the following lines to enable storing & loading OPA data from a backup file:
# - OPAL_OFFLINE_MODE_ENABLED=true
# volumes:
# - opa_backup:/opal/backup:rw

ports:
# exposes opal client on the host machine, you can access the client at: http://localhost:7000
- "7766:7000"
# exposes the OPA agent (being run by OPAL) on the host machine
# you can access the OPA api that you know and love at: http://localhost:8181
# OPA api docs are at: https://www.openpolicyagent.org/docs/latest/rest-api/
- "8181:8181"
depends_on:
- opal_server
# this command is not necessary when deploying OPAL for real, it is simply a trick for dev environments
# to make sure that opal-server is already up before starting the client.
command: sh -c "exec ./wait-for.sh opal_server:7002 --timeout=20 -- ./start.sh"

volumes:
opa_backup:
2 changes: 1 addition & 1 deletion documentation/docs/getting-started/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Please use this table as a reference.
| POLICY_REPO_WEBHOOK_PARAMS | | |
| POLICY_REPO_POLLING_INTERVAL | | |
| ALLOWED_ORIGINS | | |
| OPA_FILE_EXTENSIONS | | |
| FILTER_FILE_EXTENSIONS | | |
| NO_RPC_LOGS | | |
| SERVER_WORKER_COUNT | (If run using the CLI) - Worker count for the server [Default calculated to CPU-cores]. | |
| SERVER_HOST | (If run using the CLI) - Address for the server to bind. | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ In order to override default configuration, you'll need to set this env var:

| Env Var Name | Function |
| :--------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OPAL_INLINE_OPA_CONFIG | The value of this var should be an [OpaServerOptions](https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/opa/options.py#L19) pydantic model encoded into json string. The process is similar to the one we showed on how to encode the value of [OPAL_DATA_CONFIG_SOURCES](/getting-started/running-opal/run-opal-server/data-sources#encoding-this-value-in-an-environment-variable). |
| OPAL_INLINE_OPA_CONFIG | The value of this var should be an [OpaServerOptions](https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/engine/options.py#L19) pydantic model encoded into json string. The process is similar to the one we showed on how to encode the value of [OPAL_DATA_CONFIG_SOURCES](/getting-started/running-opal/run-opal-server/data-sources#encoding-this-value-in-an-environment-variable). |

#### Control how OPAL interacts with the policy store

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async with policy_store.transaction_context(update.id) as store_transaction:
await store_transaction.set_policy_data(policy_data, path=policy_store_path)
```

Every time a transaction [is ended](https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/policy_store/base_policy_store_client.py#L116) it is saved into OPA, by rendering the state of `OpaTransactionLogState` using the [healthcheck policy template](https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/opa/healthcheck/opal.rego).
Every time a transaction [is ended](https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/policy_store/base_policy_store_client.py#L116) it is saved into OPA, by rendering the state of `OpaTransactionLogState` using the [healthcheck policy template](https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/engine/healthcheck/opal.rego).

## <a name="callbacks"></a> Data update callbacks

Expand Down
116 changes: 75 additions & 41 deletions packages/opal-client/opal_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import signal
import uuid
from logging import disable
from typing import List, Optional
from typing import Awaitable, Callable, List, Literal, Optional, Union

import aiofiles
import aiofiles.os
Expand All @@ -18,9 +18,9 @@
from opal_client.data.api import init_data_router
from opal_client.data.fetcher import DataFetcher
from opal_client.data.updater import DataUpdater
from opal_client.engine.options import CedarServerOptions, OpaServerOptions
from opal_client.engine.runner import CedarRunner, OpaRunner
from opal_client.limiter import StartupLoadLimiter
from opal_client.opa.options import OpaServerOptions
from opal_client.opa.runner import OpaRunner
from opal_client.policy.api import init_policy_router
from opal_client.policy.updater import PolicyUpdater
from opal_client.policy_store.api import init_policy_store_router
Expand All @@ -46,6 +46,8 @@ def __init__(
policy_updater: PolicyUpdater = None,
inline_opa_enabled: bool = None,
inline_opa_options: OpaServerOptions = None,
inline_cedar_enabled: bool = None,
inline_cedar_options: CedarServerOptions = None,
verifier: Optional[JWTVerifier] = None,
store_backup_path: Optional[str] = None,
store_backup_interval: Optional[int] = None,
Expand All @@ -67,8 +69,8 @@ def __init__(
inline_opa_enabled: bool = (
inline_opa_enabled or opal_client_config.INLINE_OPA_ENABLED
)
inline_opa_options: OpaServerOptions = (
inline_opa_options or opal_client_config.INLINE_OPA_CONFIG
inline_cedar_enabled: bool = (
inline_cedar_enabled or opal_client_config.INLINE_CEDAR_ENABLED
)
opal_client_identifier: str = (
opal_client_config.OPAL_CLIENT_STAT_ID or f"CLIENT_{uuid.uuid4().hex}"
Expand Down Expand Up @@ -140,29 +142,12 @@ def __init__(

# Internal services
# Policy store
if self.policy_store_type == PolicyStoreTypes.OPA and inline_opa_enabled:
rehydration_callbacks = [
# refetches policy code (e.g: rego) and static data from server
functools.partial(
self.policy_updater.update_policy, force_full_update=True
),
]

if self.data_updater:
rehydration_callbacks.append(
functools.partial(
self.data_updater.get_base_policy_data,
data_fetch_reason="policy store rehydration",
)
)

self.opa_runner = OpaRunner.setup_opa_runner(
options=inline_opa_options,
piped_logs_format=opal_client_config.INLINE_OPA_LOG_FORMAT,
rehydration_callbacks=rehydration_callbacks,
)
else:
self.opa_runner = False
self.engine_runner = self._init_engine_runner(
inline_opa_enabled,
inline_cedar_enabled,
inline_opa_options,
inline_cedar_options,
)

custom_ssl_context = get_custom_ssl_context()
if (
Expand Down Expand Up @@ -197,6 +182,49 @@ def __init__(
# init fastapi app
self.app: FastAPI = self._init_fast_api_app()

def _init_engine_runner(
self,
inline_opa_enabled: bool,
inline_cedar_enabled: bool,
inline_opa_options: Optional[OpaServerOptions] = None,
inline_cedar_options: Optional[CedarServerOptions] = None,
) -> Union[OpaRunner, CedarRunner, Literal[False]]:
if inline_opa_enabled and self.policy_store_type == PolicyStoreTypes.OPA:
inline_opa_options = (
inline_opa_options or opal_client_config.INLINE_OPA_CONFIG
)
rehydration_callbacks = [
# refetches policy code (e.g: rego) and static data from server
functools.partial(
self.policy_updater.update_policy, force_full_update=True
),
]

if self.data_updater:
rehydration_callbacks.append(
functools.partial(
self.data_updater.get_base_policy_data,
data_fetch_reason="policy store rehydration",
)
)

return OpaRunner.setup_opa_runner(
options=inline_opa_options,
piped_logs_format=opal_client_config.INLINE_OPA_LOG_FORMAT,
rehydration_callbacks=rehydration_callbacks,
)

elif inline_cedar_enabled and self.policy_store_type == PolicyStoreTypes.CEDAR:
inline_cedar_options = (
inline_cedar_options or opal_client_config.INLINE_CEDAR_CONFIG
)
return CedarRunner.setup_cedar_runner(
options=inline_cedar_options,
piped_logs_format=opal_client_config.INLINE_CEDAR_LOG_FORMAT,
)

return False

def _init_fast_api_app(self):
"""inits the fastapi app object."""
app = FastAPI(
Expand Down Expand Up @@ -286,6 +314,20 @@ async def shutdown_event():

return app

async def _run_or_delay_for_engine_runner(
self, callback: Callable[[], Awaitable[None]]
):
if self.engine_runner:
# runs the callback after policy store is up
self.engine_runner.register_process_initial_start_callbacks([callback])
async with self.engine_runner:
await self.engine_runner.wait_until_done()
return

# we do not run the policy store in the same container
# therefore we can immediately run the callback
await callback()

async def start_client_background_tasks(self):
"""Launch OPAL client long-running tasks:

Expand All @@ -298,25 +340,17 @@ async def start_client_background_tasks(self):
if self._startup_wait:
await self._startup_wait()

if self.opa_runner:
# runs the policy store dependent tasks after policy store is up
self.opa_runner.register_opa_initial_start_callbacks(
[self.launch_policy_store_dependent_tasks]
)
async with self.opa_runner:
await self.opa_runner.wait_until_done()
else:
# we do not run the policy store in the same container
# therefore we can immediately launch dependent tasks
await self.launch_policy_store_dependent_tasks()
await self._run_or_delay_for_engine_runner(
self.launch_policy_store_dependent_tasks
)

async def stop_client_background_tasks(self):
"""stops all background tasks (called on shutdown event)"""
logger.info("stopping background tasks...")

# stopping opa runner
if self.opa_runner:
await self.opa_runner.stop()
if self.engine_runner:
await self.engine_runner.stop()

# stopping updater tasks (each updater runs a pub/sub client)
logger.info("trying to shutdown DataUpdater and PolicyUpdater gracefully...")
Expand Down
Loading
Loading