edge-connect-client/internal/infrastructure/edgeconnect_client/auth.go
Stephan Lo 7b062612f5 refactor(arch): Separate infrastructure from driven adapter
This commit introduces a significant architectural refactoring to decouple the driven adapter from low-level infrastructure concerns, adhering more strictly to the principles of Hexagonal Architecture.

Problem:
The driven adapter in `internal/adapters/driven/edgeconnect` was responsible for both adapting data structures and handling direct HTTP communication, authentication, and request/response logic. This violated the separation of concerns, making the adapter difficult to test and maintain.

Solution:
A new infrastructure layer has been created at `internal/infrastructure`. This layer now contains all the low-level details of interacting with the EdgeConnect API.

Key Changes:
- **New Infrastructure Layer:** Created `internal/infrastructure` to house components that connect to external systems.
- **Generic HTTP Client:** A new, generic `edgeconnect_client` was created in `internal/infrastructure/edgeconnect_client`. It is responsible for authentication, making HTTP requests, and handling raw responses. It has no knowledge of the application's domain models.
- **Config & Transport Moved:** The `config` and `http` (now `transport`) packages were moved into the infrastructure layer, as they are details of how the application is configured and communicates.
- **Consolidated Driven Adapter:** The logic from the numerous old adapter files (`apps.go`, `cloudlet.go`, etc.) has been consolidated into a single, true adapter at `internal/adapters/driven/edgeconnect/adapter.go`.
- **Clear Responsibility:** The new `adapter.go` is now solely responsible for:
  1. Implementing the driven port (repository) interfaces.
  2. Translating domain models into the data structures required by the `edgeconnect_client`.
  3. Calling the `edgeconnect_client` to perform the API operations.
  4. Translating the results back into domain models.
- **Updated Dependency Injection:** The application's entry point (`cmd/cli/main.go`) has been updated to construct and inject dependencies according to the new architecture: `infra_client` -> `adapter` -> `service` -> `cli_command`.
- **SDK & Apply Command:** The SDK examples and the `apply` command have been updated to use the new adapter and its repository methods, removing all direct client instantiation.
2025-10-09 00:47:45 +02:00

190 lines
5.1 KiB
Go

// ABOUTME: Authentication providers for EdgeXR Master Controller API
// ABOUTME: Supports Bearer token authentication with pluggable provider interface
package edgeconnect_client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"time"
)
// AuthProvider interface for attaching authentication to requests
type AuthProvider interface {
// Attach adds authentication headers to the request
Attach(ctx context.Context, req *http.Request) error
}
// StaticTokenProvider implements Bearer token authentication with a fixed token
type StaticTokenProvider struct {
Token string
}
// NewStaticTokenProvider creates a new static token provider
func NewStaticTokenProvider(token string) *StaticTokenProvider {
return &StaticTokenProvider{Token: token}
}
// Attach adds the Bearer token to the request Authorization header
func (s *StaticTokenProvider) Attach(ctx context.Context, req *http.Request) error {
if s.Token != "" {
req.Header.Set("Authorization", "Bearer "+s.Token)
}
return nil
}
// UsernamePasswordProvider implements dynamic token retrieval using username/password
// This matches the existing client/client.go RetrieveToken implementation
type UsernamePasswordProvider struct {
BaseURL string
Username string
Password string
HTTPClient *http.Client
// Token caching
mu sync.RWMutex
cachedToken string
tokenExpiry time.Time
}
// NewUsernamePasswordProvider creates a new username/password auth provider
func NewUsernamePasswordProvider(baseURL, username, password string, httpClient *http.Client) *UsernamePasswordProvider {
if httpClient == nil {
httpClient = &http.Client{Timeout: 30 * time.Second}
}
return &UsernamePasswordProvider{
BaseURL: strings.TrimRight(baseURL, "/"),
Username: username,
Password: password,
HTTPClient: httpClient,
}
}
// Attach retrieves a token (with caching) and adds it to the Authorization header
func (u *UsernamePasswordProvider) Attach(ctx context.Context, req *http.Request) error {
token, err := u.getToken(ctx)
if err != nil {
return fmt.Errorf("failed to get token: %w", err)
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
return nil
}
// getToken retrieves a token, using cache if valid
func (u *UsernamePasswordProvider) getToken(ctx context.Context) (string, error) {
// Check cache first
u.mu.RLock()
if u.cachedToken != "" && time.Now().Before(u.tokenExpiry) {
token := u.cachedToken
u.mu.RUnlock()
return token, nil
}
u.mu.RUnlock()
// Need to retrieve new token
u.mu.Lock()
defer u.mu.Unlock()
// Double-check after acquiring write lock
if u.cachedToken != "" && time.Now().Before(u.tokenExpiry) {
return u.cachedToken, nil
}
// Retrieve token using existing RetrieveToken logic
token, err := u.retrieveToken(ctx)
if err != nil {
return "", err
}
// Cache token with reasonable expiry (assume 1 hour, can be configurable)
u.cachedToken = token
u.tokenExpiry = time.Now().Add(1 * time.Hour)
return token, nil
}
// retrieveToken implements the same logic as the existing client/client.go RetrieveToken method
func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, error) {
// Marshal credentials - same as existing implementation
jsonData, err := json.Marshal(map[string]string{
"username": u.Username,
"password": u.Password,
})
if err != nil {
return "", err
}
// Create request - same as existing implementation
loginURL := u.BaseURL + "/api/v1/login"
request, err := http.NewRequestWithContext(ctx, "POST", loginURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := u.HTTPClient.Do(request)
if err != nil {
return "", err
}
defer func() {
if err := resp.Body.Close(); err != nil {
// Can't use c.logf here since this is in auth provider
fmt.Fprintf(os.Stderr, "Warning: failed to close auth response body: %v\n", err)
}
}()
// Read response body - same as existing implementation
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("login failed with status %d: %s", resp.StatusCode, string(body))
}
// Parse JSON response - same as existing implementation
var respData struct {
Token string `json:"token"`
}
err = json.Unmarshal(body, &respData)
if err != nil {
return "", fmt.Errorf("error parsing JSON (status %d): %v", resp.StatusCode, err)
}
return respData.Token, nil
}
// InvalidateToken clears the cached token, forcing a new login on next request
func (u *UsernamePasswordProvider) InvalidateToken() {
u.mu.Lock()
defer u.mu.Unlock()
u.cachedToken = ""
u.tokenExpiry = time.Time{}
}
// NoAuthProvider implements no authentication (for testing or public endpoints)
type NoAuthProvider struct{}
// NewNoAuthProvider creates a new no-auth provider
func NewNoAuthProvider() *NoAuthProvider {
return &NoAuthProvider{}
}
// Attach does nothing (no authentication)
func (n *NoAuthProvider) Attach(ctx context.Context, req *http.Request) error {
return nil
}