mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
- Add WebAuthn/FIDO2 registration and verification endpoints POST /webauthn/register/begin, /register/complete, /verify - Biometric approval: Face ID/Touch ID/YubiKey approve transactions - Challenge bound to transaction ID (SHA256) preventing replay - Enforce ApproverRoles from policy engine during transaction approval - Add WebAuthnCredential DB model (pending_registration, active, revoked) - Policy decisions now carry ApproverRoles for role-gated approvals
141 lines
3.2 KiB
Go
141 lines
3.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"math/big"
|
|
"sort"
|
|
|
|
"github.com/hanzoai/orm"
|
|
|
|
"github.com/luxfi/mpc/pkg/db"
|
|
)
|
|
|
|
type PolicyDecision struct {
|
|
Action string `json:"action"`
|
|
Reason string `json:"reason"`
|
|
RequiredApprovers int `json:"required_approvers"`
|
|
ApproverRoles []string `json:"approver_roles,omitempty"`
|
|
}
|
|
|
|
type PolicyConditions struct {
|
|
MaxAmount string `json:"max_amount,omitempty"`
|
|
AllowedChains []string `json:"allowed_chains,omitempty"`
|
|
AllowedAddresses []string `json:"allowed_addresses,omitempty"`
|
|
}
|
|
|
|
func evaluateTransaction(amount, chain, toAddress string, policies []*db.Policy) PolicyDecision {
|
|
// Sort by priority descending
|
|
sort.Slice(policies, func(i, j int) bool {
|
|
return policies[i].Priority > policies[j].Priority
|
|
})
|
|
|
|
for _, policy := range policies {
|
|
if !policy.Enabled {
|
|
continue
|
|
}
|
|
|
|
var conditions PolicyConditions
|
|
if err := json.Unmarshal(policy.Conditions, &conditions); err != nil {
|
|
continue
|
|
}
|
|
|
|
if !matchesConditions(amount, chain, toAddress, conditions) {
|
|
continue
|
|
}
|
|
|
|
switch policy.Action {
|
|
case "deny":
|
|
return PolicyDecision{Action: "deny", Reason: policy.Name}
|
|
case "approve":
|
|
return PolicyDecision{Action: "approve", Reason: policy.Name}
|
|
case "require_approval":
|
|
return PolicyDecision{
|
|
Action: "require_approval",
|
|
Reason: policy.Name,
|
|
RequiredApprovers: policy.RequiredApprovers,
|
|
ApproverRoles: policy.ApproverRoles,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default: require approval with 1 admin
|
|
return PolicyDecision{
|
|
Action: "require_approval",
|
|
Reason: "default policy",
|
|
RequiredApprovers: 1,
|
|
}
|
|
}
|
|
|
|
func matchesConditions(amount, chain, toAddress string, cond PolicyConditions) bool {
|
|
if len(cond.AllowedChains) > 0 {
|
|
found := false
|
|
for _, c := range cond.AllowedChains {
|
|
if c == chain {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if len(cond.AllowedAddresses) > 0 {
|
|
found := false
|
|
for _, a := range cond.AllowedAddresses {
|
|
if a == toAddress {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if cond.MaxAmount != "" && amount != "" {
|
|
max, ok1 := new(big.Float).SetString(cond.MaxAmount)
|
|
val, ok2 := new(big.Float).SetString(amount)
|
|
if ok1 && ok2 && val.Cmp(max) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (s *Server) loadPolicies(ctx context.Context, orgID string, vaultID *string) ([]*db.Policy, error) {
|
|
q := orm.TypedQuery[db.Policy](s.db.ORM).
|
|
Filter("orgId=", orgID).
|
|
Order("-priority")
|
|
|
|
policies, err := q.GetAll(ctx)
|
|
if err != nil {
|
|
// No policies defined yet — return empty list, not error
|
|
return []*db.Policy{}, nil
|
|
}
|
|
|
|
// Filter enabled policies in-memory (JSONB boolean filters unreliable)
|
|
var enabled []*db.Policy
|
|
for _, p := range policies {
|
|
if p.Enabled {
|
|
enabled = append(enabled, p)
|
|
}
|
|
}
|
|
policies = enabled
|
|
|
|
// Filter by vaultID if provided (include policies with no vaultID or matching vaultID)
|
|
if vaultID != nil {
|
|
var filtered []*db.Policy
|
|
for _, p := range policies {
|
|
if p.VaultID == nil || *p.VaultID == *vaultID {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
return filtered, nil
|
|
}
|
|
|
|
return policies, nil
|
|
}
|