diff --git a/internal/namespaces/iam/v1alpha1/custom_iam.go b/internal/namespaces/iam/v1alpha1/custom_iam.go index 818b812c81..a0489fb3ce 100644 --- a/internal/namespaces/iam/v1alpha1/custom_iam.go +++ b/internal/namespaces/iam/v1alpha1/custom_iam.go @@ -2,6 +2,7 @@ package iam import ( "context" + "errors" "fmt" "reflect" @@ -12,9 +13,9 @@ import ( ) type apiKeyResponse struct { - APIKey *iam.APIKey - UserType string `json:"user_type"` - Policies map[string][]string `json:"policies"` + APIKey *iam.APIKey + EntityType string `json:"entity_type"` + Policies map[string][]string `json:"policies"` } type iamGetAPIKeyArgs struct { AccessKey string @@ -31,6 +32,76 @@ func WithPolicies(withPolicies bool) apiKeyOptions { } } +type userEntity struct { + UserID string +} + +type applicationEntity struct { + ApplicationID string +} + +type entity interface { + entityType(ctx context.Context, api *iam.API) (string, error) + getPolicies(ctx context.Context, api *iam.API) ([]*iam.Policy, error) +} + +func (u userEntity) entityType(ctx context.Context, api *iam.API) (string, error) { + user, err := api.GetUser(&iam.GetUserRequest{ + UserID: u.UserID, + }, scw.WithContext(ctx)) + if err != nil { + return "", err + } + + return string(user.Type), nil +} + +func (a applicationEntity) entityType(ctx context.Context, api *iam.API) (string, error) { + return "application", nil +} + +func buildEntity(apiKey *iam.APIKey) (entity, error) { + if apiKey == nil { + return nil, errors.New("invalid API key") + } + if apiKey.UserID != nil { + return userEntity{UserID: *apiKey.UserID}, nil + } + if apiKey.ApplicationID != nil { + return applicationEntity{ApplicationID: *apiKey.ApplicationID}, nil + } + + return nil, errors.New("invalid API key") +} + +func (u userEntity) getPolicies(ctx context.Context, api *iam.API) ([]*iam.Policy, error) { + policies, err := api.ListPolicies(&iam.ListPoliciesRequest{ + UserIDs: []string{u.UserID}, + }, scw.WithContext(ctx), scw.WithAllPages()) + if err != nil { + return nil, err + } + if policies == nil { + return nil, errors.New("no policies found") + } + + return policies.Policies, nil +} + +func (a applicationEntity) getPolicies(ctx context.Context, api *iam.API) ([]*iam.Policy, error) { + policies, err := api.ListPolicies(&iam.ListPoliciesRequest{ + ApplicationIDs: []string{a.ApplicationID}, + }, scw.WithContext(ctx), scw.WithAllPages()) + if err != nil { + return nil, err + } + if policies == nil { + return nil, errors.New("no policies found") + } + + return policies.Policies, nil +} + func getApiKey( ctx context.Context, api *iam.API, @@ -45,40 +116,34 @@ func getApiKey( return response, err } - user, err := api.GetUser(&iam.GetUserRequest{ - UserID: *apiKey.UserID, - }, scw.WithContext(ctx)) + entity, err := buildEntity(apiKey) + if err != nil { + return response, err + } + + entityType, err := entity.entityType(ctx, api) if err != nil { return response, err } response.APIKey = apiKey - response.UserType = string(user.Type) + response.EntityType = entityType - if user.Type == iam.UserTypeOwner { - response.UserType = fmt.Sprintf( - "%s (owner has all permissions over the organization)", - user.Type, - ) + if entityType == string(iam.UserTypeOwner) { + response.EntityType = entityType + " (owner has all permissions over the organization)" return response, nil } if options.WithPolicies { - listPolicyRequest := &iam.ListPoliciesRequest{ - UserIDs: []string{*apiKey.UserID}, - } - policies, err := api.ListPolicies( - listPolicyRequest, - scw.WithAllPages(), - scw.WithContext(ctx), - ) + policies, err := entity.getPolicies(ctx, api) if err != nil { return response, err } + // Build a map of policies -> [rules...] policyMap := map[string][]string{} - for _, policy := range policies.Policies { + for _, policy := range policies { rules, err := api.ListRules( &iam.ListRulesRequest{ PolicyID: policy.ID, @@ -107,7 +172,7 @@ func apiKeyMarshalerFunc(i any, opt *human.MarshalOpt) (string, error) { opt.Sections = []*human.MarshalSection{ { - FieldName: "UserType", + FieldName: "EntityType", }, { FieldName: "APIKey", diff --git a/internal/namespaces/iam/v1alpha1/custom_iam_test.go b/internal/namespaces/iam/v1alpha1/custom_iam_test.go index 319cbabc4b..af3bb74608 100644 --- a/internal/namespaces/iam/v1alpha1/custom_iam_test.go +++ b/internal/namespaces/iam/v1alpha1/custom_iam_test.go @@ -55,6 +55,18 @@ func Test_iamAPIKeyGet(t *testing.T) { return apiKeys[0].AccessKey } + applicationResulter := func(result any) any { + applications := result.([]*iamSdk.Application) + if applications == nil { + panic("applications is nil") + } + if len(applications) == 0 { + panic("no application found") + } + + return applications[0].ID + } + t.Run("GetOwnerAPIKey", core.Test(&core.TestConfig{ Commands: commands, BeforeFunc: core.BeforeFuncCombine( @@ -97,4 +109,25 @@ func Test_iamAPIKeyGet(t *testing.T) { core.TestCheckExitCode(0), ), })) + + t.Run("GetApplicationAPIKey", core.Test(&core.TestConfig{ + Commands: commands, + BeforeFunc: core.BeforeFuncCombine( + core.ExecStoreBeforeCmdWithResulter( + "application", + "scw iam application list", + applicationResulter, + ), + core.ExecStoreBeforeCmdWithResulter( + "applicationAPIKey", + "scw iam api-key list bearer-id={{ .application }}", + apiKeyResulter, + ), + ), + Cmd: `scw iam api-key get {{ .applicationAPIKey }}`, + Check: core.TestCheckCombine( + core.TestCheckGolden(), + core.TestCheckExitCode(0), + ), + })) } diff --git a/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-application-api-key.cassette.yaml b/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-application-api-key.cassette.yaml new file mode 100644 index 0000000000..d57d09a0ba --- /dev/null +++ b/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-application-api-key.cassette.yaml @@ -0,0 +1,165 @@ +--- +version: 1 +interactions: +- request: + 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}' + form: {} + headers: + User-Agent: + - scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test + url: https://api.scaleway.com/iam/v1alpha1/applications?order_by=created_at_asc&organization_id=11111111-1111-1111-1111-111111111111&page=1 + method: GET + response: + 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}' + headers: + Content-Length: + - "691" + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Fri, 20 Jun 2025 13:53:46 GMT + Server: + - Scaleway API Gateway (fr-par-1;edge03) + Strict-Transport-Security: + - max-age=63072000 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Request-Id: + - e2ff4071-4b3b-4530-be28-fe94c1f5341c + status: 200 OK + code: 200 + duration: "" +- request: + 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}' + form: {} + headers: + User-Agent: + - scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test + 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 + method: GET + response: + 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}' + headers: + Content-Length: + - "402" + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Fri, 20 Jun 2025 13:53:46 GMT + Server: + - Scaleway API Gateway (fr-par-1;edge03) + Strict-Transport-Security: + - max-age=63072000 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Request-Id: + - 1f0fcf2b-0b50-4440-b712-7a7111ac316f + status: 200 OK + code: 200 + duration: "" +- request: + 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"}' + form: {} + headers: + User-Agent: + - scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test + url: https://api.scaleway.com/iam/v1alpha1/api-keys/SCW2NSMT7HPTHMJBWP0S + method: GET + response: + 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"}' + headers: + Content-Length: + - "371" + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Fri, 20 Jun 2025 13:53:46 GMT + Server: + - Scaleway API Gateway (fr-par-1;edge03) + Strict-Transport-Security: + - max-age=63072000 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Request-Id: + - aaec1f7c-1c09-4b15-8e9f-f31a3f678d16 + status: 200 OK + code: 200 + duration: "" +- request: + body: '{"policies":[{"id":"4feb1f44-1726-4373-bf9b-01c828ddb0ca","name":"Copy + 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}' + form: {} + headers: + User-Agent: + - scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test + 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 + method: GET + response: + body: '{"policies":[{"id":"4feb1f44-1726-4373-bf9b-01c828ddb0ca","name":"Copy + 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}' + headers: + Content-Length: + - "426" + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Fri, 20 Jun 2025 13:53:46 GMT + Server: + - Scaleway API Gateway (fr-par-1;edge03) + Strict-Transport-Security: + - max-age=63072000 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Request-Id: + - 98cd9ade-ee35-4fbf-ae9e-df865bc734c0 + status: 200 OK + code: 200 + duration: "" +- request: + 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}' + form: {} + headers: + User-Agent: + - scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.3; darwin; arm64) cli-e2e-test + url: https://api.scaleway.com/iam/v1alpha1/rules?policy_id=4feb1f44-1726-4373-bf9b-01c828ddb0ca + method: GET + response: + 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}' + headers: + Content-Length: + - "485" + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Fri, 20 Jun 2025 13:53:46 GMT + Server: + - Scaleway API Gateway (fr-par-1;edge03) + Strict-Transport-Security: + - max-age=63072000 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Request-Id: + - f676dfd8-c93d-4cec-9394-78fdb6d157dc + status: 200 OK + code: 200 + duration: "" diff --git a/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-application-api-key.golden b/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-application-api-key.golden new file mode 100644 index 0000000000..a3fd21280e --- /dev/null +++ b/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-application-api-key.golden @@ -0,0 +1,45 @@ +🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲 +🟩🟩🟩 STDOUT️ 🟩🟩🟩️ +Entity Type: +application + +Api Key: +AccessKey SCW2NSMT7HPTHMJBWP0S +ApplicationID 370d4bad-80b5-4602-8def-f2a34b7581de +Description - +CreatedAt few seconds ago +UpdatedAt few seconds ago +DefaultProjectID 6867048b-fe12-4e96-835e-41c79a39604b +Editable true +Deletable true +Managed false +CreationIP 51.159.46.153 + +Policies: +Copy of Editors (4feb1f44-1726-4373-bf9b-01c828ddb0ca) AllProductsFullAccess OrganizationReadOnly ProjectManager SupportTicketReadOnly +🟩🟩🟩 JSON STDOUT 🟩🟩🟩 +{ + "APIKey": { + "access_key": "SCW2NSMT7HPTHMJBWP0S", + "secret_key": null, + "application_id": "370d4bad-80b5-4602-8def-f2a34b7581de", + "description": "", + "created_at": "1970-01-01T00:00:00.0Z", + "updated_at": "1970-01-01T00:00:00.0Z", + "expires_at": null, + "default_project_id": "6867048b-fe12-4e96-835e-41c79a39604b", + "editable": true, + "deletable": true, + "managed": false, + "creation_ip": "51.159.46.153" + }, + "entity_type": "application", + "policies": { + "Copy of Editors (4feb1f44-1726-4373-bf9b-01c828ddb0ca)": [ + "AllProductsFullAccess", + "OrganizationReadOnly", + "ProjectManager", + "SupportTicketReadOnly" + ] + } +} diff --git a/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-member-api-key.golden b/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-member-api-key.golden index 6a9cf90efa..8e302f4e9d 100644 --- a/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-member-api-key.golden +++ b/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-member-api-key.golden @@ -1,6 +1,6 @@ 🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲 🟩🟩🟩 STDOUT️ 🟩🟩🟩️ -User Type: +Entity Type: member Api Key: @@ -34,7 +34,7 @@ Copy of test-cli-iam-api-key-get-policies (39ddc97c-457c-40df-b076-0f83c1cc7d2b) "managed": false, "creation_ip": "51.159.73.145" }, - "user_type": "member", + "entity_type": "member", "policies": { "Copy of Editors (4feb1f44-1726-4373-bf9b-01c828ddb0ca)": [ "AllProductsFullAccess", diff --git a/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-owner-api-key.golden b/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-owner-api-key.golden index 3d48567734..da8ceaadeb 100644 --- a/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-owner-api-key.golden +++ b/internal/namespaces/iam/v1alpha1/testdata/test-iam-api-key-get-get-owner-api-key.golden @@ -1,6 +1,6 @@ 🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲 🟩🟩🟩 STDOUT️ 🟩🟩🟩️ -User Type: +Entity Type: owner (owner has all permissions over the organization) Api Key: @@ -30,6 +30,6 @@ CreationIP 51.159.73.9 "managed": false, "creation_ip": "51.159.73.9" }, - "user_type": "owner (owner has all permissions over the organization)", + "entity_type": "owner (owner has all permissions over the organization)", "policies": null }