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
275 changes: 275 additions & 0 deletions base-cli/cmd/common/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"time"

"github.com/charmbracelet/huh"
"github.com/golang-jwt/jwt/v5"
"github.com/magaluCloud/mgccli/cmd/common/structs"
"github.com/magaluCloud/mgccli/cmd/common/workspace"
Expand Down Expand Up @@ -54,6 +55,10 @@ type Auth interface {
Logout() error

ListTenants(ctx context.Context) ([]*Tenant, error)
ListApiKeys(ctx context.Context, showInvalidKeys bool) ([]*ApiKeys, error)

CreateApiKey(ctx context.Context, name, description, expiration string, scopes []string) (*ApiKeyResult, error)
RevokeApiKey(ctx context.Context, ID string) error
}

type authValue struct {
Expand Down Expand Up @@ -357,3 +362,273 @@ func (a *authValue) runTokenExchange(
Scope: strings.Split(payload.Scope, " "),
}, nil
}

func (a *authValue) ListApiKeys(ctx context.Context, showInvalidKeys bool) ([]*ApiKeys, error) {
client, err := NewOAuthClient(a.service.config)
if err != nil {
return nil, fmt.Errorf("failed to create OAuth client: %w", err)
}

httpClient := client.AuthenticatedHttpClientFromContext(ctx)
if httpClient == nil {
return nil, fmt.Errorf("programming error: unable to get HTTP Client from context")
}

r, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
a.service.config.ApiKeysURLV1,
nil,
)
if err != nil {
return nil, err
}
r.Header.Set("Content-Type", "application/json")

resp, err := httpClient.Do(r)
if err != nil {
return nil, err
}

var apiKeys []*ApiKeys
if resp.StatusCode == http.StatusNoContent {
return apiKeys, nil
}

if resp.StatusCode != http.StatusOK {
return nil, cmdutils.NewHttpErrorFromResponse(resp, r)
}

defer resp.Body.Close()
var result []*ApiKeys
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}

for _, key := range result {
if !showInvalidKeys && key.RevokedAt != nil {
continue
}

if !showInvalidKeys && key.EndValidity != nil {
expDate, _ := time.Parse(time.RFC3339, *key.EndValidity)
if expDate.Before(time.Now()) {
continue
}
}

apiKeys = append(apiKeys, key)
}

return apiKeys, nil
}

func (a *authValue) CreateApiKey(ctx context.Context, name, description, expiration string, scopes []string) (*ApiKeyResult, error) {
client, err := NewOAuthClient(a.service.config)
if err != nil {
return nil, fmt.Errorf("failed to create OAuth client: %w", err)
}

httpClient := client.AuthenticatedHttpClientFromContext(ctx)
if httpClient == nil {
return nil, fmt.Errorf("programming error: unable to get HTTP Client from context")
}

scopesCreateList, err := a.processScopes(ctx, scopes)
if err != nil {
return nil, err
}

currentTenantID, err := a.GetCurrentTenantID()
if err != nil {
return nil, err
}

newApiKey := &CreateApiKey{
Name: name,
TenantID: currentTenantID,
ScopesList: scopesCreateList,
StartValidity: time.Now().Format(time.DateOnly),
Description: description,
}

if expiration != "" {
if _, err = time.Parse(time.DateOnly, expiration); err != nil {
return nil, fmt.Errorf("invalid date format for expiration, use YYYY-MM-DD")
}

newApiKey.EndValidity = expiration
}

var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(newApiKey)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
a.service.config.ApiKeysURLV2,
&buf,
)
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/json")

resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusCreated {
return nil, cmdutils.NewHttpErrorFromResponse(resp, req)
}

defer resp.Body.Close()

var result ApiKeyResult
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}

return &result, nil
}

func (a *authValue) processScopes(ctx context.Context, scopes []string) ([]ScopesCreate, error) {
client, err := NewOAuthClient(a.service.config)
if err != nil {
return nil, fmt.Errorf("failed to create OAuth client: %w", err)
}

httpClient := client.AuthenticatedHttpClientFromContext(ctx)
if httpClient == nil {
return nil, fmt.Errorf("programming error: unable to get HTTP Client from context")
}

r, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
a.service.config.ScopesURL,
nil,
)
if err != nil {
return nil, err
}

resp, err := httpClient.Do(r)
if err != nil {
return nil, err
}

var scopesListFile PlatformsResponse

defer resp.Body.Close()
if err = json.NewDecoder(resp.Body).Decode(&scopesListFile); err != nil {
return nil, err
}

scopesTitleMap := make(map[string]string)
scopesNameMap := make(map[string]string)

for _, company := range scopesListFile {
if company.Name == "Magalu Cloud" {
for _, product := range company.APIProducts {
for _, scope := range product.Scopes {
scopeName := product.Name + " [" + scope.Name + "]" + " - " + scope.Title
scopesTitleMap[scopeName] = scope.UUID
scopesNameMap[strings.ToLower(scope.Name)] = scope.UUID
}
}
}
}

var scopesCreateList []ScopesCreate
var invalidScopes []string

if len(scopes) > 0 {
for _, scope := range scopes {
if id, ok := scopesNameMap[strings.ToLower(scope)]; ok {
scopesCreateList = append(scopesCreateList, ScopesCreate{
ID: id,
})
} else {
invalidScopes = append(invalidScopes, scope)
}
}

if len(invalidScopes) > 0 {
return nil, fmt.Errorf("invalid scopes: %s", strings.Join(invalidScopes, ", "))
}
} else {
options := []huh.Option[string]{}
for title, id := range scopesTitleMap {
options = append(options, huh.NewOption(title, id))
}

var selectedScopes []string

multiSelect := huh.NewMultiSelect[string]()
multiSelect.Title("Scopes:")
multiSelect.Description("enter: confirm | space: select | ctrl + a: select/unselect all | /: to filter")
multiSelect.Options(options...)
multiSelect.Height(14)
multiSelect.Filterable(true)
multiSelect.Value(&selectedScopes)
err = multiSelect.Run()
if err != nil {
return nil, cmdutils.NewCliError(err.Error())
}

if len(selectedScopes) == 0 {
return nil, fmt.Errorf("nenhum scope selecionado")
}

for _, scopeID := range selectedScopes {
scopesCreateList = append(scopesCreateList, ScopesCreate{
ID: scopeID,
})
}
}

return scopesCreateList, nil
}

func (a *authValue) RevokeApiKey(ctx context.Context, ID string) error {
client, err := NewOAuthClient(a.service.config)
if err != nil {
return fmt.Errorf("failed to create OAuth client: %w", err)
}

httpClient := client.AuthenticatedHttpClientFromContext(ctx)
if httpClient == nil {
return fmt.Errorf("programming error: unable to get HTTP Client from context")
}

url := fmt.Sprintf("%s/%s/revoke", a.service.config.ApiKeysURLV1, ID)

req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
url,
nil,
)
if err != nil {
return err
}

req.Header.Set("Content-Type", "application/json")

resp, err := httpClient.Do(req)
if err != nil {
return err
}

if resp.StatusCode != http.StatusOK {
return cmdutils.NewHttpErrorFromResponse(resp, req)
}

return nil
}
8 changes: 7 additions & 1 deletion base-cli/cmd/common/auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type Config struct {
PrivacyURL string
TenantsListURL string
TokenExchangeURL string
ApiKeysURLV1 string
ApiKeysURLV2 string
ScopesURL string
}

// DefaultConfig retorna a configuração padrão para autenticação
Expand All @@ -40,14 +43,17 @@ func DefaultConfig() *Config {
"virtual-machine.write", "dbaas.read", "mcr.write", "gdb:ssh-pkey-r",
"gdb:ssh-pkey-w", "pa:sa:manage", "lba.loadbalancer.read",
"lba.loadbalancer.write", "gdb:azs-r", "lbaas.read", "lbaas.write",
"iam:read", "iam:write",
"iam:read", "iam:write", "pa:cloud-cli:features",
},
ListenAddr: getListenAddr(),
Timeout: 500 * time.Millisecond,
TermsURL: "https://magalu.cloud/termos-legais/termos-de-uso-magalu-cloud/",
PrivacyURL: "https://magalu.cloud/termos-legais/politica-de-privacidade/",
TenantsListURL: "https://id.magalu.com/account/api/v2/whoami/tenants",
TokenExchangeURL: "https://id.magalu.com/oauth/token/exchange",
ApiKeysURLV1: "https://id.magalu.com/account/api/v1/api-keys",
ApiKeysURLV2: "https://id.magalu.com/account/api/v2/api-keys",
ScopesURL: "https://api.magalu.cloud/iam/api/v1/scopes",
}
}

Expand Down
68 changes: 68 additions & 0 deletions base-cli/cmd/common/auth/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,71 @@ type TokenExchangeResult struct {
RefreshToken string `json:"refresh_token"`
Scope []string `json:"scope"`
}

// ApiKeys representa o retorno da requisição de API Keys
type ApiKeys struct {
UUID string `json:"uuid"`
Name string `json:"name"`
ApiKey string `json:"api_key"`
Description string `json:"description"`
KeyPairID string `json:"key_pair_id"`
KeyPairSecret string `json:"key_pair_secret"`
StartValidity string `json:"start_validity"`
EndValidity *string `json:"end_validity,omitempty"`
RevokedAt *string `json:"revoked_at,omitempty"`
TenantName *string `json:"tenant_name,omitempty"`
Tenant struct {
UUID string `json:"uuid"`
LegalName string `json:"legal_name"`
} `json:"tenant"`
Scopes []struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Title string `json:"title"`
ConsentText string `json:"consent_text"`
Icon string `json:"icon"`
APIProduct struct {
UUID string `json:"uuid"`
Name string `json:"name"`
} `json:"api_product"`
} `json:"scopes"`
}

type Scope struct {
Name string `json:"name"`
Title string `json:"title"`
UUID string `json:"uuid"`
}

type APIProduct struct {
Name string `json:"name"`
Scopes []Scope `json:"scopes"`
UUID string `json:"uuid"`
}

type Platform struct {
APIProducts []APIProduct `json:"api_products"`
Name string `json:"name"`
UUID string `json:"uuid"`
}

type PlatformsResponse []Platform

type ScopesCreate struct {
ID string `json:"id"`
RequestReason string `json:"request_reason"`
}

type CreateApiKey struct {
Name string `json:"name"`
Description string `json:"description"`
TenantID string `json:"tenant_id"`
ScopesList []ScopesCreate `json:"scopes"`
StartValidity string `json:"start_validity"`
EndValidity string `json:"end_validity"`
}

type ApiKeyResult struct {
UUID string `json:"uuid,omitempty"`
Used bool `json:"used,omitempty"`
}
Loading