mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-05-01 05:04:13 +00:00
feat(auth): integrate rootwallet agent and update service hardening
- Replace CLI-based rootwallet calls with agent-based communication - Update production provisioner to support sudo-based service management - Add API key-to-wallet resolution for gateway operator handlers
This commit is contained in:
parent
8d7d1c6621
commit
c27faa02fa
@ -3,54 +3,58 @@ package auth
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/rwagent"
|
||||
"github.com/DeBrosOfficial/network/pkg/tlsutil"
|
||||
)
|
||||
|
||||
// IsRootWalletInstalled checks if the `rw` CLI is available in PATH
|
||||
// IsRootWalletInstalled checks if the rootwallet agent is reachable.
|
||||
func IsRootWalletInstalled() bool {
|
||||
_, err := exec.LookPath("rw")
|
||||
return err == nil
|
||||
client := rwagent.New(os.Getenv("RW_AGENT_SOCK"))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
return client.IsRunning(ctx)
|
||||
}
|
||||
|
||||
// getRootWalletAddress gets the EVM address from the RootWallet keystore
|
||||
// getRootWalletAddress gets the EVM address from the rootwallet agent.
|
||||
func getRootWalletAddress() (string, error) {
|
||||
cmd := exec.Command("rw", "address", "--chain", "evm")
|
||||
cmd.Stderr = os.Stderr
|
||||
out, err := cmd.Output()
|
||||
client := rwagent.New(os.Getenv("RW_AGENT_SOCK"))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
data, err := client.GetAddress(ctx, "evm")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get address from rw: %w", err)
|
||||
return "", fmt.Errorf("failed to get address from rootwallet agent: %w", err)
|
||||
}
|
||||
addr := strings.TrimSpace(string(out))
|
||||
if addr == "" {
|
||||
return "", fmt.Errorf("rw returned empty address — run 'rw init' first")
|
||||
if data.Address == "" {
|
||||
return "", fmt.Errorf("rootwallet agent returned empty address")
|
||||
}
|
||||
return addr, nil
|
||||
return data.Address, nil
|
||||
}
|
||||
|
||||
// signWithRootWallet signs a message using RootWallet's EVM key.
|
||||
// Stdin is passed through so the user can enter their password if the session is expired.
|
||||
// signWithRootWallet signs a message using the rootwallet agent's EVM key.
|
||||
// The desktop app may prompt the user for approval.
|
||||
func signWithRootWallet(message string) (string, error) {
|
||||
cmd := exec.Command("rw", "sign", message, "--chain", "evm")
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
out, err := cmd.Output()
|
||||
client := rwagent.New(os.Getenv("RW_AGENT_SOCK"))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
data, err := client.Sign(ctx, message, "evm")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign with rw: %w", err)
|
||||
return "", fmt.Errorf("failed to sign with rootwallet agent: %w", err)
|
||||
}
|
||||
sig := strings.TrimSpace(string(out))
|
||||
if sig == "" {
|
||||
return "", fmt.Errorf("rw returned empty signature")
|
||||
if data.Signature == "" {
|
||||
return "", fmt.Errorf("rootwallet agent returned empty signature")
|
||||
}
|
||||
return sig, nil
|
||||
return data.Signature, nil
|
||||
}
|
||||
|
||||
// PerformRootWalletAuthentication performs a challenge-response authentication flow
|
||||
|
||||
@ -197,8 +197,8 @@ func (b *Builder) buildVaultGuardian() error {
|
||||
return fmt.Errorf("zig not found in PATH — install from https://ziglang.org/download/")
|
||||
}
|
||||
|
||||
// Vault source is sibling to orama project
|
||||
vaultDir := filepath.Join(b.projectDir, "..", "orama-vault")
|
||||
// Vault source is sibling to core/ within the orama monorepo
|
||||
vaultDir := filepath.Join(b.projectDir, "..", "vault")
|
||||
if _, err := os.Stat(filepath.Join(vaultDir, "build.zig")); err != nil {
|
||||
return fmt.Errorf("vault source not found at %s — expected orama-vault as sibling directory: %w", vaultDir, err)
|
||||
}
|
||||
|
||||
@ -110,12 +110,17 @@ func resolveFromNetworkWithURL(gatewayURL, apiKey, env string) ([]inspector.Node
|
||||
if user == "" {
|
||||
user = "root"
|
||||
}
|
||||
// Sandbox nodes share a single SSH key; production nodes use per-host keys.
|
||||
vaultTarget := fmt.Sprintf("%s/%s", n.IPAddress, user)
|
||||
if n.Environment == "sandbox" {
|
||||
vaultTarget = "sandbox/root"
|
||||
}
|
||||
nodes = append(nodes, inspector.Node{
|
||||
Environment: n.Environment,
|
||||
User: user,
|
||||
Host: n.IPAddress,
|
||||
Role: n.Role,
|
||||
VaultTarget: fmt.Sprintf("%s/%s", n.IPAddress, user),
|
||||
VaultTarget: vaultTarget,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -86,31 +86,44 @@ func (fp *FilesystemProvisioner) EnsureDirectoryStructure() error {
|
||||
// EnsureOramaUser creates the 'orama' system user and group for running services.
|
||||
// Sets ownership of the orama data directory to the new user.
|
||||
func (fp *FilesystemProvisioner) EnsureOramaUser() error {
|
||||
// Check if user already exists
|
||||
if err := exec.Command("id", "orama").Run(); err == nil {
|
||||
return nil // user already exists
|
||||
}
|
||||
|
||||
// Create system user with no login shell and home at /opt/orama
|
||||
cmd := exec.Command("useradd", "--system", "--no-create-home",
|
||||
"--home-dir", fp.oramaHome, "--shell", "/usr/sbin/nologin", "orama")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create orama user: %w\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// Set ownership of orama directories
|
||||
chown := exec.Command("chown", "-R", "orama:orama", fp.oramaDir)
|
||||
if output, err := chown.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to chown %s: %w\n%s", fp.oramaDir, err, string(output))
|
||||
}
|
||||
|
||||
// Also chown the bin directory
|
||||
binDir := filepath.Join(fp.oramaHome, "bin")
|
||||
if _, err := os.Stat(binDir); err == nil {
|
||||
chown = exec.Command("chown", "-R", "orama:orama", binDir)
|
||||
if output, err := chown.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to chown %s: %w\n%s", binDir, err, string(output))
|
||||
// Check if user already exists; create if not
|
||||
if err := exec.Command("id", "orama").Run(); err != nil {
|
||||
cmd := exec.Command("useradd", "--system", "--no-create-home",
|
||||
"--home-dir", fp.oramaHome, "--shell", "/usr/sbin/nologin", "orama")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create orama user: %w\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// Set ownership of orama directories (only on first create)
|
||||
chown := exec.Command("chown", "-R", "orama:orama", fp.oramaDir)
|
||||
if output, err := chown.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to chown %s: %w\n%s", fp.oramaDir, err, string(output))
|
||||
}
|
||||
|
||||
binDir := filepath.Join(fp.oramaHome, "bin")
|
||||
if _, err := os.Stat(binDir); err == nil {
|
||||
chown = exec.Command("chown", "-R", "orama:orama", binDir)
|
||||
if output, err := chown.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to chown %s: %w\n%s", binDir, err, string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always ensure the sudoers rule is up-to-date (handles upgrades too).
|
||||
// Resolve systemctl path to avoid hardcoding /bin vs /usr/bin.
|
||||
systemctlPath, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
systemctlPath = "/bin/systemctl" // fallback
|
||||
}
|
||||
|
||||
// Grant orama user permission to manage namespace and deployment services.
|
||||
sudoersRule := fmt.Sprintf(
|
||||
"orama ALL=(root) NOPASSWD: %[1]s start orama-namespace-*, %[1]s stop orama-namespace-*, %[1]s enable orama-namespace-*, %[1]s disable orama-namespace-*, %[1]s restart orama-namespace-*, %[1]s start orama-deploy-*, %[1]s stop orama-deploy-*, %[1]s enable orama-deploy-*, %[1]s disable orama-deploy-*, %[1]s restart orama-deploy-*, %[1]s daemon-reload\n",
|
||||
systemctlPath,
|
||||
)
|
||||
sudoersPath := "/etc/sudoers.d/orama-namespaces"
|
||||
if err := os.WriteFile(sudoersPath, []byte(sudoersRule), 0440); err != nil {
|
||||
return fmt.Errorf("failed to write sudoers rule: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -19,6 +19,18 @@ ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
RestrictNamespaces=yes`
|
||||
|
||||
// oramaNodeHardening is like oramaServiceHardening but WITHOUT NoNewPrivileges.
|
||||
// The node process (which includes the gateway) needs to use sudo to manage
|
||||
// namespace systemd services. NoNewPrivileges prevents sudo from working.
|
||||
const oramaNodeHardening = `User=orama
|
||||
Group=orama
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
RestrictNamespaces=yes`
|
||||
|
||||
// SystemdServiceGenerator generates systemd unit files
|
||||
type SystemdServiceGenerator struct {
|
||||
oramaHome string
|
||||
@ -233,7 +245,7 @@ OOMScoreAdjust=-500
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, ssg.oramaHome, ssg.oramaDir, configFile, logFile, oramaServiceHardening)
|
||||
`, ssg.oramaHome, ssg.oramaDir, configFile, logFile, oramaNodeHardening)
|
||||
}
|
||||
|
||||
// GenerateVaultService generates the Orama Vault Guardian systemd unit.
|
||||
|
||||
@ -6,8 +6,10 @@
|
||||
package operator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/gateway/auth"
|
||||
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
|
||||
@ -29,14 +31,61 @@ func NewHandler(logger *zap.Logger, rqliteClient rqlite.Client) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// walletFromRequest extracts the operator's wallet address from the JWT
|
||||
// stored in the request context by the auth middleware.
|
||||
func walletFromRequest(r *http.Request) string {
|
||||
claims, ok := r.Context().Value(ctxkeys.JWT).(*auth.JWTClaims)
|
||||
if !ok || claims == nil {
|
||||
// walletFromRequest extracts the operator's wallet address from the request.
|
||||
// Supports both JWT auth (wallet in Sub claim) and API key auth (wallet looked
|
||||
// up from wallet_api_keys table).
|
||||
func (h *Handler) walletFromRequest(r *http.Request) string {
|
||||
// 1. Try JWT claims first (wallet JWT auth sets Sub = "0x...")
|
||||
if claims, ok := r.Context().Value(ctxkeys.JWT).(*auth.JWTClaims); ok && claims != nil {
|
||||
sub := strings.TrimSpace(claims.Sub)
|
||||
if strings.HasPrefix(strings.ToLower(sub), "0x") {
|
||||
return sub
|
||||
}
|
||||
// JWT with API key subject
|
||||
if strings.HasPrefix(strings.ToLower(sub), "ak_") {
|
||||
return h.resolveWalletFromAPIKey(r.Context(), sub)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try API key from context (X-API-Key header, no JWT)
|
||||
if apiKey, ok := r.Context().Value(ctxkeys.APIKey).(string); ok && apiKey != "" {
|
||||
return h.resolveWalletFromAPIKey(r.Context(), apiKey)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// resolveWalletFromAPIKey looks up the wallet address linked to an API key.
|
||||
// It queries namespace_ownership for a wallet-type owner of the namespace.
|
||||
func (h *Handler) resolveWalletFromAPIKey(ctx context.Context, apiKeySub string) string {
|
||||
if h.rqliteClient == nil {
|
||||
return ""
|
||||
}
|
||||
return claims.Sub
|
||||
ns := extractNamespace(apiKeySub)
|
||||
if ns == "" {
|
||||
return ""
|
||||
}
|
||||
var rows []struct {
|
||||
OwnerID string `db:"owner_id"`
|
||||
}
|
||||
if err := h.rqliteClient.Query(ctx, &rows,
|
||||
`SELECT no.owner_id FROM namespace_ownership no
|
||||
JOIN namespaces n ON no.namespace_id = n.id
|
||||
WHERE n.name = ? AND no.owner_type = 'wallet'
|
||||
LIMIT 1`,
|
||||
ns); err != nil || len(rows) == 0 {
|
||||
return ""
|
||||
}
|
||||
return rows[0].OwnerID
|
||||
}
|
||||
|
||||
// extractNamespace extracts the namespace from an API key subject like "ak_xxx:namespace".
|
||||
func extractNamespace(apiKeySub string) string {
|
||||
parts := strings.SplitN(apiKeySub, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[1]
|
||||
}
|
||||
return apiKeySub
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
|
||||
@ -12,37 +12,73 @@ import (
|
||||
)
|
||||
|
||||
func TestWalletFromRequest_withClaims(t *testing.T) {
|
||||
h := NewHandler(nil, nil)
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
claims := &auth.JWTClaims{Sub: "0xabc123"}
|
||||
ctx := context.WithValue(r.Context(), ctxkeys.JWT, claims)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
wallet := walletFromRequest(r)
|
||||
wallet := h.walletFromRequest(r)
|
||||
if wallet != "0xabc123" {
|
||||
t.Errorf("wallet = %q, want %q", wallet, "0xabc123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalletFromRequest_noClaims(t *testing.T) {
|
||||
h := NewHandler(nil, nil)
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
wallet := walletFromRequest(r)
|
||||
wallet := h.walletFromRequest(r)
|
||||
if wallet != "" {
|
||||
t.Errorf("wallet = %q, want empty", wallet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalletFromRequest_nilClaims(t *testing.T) {
|
||||
h := NewHandler(nil, nil)
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
ctx := context.WithValue(r.Context(), ctxkeys.JWT, (*auth.JWTClaims)(nil))
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
wallet := walletFromRequest(r)
|
||||
wallet := h.walletFromRequest(r)
|
||||
if wallet != "" {
|
||||
t.Errorf("wallet = %q, want empty", wallet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalletFromRequest_apiKeyContext(t *testing.T) {
|
||||
// When auth middleware sets ctxkeys.APIKey (no JWT), walletFromRequest
|
||||
// should try to resolve via the API key. With nil rqliteClient it returns
|
||||
// empty (can't query DB), but it shouldn't panic.
|
||||
h := NewHandler(nil, nil)
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
ctx := context.WithValue(r.Context(), ctxkeys.APIKey, "ak_test:myns")
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Should not panic — returns empty because no DB to query
|
||||
wallet := h.walletFromRequest(r)
|
||||
if wallet != "" {
|
||||
t.Errorf("wallet = %q, want empty (no DB)", wallet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractNamespace(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"ak_abc123:myns", "myns"},
|
||||
{"ak_abc123", "ak_abc123"},
|
||||
{"", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := extractNamespace(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractNamespace(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeJSON_valid(t *testing.T) {
|
||||
body := strings.NewReader(`{"node_id":"test-node","environment":"devnet"}`)
|
||||
r := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
|
||||
@ -31,7 +31,7 @@ func (h *Handler) HandleInvite(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
wallet := walletFromRequest(r)
|
||||
wallet := h.walletFromRequest(r)
|
||||
if wallet == "" {
|
||||
writeError(w, http.StatusUnauthorized, "wallet authentication required")
|
||||
return
|
||||
|
||||
@ -35,7 +35,7 @@ func (h *Handler) HandleListNodes(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
wallet := walletFromRequest(r)
|
||||
wallet := h.walletFromRequest(r)
|
||||
if wallet == "" {
|
||||
writeError(w, http.StatusUnauthorized, "wallet authentication required")
|
||||
return
|
||||
|
||||
@ -37,7 +37,7 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
wallet := walletFromRequest(r)
|
||||
wallet := h.walletFromRequest(r)
|
||||
if wallet == "" {
|
||||
writeError(w, http.StatusUnauthorized, "wallet authentication required")
|
||||
return
|
||||
|
||||
@ -1199,12 +1199,26 @@ func (cm *ClusterManager) ProvisionNamespaceCluster(ctx context.Context, namespa
|
||||
// provisionClusterAsync performs the actual cluster provisioning in the background
|
||||
func (cm *ClusterManager) provisionClusterAsync(cluster *NamespaceCluster, namespaceID int, namespaceName, provisionedBy string) {
|
||||
defer func() {
|
||||
// Recover from panics (e.g., gorqlite index-out-of-range) so the
|
||||
// goroutine doesn't die silently leaving status stuck at "provisioning".
|
||||
if r := recover(); r != nil {
|
||||
cm.logger.Error("Provisioning panicked",
|
||||
zap.String("namespace", namespaceName),
|
||||
zap.Any("panic", r),
|
||||
)
|
||||
bgCtx := context.Background()
|
||||
cm.updateClusterStatus(bgCtx, cluster.ID, ClusterStatusFailed,
|
||||
fmt.Sprintf("provisioning panicked: %v", r))
|
||||
}
|
||||
cm.provisioningMu.Lock()
|
||||
delete(cm.provisioning, namespaceName)
|
||||
cm.provisioningMu.Unlock()
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
// Overall timeout — prevents the goroutine from hanging indefinitely
|
||||
// if a remote spawn request or RQLite write blocks.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
cm.logger.Info("Starting async cluster provisioning",
|
||||
zap.String("cluster_id", cluster.ID),
|
||||
|
||||
@ -25,7 +25,15 @@ type client struct {
|
||||
}
|
||||
|
||||
// Query runs an arbitrary SELECT and scans rows into dest.
|
||||
func (c *client) Query(ctx context.Context, dest any, query string, args ...any) error {
|
||||
// Query runs a SELECT and scans results into dest.
|
||||
// Includes panic recovery because the gorqlite stdlib driver can panic
|
||||
// with "index out of range" when RQLite is temporarily unavailable.
|
||||
func (c *client) Query(ctx context.Context, dest any, query string, args ...any) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("gorqlite panic (QueryContext): %v", r)
|
||||
}
|
||||
}()
|
||||
rows, err := c.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -5,6 +5,7 @@ package rqlite
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// txClient implements Tx over *sql.Tx.
|
||||
@ -13,7 +14,13 @@ type txClient struct {
|
||||
}
|
||||
|
||||
// Query executes a SELECT query within the transaction.
|
||||
func (t *txClient) Query(ctx context.Context, dest any, query string, args ...any) error {
|
||||
// Includes panic recovery for the gorqlite stdlib driver.
|
||||
func (t *txClient) Query(ctx context.Context, dest any, query string, args ...any) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("gorqlite panic (QueryContext): %v", r)
|
||||
}
|
||||
}()
|
||||
rows, err := t.tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -23,7 +30,13 @@ func (t *txClient) Query(ctx context.Context, dest any, query string, args ...an
|
||||
}
|
||||
|
||||
// Exec executes a write statement within the transaction.
|
||||
func (t *txClient) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) {
|
||||
// Includes panic recovery for the gorqlite stdlib driver.
|
||||
func (t *txClient) Exec(ctx context.Context, query string, args ...any) (result sql.Result, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("gorqlite panic (ExecContext): %v", r)
|
||||
}
|
||||
}()
|
||||
return t.tx.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
|
||||
@ -134,6 +134,21 @@ func (c *Client) GetAddress(ctx context.Context, chain string) (*WalletAddressDa
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
// Sign signs a message with the wallet's private key.
|
||||
// The desktop app may prompt the user for approval on first use.
|
||||
func (c *Client) Sign(ctx context.Context, message, chain string) (*WalletSignData, error) {
|
||||
body := map[string]any{"message": message, "chain": chain}
|
||||
|
||||
var resp apiResponse[WalletSignData]
|
||||
if err := c.doJSON(ctx, "POST", "/v1/wallet/sign", body, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.OK {
|
||||
return nil, c.apiError(resp.Error, resp.Code, 0)
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
// Unlock sends the password to unlock the agent.
|
||||
func (c *Client) Unlock(ctx context.Context, password string, ttlMinutes int) error {
|
||||
body := map[string]any{"password": password, "ttlMinutes": ttlMinutes}
|
||||
|
||||
@ -31,6 +31,11 @@ type WalletAddressData struct {
|
||||
Chain string `json:"chain"`
|
||||
}
|
||||
|
||||
// WalletSignData from POST /v1/wallet/sign.
|
||||
type WalletSignData struct {
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// AppPermission represents an approved app in the permission database.
|
||||
type AppPermission struct {
|
||||
BinaryHash string `json:"binaryHash"`
|
||||
|
||||
@ -42,6 +42,15 @@ func (m *Manager) serviceName(namespace string, serviceType ServiceType) string
|
||||
return fmt.Sprintf("orama-namespace-%s@%s.service", serviceType, namespace)
|
||||
}
|
||||
|
||||
// systemctl builds an exec.Command for systemctl, prepending sudo when
|
||||
// the current process is not running as root.
|
||||
func systemctl(args ...string) *exec.Cmd {
|
||||
if os.Getuid() == 0 {
|
||||
return exec.Command("systemctl", args...)
|
||||
}
|
||||
return exec.Command("sudo", append([]string{"systemctl"}, args...)...)
|
||||
}
|
||||
|
||||
// StartService starts a namespace service
|
||||
func (m *Manager) StartService(namespace string, serviceType ServiceType) error {
|
||||
svcName := m.serviceName(namespace, serviceType)
|
||||
@ -49,7 +58,7 @@ func (m *Manager) StartService(namespace string, serviceType ServiceType) error
|
||||
zap.String("service", svcName),
|
||||
zap.String("namespace", namespace))
|
||||
|
||||
cmd := exec.Command("systemctl", "start", svcName)
|
||||
cmd := systemctl("start", svcName)
|
||||
m.logger.Debug("Executing systemctl command",
|
||||
zap.String("cmd", cmd.String()),
|
||||
zap.Strings("args", cmd.Args))
|
||||
@ -77,7 +86,7 @@ func (m *Manager) StopService(namespace string, serviceType ServiceType) error {
|
||||
zap.String("service", svcName),
|
||||
zap.String("namespace", namespace))
|
||||
|
||||
cmd := exec.Command("systemctl", "stop", svcName)
|
||||
cmd := systemctl("stop", svcName)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
// Don't error if service is already stopped or doesn't exist
|
||||
if strings.Contains(string(output), "not loaded") || strings.Contains(string(output), "inactive") {
|
||||
@ -98,7 +107,7 @@ func (m *Manager) RestartService(namespace string, serviceType ServiceType) erro
|
||||
zap.String("service", svcName),
|
||||
zap.String("namespace", namespace))
|
||||
|
||||
cmd := exec.Command("systemctl", "restart", svcName)
|
||||
cmd := systemctl("restart", svcName)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to restart %s: %w; output: %s", svcName, err, string(output))
|
||||
}
|
||||
@ -114,7 +123,7 @@ func (m *Manager) EnableService(namespace string, serviceType ServiceType) error
|
||||
zap.String("service", svcName),
|
||||
zap.String("namespace", namespace))
|
||||
|
||||
cmd := exec.Command("systemctl", "enable", svcName)
|
||||
cmd := systemctl("enable", svcName)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to enable %s: %w; output: %s", svcName, err, string(output))
|
||||
}
|
||||
@ -130,7 +139,7 @@ func (m *Manager) DisableService(namespace string, serviceType ServiceType) erro
|
||||
zap.String("service", svcName),
|
||||
zap.String("namespace", namespace))
|
||||
|
||||
cmd := exec.Command("systemctl", "disable", svcName)
|
||||
cmd := systemctl("disable", svcName)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
// Don't error if service is already disabled or doesn't exist
|
||||
if strings.Contains(string(output), "not loaded") {
|
||||
@ -187,7 +196,7 @@ func (m *Manager) IsServiceActive(namespace string, serviceType ServiceType) (bo
|
||||
// ReloadDaemon reloads systemd daemon configuration
|
||||
func (m *Manager) ReloadDaemon() error {
|
||||
m.logger.Info("Reloading systemd daemon")
|
||||
cmd := exec.Command("systemctl", "daemon-reload")
|
||||
cmd := systemctl("daemon-reload")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to reload systemd daemon: %w; output: %s", err, string(output))
|
||||
}
|
||||
@ -290,7 +299,7 @@ func (m *Manager) StopAllNamespaceServicesGlobally() error {
|
||||
|
||||
for _, svc := range services {
|
||||
m.logger.Info("Stopping service", zap.String("service", svc))
|
||||
cmd := exec.Command("systemctl", "stop", svc)
|
||||
cmd := systemctl("stop", svc)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
m.logger.Warn("Failed to stop service",
|
||||
zap.String("service", svc),
|
||||
@ -338,7 +347,7 @@ func (m *Manager) StopDeploymentServicesForNamespace(namespace string) {
|
||||
svc := fields[0]
|
||||
|
||||
// Stop the service
|
||||
if stopOut, stopErr := exec.Command("systemctl", "stop", svc).CombinedOutput(); stopErr != nil {
|
||||
if stopOut, stopErr := systemctl("stop", svc).CombinedOutput(); stopErr != nil {
|
||||
m.logger.Warn("Failed to stop deployment service",
|
||||
zap.String("service", svc),
|
||||
zap.Error(stopErr),
|
||||
@ -346,7 +355,7 @@ func (m *Manager) StopDeploymentServicesForNamespace(namespace string) {
|
||||
}
|
||||
|
||||
// Disable the service
|
||||
if disOut, disErr := exec.Command("systemctl", "disable", svc).CombinedOutput(); disErr != nil {
|
||||
if disOut, disErr := systemctl("disable", svc).CombinedOutput(); disErr != nil {
|
||||
m.logger.Warn("Failed to disable deployment service",
|
||||
zap.String("service", svc),
|
||||
zap.Error(disErr),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user