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.
190 lines
5.1 KiB
Go
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
|
|
}
|