Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 87 additions & 22 deletions internal/namespaces/iam/v1alpha1/custom_iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package iam

import (
"context"
"errors"
"fmt"
"reflect"

Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -107,7 +172,7 @@ func apiKeyMarshalerFunc(i any, opt *human.MarshalOpt) (string, error) {

opt.Sections = []*human.MarshalSection{
{
FieldName: "UserType",
FieldName: "EntityType",
},
{
FieldName: "APIKey",
Expand Down
33 changes: 33 additions & 0 deletions internal/namespaces/iam/v1alpha1/custom_iam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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),
),
}))
}
Original file line number Diff line number Diff line change
@@ -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: ""
Loading
Loading