Skip to content

Commit e0194d6

Browse files
committed
Merge branch 'lcian/feat/batching-server' into lcian/feat/batching-service
2 parents ece0cf6 + dbee886 commit e0194d6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1433
-553
lines changed

.github/workflows/build-clients.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
2020

2121
- name: Build artifacts
22-
run: uv build --package objectstore-client
22+
run: uv build --package objectstore-client --no-dev
2323

2424
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
2525
with:

.secret_scan_ignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
objectstore-test\/config\/ed25519\.private\.pem
2+
objectstore-test\/config\/ed25519\.public\.pem
3+
clients\/python\/tests\/ed25519\.private\.pem
4+
clients\/python\/tests\/ed25519\.public\.pem

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@
1919
"[markdown]": {
2020
"editor.rulers": [80],
2121
"editor.tabSize": 2,
22+
},
23+
"[python]": {
24+
"editor.rulers": [88],
2225
}
2326
}

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ futures-util = "0.3.31"
3636
http = "1.3.1"
3737
humantime = "2.2.0"
3838
humantime-serde = "1.1.1"
39+
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
3940
merni = "0.1.3"
4041
mimalloc = { version = "0.1.48", features = ["v3", "override"] }
4142
rand = "0.9.1"
@@ -49,4 +50,5 @@ tokio = "1.47.0"
4950
tokio-stream = "0.1.17"
5051
tokio-util = "0.7.15"
5152
tracing = "0.1.41"
53+
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
5254
uuid = { version = "1.17.0", features = ["v4"] }

clients/python/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,21 @@ import urllib3
1414
from objectstore_client import (
1515
Client,
1616
NoOpMetricsBackend,
17+
Permission,
1718
TimeToIdle,
1819
TimeToLive,
20+
TokenGenerator,
1921
Usecase,
2022
)
2123

24+
# Necessary when using Objectstore instances that enforce authorization checks.
25+
token_generator = TokenGenerator(
26+
"my-key-id",
27+
"<securely inject EdDSA private key>",
28+
expiry_seconds=60,
29+
permissions=Permission.max(),
30+
)
31+
2232
# This should be stored in a global variable and reused, in order to reuse the connection
2333
client = Client(
2434
"http://localhost:8888",
@@ -31,6 +41,8 @@ client = Client(
3141
retries=3, # Number of connection retries
3242
# For further customization, provide additional kwargs for urllib3.HTTPConnectionPool
3343
connection_kwargs={"maxsize": 10},
44+
# Optionally, provide a token generator for Objectstore instances with authorization enforced
45+
token_generator=token_generator,
3446
)
3547

3648
# This could also be stored in a global/shared variable, as you will deal with a fixed number of usecases with statically defined defaults

clients/python/docs/objectstore_client.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ objectstore\_client package
99
Submodules
1010
----------
1111

12+
objectstore\_client.auth module
13+
-------------------------------
14+
15+
.. automodule:: objectstore_client.auth
16+
:members:
17+
:show-inheritance:
18+
:undoc-members:
19+
1220
objectstore\_client.client module
1321
---------------------------------
1422

@@ -32,3 +40,19 @@ objectstore\_client.metrics module
3240
:members:
3341
:show-inheritance:
3442
:undoc-members:
43+
44+
objectstore\_client.scope module
45+
--------------------------------
46+
47+
.. automodule:: objectstore_client.scope
48+
:members:
49+
:show-inheritance:
50+
:undoc-members:
51+
52+
objectstore\_client.utils module
53+
--------------------------------
54+
55+
.. automodule:: objectstore_client.utils
56+
:members:
57+
:show-inheritance:
58+
:undoc-members:

clients/python/pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@ name = "objectstore-client"
33
version = "0.0.14"
44
description = "Client SDK for Objectstore, the Sentry object storage platform"
55
readme = "README.md"
6-
authors = [
7-
{name = "Sentry", email = "oss@sentry.io"},
8-
]
9-
homepage = "https://getsentry.github.io/objectstore/"
10-
repository = "https://github.com/getsentry/objectstore"
6+
authors = [{ name = "Sentry", email = "oss@sentry.io" }]
117
license = { file = "LICENSE.md" }
128
requires-python = ">=3.11"
139
dependencies = [
1410
"sentry-sdk>=2.42.1",
1511
"urllib3>=2.2.2",
1612
"zstandard>=0.18.0",
1713
"filetype>=1.2.0",
14+
"PyJWT[crypto]>=2.10.1",
1815
]
1916

2017
[build-system]
2118
requires = ["uv_build"]
2219
build-backend = "uv_build"
20+
21+
[dependency-groups]
22+
dev = []

clients/python/src/objectstore_client/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from objectstore_client.auth import Permission, TokenGenerator
12
from objectstore_client.client import (
23
Client,
34
GetResponse,
@@ -23,8 +24,10 @@
2324
"Compression",
2425
"ExpirationPolicy",
2526
"Metadata",
27+
"Permission",
2628
"TimeToIdle",
2729
"TimeToLive",
30+
"TokenGenerator",
2831
"MetricsBackend",
2932
"NoOpMetricsBackend",
3033
]
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from datetime import UTC, datetime, timedelta
2+
from enum import StrEnum
3+
from typing import Self
4+
5+
import jwt
6+
7+
from objectstore_client.scope import Scope
8+
9+
10+
class Permission(StrEnum):
11+
"""
12+
Enum listing permissions that Objectstore tokens may be granted.
13+
"""
14+
15+
OBJECT_READ = "object.read"
16+
OBJECT_WRITE = "object.write"
17+
OBJECT_DELETE = "object.delete"
18+
19+
@classmethod
20+
def max(cls) -> list[Self]:
21+
return list(cls.__members__.values())
22+
23+
24+
class TokenGenerator:
25+
def __init__(
26+
self,
27+
kid: str,
28+
secret_key: str,
29+
expiry_seconds: int = 60,
30+
permissions: list[Permission] = Permission.max(),
31+
):
32+
self.kid = kid
33+
self.secret_key = secret_key
34+
self.expiry_seconds = expiry_seconds
35+
self.permissions = permissions
36+
37+
def sign_for_scope(self, usecase: str, scope: Scope) -> str:
38+
"""
39+
Sign a JWT for the passed-in usecase and scope using the configured key
40+
information, expiry, and permissions.
41+
42+
The JWT is signed using EdDSA, so `self.secret_key` must be an EdDSA private
43+
key. `self.kid` is used by the Objectstore server to load the corresponding
44+
public key from its configuration.
45+
"""
46+
headers = {"kid": self.kid}
47+
claims = {
48+
"res": {
49+
"os:usecase": usecase,
50+
**{k: str(v) for k, v in scope.dict().items()},
51+
},
52+
"permissions": self.permissions,
53+
"exp": datetime.now(tz=UTC) + timedelta(seconds=self.expiry_seconds),
54+
}
55+
56+
return jwt.encode(claims, self.secret_key, algorithm="EdDSA", headers=headers)

0 commit comments

Comments
 (0)