Skip to content

Commit

Permalink
feat: Add gRPC client and admin API capabilities (#29)
Browse files Browse the repository at this point in the history
* Add a new gRPC client whilst maintaining backwards compatibility with
  the old HTTP client.
* Add support to access the admin API via the new gRPC client.

Signed-off-by: Sam Lock <sam@swlock.co.uk>
Co-authored-by: Charith Ellawala <charithe@users.noreply.github.com>
  • Loading branch information
Sambigeara and charithe authored Jul 11, 2023
1 parent a15c392 commit 5151726
Show file tree
Hide file tree
Showing 101 changed files with 8,695 additions and 365 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

### Enhancements

- Refactor to use gRPC

## v0.7.1 (2023-06-07)

### Enhancements
Expand Down
111 changes: 109 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Cerbos Python SDK
=================

Python client for accessing [Cerbos](https://cerbos.dev).
Python clients for accessing [Cerbos](https://cerbos.dev).

Cerbos is the open core, language-agnostic, scalable authorization solution that makes user permissions and authorization simple to implement and manage by writing context-aware access control policies for your application resources.

Expand All @@ -13,6 +13,109 @@ This library is available from PyPI as `cerbos`. It supports both async and non-
pip install cerbos
```

There are two clients available; [gRPC](#grpc-client) and [HTTP](#http-client). New projects should use the gRPC client.

### gRPC Client

(Available from v0.8.0 onwards)

**Making a request**

```python
from cerbos.sdk.grpc.client import CerbosClient
from cerbos.engine.v1 import engine_pb2
from cerbos.request.v1 import request_pb2
from google.protobuf.struct_pb2 import Value

principal = engine_pb2.Principal(
id="john",
roles={"employee"},
policy_version="20210210",
attr={
"department": Value(string_value="marketing"),
"geography": Value(string_value="GB"),
"team": Value(string_value="design"),
},
)

resource = engine_pb2.Resource(
id="XX125",
kind="leave_request",
attr={
"id": Value(string_value="XX125"),
"department": Value(string_value="marketing"),
"geography": Value(string_value="GB"),
"team": Value(string_value="design"),
"owner": Value(string_value="john"),
}
)

plan_resource = engine_pb2.PlanResourcesInput.Resource(
kind="leave_request",
policy_version="20210210"
)

with CerbosClient("localhost:3593", tls_verify=False) as c:
# Check a single action on a single resource
if c.is_allowed("view", principal, resource):
# perform some action
pass

# Get the query plan for "view" action
plan = c.plan_resources(action="view", principal=principal, resource=plan_resource)
````

**Async usage**

```python
from cerbos.sdk.grpc.client import AsyncCerbosClient

async with AsyncCerbosClient("localhost:3593", tls_verify=False) as c:
...

allowed = await c.is_allowed("view:public", p, r)
print(allowed)

# Get the query plan for "view" action
...
plan = await c.plan_resources("view", p, rd)
print(plan.filter.to_json())

```

**Admin API**

There is also a client available for interacting with the Admin API. See [the docs](https://docs.cerbos.dev/cerbos/latest/api/admin_api.html) for information on how to configure your PDP to enable this.

```python
from cerbos.policy.v1 import policy_pb2
from cerbos.sdk.grpc.client import AdminCredentials, AsyncCerbosAdminClient

admin_credentials = AdminCredentials(username="admin", password="some_password")
async with AsyncCerbosAdminClient("localhost:3593", admin_credentials=admin_credentials) as c:
await c.add_or_update_policy(
[
policy_pb2.Policy(
api_version="api.cerbos.dev/v1",
principal_policy=policy_pb2.PrincipalPolicy(
principal="terry", version="default"
),
)
]
)
```

**Connecting to a Unix domain socket**

```python
with CerbosClient("unix:/var/cerbos.sock", tls_verify=False) as c:
...
```

### HTTP client

We maintain this for backwards compatibility. It is recommended to use the [gRPC client](#grpc-client).

**Making a request**

```python
Expand Down Expand Up @@ -52,7 +155,6 @@ with CerbosClient("https://localhost:3592", debug=True, tls_verify=False) as c:

**Async usage**


```python
from cerbos.sdk.model import *
from cerbos.sdk.client import AsyncCerbosClient
Expand Down Expand Up @@ -104,6 +206,11 @@ with container:

See the tests available in the `tests` directory for more examples.

## Contributing

The gRPC client uses protoc generated python classes from definitions retrieved from our [buf registry](https://buf.build/cerbos/cerbos-api).
When making changes to this library, be sure to run the `./proto/generate_protos.sh` to update definitions and generate python classes.

## Get help

- Visit the [Cerbos website](https://cerbos.dev)
Expand Down
47 changes: 47 additions & 0 deletions cerbos/audit/v1/audit_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 96 additions & 0 deletions cerbos/audit/v1/audit_pb2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from cerbos.engine.v1 import engine_pb2 as _engine_pb2
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union

DESCRIPTOR: _descriptor.FileDescriptor

class AccessLogEntry(_message.Message):
__slots__ = ["call_id", "timestamp", "peer", "metadata", "method", "status_code"]
class MetadataEntry(_message.Message):
__slots__ = ["key", "value"]
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: MetaValues
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[MetaValues, _Mapping]] = ...) -> None: ...
CALL_ID_FIELD_NUMBER: _ClassVar[int]
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
PEER_FIELD_NUMBER: _ClassVar[int]
METADATA_FIELD_NUMBER: _ClassVar[int]
METHOD_FIELD_NUMBER: _ClassVar[int]
STATUS_CODE_FIELD_NUMBER: _ClassVar[int]
call_id: str
timestamp: _timestamp_pb2.Timestamp
peer: Peer
metadata: _containers.MessageMap[str, MetaValues]
method: str
status_code: int
def __init__(self, call_id: _Optional[str] = ..., timestamp: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., peer: _Optional[_Union[Peer, _Mapping]] = ..., metadata: _Optional[_Mapping[str, MetaValues]] = ..., method: _Optional[str] = ..., status_code: _Optional[int] = ...) -> None: ...

class DecisionLogEntry(_message.Message):
__slots__ = ["call_id", "timestamp", "peer", "inputs", "outputs", "error", "check_resources", "plan_resources", "metadata"]
class CheckResources(_message.Message):
__slots__ = ["inputs", "outputs", "error"]
INPUTS_FIELD_NUMBER: _ClassVar[int]
OUTPUTS_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
inputs: _containers.RepeatedCompositeFieldContainer[_engine_pb2.CheckInput]
outputs: _containers.RepeatedCompositeFieldContainer[_engine_pb2.CheckOutput]
error: str
def __init__(self, inputs: _Optional[_Iterable[_Union[_engine_pb2.CheckInput, _Mapping]]] = ..., outputs: _Optional[_Iterable[_Union[_engine_pb2.CheckOutput, _Mapping]]] = ..., error: _Optional[str] = ...) -> None: ...
class PlanResources(_message.Message):
__slots__ = ["input", "output", "error"]
INPUT_FIELD_NUMBER: _ClassVar[int]
OUTPUT_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
input: _engine_pb2.PlanResourcesInput
output: _engine_pb2.PlanResourcesOutput
error: str
def __init__(self, input: _Optional[_Union[_engine_pb2.PlanResourcesInput, _Mapping]] = ..., output: _Optional[_Union[_engine_pb2.PlanResourcesOutput, _Mapping]] = ..., error: _Optional[str] = ...) -> None: ...
class MetadataEntry(_message.Message):
__slots__ = ["key", "value"]
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: MetaValues
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[MetaValues, _Mapping]] = ...) -> None: ...
CALL_ID_FIELD_NUMBER: _ClassVar[int]
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
PEER_FIELD_NUMBER: _ClassVar[int]
INPUTS_FIELD_NUMBER: _ClassVar[int]
OUTPUTS_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
CHECK_RESOURCES_FIELD_NUMBER: _ClassVar[int]
PLAN_RESOURCES_FIELD_NUMBER: _ClassVar[int]
METADATA_FIELD_NUMBER: _ClassVar[int]
call_id: str
timestamp: _timestamp_pb2.Timestamp
peer: Peer
inputs: _containers.RepeatedCompositeFieldContainer[_engine_pb2.CheckInput]
outputs: _containers.RepeatedCompositeFieldContainer[_engine_pb2.CheckOutput]
error: str
check_resources: DecisionLogEntry.CheckResources
plan_resources: DecisionLogEntry.PlanResources
metadata: _containers.MessageMap[str, MetaValues]
def __init__(self, call_id: _Optional[str] = ..., timestamp: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., peer: _Optional[_Union[Peer, _Mapping]] = ..., inputs: _Optional[_Iterable[_Union[_engine_pb2.CheckInput, _Mapping]]] = ..., outputs: _Optional[_Iterable[_Union[_engine_pb2.CheckOutput, _Mapping]]] = ..., error: _Optional[str] = ..., check_resources: _Optional[_Union[DecisionLogEntry.CheckResources, _Mapping]] = ..., plan_resources: _Optional[_Union[DecisionLogEntry.PlanResources, _Mapping]] = ..., metadata: _Optional[_Mapping[str, MetaValues]] = ...) -> None: ...

class MetaValues(_message.Message):
__slots__ = ["values"]
VALUES_FIELD_NUMBER: _ClassVar[int]
values: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, values: _Optional[_Iterable[str]] = ...) -> None: ...

class Peer(_message.Message):
__slots__ = ["address", "auth_info", "user_agent", "forwarded_for"]
ADDRESS_FIELD_NUMBER: _ClassVar[int]
AUTH_INFO_FIELD_NUMBER: _ClassVar[int]
USER_AGENT_FIELD_NUMBER: _ClassVar[int]
FORWARDED_FOR_FIELD_NUMBER: _ClassVar[int]
address: str
auth_info: str
user_agent: str
forwarded_for: str
def __init__(self, address: _Optional[str] = ..., auth_info: _Optional[str] = ..., user_agent: _Optional[str] = ..., forwarded_for: _Optional[str] = ...) -> None: ...
4 changes: 4 additions & 0 deletions cerbos/audit/v1/audit_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

27 changes: 27 additions & 0 deletions cerbos/effect/v1/effect_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions cerbos/effect/v1/effect_pb2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from typing import ClassVar as _ClassVar

DESCRIPTOR: _descriptor.FileDescriptor

class Effect(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = []
EFFECT_UNSPECIFIED: _ClassVar[Effect]
EFFECT_ALLOW: _ClassVar[Effect]
EFFECT_DENY: _ClassVar[Effect]
EFFECT_NO_MATCH: _ClassVar[Effect]
EFFECT_UNSPECIFIED: Effect
EFFECT_ALLOW: Effect
EFFECT_DENY: Effect
EFFECT_NO_MATCH: Effect
4 changes: 4 additions & 0 deletions cerbos/effect/v1/effect_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

Loading

0 comments on commit 5151726

Please sign in to comment.