Skip to content

Commit b3e1782

Browse files
Merge pull request #26 from MagaluCloud/feat/auth-tenant
feat: implemented tenant management commands
2 parents 53fa1ea + a565d37 commit b3e1782

File tree

15 files changed

+763
-10
lines changed

15 files changed

+763
-10
lines changed

base-cli/cmd/common/auth/auth.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package auth
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"fmt"
8+
"net/http"
69
"os"
710
"path"
11+
"strings"
812
"time"
913

1014
"github.com/golang-jwt/jwt/v5"
1115
"github.com/magaluCloud/mgccli/cmd/common/structs"
1216
"github.com/magaluCloud/mgccli/cmd/common/workspace"
17+
cmdutils "github.com/magaluCloud/mgccli/cmd_utils"
1318
"gopkg.in/yaml.v3"
1419
)
1520

@@ -32,17 +37,23 @@ type Auth interface {
3237
GetRefreshToken() string
3338
GetSecretAccessKey() string
3439
GetService() *Service
40+
GetCurrentTenantID() (string, error)
41+
GetCurrentTenant(ctx context.Context) (*Tenant, error)
42+
GetScopes() (string, error)
3543
TokenClaims() (*TokenClaims, error)
3644

3745
SetAccessToken(token string) error
3846
SetRefreshToken(token string) error
3947
SetSecretAccessKey(key string) error
4048
SetAccessKeyID(key string) error
49+
SetTenant(ctx context.Context, id string) (*TokenExchangeResult, error)
4150

4251
ValidateToken() error
4352
RefreshToken(ctx context.Context) error
4453

4554
Logout() error
55+
56+
ListTenants(ctx context.Context) ([]*Tenant, error)
4657
}
4758

4859
type authValue struct {
@@ -98,6 +109,54 @@ func (a *authValue) GetSecretAccessKey() string {
98109
return a.authValue.SecretAccessKey
99110
}
100111

112+
func (a *authValue) GetCurrentTenantID() (string, error) {
113+
claims, err := a.TokenClaims()
114+
if err != nil {
115+
return "", err
116+
}
117+
118+
tenantId := claims.TenantIDWithType
119+
120+
// Dot is a separator, Tenant will be <TenantType>.<ID>. We only want the ID
121+
dotIndex := strings.Index(tenantId, ".")
122+
123+
if dotIndex != -1 {
124+
tenantId = tenantId[dotIndex+1:]
125+
}
126+
127+
return tenantId, nil
128+
}
129+
130+
func (a *authValue) GetCurrentTenant(ctx context.Context) (*Tenant, error) {
131+
currentTenantId, err := a.GetCurrentTenantID()
132+
if err != nil {
133+
return nil, err
134+
}
135+
136+
tenants, err := a.ListTenants(ctx)
137+
if err != nil || len(tenants) == 0 {
138+
fmt.Printf("Não foi possível pegar as informações sobre o tenant atual, retornando apenas o ID.\nErro: %v\n\n", err)
139+
return &Tenant{UUID: currentTenantId}, nil
140+
}
141+
142+
for _, tenant := range tenants {
143+
if tenant.UUID == currentTenantId {
144+
return tenant, nil
145+
}
146+
}
147+
148+
return nil, fmt.Errorf("o ID (%s) do tenant atual não foi encontrado na lista de tenants", currentTenantId)
149+
}
150+
151+
func (a *authValue) GetScopes() (string, error) {
152+
tokenClaims, err := a.TokenClaims()
153+
if err != nil {
154+
return "", err
155+
}
156+
157+
return tokenClaims.ScopesStr, nil
158+
}
159+
101160
func (a *authValue) SetAccessToken(token string) error {
102161
a.authValue.AccessToken = token
103162
return a.Write()
@@ -182,3 +241,119 @@ func (a *authValue) RefreshToken(ctx context.Context) error {
182241
a.authValue.RefreshToken = token.RefreshToken
183242
return a.Write()
184243
}
244+
245+
func (a *authValue) ListTenants(ctx context.Context) ([]*Tenant, error) {
246+
client, err := NewOAuthClient(a.service.config)
247+
if err != nil {
248+
return nil, fmt.Errorf("failed to create OAuth client: %w", err)
249+
}
250+
251+
httpClient := client.AuthenticatedHttpClientFromContext(ctx)
252+
if httpClient == nil {
253+
return nil, fmt.Errorf("programming error: unable to get HTTP Client from context")
254+
}
255+
256+
r, err := http.NewRequestWithContext(
257+
ctx,
258+
http.MethodGet,
259+
a.service.config.TenantsListURL,
260+
nil,
261+
)
262+
if err != nil {
263+
return nil, err
264+
}
265+
r.Header.Set("Content-Type", "application/json")
266+
267+
resp, err := httpClient.Do(r)
268+
if err != nil {
269+
return nil, err
270+
}
271+
272+
if resp.StatusCode != http.StatusOK {
273+
return nil, cmdutils.NewHttpErrorFromResponse(resp, r)
274+
}
275+
276+
defer resp.Body.Close()
277+
var result []*Tenant
278+
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
279+
return nil, err
280+
}
281+
282+
return result, nil
283+
}
284+
285+
func (a *authValue) SetTenant(ctx context.Context, id string) (*TokenExchangeResult, error) {
286+
return a.runTokenExchange(ctx, id)
287+
}
288+
289+
func (a *authValue) runTokenExchange(
290+
ctx context.Context, tenantId string,
291+
) (*TokenExchangeResult, error) {
292+
client, err := NewOAuthClient(a.service.config)
293+
if err != nil {
294+
return nil, fmt.Errorf("failed to create OAuth client: %w", err)
295+
}
296+
297+
httpClient := client.AuthenticatedHttpClientFromContext(ctx)
298+
if httpClient == nil {
299+
return nil, fmt.Errorf("programming error: unable to get HTTP Client from context")
300+
}
301+
302+
scopes, err := a.GetScopes()
303+
if err != nil {
304+
return nil, err
305+
}
306+
307+
data := map[string]any{
308+
"tenant": tenantId,
309+
"scopes": scopes,
310+
}
311+
jsonData, err := json.Marshal(data)
312+
if err != nil {
313+
return nil, err
314+
}
315+
316+
bodyReader := bytes.NewReader(jsonData)
317+
r, err := http.NewRequestWithContext(ctx, http.MethodPost, a.service.config.TokenExchangeURL, bodyReader)
318+
r.Header.Set("Content-Type", "application/json")
319+
320+
if err != nil {
321+
return nil, err
322+
}
323+
324+
resp, err := httpClient.Do(r)
325+
if err != nil {
326+
return nil, err
327+
}
328+
329+
defer r.Body.Close()
330+
331+
if resp.StatusCode != http.StatusOK {
332+
return nil, cmdutils.NewHttpErrorFromResponse(resp, r)
333+
}
334+
335+
payload := &TenantResult{}
336+
if err = json.NewDecoder(resp.Body).Decode(payload); err != nil {
337+
return nil, err
338+
}
339+
340+
err = a.SetAccessToken(payload.AccessToken)
341+
if err != nil {
342+
return nil, err
343+
}
344+
345+
err = a.SetRefreshToken(payload.RefreshToken)
346+
if err != nil {
347+
return nil, err
348+
}
349+
350+
createdAt := time.Time(time.Unix(int64(payload.CreatedAt), 0))
351+
352+
return &TokenExchangeResult{
353+
AccessToken: payload.AccessToken,
354+
CreatedAt: createdAt,
355+
TenantID: tenantId,
356+
RefreshToken: payload.RefreshToken,
357+
Scope: strings.Split(payload.Scope, " "),
358+
}, nil
359+
}

base-cli/cmd/common/auth/config.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ type Config struct {
1919
Timeout time.Duration
2020

2121
// External Links
22-
TermsURL string
23-
PrivacyURL string
22+
TermsURL string
23+
PrivacyURL string
24+
TenantsListURL string
25+
TokenExchangeURL string
2426
}
2527

2628
// DefaultConfig retorna a configuração padrão para autenticação
@@ -40,10 +42,12 @@ func DefaultConfig() *Config {
4042
"lba.loadbalancer.write", "gdb:azs-r", "lbaas.read", "lbaas.write",
4143
"iam:read", "iam:write",
4244
},
43-
ListenAddr: getListenAddr(),
44-
Timeout: 500 * time.Millisecond,
45-
TermsURL: "https://magalu.cloud/termos-legais/termos-de-uso-magalu-cloud/",
46-
PrivacyURL: "https://magalu.cloud/termos-legais/politica-de-privacidade/",
45+
ListenAddr: getListenAddr(),
46+
Timeout: 500 * time.Millisecond,
47+
TermsURL: "https://magalu.cloud/termos-legais/termos-de-uso-magalu-cloud/",
48+
PrivacyURL: "https://magalu.cloud/termos-legais/politica-de-privacidade/",
49+
TenantsListURL: "https://id.magalu.com/account/api/v2/whoami/tenants",
50+
TokenExchangeURL: "https://id.magalu.com/oauth/token/exchange",
4751
}
4852
}
4953

base-cli/cmd/common/auth/http.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package auth
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"os"
11+
"syscall"
12+
"time"
13+
14+
cmdutils "github.com/magaluCloud/mgccli/cmd_utils"
15+
)
16+
17+
type authRoundTripper struct {
18+
parent http.RoundTripper
19+
auth Auth
20+
attempts int
21+
}
22+
23+
func (rt *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
24+
token := rt.auth.GetAccessToken(req.Context())
25+
26+
if token == "" {
27+
return nil, fmt.Errorf("não foi possível obter o token de acesso. Você esqueceu de fazer login?")
28+
}
29+
30+
req.Header.Set("Authorization", "Bearer "+token)
31+
32+
waitBeforeRetry := 100 * time.Millisecond
33+
var res *http.Response
34+
var err error
35+
36+
if req.Body != nil {
37+
defer req.Body.Close()
38+
}
39+
40+
for i := 0; i < rt.attempts; i++ {
41+
reqCopy := rt.cloneRequest(req)
42+
res, err = rt.parent.RoundTrip(reqCopy)
43+
44+
if err != nil {
45+
var sysErr *os.SyscallError
46+
47+
if os.IsTimeout(err) {
48+
fmt.Println("Request timeout, retrying...\n", "attempt", i+1, "\n ")
49+
time.Sleep(waitBeforeRetry)
50+
waitBeforeRetry = waitBeforeRetry * 2
51+
continue
52+
}
53+
54+
if errors.As(err, &sysErr) {
55+
if sysErr.Err == syscall.ECONNRESET {
56+
fmt.Println("\n\n\nConn reset by peer! THIS IS A SERVER PROBLEM!!!\n\n\n", "attempt", i+1, "")
57+
time.Sleep(waitBeforeRetry)
58+
waitBeforeRetry = waitBeforeRetry * 2
59+
continue
60+
}
61+
}
62+
return res, err
63+
}
64+
if res.StatusCode >= 500 {
65+
fmt.Println("\n\n\nServer responded with fail, retrying...\n\n\n", "attempt", i+1, "status code", res.StatusCode, "")
66+
time.Sleep(waitBeforeRetry)
67+
waitBeforeRetry = waitBeforeRetry * 2
68+
continue
69+
}
70+
71+
return res, err
72+
}
73+
74+
return rt.parent.RoundTrip(req)
75+
}
76+
77+
func (c *OAuthClient) AuthenticatedHttpClientFromContext(ctx context.Context) *http.Client {
78+
auth := ctx.Value(cmdutils.CTX_AUTH_KEY).(Auth)
79+
80+
transport := c.httpClient.Transport
81+
82+
if transport == nil {
83+
transport = http.DefaultTransport
84+
}
85+
86+
transport = &authRoundTripper{parent: transport, auth: auth, attempts: 5}
87+
88+
return &http.Client{
89+
Transport: transport,
90+
Timeout: c.httpClient.Timeout,
91+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
92+
// Don't follow redirects, return an error to use the last response.
93+
return http.ErrUseLastResponse
94+
}}
95+
}
96+
97+
func (rt *authRoundTripper) cloneRequest(req *http.Request) *http.Request {
98+
var body io.Reader
99+
var err error
100+
101+
if req.Body != nil {
102+
body, err = rt.cloneRequestBody(req)
103+
if err != nil {
104+
fmt.Println("Erro: %w", err)
105+
return req
106+
}
107+
}
108+
clonedRequest, err := http.NewRequestWithContext(req.Context(), req.Method, req.URL.String(), body)
109+
if err != nil {
110+
return req
111+
}
112+
clonedRequest.Header = req.Header
113+
return clonedRequest
114+
}
115+
116+
func (rt *authRoundTripper) cloneRequestBody(req *http.Request) (io.Reader, error) {
117+
body, err := io.ReadAll(req.Body)
118+
if err != nil {
119+
return nil, fmt.Errorf("erro: %w", err)
120+
}
121+
122+
req.Body = io.NopCloser(bytes.NewBuffer(body))
123+
return bytes.NewReader(body), nil
124+
}

0 commit comments

Comments
 (0)