Skip to content

Commit 9f78630

Browse files
Fix: Enable Admin API Key support in profile add command (#192)
1 parent d30ded3 commit 9f78630

5 files changed

Lines changed: 126 additions & 22 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ vendor/
1818
# local build
1919
algolia
2020

21+
.gocache/
22+
2123
# Environment variables
2224
*.env

pkg/cmd/apikeys/list/list.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import (
1616
"github.com/algolia/cli/pkg/validators"
1717
)
1818

19+
// nowFn exists to make time-based output deterministic in tests.
20+
var nowFn = time.Now
21+
1922
type ListOptions struct {
2023
Config config.IConfig
2124
IO *iostreams.IOStreams
@@ -62,6 +65,8 @@ func runListCmd(opts *ListOptions) error {
6265
return err
6366
}
6467

68+
now := nowFn()
69+
6570
opts.IO.StartProgressIndicatorWithLabel("Fetching API Keys")
6671
res, err := client.ListApiKeys()
6772
opts.IO.StopProgressIndicator()
@@ -100,7 +105,7 @@ func runListCmd(opts *ListOptions) error {
100105
return "Never expire"
101106
} else {
102107
validity := time.Duration(*key.Validity) * time.Second
103-
return humanize.Time(time.Now().Add(validity))
108+
return humanize.RelTime(now, now.Add(validity), "from now", "ago")
104109
}
105110
}(), nil, nil)
106111
if key.MaxHitsPerQuery == nil || *key.MaxHitsPerQuery == 0 {
@@ -115,7 +120,7 @@ func runListCmd(opts *ListOptions) error {
115120
}
116121
table.AddField(fmt.Sprintf("%v", key.Referers), nil, nil)
117122
createdAt := time.Unix(key.CreatedAt, 0)
118-
table.AddField(humanize.Time(createdAt), nil, nil)
123+
table.AddField(humanize.RelTime(now, createdAt, "from now", "ago"), nil, nil)
119124
table.EndRow()
120125
}
121126
return table.Render()

pkg/cmd/apikeys/list/list_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package list
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
78
"github.com/stretchr/testify/assert"
@@ -30,6 +31,10 @@ func Test_runListCmd(t *testing.T) {
3031

3132
for _, tt := range tests {
3233
t.Run(tt.name, func(t *testing.T) {
34+
oldNowFn := nowFn
35+
nowFn = func() time.Time { return time.Unix(1735689600, 0) } // 2025-01-01T00:00:00Z
36+
t.Cleanup(func() { nowFn = oldNowFn })
37+
3338
name := "test"
3439
r := httpmock.Registry{}
3540
r.Register(

pkg/cmd/profile/add/add.go

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,51 @@ import (
1818
"github.com/algolia/cli/pkg/validators"
1919
)
2020

21+
type apiKeyInspector interface {
22+
ListAPIKeys(opts ...search.RequestOption) (*search.ListApiKeysResponse, error)
23+
GetAPIKey(r search.ApiGetApiKeyRequest, opts ...search.RequestOption) (*search.GetApiKeyResponse, error)
24+
NewAPIGetAPIKeyRequest(key string) search.ApiGetApiKeyRequest
25+
}
26+
27+
// searchClientAdapter adapts the Algolia search client to our apiKeyInspector interface
28+
type searchClientAdapter struct {
29+
client *search.APIClient
30+
}
31+
32+
func (a *searchClientAdapter) ListAPIKeys(opts ...search.RequestOption) (*search.ListApiKeysResponse, error) {
33+
return a.client.ListApiKeys(opts...)
34+
}
35+
36+
func (a *searchClientAdapter) GetAPIKey(r search.ApiGetApiKeyRequest, opts ...search.RequestOption) (*search.GetApiKeyResponse, error) {
37+
return a.client.GetApiKey(r, opts...)
38+
}
39+
40+
func (a *searchClientAdapter) NewAPIGetAPIKeyRequest(key string) search.ApiGetApiKeyRequest {
41+
return a.client.NewApiGetApiKeyRequest(key)
42+
}
43+
44+
func inspectAPIKey(client apiKeyInspector, key string) (isAdmin bool, stringACLs []string, err error) {
45+
// Admin API keys are special: they can list keys but aren't themselves retrievable via GET /1/keys/{key}.
46+
// So we use ListAPIKeys() as the admin-key check and skip GetAPIKey() in that case.
47+
if _, err := client.ListAPIKeys(); err == nil {
48+
return true, nil, nil
49+
}
50+
51+
apiKey, err := client.GetAPIKey(client.NewAPIGetAPIKeyRequest(key))
52+
if err != nil {
53+
return false, nil, errors.New("invalid application credentials")
54+
}
55+
if len(apiKey.Acl) == 0 {
56+
return false, nil, errors.New("the provided API key has no ACLs")
57+
}
58+
59+
for _, a := range apiKey.Acl {
60+
stringACLs = append(stringACLs, string(a))
61+
}
62+
63+
return false, stringACLs, nil
64+
}
65+
2166
// AddOptions represents the options for the add command
2267
type AddOptions struct {
2368
config config.IConfig
@@ -128,7 +173,7 @@ func runAddCmd(opts *AddOptions) error {
128173
{
129174
Name: "APIKey",
130175
Prompt: &survey.Input{
131-
Message: "(Write) API Key:",
176+
Message: "Write API Key:",
132177
Default: opts.Profile.APIKey,
133178
},
134179
Validate: survey.Required,
@@ -151,25 +196,10 @@ func runAddCmd(opts *AddOptions) error {
151196
if err != nil {
152197
return err
153198
}
154-
var isAdminAPIKey bool
155-
156-
// Check if the provided API Key is an admin API Key
157-
_, err = client.ListApiKeys()
158-
if err == nil {
159-
isAdminAPIKey = true
160-
}
161-
162-
// Check the ACLs of the provided API Key
163-
apiKey, err := client.GetApiKey(client.NewApiGetApiKeyRequest(opts.Profile.APIKey))
199+
adapter := &searchClientAdapter{client: client}
200+
isAdminAPIKey, stringACLs, err := inspectAPIKey(adapter, opts.Profile.APIKey)
164201
if err != nil {
165-
return errors.New("invalid application credentials")
166-
}
167-
if len(apiKey.Acl) == 0 {
168-
return errors.New("the provided API key has no ACLs")
169-
}
170-
var stringACLs []string
171-
for _, a := range apiKey.Acl {
172-
stringACLs = append(stringACLs, string(a))
202+
return err
173203
}
174204

175205
// We should have at least the ACLs for a write key, otherwise warns the user, but still allows to add the profile.

pkg/cmd/profile/add/add_test.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package add
22

33
import (
4+
"errors"
45
"testing"
56

67
"github.com/google/shlex"
78
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
910

11+
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
1012
"github.com/algolia/cli/pkg/cmdutil"
1113
"github.com/algolia/cli/pkg/config"
1214
"github.com/algolia/cli/pkg/iostreams"
@@ -97,8 +99,68 @@ func TestNewAddCmd(t *testing.T) {
9799

98100
assert.Equal(t, tt.wantsOpts.Profile.Name, opts.Profile.Name)
99101
assert.Equal(t, tt.wantsOpts.Profile.ApplicationID, opts.Profile.ApplicationID)
100-
assert.Equal(t, tt.wantsOpts.Profile.AdminAPIKey, opts.Profile.AdminAPIKey)
102+
assert.Equal(t, tt.wantsOpts.Profile.APIKey, opts.Profile.APIKey)
101103
assert.Equal(t, tt.wantsOpts.Profile.Default, opts.Profile.Default)
102104
})
103105
}
104106
}
107+
108+
type stubAPIKeyInspector struct {
109+
listErr error
110+
111+
getResp *search.GetApiKeyResponse
112+
getErr error
113+
getCalled bool
114+
}
115+
116+
func (s *stubAPIKeyInspector) ListAPIKeys(opts ...search.RequestOption) (*search.ListApiKeysResponse, error) {
117+
return &search.ListApiKeysResponse{}, s.listErr
118+
}
119+
120+
func (s *stubAPIKeyInspector) GetAPIKey(r search.ApiGetApiKeyRequest, opts ...search.RequestOption) (*search.GetApiKeyResponse, error) {
121+
s.getCalled = true
122+
return s.getResp, s.getErr
123+
}
124+
125+
func (s *stubAPIKeyInspector) NewAPIGetAPIKeyRequest(key string) search.ApiGetApiKeyRequest {
126+
return search.ApiGetApiKeyRequest{}
127+
}
128+
129+
func TestInspectAPIKey_AdminKeySkipsGetApiKey(t *testing.T) {
130+
stub := &stubAPIKeyInspector{
131+
listErr: nil, // admin keys can list API keys
132+
getErr: errors.New("should not be called"),
133+
}
134+
135+
isAdmin, acls, err := inspectAPIKey(stub, "my-admin-key")
136+
require.NoError(t, err)
137+
assert.True(t, isAdmin)
138+
assert.Nil(t, acls)
139+
assert.False(t, stub.getCalled)
140+
}
141+
142+
func TestInspectAPIKey_NonAdminKeyReturnsACLs(t *testing.T) {
143+
stub := &stubAPIKeyInspector{
144+
listErr: errors.New("API error [403] forbidden"), // non-admin keys cannot list API keys
145+
getResp: &search.GetApiKeyResponse{
146+
Acl: []search.Acl{search.ACL_SEARCH, search.ACL_ADD_OBJECT},
147+
},
148+
}
149+
150+
isAdmin, acls, err := inspectAPIKey(stub, "my-write-key")
151+
require.NoError(t, err)
152+
assert.False(t, isAdmin)
153+
assert.Equal(t, []string{"search", "addObject"}, acls)
154+
assert.True(t, stub.getCalled)
155+
}
156+
157+
func TestInspectAPIKey_InvalidCredentials(t *testing.T) {
158+
stub := &stubAPIKeyInspector{
159+
listErr: errors.New("API error [403] invalid"), // fall back to GetApiKey
160+
getErr: errors.New("API error [403] invalid"),
161+
}
162+
163+
_, _, err := inspectAPIKey(stub, "bad-key")
164+
require.Error(t, err)
165+
assert.Equal(t, "invalid application credentials", err.Error())
166+
}

0 commit comments

Comments
 (0)