Skip to content

Commit 0c93e32

Browse files
updates
1 parent 8e9b714 commit 0c93e32

11 files changed

Lines changed: 504 additions & 54 deletions

File tree

cmd/auth.go

Lines changed: 167 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,77 +3,201 @@ package cmd
33
import (
44
"context"
55
"encoding/json"
6-
"log"
6+
"fmt"
7+
"io"
78
"sort"
9+
"strings"
810

911
"github.com/calypr/data-client/g3client"
1012
"github.com/calypr/data-client/logs"
1113
"github.com/spf13/cobra"
1214
)
1315

16+
const authSummaryResourceLimit = 10
17+
1418
func init() {
1519
var profile string
20+
var showAll bool
21+
var jsonOutput bool
1622
var authCmd = &cobra.Command{
17-
Use: "auth",
18-
Short: "Return resource access privileges from profile",
19-
Long: `Gets resource access privileges for specified profile.`,
20-
Example: `./data-client auth --profile=<profile-name>`,
21-
Run: func(cmd *cobra.Command, args []string) {
23+
Use: "auth",
24+
Short: "Return resource access privileges from profile",
25+
Long: `Gets resource access privileges for specified profile.`,
26+
Example: `./data-client auth --profile=<profile-name>
27+
./data-client auth --profile=<profile-name> --all
28+
./data-client auth --profile=<profile-name> --json`,
29+
RunE: func(cmd *cobra.Command, args []string) error {
2230
// don't initialize transmission logs for non-uploading related commands
2331

24-
logger, logCloser := logs.New(profile, logs.WithConsole())
32+
logger, logCloser := logs.New(profile, logs.WithNoConsole())
2533
defer logCloser()
2634

2735
g3i, err := g3client.NewGen3Interface(
2836
profile, logger,
2937
g3client.WithClients(g3client.FenceClient),
3038
)
3139
if err != nil {
32-
log.Fatalf("Fatal NewGen3Interface error: %s\n", err)
40+
return fmt.Errorf("new Gen3 interface: %w", err)
3341
}
3442

3543
resourceAccess, err := g3i.FenceClient().CheckPrivileges(context.Background())
3644
if err != nil {
37-
g3i.Logger().Fatalf("Fatal authentication error: %s\n", err)
38-
} else {
39-
if len(resourceAccess) == 0 {
40-
g3i.Logger().Printf("\nYou don't currently have access to any resources at %s\n", g3i.Credentials().Current().APIEndpoint)
41-
} else {
42-
g3i.Logger().Printf("\nYou have access to the following resource(s) at %s:\n", g3i.Credentials().Current().APIEndpoint)
43-
44-
// Sort by resource name
45-
resources := make([]string, 0, len(resourceAccess))
46-
for resource := range resourceAccess {
47-
resources = append(resources, resource)
48-
}
49-
sort.Strings(resources)
50-
51-
for _, project := range resources {
52-
// Sort by access name if permissions are from Fence
53-
permissions := resourceAccess[project].([]any)
54-
_, isFencePermission := permissions[0].(string)
55-
if isFencePermission {
56-
access := make([]string, 0, len(permissions))
57-
for _, permission := range permissions {
58-
access = append(access, permission.(string))
59-
}
60-
sort.Strings(access)
61-
g3i.Logger().Printf("%s %s\n", project, access)
62-
} else {
63-
// Permissions from Arborist already sorted, just pretty print them
64-
marshalledPermissions, err := json.MarshalIndent(permissions, "", " ")
65-
if err != nil {
66-
g3i.Logger().Printf("%s (error occurred when marshalling permissions): %s\n", project, err)
67-
}
68-
g3i.Logger().Printf("%s %s\n", project, marshalledPermissions)
69-
}
70-
}
71-
}
45+
return fmt.Errorf("authentication: %w", err)
46+
}
47+
48+
if jsonOutput {
49+
encoder := json.NewEncoder(cmd.OutOrStdout())
50+
encoder.SetIndent("", " ")
51+
return encoder.Encode(resourceAccess)
7252
}
53+
54+
writeAuthSummary(cmd.OutOrStdout(), g3i.Credentials().Current().APIEndpoint, resourceAccess, showAll)
55+
return nil
7356
},
7457
}
7558

7659
authCmd.Flags().StringVar(&profile, "profile", "", "Specify the profile to check your access privileges")
60+
authCmd.Flags().BoolVar(&showAll, "all", false, "Show every resource in each permission group")
61+
authCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output raw resource access JSON")
7762
authCmd.MarkFlagRequired("profile") // nolint: errcheck
7863
RootCmd.AddCommand(authCmd)
7964
}
65+
66+
func writeAuthSummary(w io.Writer, endpoint string, resourceAccess map[string]any, showAll bool) {
67+
if len(resourceAccess) == 0 {
68+
fmt.Fprintf(w, "No resource access found for %s\n", endpoint)
69+
return
70+
}
71+
72+
groups := make(map[string][]string)
73+
for resource, permissions := range resourceAccess {
74+
signature := authPermissionSignature(permissions)
75+
groups[signature] = append(groups[signature], resource)
76+
}
77+
78+
type accessGroup struct {
79+
signature string
80+
resources []string
81+
}
82+
83+
orderedGroups := make([]accessGroup, 0, len(groups))
84+
for signature, resources := range groups {
85+
sort.Strings(resources)
86+
orderedGroups = append(orderedGroups, accessGroup{
87+
signature: signature,
88+
resources: resources,
89+
})
90+
}
91+
92+
sort.Slice(orderedGroups, func(i, j int) bool {
93+
if len(orderedGroups[i].resources) != len(orderedGroups[j].resources) {
94+
return len(orderedGroups[i].resources) > len(orderedGroups[j].resources)
95+
}
96+
return orderedGroups[i].signature < orderedGroups[j].signature
97+
})
98+
99+
fmt.Fprintf(w, "Access for %s\n", endpoint)
100+
fmt.Fprintf(w, "%d resources in %d permission groups\n\n", len(resourceAccess), len(orderedGroups))
101+
102+
for _, group := range orderedGroups {
103+
fmt.Fprintf(w, "%d %s: %s\n", len(group.resources), pluralize("resource", len(group.resources)), group.signature)
104+
105+
limit := len(group.resources)
106+
if !showAll && limit > authSummaryResourceLimit {
107+
limit = authSummaryResourceLimit
108+
}
109+
110+
for _, resource := range group.resources[:limit] {
111+
fmt.Fprintf(w, " %s\n", resource)
112+
}
113+
if !showAll && len(group.resources) > limit {
114+
fmt.Fprintf(w, " ... %d more (use --all to show every resource)\n", len(group.resources)-limit)
115+
}
116+
fmt.Fprintln(w)
117+
}
118+
}
119+
120+
func pluralize(word string, count int) string {
121+
if count == 1 {
122+
return word
123+
}
124+
return word + "s"
125+
}
126+
127+
func authPermissionSignature(value any) string {
128+
permissions, ok := value.([]any)
129+
if !ok {
130+
return compactJSON(value)
131+
}
132+
if len(permissions) == 0 {
133+
return "no permissions"
134+
}
135+
136+
if _, ok := permissions[0].(string); ok {
137+
access := make([]string, 0, len(permissions))
138+
for _, permission := range permissions {
139+
access = append(access, fmt.Sprint(permission))
140+
}
141+
sort.Strings(access)
142+
return strings.Join(access, ", ")
143+
}
144+
145+
serviceMethods := make(map[string]map[string]struct{})
146+
unknown := make([]string, 0)
147+
148+
for _, permission := range permissions {
149+
method, service, ok := authPermissionFields(permission)
150+
if !ok {
151+
unknown = append(unknown, compactJSON(permission))
152+
continue
153+
}
154+
155+
if serviceMethods[service] == nil {
156+
serviceMethods[service] = make(map[string]struct{})
157+
}
158+
serviceMethods[service][method] = struct{}{}
159+
}
160+
161+
parts := make([]string, 0, len(serviceMethods)+len(unknown))
162+
services := make([]string, 0, len(serviceMethods))
163+
for service := range serviceMethods {
164+
services = append(services, service)
165+
}
166+
sort.Strings(services)
167+
168+
for _, service := range services {
169+
methods := make([]string, 0, len(serviceMethods[service]))
170+
for method := range serviceMethods[service] {
171+
methods = append(methods, method)
172+
}
173+
sort.Strings(methods)
174+
parts = append(parts, fmt.Sprintf("%s: %s", service, strings.Join(methods, ", ")))
175+
}
176+
177+
sort.Strings(unknown)
178+
parts = append(parts, unknown...)
179+
return strings.Join(parts, "; ")
180+
}
181+
182+
func authPermissionFields(value any) (method string, service string, ok bool) {
183+
switch permission := value.(type) {
184+
case map[string]any:
185+
method, methodOK := permission["method"].(string)
186+
service, serviceOK := permission["service"].(string)
187+
return method, service, methodOK && serviceOK
188+
case map[string]string:
189+
method, methodOK := permission["method"]
190+
service, serviceOK := permission["service"]
191+
return method, service, methodOK && serviceOK
192+
default:
193+
return "", "", false
194+
}
195+
}
196+
197+
func compactJSON(value any) string {
198+
data, err := json.Marshal(value)
199+
if err != nil {
200+
return fmt.Sprint(value)
201+
}
202+
return string(data)
203+
}

cmd/auth_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestAuthPermissionSignatureGroupsArboristPermissions(t *testing.T) {
10+
signature := authPermissionSignature([]any{
11+
map[string]any{"method": "read", "service": "*"},
12+
map[string]any{"method": "create", "service": "*"},
13+
map[string]any{"method": "*", "service": "indexd"},
14+
map[string]any{"method": "read", "service": "requestor"},
15+
})
16+
17+
want := "*: create, read; indexd: *; requestor: read"
18+
if signature != want {
19+
t.Fatalf("signature = %q, want %q", signature, want)
20+
}
21+
}
22+
23+
func TestAuthPermissionSignatureGroupsFenceProjectAccess(t *testing.T) {
24+
signature := authPermissionSignature([]any{"write", "read", "read"})
25+
26+
want := "read, read, write"
27+
if signature != want {
28+
t.Fatalf("signature = %q, want %q", signature, want)
29+
}
30+
}
31+
32+
func TestWriteAuthSummaryCondensesRepeatedPermissionSets(t *testing.T) {
33+
resourceAccess := map[string]any{
34+
"/programs/a/projects/one": []any{
35+
map[string]any{"method": "read", "service": "*"},
36+
map[string]any{"method": "create", "service": "*"},
37+
},
38+
"/programs/a/projects/two": []any{
39+
map[string]any{"method": "create", "service": "*"},
40+
map[string]any{"method": "read", "service": "*"},
41+
},
42+
"/data_file": []any{
43+
map[string]any{"method": "*", "service": "indexd"},
44+
},
45+
}
46+
47+
var out bytes.Buffer
48+
writeAuthSummary(&out, "https://example.org", resourceAccess, false)
49+
got := out.String()
50+
51+
for _, want := range []string{
52+
"Access for https://example.org",
53+
"3 resources in 2 permission groups",
54+
"2 resources: *: create, read",
55+
" /programs/a/projects/one",
56+
" /programs/a/projects/two",
57+
"1 resource: indexd: *",
58+
" /data_file",
59+
} {
60+
if !strings.Contains(got, want) {
61+
t.Fatalf("summary missing %q:\n%s", want, got)
62+
}
63+
}
64+
}
65+
66+
func TestWriteAuthSummaryCapsLongGroups(t *testing.T) {
67+
resourceAccess := make(map[string]any)
68+
for i := 0; i < authSummaryResourceLimit+2; i++ {
69+
resourceAccess[string(rune('a'+i))] = []any{
70+
map[string]any{"method": "read", "service": "*"},
71+
}
72+
}
73+
74+
var out bytes.Buffer
75+
writeAuthSummary(&out, "https://example.org", resourceAccess, false)
76+
got := out.String()
77+
78+
if !strings.Contains(got, "... 2 more (use --all to show every resource)") {
79+
t.Fatalf("summary did not cap long group:\n%s", got)
80+
}
81+
}

cmd/configure.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ import (
1010
"github.com/spf13/cobra"
1111
)
1212

13+
func mergeImportedCredential(target *conf.Credential, imported *conf.Credential) {
14+
if target == nil || imported == nil {
15+
return
16+
}
17+
target.KeyID = imported.KeyID
18+
target.APIKey = imported.APIKey
19+
if target.APIEndpoint == "" && imported.APIEndpoint != "" {
20+
target.APIEndpoint = imported.APIEndpoint
21+
}
22+
target.AccessToken = ""
23+
}
24+
1325
func init() {
1426
var profile string
1527
var credFile string
@@ -41,12 +53,7 @@ func init() {
4153
if err != nil {
4254
logger.Fatal(err) // or return proper error
4355
}
44-
cred.KeyID = readCred.KeyID
45-
cred.APIKey = readCred.APIKey
46-
if readCred.APIEndpoint != "" {
47-
cred.APIEndpoint = readCred.APIEndpoint
48-
}
49-
cred.AccessToken = ""
56+
mergeImportedCredential(cred, readCred)
5057
}
5158

5259
g3i := g3client.NewGen3InterfaceFromCredential(cred, logger, g3client.WithClients())

0 commit comments

Comments
 (0)