- Add TOTP enrollment/confirmation/removal to all clients - Add password change and admin set-password endpoints - Add account listing, status update, and tag management - Add audit log listing with filter support - Add policy rule CRUD operations - Expand test coverage for all new endpoints across clients - Fix .gitignore to exclude built binaries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
667 lines
22 KiB
Go
667 lines
22 KiB
Go
// Package mciasgoclient provides a thread-safe Go client for the MCIAS REST API.
|
|
//
|
|
// Security: bearer tokens are stored under a sync.RWMutex and are never written
|
|
// to logs or included in error messages anywhere in this package.
|
|
package mciasgoclient
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Error types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// MciasError is the base error type for all MCIAS client errors.
|
|
type MciasError struct {
|
|
StatusCode int
|
|
Message string
|
|
}
|
|
|
|
func (e *MciasError) Error() string {
|
|
return fmt.Sprintf("mciasgoclient: HTTP %d: %s", e.StatusCode, e.Message)
|
|
}
|
|
|
|
// MciasAuthError is returned for 401 Unauthorized responses.
|
|
type MciasAuthError struct{ MciasError }
|
|
|
|
// MciasForbiddenError is returned for 403 Forbidden responses.
|
|
type MciasForbiddenError struct{ MciasError }
|
|
|
|
// MciasNotFoundError is returned for 404 Not Found responses.
|
|
type MciasNotFoundError struct{ MciasError }
|
|
|
|
// MciasInputError is returned for 400 Bad Request responses.
|
|
type MciasInputError struct{ MciasError }
|
|
|
|
// MciasConflictError is returned for 409 Conflict responses.
|
|
type MciasConflictError struct{ MciasError }
|
|
|
|
// MciasServerError is returned for 5xx responses.
|
|
type MciasServerError struct{ MciasError }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Data types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Account represents a user or service account.
|
|
type Account struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
AccountType string `json:"account_type"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
TOTPEnabled bool `json:"totp_enabled"`
|
|
}
|
|
|
|
// PublicKey represents the server's Ed25519 public key in JWK format.
|
|
type PublicKey struct {
|
|
Kty string `json:"kty"`
|
|
Crv string `json:"crv"`
|
|
X string `json:"x"`
|
|
Use string `json:"use,omitempty"`
|
|
Alg string `json:"alg,omitempty"`
|
|
}
|
|
|
|
// TokenClaims is returned by ValidateToken.
|
|
type TokenClaims struct {
|
|
Valid bool `json:"valid"`
|
|
Sub string `json:"sub,omitempty"`
|
|
Roles []string `json:"roles,omitempty"`
|
|
ExpiresAt string `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// PGCreds holds Postgres connection credentials.
|
|
type PGCreds struct {
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
Database string `json:"database"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// TOTPEnrollResponse is returned by EnrollTOTP.
|
|
type TOTPEnrollResponse struct {
|
|
Secret string `json:"secret"`
|
|
OTPAuthURI string `json:"otpauth_uri"`
|
|
}
|
|
|
|
// AuditEvent is a single entry in the audit log.
|
|
type AuditEvent struct {
|
|
ID int `json:"id"`
|
|
EventType string `json:"event_type"`
|
|
EventTime string `json:"event_time"`
|
|
ActorID string `json:"actor_id,omitempty"`
|
|
TargetID string `json:"target_id,omitempty"`
|
|
IPAddress string `json:"ip_address"`
|
|
Details string `json:"details,omitempty"`
|
|
}
|
|
|
|
// AuditListResponse is returned by ListAudit.
|
|
type AuditListResponse struct {
|
|
Events []AuditEvent `json:"events"`
|
|
Total int `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
}
|
|
|
|
// AuditFilter holds optional filter parameters for ListAudit.
|
|
type AuditFilter struct {
|
|
Limit int
|
|
Offset int
|
|
EventType string
|
|
ActorID string
|
|
}
|
|
|
|
// PolicyRuleBody holds the match conditions and effect of a policy rule.
|
|
// All fields except Effect are optional; an omitted field acts as a wildcard.
|
|
type PolicyRuleBody struct {
|
|
Effect string `json:"effect"`
|
|
Roles []string `json:"roles,omitempty"`
|
|
AccountTypes []string `json:"account_types,omitempty"`
|
|
SubjectUUID string `json:"subject_uuid,omitempty"`
|
|
Actions []string `json:"actions,omitempty"`
|
|
ResourceType string `json:"resource_type,omitempty"`
|
|
OwnerMatchesSubject bool `json:"owner_matches_subject,omitempty"`
|
|
ServiceNames []string `json:"service_names,omitempty"`
|
|
RequiredTags []string `json:"required_tags,omitempty"`
|
|
}
|
|
|
|
// PolicyRule is a complete operator-defined policy rule as returned by the API.
|
|
type PolicyRule struct {
|
|
ID int `json:"id"`
|
|
Priority int `json:"priority"`
|
|
Description string `json:"description"`
|
|
Rule PolicyRuleBody `json:"rule"`
|
|
Enabled bool `json:"enabled"`
|
|
NotBefore string `json:"not_before,omitempty"`
|
|
ExpiresAt string `json:"expires_at,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// CreatePolicyRuleRequest holds the parameters for creating a policy rule.
|
|
type CreatePolicyRuleRequest struct {
|
|
Description string `json:"description"`
|
|
Priority int `json:"priority,omitempty"`
|
|
Rule PolicyRuleBody `json:"rule"`
|
|
NotBefore string `json:"not_before,omitempty"`
|
|
ExpiresAt string `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// UpdatePolicyRuleRequest holds the parameters for updating a policy rule.
|
|
// All fields are optional; omitted fields are left unchanged.
|
|
// Set ClearNotBefore or ClearExpiresAt to true to remove those constraints.
|
|
type UpdatePolicyRuleRequest struct {
|
|
Description string `json:"description,omitempty"`
|
|
Priority *int `json:"priority,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
Rule *PolicyRuleBody `json:"rule,omitempty"`
|
|
NotBefore string `json:"not_before,omitempty"`
|
|
ExpiresAt string `json:"expires_at,omitempty"`
|
|
ClearNotBefore bool `json:"clear_not_before,omitempty"`
|
|
ClearExpiresAt bool `json:"clear_expires_at,omitempty"`
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Options and Client struct
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Options configures the MCIAS client.
|
|
type Options struct {
|
|
// CACertPath is an optional path to a PEM-encoded CA certificate for TLS
|
|
// verification of self-signed or private-CA certificates.
|
|
CACertPath string
|
|
// Token is an optional pre-existing bearer token.
|
|
Token string
|
|
}
|
|
|
|
// Client is a thread-safe MCIAS REST API client.
|
|
// Security: the bearer token is guarded by a sync.RWMutex; it is never
|
|
// written to logs or included in error messages in this library.
|
|
type Client struct {
|
|
baseURL string
|
|
http *http.Client
|
|
mu sync.RWMutex
|
|
token string
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constructor
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// New creates a new Client for the given serverURL.
|
|
// TLS 1.2 is the minimum version enforced on all connections.
|
|
// If opts.CACertPath is set, that CA certificate is added to the trust pool.
|
|
func New(serverURL string, opts Options) (*Client, error) {
|
|
serverURL = strings.TrimRight(serverURL, "/")
|
|
// Security: never negotiate TLS < 1.2; this prevents POODLE/BEAST-class
|
|
// downgrade attacks against the token-bearing transport.
|
|
tlsCfg := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
if opts.CACertPath != "" {
|
|
pem, err := os.ReadFile(opts.CACertPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mciasgoclient: read CA cert: %w", err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(pem) {
|
|
return nil, fmt.Errorf("mciasgoclient: no valid certs in CA file")
|
|
}
|
|
tlsCfg.RootCAs = pool
|
|
}
|
|
transport := &http.Transport{TLSClientConfig: tlsCfg}
|
|
c := &Client{
|
|
baseURL: serverURL,
|
|
http: &http.Client{Transport: transport},
|
|
token: opts.Token,
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Token returns the current bearer token (empty string if not logged in).
|
|
func (c *Client) Token() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.token
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Internal helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (c *Client) setToken(tok string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.token = tok
|
|
}
|
|
|
|
func (c *Client) do(method, path string, body interface{}, out interface{}) error {
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("mciasgoclient: marshal request: %w", err)
|
|
}
|
|
reqBody = bytes.NewReader(b)
|
|
}
|
|
req, err := http.NewRequest(method, c.baseURL+path, reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("mciasgoclient: build request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
// Security: token is read under the lock and added to the Authorization
|
|
// header only; it is never written to any log or error message in this
|
|
// library.
|
|
c.mu.RLock()
|
|
tok := c.token
|
|
c.mu.RUnlock()
|
|
if tok != "" {
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
}
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("mciasgoclient: request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("mciasgoclient: read response: %w", err)
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
var errResp struct {
|
|
Error string `json:"error"`
|
|
}
|
|
_ = json.Unmarshal(respBytes, &errResp)
|
|
msg := errResp.Error
|
|
if msg == "" {
|
|
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return makeError(resp.StatusCode, msg)
|
|
}
|
|
if out != nil && len(respBytes) > 0 {
|
|
dec := json.NewDecoder(bytes.NewReader(respBytes))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(out); err != nil {
|
|
return fmt.Errorf("mciasgoclient: decode response: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func makeError(status int, msg string) error {
|
|
base := MciasError{StatusCode: status, Message: msg}
|
|
switch {
|
|
case status == 401:
|
|
return &MciasAuthError{base}
|
|
case status == 403:
|
|
return &MciasForbiddenError{base}
|
|
case status == 404:
|
|
return &MciasNotFoundError{base}
|
|
case status == 400:
|
|
return &MciasInputError{base}
|
|
case status == 409:
|
|
return &MciasConflictError{base}
|
|
default:
|
|
return &MciasServerError{base}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API methods — Public
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Health calls GET /v1/health. Returns nil if the server is healthy.
|
|
func (c *Client) Health() error {
|
|
return c.do(http.MethodGet, "/v1/health", nil, nil)
|
|
}
|
|
|
|
// GetPublicKey returns the server's Ed25519 public key in JWK format.
|
|
func (c *Client) GetPublicKey() (*PublicKey, error) {
|
|
var pk PublicKey
|
|
if err := c.do(http.MethodGet, "/v1/keys/public", nil, &pk); err != nil {
|
|
return nil, err
|
|
}
|
|
return &pk, nil
|
|
}
|
|
|
|
// Login authenticates with username and password. On success the token is
|
|
// stored in the Client and returned along with the expiry timestamp.
|
|
// totpCode may be empty for accounts without TOTP.
|
|
func (c *Client) Login(username, password, totpCode string) (token, expiresAt string, err error) {
|
|
req := map[string]string{"username": username, "password": password}
|
|
if totpCode != "" {
|
|
req["totp_code"] = totpCode
|
|
}
|
|
var resp struct {
|
|
Token string `json:"token"`
|
|
ExpiresAt string `json:"expires_at"`
|
|
}
|
|
if err := c.do(http.MethodPost, "/v1/auth/login", req, &resp); err != nil {
|
|
return "", "", err
|
|
}
|
|
c.setToken(resp.Token)
|
|
return resp.Token, resp.ExpiresAt, nil
|
|
}
|
|
|
|
// ValidateToken validates a token string against the server.
|
|
// Returns claims; Valid is false (not an error) if the token is expired or
|
|
// revoked.
|
|
func (c *Client) ValidateToken(token string) (*TokenClaims, error) {
|
|
var claims TokenClaims
|
|
if err := c.do(http.MethodPost, "/v1/token/validate",
|
|
map[string]string{"token": token}, &claims); err != nil {
|
|
return nil, err
|
|
}
|
|
return &claims, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API methods — Authenticated
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Logout revokes the current token on the server and clears it from the client.
|
|
func (c *Client) Logout() error {
|
|
if err := c.do(http.MethodPost, "/v1/auth/logout", nil, nil); err != nil {
|
|
return err
|
|
}
|
|
c.setToken("")
|
|
return nil
|
|
}
|
|
|
|
// RenewToken exchanges the current token for a fresh one.
|
|
// The new token is stored in the client and returned.
|
|
func (c *Client) RenewToken() (token, expiresAt string, err error) {
|
|
var resp struct {
|
|
Token string `json:"token"`
|
|
ExpiresAt string `json:"expires_at"`
|
|
}
|
|
if err := c.do(http.MethodPost, "/v1/auth/renew", map[string]string{}, &resp); err != nil {
|
|
return "", "", err
|
|
}
|
|
c.setToken(resp.Token)
|
|
return resp.Token, resp.ExpiresAt, nil
|
|
}
|
|
|
|
// EnrollTOTP begins TOTP enrollment for the authenticated account.
|
|
// Returns a base32 secret and an otpauth:// URI for QR-code generation.
|
|
// The secret is shown once; it is not retrievable after this call.
|
|
// TOTP is not enforced until confirmed via ConfirmTOTP.
|
|
func (c *Client) EnrollTOTP() (*TOTPEnrollResponse, error) {
|
|
var resp TOTPEnrollResponse
|
|
if err := c.do(http.MethodPost, "/v1/auth/totp/enroll", nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ConfirmTOTP completes TOTP enrollment by verifying the current code against
|
|
// the pending secret. On success, TOTP becomes required for all future logins.
|
|
func (c *Client) ConfirmTOTP(code string) error {
|
|
return c.do(http.MethodPost, "/v1/auth/totp/confirm",
|
|
map[string]string{"code": code}, nil)
|
|
}
|
|
|
|
// ChangePassword changes the password of the currently authenticated human
|
|
// account. currentPassword is required to prevent token-theft attacks.
|
|
// On success, all active sessions except the caller's are revoked.
|
|
//
|
|
// Security: both passwords are transmitted over TLS only; the server verifies
|
|
// currentPassword with constant-time comparison before accepting the change.
|
|
func (c *Client) ChangePassword(currentPassword, newPassword string) error {
|
|
return c.do(http.MethodPut, "/v1/auth/password", map[string]string{
|
|
"current_password": currentPassword,
|
|
"new_password": newPassword,
|
|
}, nil)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API methods — Admin: Auth
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// RemoveTOTP clears TOTP enrollment for the given account (admin).
|
|
// Use for account recovery when a user has lost their TOTP device.
|
|
func (c *Client) RemoveTOTP(accountID string) error {
|
|
return c.do(http.MethodDelete, "/v1/auth/totp",
|
|
map[string]string{"account_id": accountID}, nil)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API methods — Admin: Accounts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ListAccounts returns all accounts. Requires admin role.
|
|
func (c *Client) ListAccounts() ([]Account, error) {
|
|
var accounts []Account
|
|
if err := c.do(http.MethodGet, "/v1/accounts", nil, &accounts); err != nil {
|
|
return nil, err
|
|
}
|
|
return accounts, nil
|
|
}
|
|
|
|
// CreateAccount creates a new account. accountType is "human" or "system".
|
|
// password is required for human accounts.
|
|
func (c *Client) CreateAccount(username, accountType, password string) (*Account, error) {
|
|
req := map[string]string{
|
|
"username": username,
|
|
"account_type": accountType,
|
|
}
|
|
if password != "" {
|
|
req["password"] = password
|
|
}
|
|
var acct Account
|
|
if err := c.do(http.MethodPost, "/v1/accounts", req, &acct); err != nil {
|
|
return nil, err
|
|
}
|
|
return &acct, nil
|
|
}
|
|
|
|
// GetAccount returns the account with the given ID. Requires admin role.
|
|
func (c *Client) GetAccount(id string) (*Account, error) {
|
|
var acct Account
|
|
if err := c.do(http.MethodGet, "/v1/accounts/"+id, nil, &acct); err != nil {
|
|
return nil, err
|
|
}
|
|
return &acct, nil
|
|
}
|
|
|
|
// UpdateAccount updates mutable account fields (currently only status).
|
|
// Requires admin role. Returns nil on success (HTTP 204).
|
|
func (c *Client) UpdateAccount(id, status string) error {
|
|
req := map[string]string{}
|
|
if status != "" {
|
|
req["status"] = status
|
|
}
|
|
return c.do(http.MethodPatch, "/v1/accounts/"+id, req, nil)
|
|
}
|
|
|
|
// DeleteAccount soft-deletes the account with the given ID. Requires admin.
|
|
func (c *Client) DeleteAccount(id string) error {
|
|
return c.do(http.MethodDelete, "/v1/accounts/"+id, nil, nil)
|
|
}
|
|
|
|
// GetRoles returns the roles for accountID. Requires admin.
|
|
func (c *Client) GetRoles(accountID string) ([]string, error) {
|
|
var resp struct {
|
|
Roles []string `json:"roles"`
|
|
}
|
|
if err := c.do(http.MethodGet, "/v1/accounts/"+accountID+"/roles", nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Roles, nil
|
|
}
|
|
|
|
// SetRoles replaces the role set for accountID. Requires admin.
|
|
func (c *Client) SetRoles(accountID string, roles []string) error {
|
|
return c.do(http.MethodPut, "/v1/accounts/"+accountID+"/roles",
|
|
map[string][]string{"roles": roles}, nil)
|
|
}
|
|
|
|
// AdminSetPassword resets a human account's password without requiring the
|
|
// current password. Requires admin. All active sessions for the target account
|
|
// are revoked on success.
|
|
func (c *Client) AdminSetPassword(accountID, newPassword string) error {
|
|
return c.do(http.MethodPut, "/v1/accounts/"+accountID+"/password",
|
|
map[string]string{"new_password": newPassword}, nil)
|
|
}
|
|
|
|
// GetAccountTags returns the current tag set for an account. Requires admin.
|
|
func (c *Client) GetAccountTags(accountID string) ([]string, error) {
|
|
var resp struct {
|
|
Tags []string `json:"tags"`
|
|
}
|
|
if err := c.do(http.MethodGet, "/v1/accounts/"+accountID+"/tags", nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Tags, nil
|
|
}
|
|
|
|
// SetAccountTags replaces the full tag set for an account atomically.
|
|
// Pass an empty slice to clear all tags. Requires admin.
|
|
func (c *Client) SetAccountTags(accountID string, tags []string) ([]string, error) {
|
|
var resp struct {
|
|
Tags []string `json:"tags"`
|
|
}
|
|
if err := c.do(http.MethodPut, "/v1/accounts/"+accountID+"/tags",
|
|
map[string][]string{"tags": tags}, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Tags, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API methods — Admin: Tokens
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// IssueServiceToken issues a long-lived token for a system account. Requires admin.
|
|
func (c *Client) IssueServiceToken(accountID string) (token, expiresAt string, err error) {
|
|
var resp struct {
|
|
Token string `json:"token"`
|
|
ExpiresAt string `json:"expires_at"`
|
|
}
|
|
if err := c.do(http.MethodPost, "/v1/token/issue",
|
|
map[string]string{"account_id": accountID}, &resp); err != nil {
|
|
return "", "", err
|
|
}
|
|
return resp.Token, resp.ExpiresAt, nil
|
|
}
|
|
|
|
// RevokeToken revokes a token by JTI. Requires admin.
|
|
func (c *Client) RevokeToken(jti string) error {
|
|
return c.do(http.MethodDelete, "/v1/token/"+jti, nil, nil)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API methods — Admin: Credentials
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GetPGCreds returns Postgres credentials for accountID. Requires admin.
|
|
func (c *Client) GetPGCreds(accountID string) (*PGCreds, error) {
|
|
var creds PGCreds
|
|
if err := c.do(http.MethodGet, "/v1/accounts/"+accountID+"/pgcreds", nil, &creds); err != nil {
|
|
return nil, err
|
|
}
|
|
return &creds, nil
|
|
}
|
|
|
|
// SetPGCreds stores Postgres credentials for accountID. Requires admin.
|
|
// The password is sent over TLS and encrypted at rest server-side.
|
|
func (c *Client) SetPGCreds(accountID, host string, port int, database, username, password string) error {
|
|
return c.do(http.MethodPut, "/v1/accounts/"+accountID+"/pgcreds", map[string]interface{}{
|
|
"host": host,
|
|
"port": port,
|
|
"database": database,
|
|
"username": username,
|
|
"password": password,
|
|
}, nil)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API methods — Admin: Audit
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ListAudit retrieves audit log entries, newest first. Requires admin.
|
|
// f may be zero-valued to use defaults (limit=50, offset=0, no filter).
|
|
func (c *Client) ListAudit(f AuditFilter) (*AuditListResponse, error) {
|
|
path := "/v1/audit?"
|
|
if f.Limit > 0 {
|
|
path += fmt.Sprintf("limit=%d&", f.Limit)
|
|
}
|
|
if f.Offset > 0 {
|
|
path += fmt.Sprintf("offset=%d&", f.Offset)
|
|
}
|
|
if f.EventType != "" {
|
|
path += fmt.Sprintf("event_type=%s&", f.EventType)
|
|
}
|
|
if f.ActorID != "" {
|
|
path += fmt.Sprintf("actor_id=%s&", f.ActorID)
|
|
}
|
|
path = strings.TrimRight(path, "&?")
|
|
var resp AuditListResponse
|
|
if err := c.do(http.MethodGet, path, nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API methods — Admin: Policy
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ListPolicyRules returns all operator-defined policy rules ordered by
|
|
// priority (ascending). Requires admin.
|
|
func (c *Client) ListPolicyRules() ([]PolicyRule, error) {
|
|
var rules []PolicyRule
|
|
if err := c.do(http.MethodGet, "/v1/policy/rules", nil, &rules); err != nil {
|
|
return nil, err
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// CreatePolicyRule creates a new policy rule. Requires admin.
|
|
func (c *Client) CreatePolicyRule(req CreatePolicyRuleRequest) (*PolicyRule, error) {
|
|
var rule PolicyRule
|
|
if err := c.do(http.MethodPost, "/v1/policy/rules", req, &rule); err != nil {
|
|
return nil, err
|
|
}
|
|
return &rule, nil
|
|
}
|
|
|
|
// GetPolicyRule returns a single policy rule by integer ID. Requires admin.
|
|
func (c *Client) GetPolicyRule(id int) (*PolicyRule, error) {
|
|
var rule PolicyRule
|
|
if err := c.do(http.MethodGet, fmt.Sprintf("/v1/policy/rules/%d", id), nil, &rule); err != nil {
|
|
return nil, err
|
|
}
|
|
return &rule, nil
|
|
}
|
|
|
|
// UpdatePolicyRule updates one or more fields of an existing policy rule.
|
|
// Requires admin.
|
|
func (c *Client) UpdatePolicyRule(id int, req UpdatePolicyRuleRequest) (*PolicyRule, error) {
|
|
var rule PolicyRule
|
|
if err := c.do(http.MethodPatch, fmt.Sprintf("/v1/policy/rules/%d", id), req, &rule); err != nil {
|
|
return nil, err
|
|
}
|
|
return &rule, nil
|
|
}
|
|
|
|
// DeletePolicyRule permanently deletes a policy rule. Requires admin.
|
|
func (c *Client) DeletePolicyRule(id int) error {
|
|
return c.do(http.MethodDelete, fmt.Sprintf("/v1/policy/rules/%d", id), nil, nil)
|
|
}
|