Skip to content

Commit 6eb14c1

Browse files
authored
fix(iam): support application Entity Type (#4855)
1 parent aab066d commit 6eb14c1

6 files changed

+334
-26
lines changed

internal/namespaces/iam/v1alpha1/custom_iam.go

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

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"reflect"
78

@@ -12,9 +13,9 @@ import (
1213
)
1314

1415
type apiKeyResponse struct {
15-
APIKey *iam.APIKey
16-
UserType string `json:"user_type"`
17-
Policies map[string][]string `json:"policies"`
16+
APIKey *iam.APIKey
17+
EntityType string `json:"entity_type"`
18+
Policies map[string][]string `json:"policies"`
1819
}
1920
type iamGetAPIKeyArgs struct {
2021
AccessKey string
@@ -31,6 +32,76 @@ func WithPolicies(withPolicies bool) apiKeyOptions {
3132
}
3233
}
3334

35+
type userEntity struct {
36+
UserID string
37+
}
38+
39+
type applicationEntity struct {
40+
ApplicationID string
41+
}
42+
43+
type entity interface {
44+
entityType(ctx context.Context, api *iam.API) (string, error)
45+
getPolicies(ctx context.Context, api *iam.API) ([]*iam.Policy, error)
46+
}
47+
48+
func (u userEntity) entityType(ctx context.Context, api *iam.API) (string, error) {
49+
user, err := api.GetUser(&iam.GetUserRequest{
50+
UserID: u.UserID,
51+
}, scw.WithContext(ctx))
52+
if err != nil {
53+
return "", err
54+
}
55+
56+
return string(user.Type), nil
57+
}
58+
59+
func (a applicationEntity) entityType(ctx context.Context, api *iam.API) (string, error) {
60+
return "application", nil
61+
}
62+
63+
func buildEntity(apiKey *iam.APIKey) (entity, error) {
64+
if apiKey == nil {
65+
return nil, errors.New("invalid API key")
66+
}
67+
if apiKey.UserID != nil {
68+
return userEntity{UserID: *apiKey.UserID}, nil
69+
}
70+
if apiKey.ApplicationID != nil {
71+
return applicationEntity{ApplicationID: *apiKey.ApplicationID}, nil
72+
}
73+
74+
return nil, errors.New("invalid API key")
75+
}
76+
77+
func (u userEntity) getPolicies(ctx context.Context, api *iam.API) ([]*iam.Policy, error) {
78+
policies, err := api.ListPolicies(&iam.ListPoliciesRequest{
79+
UserIDs: []string{u.UserID},
80+
}, scw.WithContext(ctx), scw.WithAllPages())
81+
if err != nil {
82+
return nil, err
83+
}
84+
if policies == nil {
85+
return nil, errors.New("no policies found")
86+
}
87+
88+
return policies.Policies, nil
89+
}
90+
91+
func (a applicationEntity) getPolicies(ctx context.Context, api *iam.API) ([]*iam.Policy, error) {
92+
policies, err := api.ListPolicies(&iam.ListPoliciesRequest{
93+
ApplicationIDs: []string{a.ApplicationID},
94+
}, scw.WithContext(ctx), scw.WithAllPages())
95+
if err != nil {
96+
return nil, err
97+
}
98+
if policies == nil {
99+
return nil, errors.New("no policies found")
100+
}
101+
102+
return policies.Policies, nil
103+
}
104+
34105
func getApiKey(
35106
ctx context.Context,
36107
api *iam.API,
@@ -45,40 +116,34 @@ func getApiKey(
45116
return response, err
46117
}
47118

48-
user, err := api.GetUser(&iam.GetUserRequest{
49-
UserID: *apiKey.UserID,
50-
}, scw.WithContext(ctx))
119+
entity, err := buildEntity(apiKey)
120+
if err != nil {
121+
return response, err
122+
}
123+
124+
entityType, err := entity.entityType(ctx, api)
51125
if err != nil {
52126
return response, err
53127
}
54128

55129
response.APIKey = apiKey
56-
response.UserType = string(user.Type)
130+
response.EntityType = entityType
57131

58-
if user.Type == iam.UserTypeOwner {
59-
response.UserType = fmt.Sprintf(
60-
"%s (owner has all permissions over the organization)",
61-
user.Type,
62-
)
132+
if entityType == string(iam.UserTypeOwner) {
133+
response.EntityType = entityType + " (owner has all permissions over the organization)"
63134

64135
return response, nil
65136
}
66137

67138
if options.WithPolicies {
68-
listPolicyRequest := &iam.ListPoliciesRequest{
69-
UserIDs: []string{*apiKey.UserID},
70-
}
71-
policies, err := api.ListPolicies(
72-
listPolicyRequest,
73-
scw.WithAllPages(),
74-
scw.WithContext(ctx),
75-
)
139+
policies, err := entity.getPolicies(ctx, api)
76140
if err != nil {
77141
return response, err
78142
}
143+
79144
// Build a map of policies -> [rules...]
80145
policyMap := map[string][]string{}
81-
for _, policy := range policies.Policies {
146+
for _, policy := range policies {
82147
rules, err := api.ListRules(
83148
&iam.ListRulesRequest{
84149
PolicyID: policy.ID,
@@ -107,7 +172,7 @@ func apiKeyMarshalerFunc(i any, opt *human.MarshalOpt) (string, error) {
107172

108173
opt.Sections = []*human.MarshalSection{
109174
{
110-
FieldName: "UserType",
175+
FieldName: "EntityType",
111176
},
112177
{
113178
FieldName: "APIKey",

internal/namespaces/iam/v1alpha1/custom_iam_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ func Test_iamAPIKeyGet(t *testing.T) {
5555
return apiKeys[0].AccessKey
5656
}
5757

58+
applicationResulter := func(result any) any {
59+
applications := result.([]*iamSdk.Application)
60+
if applications == nil {
61+
panic("applications is nil")
62+
}
63+
if len(applications) == 0 {
64+
panic("no application found")
65+
}
66+
67+
return applications[0].ID
68+
}
69+
5870
t.Run("GetOwnerAPIKey", core.Test(&core.TestConfig{
5971
Commands: commands,
6072
BeforeFunc: core.BeforeFuncCombine(
@@ -97,4 +109,25 @@ func Test_iamAPIKeyGet(t *testing.T) {
97109
core.TestCheckExitCode(0),
98110
),
99111
}))
112+
113+
t.Run("GetApplicationAPIKey", core.Test(&core.TestConfig{
114+
Commands: commands,
115+
BeforeFunc: core.BeforeFuncCombine(
116+
core.ExecStoreBeforeCmdWithResulter(
117+
"application",
118+
"scw iam application list",
119+
applicationResulter,
120+
),
121+
core.ExecStoreBeforeCmdWithResulter(
122+
"applicationAPIKey",
123+
"scw iam api-key list bearer-id={{ .application }}",
124+
apiKeyResulter,
125+
),
126+
),
127+
Cmd: `scw iam api-key get {{ .applicationAPIKey }}`,
128+
Check: core.TestCheckCombine(
129+
core.TestCheckGolden(),
130+
core.TestCheckExitCode(0),
131+
),
132+
}))
100133
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
---
2+
version: 1
3+
interactions:
4+
- request:
5+
body: '{"applications":[{"id":"370d4bad-80b5-4602-8def-f2a34b7581de","name":"tf-tests-iam-group-membership-basic-app1","description":"","created_at":"2025-06-02T13:13:41.151439Z","updated_at":"2025-06-02T13:13:41.151439Z","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b","editable":true,"deletable":true,"managed":false,"nb_api_keys":1,"tags":[]},{"id":"e40e18d2-9d2d-4139-82bd-eabcf776669a","name":"tf-tests-iam-group-membership-basic","description":"","created_at":"2025-06-11T09:06:37.802556Z","updated_at":"2025-06-11T09:06:37.802556Z","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b","editable":true,"deletable":true,"managed":false,"nb_api_keys":0,"tags":[]}],"total_count":2}'
6+
form: {}
7+
headers:
8+
User-Agent:
9+
- scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test
10+
url: https://api.scaleway.com/iam/v1alpha1/applications?order_by=created_at_asc&organization_id=11111111-1111-1111-1111-111111111111&page=1
11+
method: GET
12+
response:
13+
body: '{"applications":[{"id":"370d4bad-80b5-4602-8def-f2a34b7581de","name":"tf-tests-iam-group-membership-basic-app1","description":"","created_at":"2025-06-02T13:13:41.151439Z","updated_at":"2025-06-02T13:13:41.151439Z","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b","editable":true,"deletable":true,"managed":false,"nb_api_keys":1,"tags":[]},{"id":"e40e18d2-9d2d-4139-82bd-eabcf776669a","name":"tf-tests-iam-group-membership-basic","description":"","created_at":"2025-06-11T09:06:37.802556Z","updated_at":"2025-06-11T09:06:37.802556Z","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b","editable":true,"deletable":true,"managed":false,"nb_api_keys":0,"tags":[]}],"total_count":2}'
14+
headers:
15+
Content-Length:
16+
- "691"
17+
Content-Security-Policy:
18+
- default-src 'none'; frame-ancestors 'none'
19+
Content-Type:
20+
- application/json
21+
Date:
22+
- Fri, 20 Jun 2025 13:53:46 GMT
23+
Server:
24+
- Scaleway API Gateway (fr-par-1;edge03)
25+
Strict-Transport-Security:
26+
- max-age=63072000
27+
X-Content-Type-Options:
28+
- nosniff
29+
X-Frame-Options:
30+
- DENY
31+
X-Request-Id:
32+
- e2ff4071-4b3b-4530-be28-fe94c1f5341c
33+
status: 200 OK
34+
code: 200
35+
duration: ""
36+
- request:
37+
body: '{"api_keys":[{"access_key":"SCW2NSMT7HPTHMJBWP0S","secret_key":null,"description":"","created_at":"2025-06-19T15:14:18.042537Z","updated_at":"2025-06-19T15:14:18.042537Z","expires_at":null,"default_project_id":"6867048b-fe12-4e96-835e-41c79a39604b","editable":true,"deletable":true,"managed":false,"creation_ip":"51.159.46.153","application_id":"370d4bad-80b5-4602-8def-f2a34b7581de"}],"total_count":1}'
38+
form: {}
39+
headers:
40+
User-Agent:
41+
- scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test
42+
url: https://api.scaleway.com/iam/v1alpha1/api-keys?bearer_id=370d4bad-80b5-4602-8def-f2a34b7581de&bearer_type=unknown_bearer_type&order_by=created_at_asc&organization_id=11111111-1111-1111-1111-111111111111&page=1
43+
method: GET
44+
response:
45+
body: '{"api_keys":[{"access_key":"SCW2NSMT7HPTHMJBWP0S","secret_key":null,"description":"","created_at":"2025-06-19T15:14:18.042537Z","updated_at":"2025-06-19T15:14:18.042537Z","expires_at":null,"default_project_id":"6867048b-fe12-4e96-835e-41c79a39604b","editable":true,"deletable":true,"managed":false,"creation_ip":"51.159.46.153","application_id":"370d4bad-80b5-4602-8def-f2a34b7581de"}],"total_count":1}'
46+
headers:
47+
Content-Length:
48+
- "402"
49+
Content-Security-Policy:
50+
- default-src 'none'; frame-ancestors 'none'
51+
Content-Type:
52+
- application/json
53+
Date:
54+
- Fri, 20 Jun 2025 13:53:46 GMT
55+
Server:
56+
- Scaleway API Gateway (fr-par-1;edge03)
57+
Strict-Transport-Security:
58+
- max-age=63072000
59+
X-Content-Type-Options:
60+
- nosniff
61+
X-Frame-Options:
62+
- DENY
63+
X-Request-Id:
64+
- 1f0fcf2b-0b50-4440-b712-7a7111ac316f
65+
status: 200 OK
66+
code: 200
67+
duration: ""
68+
- request:
69+
body: '{"access_key":"SCW2NSMT7HPTHMJBWP0S","secret_key":null,"description":"","created_at":"2025-06-19T15:14:18.042537Z","updated_at":"2025-06-19T15:14:18.042537Z","expires_at":null,"default_project_id":"6867048b-fe12-4e96-835e-41c79a39604b","editable":true,"deletable":true,"managed":false,"creation_ip":"51.159.46.153","application_id":"370d4bad-80b5-4602-8def-f2a34b7581de"}'
70+
form: {}
71+
headers:
72+
User-Agent:
73+
- scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test
74+
url: https://api.scaleway.com/iam/v1alpha1/api-keys/SCW2NSMT7HPTHMJBWP0S
75+
method: GET
76+
response:
77+
body: '{"access_key":"SCW2NSMT7HPTHMJBWP0S","secret_key":null,"description":"","created_at":"2025-06-19T15:14:18.042537Z","updated_at":"2025-06-19T15:14:18.042537Z","expires_at":null,"default_project_id":"6867048b-fe12-4e96-835e-41c79a39604b","editable":true,"deletable":true,"managed":false,"creation_ip":"51.159.46.153","application_id":"370d4bad-80b5-4602-8def-f2a34b7581de"}'
78+
headers:
79+
Content-Length:
80+
- "371"
81+
Content-Security-Policy:
82+
- default-src 'none'; frame-ancestors 'none'
83+
Content-Type:
84+
- application/json
85+
Date:
86+
- Fri, 20 Jun 2025 13:53:46 GMT
87+
Server:
88+
- Scaleway API Gateway (fr-par-1;edge03)
89+
Strict-Transport-Security:
90+
- max-age=63072000
91+
X-Content-Type-Options:
92+
- nosniff
93+
X-Frame-Options:
94+
- DENY
95+
X-Request-Id:
96+
- aaec1f7c-1c09-4b15-8e9f-f31a3f678d16
97+
status: 200 OK
98+
code: 200
99+
duration: ""
100+
- request:
101+
body: '{"policies":[{"id":"4feb1f44-1726-4373-bf9b-01c828ddb0ca","name":"Copy
102+
of Editors","description":"","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b","created_at":"2025-05-09T10:39:40.931178Z","updated_at":"2025-05-09T10:39:40.931178Z","editable":true,"deletable":true,"managed":false,"nb_rules":2,"nb_scopes":2,"nb_permission_sets":4,"tags":[],"application_id":"370d4bad-80b5-4602-8def-f2a34b7581de"}],"total_count":1}'
103+
form: {}
104+
headers:
105+
User-Agent:
106+
- scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test
107+
url: https://api.scaleway.com/iam/v1alpha1/policies?application_ids=370d4bad-80b5-4602-8def-f2a34b7581de&order_by=policy_name_asc&organization_id=11111111-1111-1111-1111-111111111111&page=1
108+
method: GET
109+
response:
110+
body: '{"policies":[{"id":"4feb1f44-1726-4373-bf9b-01c828ddb0ca","name":"Copy
111+
of Editors","description":"","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b","created_at":"2025-05-09T10:39:40.931178Z","updated_at":"2025-05-09T10:39:40.931178Z","editable":true,"deletable":true,"managed":false,"nb_rules":2,"nb_scopes":2,"nb_permission_sets":4,"tags":[],"application_id":"370d4bad-80b5-4602-8def-f2a34b7581de"}],"total_count":1}'
112+
headers:
113+
Content-Length:
114+
- "426"
115+
Content-Security-Policy:
116+
- default-src 'none'; frame-ancestors 'none'
117+
Content-Type:
118+
- application/json
119+
Date:
120+
- Fri, 20 Jun 2025 13:53:46 GMT
121+
Server:
122+
- Scaleway API Gateway (fr-par-1;edge03)
123+
Strict-Transport-Security:
124+
- max-age=63072000
125+
X-Content-Type-Options:
126+
- nosniff
127+
X-Frame-Options:
128+
- DENY
129+
X-Request-Id:
130+
- 98cd9ade-ee35-4fbf-ae9e-df865bc734c0
131+
status: 200 OK
132+
code: 200
133+
duration: ""
134+
- request:
135+
body: '{"rules":[{"id":"a7d5a179-d818-4cf2-a280-3d9445381cd2","permission_set_names":["OrganizationReadOnly","ProjectManager","SupportTicketReadOnly"],"permission_sets_scope_type":"organization","condition":"","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b"},{"id":"5b5c2ab4-48e3-45a5-a9a3-ac3ea382f773","permission_set_names":["AllProductsFullAccess"],"permission_sets_scope_type":"projects","condition":"","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b"}],"total_count":2}'
136+
form: {}
137+
headers:
138+
User-Agent:
139+
- scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test
140+
url: https://api.scaleway.com/iam/v1alpha1/rules?policy_id=4feb1f44-1726-4373-bf9b-01c828ddb0ca
141+
method: GET
142+
response:
143+
body: '{"rules":[{"id":"a7d5a179-d818-4cf2-a280-3d9445381cd2","permission_set_names":["OrganizationReadOnly","ProjectManager","SupportTicketReadOnly"],"permission_sets_scope_type":"organization","condition":"","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b"},{"id":"5b5c2ab4-48e3-45a5-a9a3-ac3ea382f773","permission_set_names":["AllProductsFullAccess"],"permission_sets_scope_type":"projects","condition":"","organization_id":"6867048b-fe12-4e96-835e-41c79a39604b"}],"total_count":2}'
144+
headers:
145+
Content-Length:
146+
- "485"
147+
Content-Security-Policy:
148+
- default-src 'none'; frame-ancestors 'none'
149+
Content-Type:
150+
- application/json
151+
Date:
152+
- Fri, 20 Jun 2025 13:53:46 GMT
153+
Server:
154+
- Scaleway API Gateway (fr-par-1;edge03)
155+
Strict-Transport-Security:
156+
- max-age=63072000
157+
X-Content-Type-Options:
158+
- nosniff
159+
X-Frame-Options:
160+
- DENY
161+
X-Request-Id:
162+
- f676dfd8-c93d-4cec-9394-78fdb6d157dc
163+
status: 200 OK
164+
code: 200
165+
duration: ""

0 commit comments

Comments
 (0)