Skip to content

Commit 31c4a8e

Browse files
Merge pull request #189 from overmindtech/allow-write-for-read
Allow write for read
2 parents 0de6f44 + 4e54ca4 commit 31c4a8e

File tree

2 files changed

+81
-3
lines changed

2 files changed

+81
-3
lines changed

cmd/root.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,14 +307,48 @@ func ensureToken(ctx context.Context, requiredScopes []string) (context.Context,
307307
return ctx, fmt.Errorf("error extracting claims from token: %w", err)
308308
}
309309

310+
ok, missing := HasScopesFlexible(claims, requiredScopes)
311+
312+
if !ok {
313+
return ctx, fmt.Errorf("authenticated successfully, but you don't have the required permission: '%v'", missing)
314+
}
315+
316+
// Add the token to the context
317+
return context.WithValue(ctx, sdp.UserTokenContextKey{}, accessToken), nil
318+
}
319+
320+
// Returns whether a set of claims has all of the required scopes. It also
321+
// accounts for when a user has write access but required read access, they
322+
// aren't the same but the user will have access anyway so this will pass
323+
//
324+
// Returns a bool and the missing permission as a string of any
325+
func HasScopesFlexible(claims *sdp.CustomClaims, requiredScopes []string) (bool, string) {
326+
if claims == nil {
327+
return false, ""
328+
}
329+
310330
for _, scope := range requiredScopes {
311331
if !claims.HasScope(scope) {
312-
return ctx, fmt.Errorf("authenticated successfully, but you don't have the required permission: '%v'", scope)
332+
// If they don't have the *exact* scope, check to see if they have
333+
// write access to the same service
334+
sections := strings.Split(scope, ":")
335+
var hasWriteInstead bool
336+
337+
if len(sections) == 2 {
338+
service, action := sections[0], sections[1]
339+
340+
if action == "read" {
341+
hasWriteInstead = claims.HasScope(fmt.Sprintf("%v:write", service))
342+
}
343+
}
344+
345+
if !hasWriteInstead {
346+
return false, scope
347+
}
313348
}
314349
}
315350

316-
// Add the token to the context
317-
return context.WithValue(ctx, sdp.UserTokenContextKey{}, accessToken), nil
351+
return true, ""
318352
}
319353

320354
// getChangeUuid returns the UUID of a change, as selected by --uuid or --change, or a state with the specified status and having --ticket-link

cmd/root_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cmd
22

33
import (
44
"testing"
5+
6+
"github.com/overmindtech/sdp-go"
57
)
68

79
func TestParseChangeUrl(t *testing.T) {
@@ -24,3 +26,45 @@ func TestParseChangeUrl(t *testing.T) {
2426
}
2527
}
2628
}
29+
30+
func TestHasScopesFlexible(t *testing.T) {
31+
claims := &sdp.CustomClaims{
32+
Scope: "changes:read users:write",
33+
AccountName: "test",
34+
}
35+
36+
tests := []struct {
37+
Name string
38+
RequiredScopes []string
39+
ShouldPass bool
40+
}{
41+
{
42+
Name: "Same scope",
43+
RequiredScopes: []string{"changes:read"},
44+
ShouldPass: true,
45+
},
46+
{
47+
Name: "Multiple scopes",
48+
RequiredScopes: []string{"changes:read", "users:write"},
49+
ShouldPass: true,
50+
},
51+
{
52+
Name: "Missing scope",
53+
RequiredScopes: []string{"changes:read", "users:write", "colours:create"},
54+
ShouldPass: false,
55+
},
56+
{
57+
Name: "Write instead of read",
58+
RequiredScopes: []string{"users:read"},
59+
ShouldPass: true,
60+
},
61+
}
62+
63+
for _, tc := range tests {
64+
t.Run(tc.Name, func(t *testing.T) {
65+
if pass, _ := HasScopesFlexible(claims, tc.RequiredScopes); pass != tc.ShouldPass {
66+
t.Fatalf("expected: %v, got: %v", tc.ShouldPass, !tc.ShouldPass)
67+
}
68+
})
69+
}
70+
}

0 commit comments

Comments
 (0)