Skip to content

Commit c184b80

Browse files
author
Shaul Kremer
committed
Implemented inline Cedar support.
1 parent e51ce93 commit c184b80

File tree

4 files changed

+170
-44
lines changed

4 files changed

+170
-44
lines changed

packages/opal-client/opal_client/client.py

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import signal
55
import uuid
66
from logging import disable
7-
from typing import List, Optional
7+
from typing import List, Optional, Literal, Union
88

99
import aiofiles
1010
import aiofiles.os
@@ -19,8 +19,8 @@
1919
from opal_client.data.fetcher import DataFetcher
2020
from opal_client.data.updater import DataUpdater
2121
from opal_client.limiter import StartupLoadLimiter
22-
from opal_client.opa.options import OpaServerOptions
23-
from opal_client.opa.runner import OpaRunner
22+
from opal_client.opa.options import OpaServerOptions, CedarServerOptions
23+
from opal_client.opa.runner import OpaRunner, CedarRunner
2424
from opal_client.policy.api import init_policy_router
2525
from opal_client.policy.updater import PolicyUpdater
2626
from opal_client.policy_store.api import init_policy_store_router
@@ -46,6 +46,8 @@ def __init__(
4646
policy_updater: PolicyUpdater = None,
4747
inline_opa_enabled: bool = None,
4848
inline_opa_options: OpaServerOptions = None,
49+
inline_cedar_enabled: bool = None,
50+
inline_cedar_options: CedarServerOptions = None,
4951
verifier: Optional[JWTVerifier] = None,
5052
store_backup_path: Optional[str] = None,
5153
store_backup_interval: Optional[int] = None,
@@ -67,8 +69,8 @@ def __init__(
6769
inline_opa_enabled: bool = (
6870
inline_opa_enabled or opal_client_config.INLINE_OPA_ENABLED
6971
)
70-
inline_opa_options: OpaServerOptions = (
71-
inline_opa_options or opal_client_config.INLINE_OPA_CONFIG
72+
inline_cedar_enabled: bool = (
73+
inline_cedar_enabled or opal_client_config.INLINE_CEDAR_ENABLED
7274
)
7375
opal_client_identifier: str = (
7476
opal_client_config.OPAL_CLIENT_STAT_ID or f"CLIENT_{uuid.uuid4().hex}"
@@ -140,29 +142,7 @@ def __init__(
140142

141143
# Internal services
142144
# Policy store
143-
if self.policy_store_type == PolicyStoreTypes.OPA and inline_opa_enabled:
144-
rehydration_callbacks = [
145-
# refetches policy code (e.g: rego) and static data from server
146-
functools.partial(
147-
self.policy_updater.update_policy, force_full_update=True
148-
),
149-
]
150-
151-
if self.data_updater:
152-
rehydration_callbacks.append(
153-
functools.partial(
154-
self.data_updater.get_base_policy_data,
155-
data_fetch_reason="policy store rehydration",
156-
)
157-
)
158-
159-
self.opa_runner = OpaRunner.setup_opa_runner(
160-
options=inline_opa_options,
161-
piped_logs_format=opal_client_config.INLINE_OPA_LOG_FORMAT,
162-
rehydration_callbacks=rehydration_callbacks,
163-
)
164-
else:
165-
self.opa_runner = False
145+
self.opa_runner = self._init_engine_runner(inline_opa_enabled, inline_cedar_enabled, inline_opa_options, inline_cedar_options)
166146

167147
custom_ssl_context = get_custom_ssl_context()
168148
if (
@@ -197,6 +177,47 @@ def __init__(
197177
# init fastapi app
198178
self.app: FastAPI = self._init_fast_api_app()
199179

180+
def _init_engine_runner(self,
181+
inline_opa_enabled: bool, inline_cedar_enabled: bool,
182+
inline_opa_options: Optional[OpaServerOptions] = None,
183+
inline_cedar_options: Optional[CedarServerOptions] = None,
184+
) -> Union[OpaRunner, CedarRunner, Literal[False]]:
185+
if inline_opa_enabled and self.policy_store_type == PolicyStoreTypes.OPA:
186+
inline_opa_options = (
187+
inline_opa_options or opal_client_config.INLINE_OPA_CONFIG
188+
)
189+
rehydration_callbacks = [
190+
# refetches policy code (e.g: rego) and static data from server
191+
functools.partial(
192+
self.policy_updater.update_policy, force_full_update=True
193+
),
194+
]
195+
196+
if self.data_updater:
197+
rehydration_callbacks.append(
198+
functools.partial(
199+
self.data_updater.get_base_policy_data,
200+
data_fetch_reason="policy store rehydration",
201+
)
202+
)
203+
204+
return OpaRunner.setup_opa_runner(
205+
options=inline_opa_options,
206+
piped_logs_format=opal_client_config.INLINE_OPA_LOG_FORMAT,
207+
rehydration_callbacks=rehydration_callbacks,
208+
)
209+
210+
elif inline_cedar_enabled and self.policy_store_type == PolicyStoreTypes.CEDAR:
211+
inline_cedar_options = (
212+
inline_cedar_options or opal_client_config.INLINE_CEDAR_CONFIG
213+
)
214+
return CedarRunner.setup_cedar_runner(
215+
options=inline_cedar_options,
216+
piped_logs_format=opal_client_config.INLINE_OPA_LOG_FORMAT,
217+
)
218+
219+
return False
220+
200221
def _init_fast_api_app(self):
201222
"""inits the fastapi app object."""
202223
app = FastAPI(

packages/opal-client/opal_client/config.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from enum import Enum
22

3-
from opal_client.opa.options import OpaServerOptions
3+
from opal_client.opa.options import OpaServerOptions, CedarServerOptions
44
from opal_client.policy.options import PolicyConnRetryOptions
55
from opal_client.policy_store.schemas import PolicyStoreAuth, PolicyStoreTypes
66
from opal_common.confi import Confi, confi
@@ -96,6 +96,18 @@ def load_policy_store():
9696
description="cli options used when running `opa run --server` inline",
9797
)
9898

99+
# whether or not OPAL should run the Cedar agent by itself in the same container
100+
INLINE_CEDAR_ENABLED = confi.bool("INLINE_CEDAR_ENABLED", True)
101+
102+
# if inline Cedar is indeed enabled, user can pass cli options
103+
# (configuration) that affects how the agent will run
104+
INLINE_CEDAR_CONFIG = confi.model(
105+
"INLINE_CEDAR_CONFIG",
106+
CedarServerOptions,
107+
{}, # defaults are being set according to CedarServerOptions pydantic definitions (see class)
108+
description="cli options used when running the Cedar agent inline",
109+
)
110+
99111
INLINE_OPA_LOG_FORMAT: OpaLogFormat = confi.enum(
100112
"INLINE_OPA_LOG_FORMAT", OpaLogFormat, OpaLogFormat.NONE
101113
)

packages/opal-client/opal_client/opa/options.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from enum import Enum
2-
from typing import List, Optional
2+
from typing import List, Optional, Any
33

4-
from pydantic import BaseModel, Field
4+
from pydantic import BaseModel, Field, validator
55

66

77
class LogLevel(str, Enum):
@@ -81,3 +81,55 @@ def get_opa_startup_files(self) -> str:
8181
"""returns a list of startup policies and data."""
8282
files = self.files if self.files is not None else []
8383
return " ".join(files)
84+
85+
class CedarServerOptions(BaseModel):
86+
"""Options to configure the Cedar agent (apply when choosing to run Cedar inline).
87+
"""
88+
89+
addr: str = Field(
90+
":8181",
91+
description="listening address of the Cedar agent (e.g., [ip]:<port> for TCP)",
92+
)
93+
authentication: AuthenticationScheme = Field(
94+
AuthenticationScheme.off, description="Cedar agent authentication scheme (default off)"
95+
)
96+
authentication_token: Optional[str] = Field(
97+
None, description="If authentication is 'token', this specifies the token to use."
98+
)
99+
files: Optional[List[str]] = Field(
100+
None,
101+
description="list of built-in policies files that must be loaded on startup.",
102+
)
103+
104+
class Config:
105+
use_enum_values = True
106+
allow_population_by_field_name = True
107+
108+
@classmethod
109+
def alias_generator(cls, string: str) -> str:
110+
"""converts field named tls_private_key_file to --tls-private-key-
111+
file (to be used by opa cli)"""
112+
return "--{}".format(string.replace("_", "-"))
113+
114+
@validator("authentication")
115+
def validate_authentication(cls, v: AuthenticationScheme):
116+
if v not in [AuthenticationScheme.off, AuthenticationScheme.token]:
117+
raise ValueError("Invalid AuthenticationScheme for Cedar.")
118+
return v
119+
120+
@validator("authentication_token")
121+
def validate_authentication_token(cls, v: Optional[str], values: dict[str, Any]):
122+
if values['authentication'] == AuthenticationScheme.token and v is None:
123+
raise ValueError("A token must be speicified for AuthenticationScheme.token.")
124+
return v
125+
126+
def get_cmdline(self) -> str:
127+
result = [
128+
"cedar-agent",
129+
]
130+
if self.authentication == AuthenticationScheme.token and self.authentication_token is not None:
131+
result += [
132+
"-a", self.authentication_token,
133+
]
134+
# TODO: files
135+
return " ".join(result)

packages/opal-client/opal_client/opa/runner.py

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from opal_client.config import OpaLogFormat
77
from opal_client.logger import logger
88
from opal_client.opa.logger import pipe_opa_logs
9-
from opal_client.opa.options import OpaServerOptions
9+
from opal_client.opa.options import OpaServerOptions, CedarServerOptions
1010
from tenacity import retry, wait_random_exponential
1111

1212
AsyncCallback = Callable[[], Coroutine]
@@ -183,8 +183,24 @@ def _init_events(self):
183183
if self._should_stop is None:
184184
self._should_stop = asyncio.Event()
185185

186+
class OpaRunner(PolicyEngineRunner):
187+
def __init__(
188+
self,
189+
options: Optional[OpaServerOptions] = None,
190+
piped_logs_format: OpaLogFormat = OpaLogFormat.NONE,
191+
):
192+
super().__init__(piped_logs_format)
193+
self._options = options or OpaServerOptions()
194+
195+
@property
196+
def command(self) -> str:
197+
opts = self._options.get_cli_options_dict()
198+
opts_string = " ".join([f"{k}={v}" for k, v in opts.items()])
199+
startup_files = self._options.get_opa_startup_files()
200+
return f"opa run --server {opts_string} {startup_files}".strip()
201+
186202
@staticmethod
187-
def setup_process_runner(
203+
def setup_opa_runner(
188204
options: Optional[OpaServerOptions] = None,
189205
piped_logs_format: OpaLogFormat = OpaLogFormat.NONE,
190206
initial_start_callbacks: Optional[List[AsyncCallback]] = None,
@@ -202,25 +218,50 @@ def setup_process_runner(
202218
to handle authorization queries. therefore it is necessary that we rehydrate the
203219
cache with fresh state fetched from the server.
204220
"""
205-
process_runner = OpaRunner(options=options, piped_logs_format=piped_logs_format)
221+
opa_runner = OpaRunner(options=options, piped_logs_format=piped_logs_format)
206222
if initial_start_callbacks:
207-
process_runner.register_process_initial_start_callbacks(initial_start_callbacks)
223+
opa_runner.register_process_initial_start_callbacks(initial_start_callbacks)
208224
if rehydration_callbacks:
209-
process_runner.register_process_restart_callbacks(rehydration_callbacks)
210-
return process_runner
225+
opa_runner.register_process_restart_callbacks(rehydration_callbacks)
226+
return opa_runner
211227

212-
class OpaRunner(PolicyEngineRunner):
228+
229+
230+
class CedarRunner(PolicyEngineRunner):
213231
def __init__(
214232
self,
215-
options: Optional[OpaServerOptions] = None,
233+
options: Optional[CedarServerOptions] = None,
216234
piped_logs_format: OpaLogFormat = OpaLogFormat.NONE,
217235
):
218236
super().__init__(piped_logs_format)
219-
self._options = options or OpaServerOptions()
237+
self._options = options or CedarServerOptions()
220238

221239
@property
222240
def command(self) -> str:
223-
opts = self._options.get_cli_options_dict()
224-
opts_string = " ".join([f"{k}={v}" for k, v in opts.items()])
225-
startup_files = self._options.get_opa_startup_files()
226-
return f"opa run --server {opts_string} {startup_files}".strip()
241+
return self._options.get_cmdline()
242+
243+
@staticmethod
244+
def setup_cedar_runner(
245+
options: Optional[CedarServerOptions] = None,
246+
piped_logs_format: OpaLogFormat = OpaLogFormat.NONE,
247+
initial_start_callbacks: Optional[List[AsyncCallback]] = None,
248+
rehydration_callbacks: Optional[List[AsyncCallback]] = None,
249+
):
250+
"""factory for CedarRunner, accept optional callbacks to run in certain
251+
lifecycle events.
252+
253+
Initial Start Callbacks:
254+
The first time we start the engine, we might want to do certain actions (like launch tasks)
255+
that are dependent on the policy store being up (such as PolicyUpdater, DataUpdater).
256+
257+
Rehydration Callbacks:
258+
when the engine restarts, its cache is clean and it does not have the state necessary
259+
to handle authorization queries. therefore it is necessary that we rehydrate the
260+
cache with fresh state fetched from the server.
261+
"""
262+
cedar_runner = CedarRunner(options=options, piped_logs_format=piped_logs_format)
263+
if initial_start_callbacks:
264+
cedar_runner.register_process_initial_start_callbacks(initial_start_callbacks)
265+
if rehydration_callbacks:
266+
cedar_runner.register_process_restart_callbacks(rehydration_callbacks)
267+
return cedar_runner

0 commit comments

Comments
 (0)