Skip to content

Commit 65879d3

Browse files
authored
jwt: support no-force-push ops (#16)
1 parent 3e9187c commit 65879d3

17 files changed

Lines changed: 173 additions & 8 deletions

File tree

File renamed without changes.

README.md renamed to README

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
```
21
PIERRE COMPUTER COMPANY █
32
PROJECT: SDKS
43

@@ -15,4 +14,5 @@ SDKS:
1514
- Typescript
1615
- Python
1716
- Go
18-
```
17+
18+
[SDK DOCUMENTATION](https://code.storage/docs/reference/sdk)

packages/code-storage-go/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,9 @@ func (c *Client) generateJWT(repoID string, options RemoteURLOptions) (string, e
467467
"iat": issuedAt.Unix(),
468468
"exp": issuedAt.Add(ttl).Unix(),
469469
}
470+
if len(options.Ops) > 0 {
471+
claims["ops"] = options.Ops
472+
}
470473

471474
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
472475
return token.SignedString(c.privateKey)

packages/code-storage-go/repo_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,42 @@ func TestImportRemoteURL(t *testing.T) {
6767
}
6868
}
6969

70+
func TestRemoteURLOps(t *testing.T) {
71+
client, err := NewClient(Options{Name: "acme", Key: testKey, StorageBaseURL: "acme.code.storage"})
72+
if err != nil {
73+
t.Fatalf("client error: %v", err)
74+
}
75+
repo := &Repo{ID: "repo-1", DefaultBranch: "main", client: client}
76+
77+
t.Run("includes ops in JWT when provided", func(t *testing.T) {
78+
remote, err := repo.RemoteURL(nil, RemoteURLOptions{
79+
Ops: Ops{OpNoForcePush},
80+
})
81+
if err != nil {
82+
t.Fatalf("remote url error: %v", err)
83+
}
84+
claims := parseJWTFromURL(t, remote)
85+
ops, ok := claims["ops"].([]interface{})
86+
if !ok {
87+
t.Fatalf("expected ops claim to be a list")
88+
}
89+
if len(ops) != 1 || ops[0] != "no-force-push" {
90+
t.Fatalf("unexpected ops: %v", ops)
91+
}
92+
})
93+
94+
t.Run("omits ops from JWT when not provided", func(t *testing.T) {
95+
remote, err := repo.RemoteURL(nil, RemoteURLOptions{})
96+
if err != nil {
97+
t.Fatalf("remote url error: %v", err)
98+
}
99+
claims := parseJWTFromURL(t, remote)
100+
if _, ok := claims["ops"]; ok {
101+
t.Fatalf("expected no ops claim")
102+
}
103+
})
104+
}
105+
70106
func TestListFilesEphemeral(t *testing.T) {
71107
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
72108
if r.URL.Path != "/api/v1/repos/files" {

packages/code-storage-go/types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,21 @@ type Options struct {
3030
HTTPClient *http.Client
3131
}
3232

33+
// Op is a policy operation included in the JWT.
34+
type Op = string
35+
36+
const (
37+
OpNoForcePush Op = "no-force-push"
38+
)
39+
40+
// Ops is a list of policy operations.
41+
type Ops []Op
42+
3343
// RemoteURLOptions configure token generation for remote URLs.
3444
type RemoteURLOptions struct {
3545
Permissions []Permission
3646
TTL time.Duration
47+
Ops Ops
3748
}
3849

3950
// InvocationOptions holds common request options.

packages/code-storage-python/pierre_storage/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
GetBranchDiffResult,
2626
GetCommitDiffResult,
2727
GitStorageOptions,
28+
Op,
29+
OP_NO_FORCE_PUSH,
30+
Ops,
2831
GrepFileMatch,
2932
GrepLine,
3033
GrepResult,
@@ -83,6 +86,9 @@
8386
"GrepLine",
8487
"GrepResult",
8588
"GitStorageOptions",
89+
"Op",
90+
"OP_NO_FORCE_PUSH",
91+
"Ops",
8692
"ListBranchesResult",
8793
"ListCommitsResult",
8894
"ListFilesResult",

packages/code-storage-python/pierre_storage/auth.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def generate_jwt(
1313
repo_id: str,
1414
scopes: Optional[List[str]] = None,
1515
ttl: int = 31536000, # 1 year default
16+
ops: Optional[List[str]] = None,
1617
) -> str:
1718
"""Generate a JWT token for Git storage authentication.
1819
@@ -22,6 +23,7 @@ def generate_jwt(
2223
repo_id: Repository identifier
2324
scopes: List of permission scopes (defaults to ['git:write', 'git:read'])
2425
ttl: Time-to-live in seconds (defaults to 1 year)
26+
ops: List of policy operations (e.g., ['no-force-push'])
2527
2628
Returns:
2729
Signed JWT token string
@@ -41,6 +43,8 @@ def generate_jwt(
4143
"iat": now,
4244
"exp": now + ttl,
4345
}
46+
if ops:
47+
payload["ops"] = ops
4448

4549
# Load the private key and determine algorithm
4650
try:

packages/code-storage-python/pierre_storage/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import uuid
44
from datetime import datetime, timezone
5-
from typing import Any, Dict, Optional, Union, cast
5+
from typing import Any, Dict, List, Optional, Union, cast
66
from urllib.parse import urlencode
77

88
import httpx
@@ -608,6 +608,7 @@ def _generate_jwt(
608608
"""
609609
permissions = ["git:write", "git:read"]
610610
ttl: int = 31536000 # 1 year default
611+
ops: Optional[List[str]] = None
611612

612613
if options:
613614
if "permissions" in options:
@@ -616,6 +617,8 @@ def _generate_jwt(
616617
option_ttl = options["ttl"]
617618
if isinstance(option_ttl, int):
618619
ttl = option_ttl
620+
if "ops" in options:
621+
ops = options["ops"]
619622
elif "default_ttl" in self.options:
620623
default_ttl = self.options["default_ttl"]
621624
if isinstance(default_ttl, int):
@@ -627,6 +630,7 @@ def _generate_jwt(
627630
repo_id,
628631
permissions,
629632
ttl,
633+
ops,
630634
)
631635

632636

packages/code-storage-python/pierre_storage/repo.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,14 @@ async def get_remote_url(
167167
*,
168168
permissions: Optional[list[str]] = None,
169169
ttl: Optional[int] = None,
170+
ops: Optional[list[str]] = None,
170171
) -> str:
171172
"""Get remote URL for Git operations.
172173
173174
Args:
174175
permissions: List of permissions (e.g., ["git:write", "git:read"])
175176
ttl: Token TTL in seconds
177+
ops: List of policy operations (e.g., ["no-force-push"])
176178
177179
Returns:
178180
Git remote URL with embedded JWT
@@ -182,6 +184,8 @@ async def get_remote_url(
182184
options["permissions"] = permissions
183185
if ttl is not None:
184186
options["ttl"] = ttl
187+
if ops is not None:
188+
options["ops"] = ops
185189

186190
jwt_token = self.generate_jwt(self._id, options if options else None)
187191
url = f"https://t:{jwt_token}@{self.storage_base_url}/{self._id}.git"
@@ -192,35 +196,39 @@ async def get_import_remote_url(
192196
*,
193197
permissions: Optional[list[str]] = None,
194198
ttl: Optional[int] = None,
199+
ops: Optional[list[str]] = None,
195200
) -> str:
196201
"""Get import remote URL for Git operations.
197202
198203
Args:
199204
permissions: List of permissions (e.g., ["git:write", "git:read"])
200205
ttl: Token TTL in seconds
206+
ops: List of policy operations (e.g., ["no-force-push"])
201207
202208
Returns:
203209
Git remote URL with embedded JWT pointing to import namespace
204210
"""
205-
url = await self.get_remote_url(permissions=permissions, ttl=ttl)
211+
url = await self.get_remote_url(permissions=permissions, ttl=ttl, ops=ops)
206212
return url.replace(".git", "+import.git")
207213

208214
async def get_ephemeral_remote_url(
209215
self,
210216
*,
211217
permissions: Optional[list[str]] = None,
212218
ttl: Optional[int] = None,
219+
ops: Optional[list[str]] = None,
213220
) -> str:
214221
"""Get ephemeral remote URL for Git operations.
215222
216223
Args:
217224
permissions: List of permissions (e.g., ["git:write", "git:read"])
218225
ttl: Token TTL in seconds
226+
ops: List of policy operations (e.g., ["no-force-push"])
219227
220228
Returns:
221229
Git remote URL with embedded JWT pointing to ephemeral namespace
222230
"""
223-
url = await self.get_remote_url(permissions=permissions, ttl=ttl)
231+
url = await self.get_remote_url(permissions=permissions, ttl=ttl, ops=ops)
224232
return url.replace(".git", "+ephemeral.git")
225233

226234
async def get_file_stream(

packages/code-storage-python/pierre_storage/types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ class GitStorageOptions(TypedDict, total=False):
4141
default_ttl: Optional[int]
4242

4343

44+
# Op is a policy operation included in the JWT.
45+
Op = str
46+
47+
OP_NO_FORCE_PUSH: Op = "no-force-push"
48+
49+
# Ops is a list of policy operations.
50+
Ops = List[Op]
51+
52+
4453
class PublicGitHubBaseRepoAuth(TypedDict):
4554
"""Authentication mode for GitHub base repositories."""
4655

@@ -508,6 +517,7 @@ async def get_remote_url(
508517
*,
509518
permissions: Optional[list[str]] = None,
510519
ttl: Optional[int] = None,
520+
ops: Optional[list[str]] = None,
511521
) -> str:
512522
"""Get the remote URL for the repository."""
513523
...
@@ -517,6 +527,7 @@ async def get_import_remote_url(
517527
*,
518528
permissions: Optional[list[str]] = None,
519529
ttl: Optional[int] = None,
530+
ops: Optional[list[str]] = None,
520531
) -> str:
521532
"""Get the import remote URL for the repository."""
522533
...
@@ -526,6 +537,7 @@ async def get_ephemeral_remote_url(
526537
*,
527538
permissions: Optional[list[str]] = None,
528539
ttl: Optional[int] = None,
540+
ops: Optional[list[str]] = None,
529541
) -> str:
530542
"""Get the ephemeral remote URL for the repository."""
531543
...

0 commit comments

Comments
 (0)