Skip to content
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 packages/code-storage-go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,9 @@ func (c *Client) generateJWT(repoID string, options RemoteURLOptions) (string, e
"iat": issuedAt.Unix(),
"exp": issuedAt.Add(ttl).Unix(),
}
if len(options.RefPolicies) > 0 {
claims["refs"] = encodeRefsClaim(options.RefPolicies)
}
if len(options.Ops) > 0 {
claims["ops"] = options.Ops
}
Expand Down
2 changes: 1 addition & 1 deletion packages/code-storage-go/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func (b *CommitBuilder) Send(ctx context.Context) (CommitResult, error) {
}

ttl := resolveCommitTTL(b.options.InvocationOptions, defaultTokenTTL)
jwtToken, err := b.client.generateJWT(b.repoID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := b.client.generateJWT(b.repoID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: b.options.RefPolicies})
if err != nil {
return CommitResult{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion packages/code-storage-go/diff_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (d *diffCommitExecutor) send(ctx context.Context, repoID string) (CommitRes
}

ttl := resolveCommitTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := d.client.generateJWT(repoID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := d.client.generateJWT(repoID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return CommitResult{}, err
}
Expand Down
14 changes: 14 additions & 0 deletions packages/code-storage-go/jwt_claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package storage

// encodeRefsClaim builds the JWT `refs` claim as an array of [pattern, ops] tuples.
func encodeRefsClaim(refs RefPolicyList) []any {
out := make([]any, len(refs))
for i, rule := range refs {
opList := []string(rule.Ops)
if opList == nil {
opList = []string{}
}
out[i] = []any{rule.Pattern, opList}
}
Comment thread
unknwon marked this conversation as resolved.
return out
}
36 changes: 36 additions & 0 deletions packages/code-storage-go/jwt_claims_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package storage

import "testing"

func TestEncodeRefsClaim(t *testing.T) {
encoded := encodeRefsClaim(RefPolicyList{
{Pattern: "refs/heads/main", Ops: Ops{OpNoPush}},
{Pattern: "refs/heads/release/*", Ops: nil},
{Pattern: "*", Ops: Ops{OpNoForcePush}},
})

if len(encoded) != 3 {
t.Fatalf("expected 3 rules, got %d", len(encoded))
}

mainRule, ok := encoded[0].([]any)
if !ok {
t.Fatalf("unexpected rule type: %T", encoded[0])
}
if mainRule[0] != "refs/heads/main" {
t.Fatalf("unexpected pattern: %v", mainRule[0])
}
mainOps, ok := mainRule[1].([]string)
if !ok || len(mainOps) != 1 || mainOps[0] != OpNoPush {
Comment thread
unknwon marked this conversation as resolved.
t.Fatalf("unexpected ops: %v", mainRule[1])
}

releaseRule, ok := encoded[1].([]any)
if !ok {
t.Fatalf("unexpected rule type: %T", encoded[1])
}
releaseOps, ok := releaseRule[1].([]string)
if !ok || len(releaseOps) != 0 {
t.Fatalf("expected empty ops slice, got %v", releaseRule[1])
}
}
24 changes: 12 additions & 12 deletions packages/code-storage-go/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,12 +536,12 @@ func (r *Repo) GetNote(ctx context.Context, options GetNoteOptions) (GetNoteResu

// CreateNote adds a git note.
func (r *Repo) CreateNote(ctx context.Context, options CreateNoteOptions) (NoteWriteResult, error) {
return r.writeNote(ctx, options.InvocationOptions, "add", options.SHA, options.Note, options.ExpectedRefSHA, options.Author)
return r.writeNote(ctx, options.InvocationOptions, "add", options.SHA, options.Note, options.ExpectedRefSHA, options.Author, options.RefPolicies)
}

// AppendNote appends to a git note.
func (r *Repo) AppendNote(ctx context.Context, options AppendNoteOptions) (NoteWriteResult, error) {
return r.writeNote(ctx, options.InvocationOptions, "append", options.SHA, options.Note, options.ExpectedRefSHA, options.Author)
return r.writeNote(ctx, options.InvocationOptions, "append", options.SHA, options.Note, options.ExpectedRefSHA, options.Author, options.RefPolicies)
}

// DeleteNote deletes a git note.
Expand All @@ -552,7 +552,7 @@ func (r *Repo) DeleteNote(ctx context.Context, options DeleteNoteOptions) (NoteW
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return NoteWriteResult{}, err
}
Expand Down Expand Up @@ -592,7 +592,7 @@ func (r *Repo) DeleteNote(ctx context.Context, options DeleteNoteOptions) (NoteW
return result, nil
}

func (r *Repo) writeNote(ctx context.Context, invocation InvocationOptions, action string, sha string, note string, expectedRefSHA string, author *NoteAuthor) (NoteWriteResult, error) {
func (r *Repo) writeNote(ctx context.Context, invocation InvocationOptions, action string, sha string, note string, expectedRefSHA string, author *NoteAuthor, refPolicies RefPolicyList) (NoteWriteResult, error) {
sha = strings.TrimSpace(sha)
if sha == "" {
return NoteWriteResult{}, errors.New("note sha is required")
Expand All @@ -604,7 +604,7 @@ func (r *Repo) writeNote(ctx context.Context, invocation InvocationOptions, acti
}

ttl := resolveInvocationTTL(invocation, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: refPolicies})
if err != nil {
return NoteWriteResult{}, err
}
Expand Down Expand Up @@ -862,7 +862,7 @@ func (r *Repo) Grep(ctx context.Context, options GrepOptions) (GrepResult, error
// PullUpstream triggers a pull-upstream operation.
func (r *Repo) PullUpstream(ctx context.Context, options PullUpstreamOptions) error {
ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return err
}
Expand Down Expand Up @@ -897,7 +897,7 @@ func (r *Repo) CreateBranch(ctx context.Context, options CreateBranchOptions) (C
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return CreateBranchResult{}, err
}
Expand Down Expand Up @@ -944,7 +944,7 @@ func (r *Repo) DeleteBranch(ctx context.Context, options DeleteBranchOptions) (D
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return DeleteBranchResult{}, err
}
Expand Down Expand Up @@ -1020,7 +1020,7 @@ func (r *Repo) Merge(ctx context.Context, options MergeOptions) (MergeResult, er
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return MergeResult{}, err
}
Expand Down Expand Up @@ -1081,7 +1081,7 @@ func (r *Repo) CreateTag(ctx context.Context, options CreateTagOptions) (CreateT
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return CreateTagResult{}, err
}
Expand Down Expand Up @@ -1116,7 +1116,7 @@ func (r *Repo) DeleteTag(ctx context.Context, options DeleteTagOptions) (DeleteT
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitRead, PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitRead, PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return DeleteTagResult{}, err
}
Expand Down Expand Up @@ -1159,7 +1159,7 @@ func (r *Repo) RestoreCommit(ctx context.Context, options RestoreCommitOptions)
}

ttl := resolveCommitTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, RefPolicies: options.RefPolicies})
if err != nil {
return RestoreCommitResult{}, err
}
Expand Down
50 changes: 50 additions & 0 deletions packages/code-storage-go/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,56 @@ func TestRemoteURLOps(t *testing.T) {
})
}

func TestRemoteURLRefs(t *testing.T) {
client, err := NewClient(Options{Name: "acme", Key: testKey, StorageBaseURL: "acme.code.storage"})
if err != nil {
t.Fatalf("client error: %v", err)
}
repo := &Repo{ID: "repo-1", DefaultBranch: "main", client: client}

t.Run("includes refs in JWT when provided", func(t *testing.T) {
remote, err := repo.RemoteURL(nil, RemoteURLOptions{
RefPolicies: RefPolicyList{
{Pattern: "refs/heads/main", Ops: Ops{OpNoPush}},
{Pattern: "*", Ops: Ops{OpNoForcePush}},
},
})
if err != nil {
t.Fatalf("remote url error: %v", err)
}
claims := parseJWTFromURL(t, remote)
refs, ok := claims["refs"].([]interface{})
if !ok {
t.Fatalf("expected refs claim to be a list, got %T", claims["refs"])
}
if len(refs) != 2 {
t.Fatalf("expected 2 ref rules, got %d", len(refs))
}
mainRule, ok := refs[0].([]interface{})
if !ok || len(mainRule) != 2 {
t.Fatalf("unexpected main rule shape: %v", refs[0])
}
if mainRule[0] != "refs/heads/main" {
t.Fatalf("unexpected pattern: %v", mainRule[0])
}
mainOps, ok := mainRule[1].([]interface{})
if !ok || len(mainOps) != 1 || mainOps[0] != "no-push" {
t.Fatalf("unexpected main ops: %v", mainRule[1])
}
})

t.Run("omits refs from JWT when not provided", func(t *testing.T) {
remote, err := repo.RemoteURL(nil, RemoteURLOptions{})
if err != nil {
t.Fatalf("remote url error: %v", err)
}
claims := parseJWTFromURL(t, remote)
if _, ok := claims["refs"]; ok {
t.Fatalf("expected no refs claim")
}
})
}

func TestListFilesEphemeral(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/files" {
Expand Down
41 changes: 40 additions & 1 deletion packages/code-storage-go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,31 @@ type Op = string

const (
OpNoForcePush Op = "no-force-push"
OpNoPush Op = "no-push"
)

// Ops is a list of policy operations.
type Ops []Op

// RefPolicy is a single ordered ref-matching policy rule (first match wins).
type RefPolicy struct {
Pattern string
Ops Ops
}

// RefPolicyList is an ordered list of per-ref policy rules for the JWT `refs` claim.
type RefPolicyList []RefPolicy

// RemoteURLOptions configure token generation for remote URLs.
type RemoteURLOptions struct {
Permissions []Permission
TTL time.Duration
Ops Ops
// Ops is a repo-wide policy ops list.
//
// Deprecated: Use RefPolicies instead.
Ops Ops
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// InvocationOptions holds common request options.
Expand Down Expand Up @@ -232,6 +247,8 @@ type ArchiveOptions struct {
type PullUpstreamOptions struct {
InvocationOptions
Ref string
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// ListFilesOptions configures list files.
Expand Down Expand Up @@ -309,6 +326,8 @@ type CreateBranchOptions struct {
TargetBranch string
BaseIsEphemeral bool
TargetIsEphemeral bool
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// CreateBranchResult describes branch creation result.
Expand All @@ -324,6 +343,8 @@ type DeleteBranchOptions struct {
InvocationOptions
Name string
Ephemeral *bool
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// DeleteBranchResult describes branch deletion result.
Expand Down Expand Up @@ -357,6 +378,8 @@ type MergeOptions struct {
AllowUnrelatedHistories bool
// Squash is incompatible with MergeStrategyFFOnly.
Squash bool
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// MergeResultStatus describes a merge operation outcome.
Expand Down Expand Up @@ -422,6 +445,8 @@ type CreateTagOptions struct {
InvocationOptions
Name string
Target string
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// CreateTagResult describes tag creation result.
Expand All @@ -435,6 +460,8 @@ type CreateTagResult struct {
type DeleteTagOptions struct {
InvocationOptions
Name string
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// DeleteTagResult describes tag deletion result.
Expand Down Expand Up @@ -544,6 +571,8 @@ type CreateNoteOptions struct {
Note string
ExpectedRefSHA string
Author *NoteAuthor
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// AppendNoteOptions configures note append.
Expand All @@ -553,6 +582,8 @@ type AppendNoteOptions struct {
Note string
ExpectedRefSHA string
Author *NoteAuthor
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// DeleteNoteOptions configures note delete.
Expand All @@ -561,6 +592,8 @@ type DeleteNoteOptions struct {
SHA string
ExpectedRefSHA string
Author *NoteAuthor
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// NoteWriteResult describes note write response.
Expand Down Expand Up @@ -799,6 +832,8 @@ type CommitOptions struct {
EphemeralBase bool
Author CommitSignature
Committer *CommitSignature
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// CommitFromDiffOptions configures diff commit.
Expand All @@ -813,6 +848,8 @@ type CommitFromDiffOptions struct {
EphemeralBase bool
Author CommitSignature
Committer *CommitSignature
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// RestoreCommitOptions configures restore commit.
Expand All @@ -824,6 +861,8 @@ type RestoreCommitOptions struct {
ExpectedHeadSHA string
Author CommitSignature
Committer *CommitSignature
// RefPolicies is evaluated in declaration order. The first matching rule wins.
RefPolicies RefPolicyList
}

// RestoreCommitResult describes restore commit.
Expand Down
2 changes: 1 addition & 1 deletion packages/code-storage-go/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package storage

const (
PackageName = "code-storage-go-sdk"
PackageVersion = "0.8.0"
PackageVersion = "0.9.0"
)

func userAgent() string {
Expand Down
Loading
Loading