Add client package.
This commit is contained in:
parent
23c7a65799
commit
ccdbcce9c0
|
@ -0,0 +1,177 @@
|
||||||
|
#+TITLE: MCIAS Client SDK
|
||||||
|
|
||||||
|
The MCIAS Client SDK provides a Go client for interacting with the Metacircular Identity and Access System (MCIAS). It allows applications to authenticate users and retrieve database credentials from an MCIAS server.
|
||||||
|
|
||||||
|
* Installation
|
||||||
|
|
||||||
|
#+BEGIN_SRC bash
|
||||||
|
go get git.wntrmute.dev/kyle/mcias/client
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
* Usage
|
||||||
|
|
||||||
|
** Creating a Client
|
||||||
|
|
||||||
|
#+BEGIN_SRC go
|
||||||
|
import "git.wntrmute.dev/kyle/mcias/client"
|
||||||
|
|
||||||
|
// Create a client with default settings (connects to http://localhost:8080)
|
||||||
|
c := client.NewClient()
|
||||||
|
|
||||||
|
// Create a client with custom settings
|
||||||
|
c := client.NewClient(
|
||||||
|
client.WithBaseURL("https://mcias.example.com"),
|
||||||
|
client.WithUsername("username"),
|
||||||
|
client.WithToken("existing-token"),
|
||||||
|
)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Authentication
|
||||||
|
|
||||||
|
*** Password Authentication
|
||||||
|
|
||||||
|
#+BEGIN_SRC go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenResp, err := c.LoginWithPassword(ctx, "username", "password")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authenticated with token: %s\n", tokenResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
|
||||||
|
// Check if TOTP verification is required
|
||||||
|
if tokenResp.TOTPEnabled {
|
||||||
|
fmt.Println("TOTP verification required")
|
||||||
|
// See TOTP Verification section
|
||||||
|
}
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** Token Authentication
|
||||||
|
|
||||||
|
#+BEGIN_SRC go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenResp, err := c.LoginWithToken(ctx, "username", "existing-token")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to login with token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authenticated with token: %s\n", tokenResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** TOTP Verification
|
||||||
|
|
||||||
|
If TOTP is enabled for a user, you'll need to verify a TOTP code after password authentication:
|
||||||
|
|
||||||
|
#+BEGIN_SRC go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
totpResp, err := c.VerifyTOTP(ctx, "username", "123456") // Replace with actual TOTP code
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to verify TOTP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("TOTP verified, token: %s\n", totpResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(totpResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Retrieving Database Credentials
|
||||||
|
|
||||||
|
Once authenticated, you can retrieve database credentials:
|
||||||
|
|
||||||
|
#+BEGIN_SRC go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dbCreds, err := c.GetDatabaseCredentials(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get database credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Database Host: %s\n", dbCreds.Host)
|
||||||
|
fmt.Printf("Database Port: %d\n", dbCreds.Port)
|
||||||
|
fmt.Printf("Database Name: %s\n", dbCreds.Name)
|
||||||
|
fmt.Printf("Database User: %s\n", dbCreds.User)
|
||||||
|
fmt.Printf("Database Password: %s\n", dbCreds.Password)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
* Complete Example
|
||||||
|
|
||||||
|
Here's a complete example showing the authentication flow and database credential retrieval:
|
||||||
|
|
||||||
|
#+BEGIN_SRC go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/mcias/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create a new client
|
||||||
|
c := client.NewClient()
|
||||||
|
|
||||||
|
// Create a context with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Authenticate with username and password
|
||||||
|
tokenResp, err := c.LoginWithPassword(ctx, "username", "password")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authenticated with token: %s\n", tokenResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
|
||||||
|
// If TOTP is enabled, verify the TOTP code
|
||||||
|
if tokenResp.TOTPEnabled {
|
||||||
|
fmt.Println("TOTP is enabled, please enter your TOTP code")
|
||||||
|
var totpCode string
|
||||||
|
fmt.Scanln(&totpCode)
|
||||||
|
|
||||||
|
totpResp, err := c.VerifyTOTP(ctx, "username", totpCode)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to verify TOTP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("TOTP verified, new token: %s\n", totpResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(totpResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get database credentials
|
||||||
|
dbCreds, err := c.GetDatabaseCredentials(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get database credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Database Host: %s\n", dbCreds.Host)
|
||||||
|
fmt.Printf("Database Port: %d\n", dbCreds.Port)
|
||||||
|
fmt.Printf("Database Name: %s\n", dbCreds.Name)
|
||||||
|
fmt.Printf("Database User: %s\n", dbCreds.User)
|
||||||
|
fmt.Printf("Database Password: %s\n", dbCreds.Password)
|
||||||
|
}
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
* Error Handling
|
||||||
|
|
||||||
|
All methods return errors that should be checked. The errors include detailed information about what went wrong, including API error messages when available.
|
||||||
|
|
||||||
|
* Configuration Options
|
||||||
|
|
||||||
|
The client can be configured with the following options:
|
||||||
|
|
||||||
|
- =WithBaseURL(baseURL string)=: Sets the base URL of the MCIAS server (default: "http://localhost:8080")
|
||||||
|
- =WithHTTPClient(httpClient *http.Client)=: Sets a custom HTTP client (default: http.Client with 10s timeout)
|
||||||
|
- =WithToken(token string)=: Sets an authentication token
|
||||||
|
- =WithUsername(username string)=: Sets a username
|
|
@ -0,0 +1,212 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginParams represents the parameters for login.
|
||||||
|
type LoginParams struct {
|
||||||
|
User string `json:"user"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
TOTPCode string `json:"totp_code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest represents a login request to the MCIAS API.
|
||||||
|
type LoginRequest struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Login LoginParams `json:"login"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOTPVerifyRequest represents a TOTP verification request.
|
||||||
|
type TOTPVerifyRequest struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
TOTPCode string `json:"totp_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenResponse represents the response from a login or TOTP verification request.
|
||||||
|
type TokenResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Expires int64 `json:"expires"`
|
||||||
|
TOTPEnabled bool `json:"totp_enabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorResponse represents an error response from the API.
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
ErrorCode string `json:"error_code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginWithPassword authenticates with the MCIAS server using a username and password.
|
||||||
|
// If TOTP is enabled for the user, the TOTPEnabled field in the response will be true,
|
||||||
|
// and the client will need to call VerifyTOTP to complete authentication.
|
||||||
|
func (c *Client) LoginWithPassword(ctx context.Context, username, password string) (*TokenResponse, error) {
|
||||||
|
loginReq := LoginRequest{
|
||||||
|
Version: "v1",
|
||||||
|
Login: LoginParams{
|
||||||
|
User: username,
|
||||||
|
Password: password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(loginReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/v1/login/password", c.BaseURL)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var errResp ErrorResponse
|
||||||
|
if unmarshalErr := json.Unmarshal(body, &errResp); unmarshalErr == nil {
|
||||||
|
return nil, fmt.Errorf("API error: %s (code: %s)", errResp.Error, errResp.ErrorCode)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("API error: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp TokenResponse
|
||||||
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update client with authentication info
|
||||||
|
c.Token = tokenResp.Token
|
||||||
|
c.Username = username
|
||||||
|
|
||||||
|
return &tokenResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginWithToken authenticates with the MCIAS server using a token.
|
||||||
|
func (c *Client) LoginWithToken(ctx context.Context, username, token string) (*TokenResponse, error) {
|
||||||
|
loginReq := LoginRequest{
|
||||||
|
Version: "v1",
|
||||||
|
Login: LoginParams{
|
||||||
|
User: username,
|
||||||
|
Token: token,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(loginReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/v1/login/token", c.BaseURL)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var errResp ErrorResponse
|
||||||
|
if unmarshalErr := json.Unmarshal(body, &errResp); unmarshalErr == nil {
|
||||||
|
return nil, fmt.Errorf("API error: %s (code: %s)", errResp.Error, errResp.ErrorCode)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("API error: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp TokenResponse
|
||||||
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update client with authentication info
|
||||||
|
c.Token = tokenResp.Token
|
||||||
|
c.Username = username
|
||||||
|
|
||||||
|
return &tokenResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyTOTP verifies a TOTP code for a user.
|
||||||
|
func (c *Client) VerifyTOTP(ctx context.Context, username, totpCode string) (*TokenResponse, error) {
|
||||||
|
totpReq := TOTPVerifyRequest{
|
||||||
|
Version: "v1",
|
||||||
|
Username: username,
|
||||||
|
TOTPCode: totpCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(totpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/v1/login/totp", c.BaseURL)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var errResp ErrorResponse
|
||||||
|
if unmarshalErr := json.Unmarshal(body, &errResp); unmarshalErr == nil {
|
||||||
|
return nil, fmt.Errorf("API error: %s (code: %s)", errResp.Error, errResp.ErrorCode)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("API error: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp TokenResponse
|
||||||
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update client with authentication info
|
||||||
|
c.Token = tokenResp.Token
|
||||||
|
c.Username = username
|
||||||
|
|
||||||
|
return &tokenResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTokenExpired checks if the token is expired.
|
||||||
|
func (c *Client) IsTokenExpired(expiryTime int64) bool {
|
||||||
|
return expiryTime > 0 && expiryTime < time.Now().Unix()
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Package client provides a Go client for interacting with the MCIAS (Metacircular Identity and Access System).
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the main struct for interacting with the MCIAS API.
|
||||||
|
type Client struct {
|
||||||
|
// BaseURL is the base URL of the MCIAS server.
|
||||||
|
BaseURL string
|
||||||
|
|
||||||
|
// HTTPClient is the HTTP client used for making requests.
|
||||||
|
HTTPClient *http.Client
|
||||||
|
|
||||||
|
// Token is the authentication token.
|
||||||
|
Token string
|
||||||
|
|
||||||
|
// Username is the authenticated username.
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientOption is a function that configures a Client.
|
||||||
|
type ClientOption func(*Client)
|
||||||
|
|
||||||
|
// WithBaseURL sets the base URL for the client.
|
||||||
|
func WithBaseURL(baseURL string) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.BaseURL = baseURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHTTPClient sets the HTTP client for the client.
|
||||||
|
func WithHTTPClient(httpClient *http.Client) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.HTTPClient = httpClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithToken sets the authentication token for the client.
|
||||||
|
func WithToken(token string) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.Token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUsername sets the username for the client.
|
||||||
|
func WithUsername(username string) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.Username = username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new MCIAS client with the given options.
|
||||||
|
func NewClient(options ...ClientOption) *Client {
|
||||||
|
client := &Client{
|
||||||
|
BaseURL: "http://localhost:8080",
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAuthenticated returns true if the client has a token.
|
||||||
|
func (c *Client) IsAuthenticated() bool {
|
||||||
|
return c.Token != ""
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewClient(t *testing.T) {
|
||||||
|
// Test default client
|
||||||
|
client := NewClient()
|
||||||
|
if client.BaseURL != "http://localhost:8080" {
|
||||||
|
t.Errorf("Expected BaseURL to be http://localhost:8080, got %s", client.BaseURL)
|
||||||
|
}
|
||||||
|
if client.HTTPClient == nil {
|
||||||
|
t.Error("Expected HTTPClient to be initialized")
|
||||||
|
}
|
||||||
|
if client.Token != "" {
|
||||||
|
t.Errorf("Expected Token to be empty, got %s", client.Token)
|
||||||
|
}
|
||||||
|
if client.Username != "" {
|
||||||
|
t.Errorf("Expected Username to be empty, got %s", client.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test client with options
|
||||||
|
customHTTPClient := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
client = NewClient(
|
||||||
|
WithBaseURL("https://mcias.example.com"),
|
||||||
|
WithHTTPClient(customHTTPClient),
|
||||||
|
WithToken("test-token"),
|
||||||
|
WithUsername("test-user"),
|
||||||
|
)
|
||||||
|
if client.BaseURL != "https://mcias.example.com" {
|
||||||
|
t.Errorf("Expected BaseURL to be https://mcias.example.com, got %s", client.BaseURL)
|
||||||
|
}
|
||||||
|
if client.HTTPClient != customHTTPClient {
|
||||||
|
t.Error("Expected HTTPClient to be the custom client")
|
||||||
|
}
|
||||||
|
if client.Token != "test-token" {
|
||||||
|
t.Errorf("Expected Token to be test-token, got %s", client.Token)
|
||||||
|
}
|
||||||
|
if client.Username != "test-user" {
|
||||||
|
t.Errorf("Expected Username to be test-user, got %s", client.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAuthenticated(t *testing.T) {
|
||||||
|
// Test unauthenticated client
|
||||||
|
client := NewClient()
|
||||||
|
if client.IsAuthenticated() {
|
||||||
|
t.Error("Expected IsAuthenticated to return false for client without token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test authenticated client
|
||||||
|
client = NewClient(WithToken("test-token"))
|
||||||
|
if !client.IsAuthenticated() {
|
||||||
|
t.Error("Expected IsAuthenticated to return true for client with token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsTokenExpired(t *testing.T) {
|
||||||
|
client := NewClient()
|
||||||
|
|
||||||
|
// Test expired token
|
||||||
|
pastTime := time.Now().Add(-1 * time.Hour).Unix()
|
||||||
|
if !client.IsTokenExpired(pastTime) {
|
||||||
|
t.Error("Expected IsTokenExpired to return true for past time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test valid token
|
||||||
|
futureTime := time.Now().Add(1 * time.Hour).Unix()
|
||||||
|
if client.IsTokenExpired(futureTime) {
|
||||||
|
t.Error("Expected IsTokenExpired to return false for future time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test zero time (no expiry)
|
||||||
|
if client.IsTokenExpired(0) {
|
||||||
|
t.Error("Expected IsTokenExpired to return false for zero time")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatabaseCredentials represents the database connection credentials.
|
||||||
|
type DatabaseCredentials struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatabaseCredentials retrieves database credentials from the MCIAS server.
|
||||||
|
// This method requires the client to be authenticated (have a valid token).
|
||||||
|
func (c *Client) GetDatabaseCredentials(ctx context.Context) (*DatabaseCredentials, error) {
|
||||||
|
if !c.IsAuthenticated() {
|
||||||
|
return nil, fmt.Errorf("client is not authenticated, call LoginWithPassword or LoginWithToken first")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Username == "" {
|
||||||
|
return nil, fmt.Errorf("username is not set, call LoginWithPassword or LoginWithToken first")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the URL with query parameters
|
||||||
|
baseURL := fmt.Sprintf("%s/v1/database/credentials", c.BaseURL)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("username", c.Username)
|
||||||
|
requestURL := fmt.Sprintf("%s?%s", baseURL, params.Encode())
|
||||||
|
|
||||||
|
// Create the request
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add authorization header
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token))
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Read the response body
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var errResp ErrorResponse
|
||||||
|
if unmarshalErr := json.Unmarshal(body, &errResp); unmarshalErr == nil {
|
||||||
|
return nil, fmt.Errorf("API error: %s (code: %s)", errResp.Error, errResp.ErrorCode)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("API error: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var creds DatabaseCredentials
|
||||||
|
if err := json.Unmarshal(body, &creds); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &creds, nil
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package client_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/mcias/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
// Create a new client with default settings
|
||||||
|
c := client.NewClient()
|
||||||
|
|
||||||
|
// Create a context with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Authenticate with username and password
|
||||||
|
tokenResp, err := c.LoginWithPassword(ctx, "username", "password")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authenticated with token: %s\n", tokenResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
|
||||||
|
// If TOTP is enabled, verify the TOTP code
|
||||||
|
if tokenResp.TOTPEnabled {
|
||||||
|
fmt.Println("TOTP is enabled, please enter your TOTP code")
|
||||||
|
totpCode := "123456" // In a real application, this would be user input
|
||||||
|
|
||||||
|
totpResp, err := c.VerifyTOTP(ctx, "username", totpCode)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to verify TOTP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("TOTP verified, new token: %s\n", totpResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(totpResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get database credentials
|
||||||
|
dbCreds, err := c.GetDatabaseCredentials(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get database credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Database Host: %s\n", dbCreds.Host)
|
||||||
|
fmt.Printf("Database Port: %d\n", dbCreds.Port)
|
||||||
|
fmt.Printf("Database Name: %s\n", dbCreds.Name)
|
||||||
|
fmt.Printf("Database User: %s\n", dbCreds.User)
|
||||||
|
fmt.Printf("Database Password: %s\n", dbCreds.Password)
|
||||||
|
|
||||||
|
// Example of authenticating with a token
|
||||||
|
tokenClient := client.NewClient()
|
||||||
|
tokenResp, err = tokenClient.LoginWithToken(ctx, "username", "existing-token")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to login with token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authenticated with token: %s\n", tokenResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_LoginWithPassword() {
|
||||||
|
c := client.NewClient(
|
||||||
|
client.WithBaseURL("https://mcias.example.com"),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenResp, err := c.LoginWithPassword(ctx, "username", "password")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authenticated with token: %s\n", tokenResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
|
||||||
|
if tokenResp.TOTPEnabled {
|
||||||
|
fmt.Println("TOTP verification required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_LoginWithToken() {
|
||||||
|
c := client.NewClient()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenResp, err := c.LoginWithToken(ctx, "username", "existing-token")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to login with token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authenticated with token: %s\n", tokenResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_VerifyTOTP() {
|
||||||
|
c := client.NewClient()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
totpResp, err := c.VerifyTOTP(ctx, "username", "123456")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to verify TOTP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("TOTP verified, token: %s\n", totpResp.Token)
|
||||||
|
fmt.Printf("Token expires at: %s\n", time.Unix(totpResp.Expires, 0).Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_GetDatabaseCredentials() {
|
||||||
|
// Create a client with pre-configured authentication
|
||||||
|
c := client.NewClient(
|
||||||
|
client.WithUsername("username"),
|
||||||
|
client.WithToken("existing-token"),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dbCreds, err := c.GetDatabaseCredentials(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get database credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Database Host: %s\n", dbCreds.Host)
|
||||||
|
fmt.Printf("Database Port: %d\n", dbCreds.Port)
|
||||||
|
fmt.Printf("Database Name: %s\n", dbCreds.Name)
|
||||||
|
fmt.Printf("Database User: %s\n", dbCreds.User)
|
||||||
|
fmt.Printf("Database Password: %s\n", dbCreds.Password)
|
||||||
|
}
|
Loading…
Reference in New Issue